feat: update base to official upstream 2.8.0 with all cracked features intact (2.8.0-c1)
|
|
@ -7,9 +7,9 @@ on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
tag:
|
tag:
|
||||||
description: 'Release Tag (e.g. v2.5.1-c3)'
|
description: 'Release Tag (e.g. v2.8.0-c1)'
|
||||||
required: false
|
required: false
|
||||||
default: 'v2.5.1-c3'
|
default: 'v2.8.0-c1'
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -79,7 +79,7 @@ jobs:
|
||||||
elif [ -n "${{ inputs.tag }}" ]; then
|
elif [ -n "${{ inputs.tag }}" ]; then
|
||||||
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "tag=v2.5.1-c3" >> $GITHUB_OUTPUT
|
echo "tag=v2.8.0-c1" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Assets to Forgejo Release
|
- name: Publish Assets to Forgejo Release
|
||||||
|
|
|
||||||
35
.github/workflows/changelog_publish.yml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
53
.github/workflows/oneclient_nightly.yml
vendored
|
|
@ -85,6 +85,7 @@ 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
|
||||||
|
|
@ -263,21 +264,61 @@ 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 \
|
"${{ steps.outdir.outputs.dir }}"/*.AppImage; do
|
||||||
"${{ 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 installers
|
- name: Upload unsigned executable
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: oneclient-nightly-${{ matrix.os_key }}-${{ matrix.arch }}
|
path: dist/oneclient_app-${{ matrix.os_key }}-${{ matrix.arch }}*
|
||||||
path: dist/*
|
archive: false
|
||||||
|
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
|
||||||
|
|
|
||||||
104
.github/workflows/oneclient_release.yml
vendored
|
|
@ -3,17 +3,6 @@ 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
|
||||||
|
|
@ -37,18 +26,25 @@ jobs:
|
||||||
sha: ${{ steps.version.outputs.sha }}
|
sha: ${{ steps.version.outputs.sha }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||||
|
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: Resolve version, bump if requested, refuse to clobber a release
|
- name: Install Knope
|
||||||
|
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
|
||||||
|
|
@ -57,63 +53,29 @@ 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. Re-run with a bump, or bump the version in Cargo.toml."
|
echo "::error::Tag $TAG already exists on origin - OneClient $VERSION has already been released. Add a change file with \`knope document-change\` first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -123,17 +85,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. Re-run with a bump, or bump the version in Cargo.toml."
|
echo "::error::Release $TAG already exists and is published - refusing to overwrite OneClient $VERSION. Add a change file with \`knope document-change\` first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Commit the bump only after the guards pass, so a rejected release
|
# Commit only after the guards pass, so a rejected release does not
|
||||||
# does not leave a stray version bump on the branch.
|
# leave a stray version bump on the branch.
|
||||||
if [ "$BUMP" != "none" ]; then
|
if [ "$RELEASED" = "1" ]; 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 Cargo.toml Cargo.lock
|
git add -A Cargo.toml Cargo.lock .changeset CHANGELOG.md
|
||||||
git commit -m "chore: bump to $VERSION"
|
git commit -m "chore: release $VERSION"
|
||||||
git push origin "HEAD:${GITHUB_REF_NAME}"
|
git push origin "HEAD:${GITHUB_REF_NAME}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
@ -143,11 +105,18 @@ 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 "Automated release for OneClient $VERSION."
|
--notes-file notes.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
@ -189,7 +158,7 @@ jobs:
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
# Pin to the commit `prepare` resolved - with `bump` that is the version
|
# Pin to the commit `prepare` resolved - after a release 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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||||
with:
|
with:
|
||||||
|
|
@ -216,6 +185,7 @@ 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
|
||||||
|
|
|
||||||
12
.sqlx/query-001a994c740f7a015152319eb16fc113954af25ebd2a44d55e95833adda53268.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
170
.sqlx/query-01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0.json
generated
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tORDER BY name ASC\n\t\t",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java_path",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "java_path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolution",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "resolution"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "force_fullscreen",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "force_fullscreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mem_max",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "mem_max"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_args",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "game_args",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "game_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_env",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_pre",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_pre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_wrapper",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_wrapper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_post",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "os_extra",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "os_extra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "browser_update_mode",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "browser_update_mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "01b2ef24541af92505a3c2bc14037a61470a393dffa06da8bebfba98138709d0"
|
||||||
|
}
|
||||||
110
.sqlx/query-15dc690e91fe0bc7e1932a5051f8edd855deaeb31e52209ae436680962dea954.json
generated
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
38
.sqlx/query-171c6771d06656c2fe375f6a0da889f828f400e950f103f0ed44f5f98a48dc50.json
generated
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
12
.sqlx/query-1e43f93b56bb495e13826c734fe5fb64515cf1d7c28bc6a1bf1fb1645fdba2a1.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
218
.sqlx/query-4c6d40e97401e1321abccd7f413b49dda16a5bdf29eb90459ac140d8208db479.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tUPDATE clusters\n\t\tSET overall_played = COALESCE(overall_played, 0) + ?,\n\t\t last_played = ?\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": 3
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "4c6d40e97401e1321abccd7f413b49dda16a5bdf29eb90459ac140d8208db479"
|
||||||
|
}
|
||||||
218
.sqlx/query-54dea3f4093ceee9dcd5f0ae9bf072ec3615954b60bffa33bff7fd3f9bf14210.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
170
.sqlx/query-598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e.json
generated
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tINSERT INTO setting_profiles (\n\t\t\tname, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t\thook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t)\n\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\tON CONFLICT(name) DO UPDATE SET\n\t\t\tjava_path = excluded.java_path,\n\t\t\tresolution = excluded.resolution,\n\t\t\tforce_fullscreen = excluded.force_fullscreen,\n\t\t\tmem_max = excluded.mem_max,\n\t\t\tlaunch_args = excluded.launch_args,\n\t\t\tgame_args = excluded.game_args,\n\t\t\tlaunch_env = excluded.launch_env,\n\t\t\thook_pre = excluded.hook_pre,\n\t\t\thook_wrapper = excluded.hook_wrapper,\n\t\t\thook_post = excluded.hook_post,\n\t\t\tos_extra = excluded.os_extra,\n\t\t\tbrowser_update_mode = excluded.browser_update_mode\n\t\tRETURNING name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\t",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java_path",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "java_path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolution",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "resolution"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "force_fullscreen",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "force_fullscreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mem_max",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "mem_max"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_args",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "game_args",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "game_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_env",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_pre",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_pre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_wrapper",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_wrapper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_post",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "os_extra",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "os_extra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "browser_update_mode",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "browser_update_mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 13
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "598a34a0eb0f4d497cfd9bd77696310d1088e8baec677e07843a21ce6978652e"
|
||||||
|
}
|
||||||
12
.sqlx/query-59a29c97c422e8721c29b9517ae834ea97776c57bb129a9d67007cdd979ae841.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
218
.sqlx/query-5e18763a28af3660503e223c35875454b1e3fc4bb10c19d1c286d3e45afdd159.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT\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\tFROM clusters\n\t\tWHERE folder_name = ?\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": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "5e18763a28af3660503e223c35875454b1e3fc4bb10c19d1c286d3e45afdd159"
|
||||||
|
}
|
||||||
38
.sqlx/query-5f45ecf4e5ac27ad3a7ef4e5f9c2e6466ef51af11c332a2e6089c90759b9871f.json
generated
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
170
.sqlx/query-86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318.json
generated
Normal file
|
|
@ -0,0 +1,170 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT name, java_path, resolution, force_fullscreen, mem_max, launch_args, game_args, launch_env,\n\t\t hook_pre, hook_wrapper, hook_post, os_extra, browser_update_mode\n\t\tFROM setting_profiles\n\t\tWHERE name = ?\n\t\t",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "java_path",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "java_path"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "resolution",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "resolution"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "force_fullscreen",
|
||||||
|
"ordinal": 3,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "force_fullscreen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mem_max",
|
||||||
|
"ordinal": 4,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "mem_max"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_args",
|
||||||
|
"ordinal": 5,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "game_args",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "game_args"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "launch_env",
|
||||||
|
"ordinal": 7,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "launch_env"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_pre",
|
||||||
|
"ordinal": 8,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_pre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_wrapper",
|
||||||
|
"ordinal": 9,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_wrapper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hook_post",
|
||||||
|
"ordinal": 10,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "hook_post"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "os_extra",
|
||||||
|
"ordinal": 11,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "os_extra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "browser_update_mode",
|
||||||
|
"ordinal": 12,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "setting_profiles",
|
||||||
|
"name": "browser_update_mode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "86e3ba70da419464c0be3b3971f613c5a112558472cf2dcdeffa5907e3785318"
|
||||||
|
}
|
||||||
218
.sqlx/query-9b769a1467201ee998290c51b2cf51144a34c01975b898ee749b4e2111856119.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT\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\tFROM clusters\n\t\tWHERE id = ?\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": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9b769a1467201ee998290c51b2cf51144a34c01975b898ee749b4e2111856119"
|
||||||
|
}
|
||||||
218
.sqlx/query-9e4cbbc2c299861132749196c4e248e4309c2449e181b3b9ae16e7f7b4d9e627.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tUPDATE clusters\n\t\tSET name = ?,\n\t\t setting_profile_name = ?,\n\t\t mc_loader_version = ?,\n\t\t linked_modpack_hash = ?,\n\t\t description = ?,\n\t\t tags = ?,\n\t\t cover_path = ?\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": 8
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "9e4cbbc2c299861132749196c4e248e4309c2449e181b3b9ae16e7f7b4d9e627"
|
||||||
|
}
|
||||||
218
.sqlx/query-aeb2fbb78cfa536d6478878ccafb4f4754dfe057468fe06c22d87c3a062132b7.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
218
.sqlx/query-bc1502f185c5e73b59057f9caa0a892fbc2fa5dc45e63b391176e61f83a7676d.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT\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\tFROM clusters\n\t\tWHERE mc_version = ? AND mc_loader = ? AND user_created = 0\n\t\tLIMIT 1\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": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "bc1502f185c5e73b59057f9caa0a892fbc2fa5dc45e63b391176e61f83a7676d"
|
||||||
|
}
|
||||||
12
.sqlx/query-c06af5530a824d31e6e18a3a52cd57639b74e7e4d2ae30e29a6c49fda9521b4d.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
218
.sqlx/query-c3084a5369554a3ada17a50c5e33f8a6c414a9a213fd35c001c957eeebca3ff0.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tUPDATE clusters\n\t\tSET stage = ?\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": 2
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "c3084a5369554a3ada17a50c5e33f8a6c414a9a213fd35c001c957eeebca3ff0"
|
||||||
|
}
|
||||||
218
.sqlx/query-d636f4db52b3bb77d9d887500afbff5658afa10db3cf56794b8fa5e12535f3f7.json
generated
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n\t\tSELECT\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\tFROM clusters\n\t\tORDER BY last_played IS NULL, last_played DESC, name ASC\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": 0
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "d636f4db52b3bb77d9d887500afbff5658afa10db3cf56794b8fa5e12535f3f7"
|
||||||
|
}
|
||||||
12
.sqlx/query-dff3f2fe8e50eaa7ffebab7490306bc40e846ad708975991346aab83870e0968.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
12
.sqlx/query-e12409b04c8a3eea09a4bfc1014bca6fe5fbc1256b3537ed37195ed20075f944.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
12
.sqlx/query-ff07b78c0e97f1f38a6900092391d22e475471578b5ef73927e5427c2c0abc77.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM release_migration_waitlist WHERE expires_at <= ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "ff07b78c0e97f1f38a6900092391d22e475471578b5ef73927e5427c2c0abc77"
|
||||||
|
}
|
||||||
34
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
## 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)
|
||||||
|
|
@ -73,6 +73,15 @@ 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.
|
||||||
|
|
|
||||||
2197
Cargo.lock
generated
92
Cargo.toml
|
|
@ -16,7 +16,7 @@ license = "GPL-3.0-only"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked"
|
repository = "https://git.oiupoyt.space/oiupoyt/OneLauncher-Cracked"
|
||||||
rust-version = "1.97"
|
rust-version = "1.97"
|
||||||
version = "2.5.1-c3"
|
version = "2.8.0-c1"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# core
|
# core
|
||||||
|
|
@ -37,10 +37,10 @@ polyio = {path = "./packages/polyio"}
|
||||||
|
|
||||||
# core (extra)
|
# core (extra)
|
||||||
discord-rich-presence = {version = "=1.1.0"}
|
discord-rich-presence = {version = "=1.1.0"}
|
||||||
interfrost = {version = "=1.4.0"}
|
interfrost = {version = "=1.5.0"}
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
sentry = {version = "=0.48.5", default-features = false, features = [
|
sentry = {version = "=0.49.3", default-features = false, features = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"contexts",
|
"contexts",
|
||||||
"debug-images",
|
"debug-images",
|
||||||
|
|
@ -49,7 +49,7 @@ sentry = {version = "=0.48.5", default-features = false, features = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustls",
|
"rustls",
|
||||||
]}
|
]}
|
||||||
sentry-tracing = {version = "=0.48.5"}
|
sentry-tracing = {version = "=0.49.3"}
|
||||||
tracing = {version = "=0.1.44"}
|
tracing = {version = "=0.1.44"}
|
||||||
tracing-futures = {version = "=0.2.5"}
|
tracing-futures = {version = "=0.2.5"}
|
||||||
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
|
tracing-subscriber = {version = "=0.3.23", features = ["env-filter", "fmt", "ansi", "json", "parking_lot", "registry"]}
|
||||||
|
|
@ -68,15 +68,15 @@ sqlx = {version = "=0.9.0", default-features = false, features = [
|
||||||
]}
|
]}
|
||||||
|
|
||||||
# async
|
# async
|
||||||
astral-tokio-tar = {version = "=0.6.2"}
|
astral-tokio-tar = {version = "=0.7.0"}
|
||||||
async-compression = {version = "=0.4.42", features = ["tokio", "gzip"]}
|
async-compression = {version = "=0.4.48", features = ["tokio", "gzip"]}
|
||||||
async-stream = {version = "=0.3.6"}
|
async-stream = {version = "=0.3.6"}
|
||||||
async-tempfile = {version = "=0.7.0", features = ["uuid"]}
|
async-tempfile = {version = "=0.8.0", features = ["uuid"]}
|
||||||
async-trait = {version = "=0.1.89"}
|
async-trait = {version = "=0.1.92"}
|
||||||
async_zip = {version = "=0.0.18", features = ["chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
|
async_zip = {version = "=0.0.19", features = ["tracing", "chrono", "tokio", "tokio-fs", "deflate", "deflate64"]}
|
||||||
futures-lite = {version = "=2.6.1"}
|
futures-lite = {version = "=2.6.1"}
|
||||||
futures-util = {version = "=0.3.32", features = ["tokio-io"]}
|
futures-util = {version = "=0.3.34", features = ["tokio-io"]}
|
||||||
reqwest = {version = "=0.13.3", default-features = false, features = [
|
reqwest = {version = "=0.13.5", default-features = false, features = [
|
||||||
"json",
|
"json",
|
||||||
"stream",
|
"stream",
|
||||||
"multipart",
|
"multipart",
|
||||||
|
|
@ -91,67 +91,69 @@ reqwest = {version = "=0.13.3", default-features = false, features = [
|
||||||
"system-proxy",
|
"system-proxy",
|
||||||
]}
|
]}
|
||||||
reqwest-websocket = {version = "=0.6.0"}
|
reqwest-websocket = {version = "=0.6.0"}
|
||||||
tokio = {version = "=1.52.3", features = ["full"]}
|
tokio = {version = "=1.53.1", features = ["full"]}
|
||||||
tokio-stream = {version = "=0.1.18", features = ["full"]}
|
tokio-stream = {version = "=0.1.19", features = ["full"]}
|
||||||
tokio-util = {version = "=0.7.18", features = ["full"]}
|
tokio-util = {version = "=0.7.19", features = ["full"]}
|
||||||
|
|
||||||
# crypto (content hashing)
|
# crypto (content hashing)
|
||||||
byteorder = {version = "=1.5.0"}
|
byteorder = {version = "=1.5.0"}
|
||||||
digest = {version = "=0.10.7", features = ["std"]}
|
digest = {version = "=0.11.3"}
|
||||||
md-5 = {version = "=0.10.6", default-features = false}
|
md-5 = {version = "=0.11.0", default-features = false}
|
||||||
p256 = {version = "=0.13.2", features = ["ecdsa", "pkcs8"]}
|
p256 = {version = "=0.14.0", features = ["ecdsa", "pkcs8"]}
|
||||||
sha1 = {version = "=0.10.6", default-features = false}
|
sha1 = {version = "=0.11.0", default-features = false}
|
||||||
sha2 = {version = "=0.10.9", default-features = false}
|
sha2 = {version = "=0.11.0", default-features = false}
|
||||||
|
|
||||||
# util
|
# util
|
||||||
arboard = {version = "=3.6.1", features = ["wayland-data-control"]}
|
arboard = {version = "=3.6.1", features = ["wayland-data-control"]}
|
||||||
arc-swap = {version = "=1.7.1"}
|
arc-swap = {version = "=1.9.2"}
|
||||||
base64 = {version = "=0.22.1"}
|
base64 = {version = "=0.23.1"}
|
||||||
bytes = {version = "=1.11.1"}
|
bytes = {version = "=1.12.1"}
|
||||||
chrono = {version = "=0.4.44", features = ["serde"]}
|
chrono = {version = "=0.4.45", features = ["serde"]}
|
||||||
directories = {version = "=6.0.0"}
|
directories = {version = "=6.0.0"}
|
||||||
dunce = {version = "=1.0.5"}
|
dunce = {version = "=1.0.5"}
|
||||||
htmd = {version = "=0.5.4"}
|
htmd = {version = "=0.5.5"}
|
||||||
indicatif = {version = "=0.18.4", features = ["tokio"]}
|
indicatif = {version = "=0.18.6", features = ["tokio"]}
|
||||||
junction = {version = "=2.0.0"}
|
junction = {version = "=2.1.0"}
|
||||||
libc = {version = "=0.2.189"}
|
libc = {version = "=0.2.189"}
|
||||||
notify = {version = "=8.2.0"}
|
notify = {version = "=8.2.0"}
|
||||||
|
open = {version = "=5.4.4", features = ["shellexecute-on-windows"]}
|
||||||
parking_lot = {version = "=0.12.5"}
|
parking_lot = {version = "=0.12.5"}
|
||||||
pulldown-cmark = {version = "=0.13.4", default-features = false}
|
pulldown-cmark = {version = "=0.13.4", default-features = false}
|
||||||
rand = {version = "=0.10.1"}
|
rand = {version = "=0.10.3"}
|
||||||
regex = {version = "=1.12.3"}
|
regex = {version = "=1.13.1"}
|
||||||
|
rfd = {version = "=0.17.2"}
|
||||||
rust-embed = {version = "=8.12.0", features = ["include-exclude", "compression", "debug-embed"]}
|
rust-embed = {version = "=8.12.0", features = ["include-exclude", "compression", "debug-embed"]}
|
||||||
sanitize-filename = {version = "=0.6.0"}
|
sanitize-filename = {version = "=0.6.0"}
|
||||||
serde = {version = "=1.0.228"}
|
serde = {version = "=1.0.229"}
|
||||||
serde_json = {version = "=1.0.150"}
|
serde_json = {version = "=1.0.151"}
|
||||||
serde_repr = {version = "=0.1.20"}
|
serde_repr = {version = "=0.1.21"}
|
||||||
sysinfo = {version = "=0.39.5"}
|
sysinfo = {version = "=0.39.6"}
|
||||||
toml = {version = "=1.1.4", default-features = false, features = ["parse", "serde"]}
|
toml = {version = "=1.1.6", default-features = false, features = ["parse", "serde"]}
|
||||||
trash = {version = "=5.2.6"}
|
trash = {version = "=5.2.9"}
|
||||||
percent-encoding = {version = "=2.3.2"}
|
percent-encoding = {version = "=2.3.2"}
|
||||||
url = {version = "=2.5.8", features = ["serde"]}
|
url = {version = "=2.5.8", features = ["serde"]}
|
||||||
uuid = {version = "=1.23.1", features = ["serde", "v4"]}
|
uuid = {version = "=1.26.1", features = ["serde", "v4"]}
|
||||||
windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}
|
windows-sys = {version = "=0.61.2", features = ["Win32_Foundation", "Win32_Storage_FileSystem"]}
|
||||||
|
|
||||||
# util (codegen/macro)
|
# util (codegen/macro)
|
||||||
anyhow = {version = "=1.0.102"}
|
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"}
|
||||||
strum = {version = "=0.28.0", features = ["derive"]}
|
strum = {version = "=0.28.0", features = ["derive"]}
|
||||||
syn = {version = "=2.0.117"}
|
syn = {version = "=3.0.6"}
|
||||||
thiserror = {version = "=2.0.18"}
|
thiserror = {version = "=2.0.21"}
|
||||||
|
|
||||||
# ui
|
# ui
|
||||||
freya = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver", features = ["engine", "winit", "router", "remote-asset", "sdk", "query", "radio", "titlebar"]}
|
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/marc2332/freya", branch = "fix/skip-basic-render-driver"}
|
freya-core = {git = "https://github.com/Polyfrost/freya", rev = "12f696f68c4936617b8627c862b39acb3d9cca2b"}
|
||||||
freya-testing = {git = "https://github.com/marc2332/freya", branch = "fix/skip-basic-render-driver"}
|
freya-testing = {git = "https://github.com/Polyfrost/freya", rev = "12f696f68c4936617b8627c862b39acb3d9cca2b"}
|
||||||
freya-skia-safe = "=0.100.0" # required for the SKSL player renderer
|
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"]}
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
debug = "line-tables-only"
|
debug = "line-tables-only"
|
||||||
lto = "fat"
|
lto = "thin"
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
split-debuginfo = "packed"
|
split-debuginfo = "packed"
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ depends=(
|
||||||
'wayland'
|
'wayland'
|
||||||
'dbus'
|
'dbus'
|
||||||
'gtk3'
|
'gtk3'
|
||||||
|
'libayatana-appindicator'
|
||||||
)
|
)
|
||||||
provides=('oneclient')
|
provides=('oneclient')
|
||||||
conflicts=('oneclient')
|
conflicts=('oneclient')
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ depends=(
|
||||||
'wayland'
|
'wayland'
|
||||||
'dbus'
|
'dbus'
|
||||||
'gtk3'
|
'gtk3'
|
||||||
|
'libayatana-appindicator'
|
||||||
)
|
)
|
||||||
makedepends=(
|
makedepends=(
|
||||||
'rust'
|
'rust'
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ Source0: %{url}/releases/download/oneclient-%{version}/%{_rpmfile}
|
||||||
ExclusiveArch: x86_64
|
ExclusiveArch: x86_64
|
||||||
BuildRequires: bsdtar
|
BuildRequires: bsdtar
|
||||||
Requires: gtk3
|
Requires: gtk3
|
||||||
|
Requires: libayatana-appindicator-gtk3
|
||||||
|
Requires: libxdo
|
||||||
Requires: dbus-libs
|
Requires: dbus-libs
|
||||||
Provides: oneclient = %{version}-%{release}
|
Provides: oneclient = %{version}-%{release}
|
||||||
|
|
||||||
|
|
|
||||||
108
docs/superpowers/plans/2026-10-10-update-to-2.8.0.md
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
# 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
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
[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"
|
||||||
|
|
@ -33,6 +33,7 @@ rust-embed.workspace = true
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -48,12 +49,13 @@ percent-encoding.workspace = true
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
futures-util = { workspace = true, features = ["std"] }
|
|
||||||
|
|
||||||
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,6 +65,8 @@ freya-skia-safe.workspace = true
|
||||||
freya.workspace = true
|
freya.workspace = true
|
||||||
freya-core.workspace = true
|
freya-core.workspace = true
|
||||||
pulldown-cmark.workspace = true
|
pulldown-cmark.workspace = true
|
||||||
|
# inlines svg logos embedded in markdown badges as data urls
|
||||||
|
base64.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
freya-testing.workspace = true
|
freya-testing.workspace = true
|
||||||
|
|
@ -70,6 +74,10 @@ 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]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows = { version = "=0.62.2", features = [
|
windows = { version = "=0.62.2", features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
|
|
@ -154,6 +162,8 @@ 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"
|
||||||
|
|
|
||||||
BIN
packages/oneclient_app/assets/icons/fabric.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
packages/oneclient_app/assets/icons/forge.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
1
packages/oneclient_app/assets/icons/github.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 678 B |
|
|
@ -1,4 +1,7 @@
|
||||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M54.4179 7.49976H46.4793C37.369 7.49976 28.9508 12.36 24.3956 20.2498L6.61807 51.0414C6.00926 52.0959 5.48182 53.1831 5.03576 54.2942L30.2924 49.6414C40.4908 47.7626 48.4706 39.7829 50.3494 29.5844L54.4179 7.49976ZM5 73.199C5.45356 74.3413 5.99291 75.4586 6.61807 76.5414L24.3956 107.333C28.9508 115.223 37.369 120.083 46.4793 120.083H54.4331L50.3494 97.9154C48.4706 87.717 40.4908 79.7372 30.2924 77.8585L5 73.199ZM74.4827 120.083H82.0344C91.1447 120.083 99.5629 115.223 104.118 107.333L121.896 76.5414C122.506 75.4839 123.035 74.3935 123.482 73.279L98.6234 77.8585C88.425 79.7372 80.4452 87.717 78.5664 97.9154L74.4827 120.083ZM123.446 54.2142C123.006 53.1309 122.49 52.0706 121.896 51.0414L104.118 20.2498C99.5629 12.36 91.1447 7.49976 82.0344 7.49976H74.498L78.5664 29.5844C80.4452 39.7828 88.425 47.7626 98.6234 49.6414L123.446 54.2142Z" fill="currentColor"/>
|
<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="M64.458 24.5L67.962 45.0947C69.2479 52.6528 75.0625 58.6345 82.5811 60.1343L100.708 63.75L82.5811 67.3657C75.0625 68.8655 69.2479 74.8472 67.962 82.4053L64.458 103L60.954 82.4053C59.6681 74.8472 53.8535 68.8655 46.335 67.3657L28.208 63.75L46.335 60.1343C53.8536 58.6345 59.6681 52.6528 60.954 45.0947L64.458 24.5Z" 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>
|
</svg>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
BIN
packages/oneclient_app/assets/icons/neo-forge.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/oneclient_app/assets/icons/vanilla.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
|
|
@ -1,4 +1,4 @@
|
||||||
<svg width="156" height="32" viewBox="0 0 156 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="156" height="32" viewBox="0 0.41 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 |
BIN
packages/oneclient_app/icons/tray.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -47,8 +47,14 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn both_spellings_carry_the_folder() {
|
fn both_spellings_carry_the_folder() {
|
||||||
assert_eq!(parse(&["--launch", "fabric-1-20"]).launch.as_deref(), Some("fabric-1-20"));
|
assert_eq!(
|
||||||
assert_eq!(parse(&["--launch=fabric-1-20"]).launch.as_deref(), Some("fabric-1-20"));
|
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]
|
#[test]
|
||||||
|
|
@ -68,7 +74,12 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_flags_are_ignored_not_fatal() {
|
fn unknown_flags_are_ignored_not_fatal() {
|
||||||
assert_eq!(parse(&["--verbose", "--launch", "x", "leftover"]).launch.as_deref(), Some("x"));
|
assert_eq!(
|
||||||
|
parse(&["--verbose", "--launch", "x", "leftover"])
|
||||||
|
.launch
|
||||||
|
.as_deref(),
|
||||||
|
Some("x")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ 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::{
|
||||||
|
|
@ -14,6 +13,7 @@ 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)]
|
||||||
|
|
@ -91,7 +91,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))
|
||||||
.blur(12.)
|
.backdrop_blur(12.)
|
||||||
.corner_radius(CornerRadius::new_all(12.))
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
.border(
|
.border(
|
||||||
Border::new()
|
Border::new()
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,16 @@ 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::{settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch, use_game_snapshot, use_launcher, use_version_metadata};
|
use crate::hooks::{
|
||||||
|
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;
|
use crate::view::app::{launch_button_state, launch_syncing, shared_dir_taken};
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct ActiveClusterPanel;
|
pub struct ActiveClusterPanel;
|
||||||
|
|
@ -21,7 +25,6 @@ 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();
|
||||||
|
|
||||||
|
|
@ -72,11 +75,24 @@ impl Component for ActiveClusterPanel {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let title = format!("{} {}", cluster.mc_version, cluster.mc_loader);
|
let version = format!("{} {}", cluster.mc_version, cluster.mc_loader);
|
||||||
let subtitle = metadata
|
let (title, subtitle) = if cluster.user_created && cluster.kind != ClusterKind::OneClient {
|
||||||
.map(|m| m.name)
|
(cluster.name.clone(), version)
|
||||||
.unwrap_or_else(|| cluster.name.clone());
|
} else {
|
||||||
|
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()
|
||||||
|
|
@ -88,6 +104,9 @@ 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)
|
||||||
|
|
@ -109,7 +128,7 @@ impl Component for ActiveClusterPanel {
|
||||||
.child(launch_button(
|
.child(launch_button(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
dispatch,
|
dispatch,
|
||||||
launch_button_state(&game, cluster_id, syncing),
|
launch_button_state(&game, cluster_id, syncing, shared_dir_taken),
|
||||||
))
|
))
|
||||||
.child(cluster_settings_button(cluster_id)),
|
.child(cluster_settings_button(cluster_id)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
41
packages/oneclient_app/src/components/asset_image.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
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()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
528
packages/oneclient_app/src/components/blocked_downloads_popup.rs
Normal file
|
|
@ -0,0 +1,528 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
157
packages/oneclient_app/src/components/bundle_choices_popup.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -258,6 +258,8 @@ 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
|
||||||
|
|
@ -346,7 +348,7 @@ 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_press(move |_| pressing.set(false))
|
.on_global_pointer_up(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);
|
||||||
|
|
@ -478,3 +480,29 @@ pub fn open_folder_button(folder: std::path::PathBuf) -> Button {
|
||||||
})
|
})
|
||||||
.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")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,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 CanvasContext| {
|
let render_cb = RenderCallback::new(move |ctx: &mut FillRenderContext| {
|
||||||
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;
|
||||||
|
|
@ -182,8 +182,13 @@ impl Component for PieChart {
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.interactive(false)
|
.interactive(false)
|
||||||
// Childless canvas auto-sizes to 0x0 without an explicit size
|
// Childless rect auto-sizes to 0x0 without an explicit size
|
||||||
.child(canvas(render_cb).width(Size::fill()).height(Size::fill())),
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.background(render_cb),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ 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::{package_meta_batch, use_dispatch, use_notifications_snapshot, use_package_meta_batch};
|
use crate::hooks::{
|
||||||
|
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;
|
||||||
|
|
@ -108,7 +110,11 @@ impl Component for ClusterUpdatePopup {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let mut meta = MetaMap::new();
|
let mut meta = MetaMap::new();
|
||||||
for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
|
for provider in ProviderId::REMOTE_PROVIDERS
|
||||||
|
.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)
|
||||||
|
|
@ -355,11 +361,7 @@ fn change_list(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Multi-cluster stand-in for the footer's "Open cluster" shortcut
|
/// Multi-cluster stand-in for the footer's "Open cluster" shortcut
|
||||||
fn cluster_header(
|
fn cluster_header(group: &ClusterGroup, first: bool, dispatch: crate::Actions) -> impl IntoElement {
|
||||||
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();
|
||||||
|
|
||||||
|
|
|
||||||
107
packages/oneclient_app/src/components/code_block.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::ScrollArea;
|
||||||
|
use crate::theme::{self, colors};
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct CodeBlock {
|
||||||
|
code: String,
|
||||||
|
color: Color,
|
||||||
|
background: Color,
|
||||||
|
font_family: Cow<'static, str>,
|
||||||
|
font_size: f32,
|
||||||
|
max_lines: Option<usize>,
|
||||||
|
height: Option<f32>,
|
||||||
|
key: DiffKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyExt for CodeBlock {
|
||||||
|
fn write_key(&mut self) -> &mut DiffKey {
|
||||||
|
&mut self.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodeBlock {
|
||||||
|
pub fn new(code: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
code: code.into(),
|
||||||
|
color: colors::fg_primary(),
|
||||||
|
background: colors::component_bg(),
|
||||||
|
font_family: Cow::Borrowed(theme::MONO_FONT),
|
||||||
|
font_size: 12.,
|
||||||
|
max_lines: None,
|
||||||
|
height: None,
|
||||||
|
key: DiffKey::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn color(mut self, color: Color) -> Self {
|
||||||
|
self.color = color;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn background(mut self, background: Color) -> Self {
|
||||||
|
self.background = background;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_family(mut self, font_family: Cow<'static, str>) -> Self {
|
||||||
|
self.font_family = font_family;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn font_size(mut self, font_size: f32) -> Self {
|
||||||
|
self.font_size = font_size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max_lines(mut self, max_lines: usize) -> Self {
|
||||||
|
self.max_lines = Some(max_lines);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fixes the block's height and scrolls the code inside it
|
||||||
|
pub fn height(mut self, height: f32) -> Self {
|
||||||
|
self.height = Some(height);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for CodeBlock {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let text = SelectableText::new()
|
||||||
|
.span(self.code.clone())
|
||||||
|
.font_family(self.font_family.clone())
|
||||||
|
.font_size(self.font_size)
|
||||||
|
.max_lines(self.max_lines)
|
||||||
|
.width(Size::fill())
|
||||||
|
.color(self.color);
|
||||||
|
|
||||||
|
let block = rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.padding(Gaps::new_all(12.))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.background(self.background)
|
||||||
|
.border(border_all_color(1., colors::component_border()));
|
||||||
|
|
||||||
|
match self.height {
|
||||||
|
Some(height) => block
|
||||||
|
.height(Size::px(height))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.child(
|
||||||
|
ScrollArea::new()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.child(text),
|
||||||
|
),
|
||||||
|
None => block.child(text),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_key(&self) -> DiffKey {
|
||||||
|
self.key.clone().or(self.default_key())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ impl Component for ControlPanel {
|
||||||
.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))
|
||||||
.blur(12.)
|
.backdrop_blur(12.)
|
||||||
.corner_radius(CornerRadius::new_all(14.))
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
.border(
|
.border(
|
||||||
Border::new()
|
Border::new()
|
||||||
|
|
@ -120,7 +120,12 @@ impl Component for AccountHeader {
|
||||||
let unusable = query_error(¤t).is_some();
|
let unusable = query_error(¤t).is_some();
|
||||||
let account = try_default_account(¤t).or_else(|| {
|
let account = try_default_account(¤t).or_else(|| {
|
||||||
unusable
|
unusable
|
||||||
.then(|| try_accounts(&accounts_query).unwrap_or_default().into_iter().next())
|
.then(|| {
|
||||||
|
try_accounts(&accounts_query)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
})
|
||||||
.flatten()
|
.flatten()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -164,11 +169,7 @@ impl Component for AccountHeader {
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(10.)
|
.spacing(10.)
|
||||||
.padding(Gaps::new_all(4.))
|
.padding(Gaps::new_all(4.))
|
||||||
.child(
|
.child(Avatar::new(uuid).width(Size::px(40.)).height(Size::px(40.)))
|
||||||
Avatar::new(uuid)
|
|
||||||
.width(Size::px(40.))
|
|
||||||
.height(Size::px(40.)),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
@ -257,11 +258,7 @@ impl Component for NavRow {
|
||||||
.on_pointer_enter(move |_| hovered.set(true))
|
.on_pointer_enter(move |_| hovered.set(true))
|
||||||
.on_pointer_leave(move |_| hovered.set(false))
|
.on_pointer_leave(move |_| hovered.set(false))
|
||||||
.on_press(go)
|
.on_press(go)
|
||||||
.child(
|
.child(Icon::new(self.icon).size(20.).color(colors::fg_secondary()))
|
||||||
Icon::new(self.icon)
|
|
||||||
.size(20.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(self.row_label)
|
.text(self.row_label)
|
||||||
|
|
@ -286,8 +283,11 @@ impl Component for QuickSettings {
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
|
|
||||||
let discord_on = settings.discord_enabled;
|
let discord_on = settings.discord_enabled;
|
||||||
let toggle_discord = move |_| {
|
let toggle_discord = {
|
||||||
|
let dispatch = dispatch.clone();
|
||||||
|
move |_| {
|
||||||
dispatch.edit_settings(|s| s.discord_enabled = !s.discord_enabled);
|
dispatch.edit_settings(|s| s.discord_enabled = !s.discord_enabled);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -299,7 +299,6 @@ impl Component for QuickSettings {
|
||||||
QuickTile::new(IconType::Discord, "Discord RPC", discord_on)
|
QuickTile::new(IconType::Discord, "Discord RPC", discord_on)
|
||||||
.on_press(toggle_discord),
|
.on_press(toggle_discord),
|
||||||
)
|
)
|
||||||
.child(QuickTile::new(IconType::Moon01, "Close on launch", false).disabled())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,11 +330,6 @@ impl QuickTile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disabled(mut self) -> Self {
|
|
||||||
self.enabled = false;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
|
fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
|
||||||
self.on_press = Some(on_press.into());
|
self.on_press = Some(on_press.into());
|
||||||
self
|
self
|
||||||
|
|
@ -619,9 +613,7 @@ impl Component for FooterAction {
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
.on_pointer_enter(move |_| hovered.set(true))
|
.on_pointer_enter(move |_| hovered.set(true))
|
||||||
.on_pointer_leave(move |_| hovered.set(false))
|
.on_pointer_leave(move |_| hovered.set(false))
|
||||||
.map(handler, |el, handler| {
|
.map(handler, |el, handler| el.on_press(move |e| handler.call(e)))
|
||||||
el.on_press(move |e| handler.call(e))
|
|
||||||
})
|
|
||||||
.child(Icon::new(self.icon).size(16.).color(self.color))
|
.child(Icon::new(self.icon).size(16.).color(self.color))
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
|
|
|
||||||
|
|
@ -270,8 +270,7 @@ fn month_grid(
|
||||||
outside: day.month0() != shown.month0(),
|
outside: day.month0() != shown.month0(),
|
||||||
enabled: (min..=max).contains(&day),
|
enabled: (min..=max).contains(&day),
|
||||||
in_range: selected.is_some_and(|(a, b)| (a..=b).contains(&day)),
|
in_range: selected.is_some_and(|(a, b)| (a..=b).contains(&day)),
|
||||||
edge: selected.is_some_and(|(a, b)| day == a || day == b)
|
edge: selected.is_some_and(|(a, b)| day == a || day == b) || pending == Some(day),
|
||||||
|| pending == Some(day),
|
|
||||||
pick: pick.clone(),
|
pick: pick.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -349,7 +348,11 @@ impl<F: FnMut(NaiveDate) + Clone + 'static> Component for DayCell<F> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn footer(has_range: bool, pending: Option<NaiveDate>, mut clear: impl FnMut() + 'static) -> Element {
|
fn footer(
|
||||||
|
has_range: bool,
|
||||||
|
pending: Option<NaiveDate>,
|
||||||
|
mut clear: impl FnMut() + 'static,
|
||||||
|
) -> Element {
|
||||||
let hint = match pending {
|
let hint = match pending {
|
||||||
Some(day) => format!("From {} — pick an end date", format_day(day)),
|
Some(day) => format!("From {} — pick an end date", format_day(day)),
|
||||||
None => "Click a start date, then an end date".to_string(),
|
None => "Click a start date, then an end date".to_string(),
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,11 @@ pub struct Dropdown {
|
||||||
selected: String,
|
selected: String,
|
||||||
options: Vec<String>,
|
options: Vec<String>,
|
||||||
on_select: Option<EventHandler<usize>>,
|
on_select: Option<EventHandler<usize>>,
|
||||||
|
leading: Option<Element>,
|
||||||
width: Size,
|
width: Size,
|
||||||
height: Size,
|
height: Size,
|
||||||
|
outlined: bool,
|
||||||
|
checked: Option<Vec<bool>>,
|
||||||
key: DiffKey,
|
key: DiffKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,8 +28,11 @@ impl Dropdown {
|
||||||
selected: selected.into(),
|
selected: selected.into(),
|
||||||
options,
|
options,
|
||||||
on_select: None,
|
on_select: None,
|
||||||
|
leading: None,
|
||||||
width: Size::px(72.),
|
width: Size::px(72.),
|
||||||
height: Size::px(24.),
|
height: Size::px(24.),
|
||||||
|
outlined: false,
|
||||||
|
checked: None,
|
||||||
key: DiffKey::None,
|
key: DiffKey::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,6 +49,21 @@ impl Dropdown {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn outlined(mut self) -> Self {
|
||||||
|
self.outlined = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn checked(mut self, checked: Vec<bool>) -> Self {
|
||||||
|
self.checked = Some(checked);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn leading(mut self, leading: impl IntoElement) -> Self {
|
||||||
|
self.leading = Some(leading.into_element());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_select(mut self, handler: impl Into<EventHandler<usize>>) -> Self {
|
pub fn on_select(mut self, handler: impl Into<EventHandler<usize>>) -> Self {
|
||||||
self.on_select = Some(handler.into());
|
self.on_select = Some(handler.into());
|
||||||
self
|
self
|
||||||
|
|
@ -64,17 +85,21 @@ impl Component for Dropdown {
|
||||||
let focus = use_focus(a11y_id);
|
let focus = use_focus(a11y_id);
|
||||||
|
|
||||||
let mut button_area = use_state(|| None::<Area>);
|
let mut button_area = use_state(|| None::<Area>);
|
||||||
let mut list_size = use_state(|| None::<Size2D>);
|
let mut list_area = use_state(|| None::<Area>);
|
||||||
|
|
||||||
let selected = self.selected.clone();
|
let selected = self.selected.clone();
|
||||||
|
let leading = self.leading.clone();
|
||||||
let options = self.options.clone();
|
let options = self.options.clone();
|
||||||
let on_select = self.on_select.clone();
|
let on_select = self.on_select.clone();
|
||||||
|
let outlined = self.outlined;
|
||||||
|
let checked = self.checked.clone();
|
||||||
|
let multi = checked.is_some();
|
||||||
let is_open = open();
|
let is_open = open();
|
||||||
|
|
||||||
use_overlay_claim_when(is_open);
|
use_overlay_claim_when(is_open);
|
||||||
|
|
||||||
if !is_open && list_size().is_some() {
|
if !is_open && list_area().is_some() {
|
||||||
let _ = list_size.take();
|
let _ = list_area.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
let trigger_bg = if hovering() {
|
let trigger_bg = if hovering() {
|
||||||
|
|
@ -87,13 +112,13 @@ impl Component for Dropdown {
|
||||||
let list_h =
|
let list_h =
|
||||||
visible as f32 * OPTION_HEIGHT + visible.saturating_sub(1) as f32 * OPTION_SPACING;
|
visible as f32 * OPTION_HEIGHT + visible.saturating_sub(1) as f32 * OPTION_SPACING;
|
||||||
|
|
||||||
let offset_y = match (button_area(), list_size()) {
|
let offset_y = match (button_area(), list_area()) {
|
||||||
(Some(button), Some(list)) => {
|
(Some(button), Some(list)) => {
|
||||||
let root_height = Platform::get().root_size.peek().height;
|
let root_height = Platform::get().root_size.peek().height;
|
||||||
let space_below = root_height - button.max_y();
|
let space_below = root_height - button.max_y();
|
||||||
let space_above = button.min_y();
|
let space_above = button.min_y();
|
||||||
if list.height > space_below && list.height <= space_above {
|
if list.height() > space_below && list.height() <= space_above {
|
||||||
-(button.height() + list.height + 8.)
|
-(button.height() + list.height() + 8.)
|
||||||
} else {
|
} else {
|
||||||
0.
|
0.
|
||||||
}
|
}
|
||||||
|
|
@ -105,8 +130,15 @@ impl Component for Dropdown {
|
||||||
.map(|b| Size::px(b.width()))
|
.map(|b| Size::px(b.width()))
|
||||||
.unwrap_or_else(|| self.width.clone());
|
.unwrap_or_else(|| self.width.clone());
|
||||||
|
|
||||||
let on_global_pointer_press = move |_: Event<PointerEventData>| {
|
let on_global_pointer_up = move |e: Event<PointerEventData>| {
|
||||||
|
let loc = e.global_location();
|
||||||
|
let inside = list_area().is_some_and(|area| {
|
||||||
|
area.translate((0., offset_y).into())
|
||||||
|
.contains((loc.x as f32, loc.y as f32).into())
|
||||||
|
});
|
||||||
|
if !(multi && inside) {
|
||||||
open.set_if_modified(false);
|
open.set_if_modified(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -123,9 +155,13 @@ impl Component for Dropdown {
|
||||||
.horizontal()
|
.horizontal()
|
||||||
.center()
|
.center()
|
||||||
.spacing(4.)
|
.spacing(4.)
|
||||||
.padding(Gaps::new_symmetric(0., 8.))
|
.padding(Gaps::new_symmetric(0., if outlined { 12. } else { 8. }))
|
||||||
.corner_radius(CornerRadius::new_all(6.))
|
.corner_radius(CornerRadius::new_all(if outlined { 8. } else { 6. }))
|
||||||
.background(trigger_bg)
|
.background(trigger_bg)
|
||||||
|
.maybe(outlined, |el| {
|
||||||
|
el.main_align(Alignment::SpaceBetween)
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
})
|
||||||
.a11y_id(a11y_id)
|
.a11y_id(a11y_id)
|
||||||
.a11y_focusable(true)
|
.a11y_focusable(true)
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
|
@ -134,7 +170,7 @@ impl Component for Dropdown {
|
||||||
})
|
})
|
||||||
.on_pointer_enter(move |_| hovering.set(true))
|
.on_pointer_enter(move |_| hovering.set(true))
|
||||||
.on_pointer_leave(move |_| hovering.set(false))
|
.on_pointer_leave(move |_| hovering.set(false))
|
||||||
.on_global_pointer_press(on_global_pointer_press)
|
.on_global_pointer_up(on_global_pointer_up)
|
||||||
.on_sized(move |e: Event<SizedEventData>| {
|
.on_sized(move |e: Event<SizedEventData>| {
|
||||||
button_area.set_if_modified(Some(e.area));
|
button_area.set_if_modified(Some(e.area));
|
||||||
})
|
})
|
||||||
|
|
@ -143,10 +179,11 @@ impl Component for Dropdown {
|
||||||
e.stop_propagation();
|
e.stop_propagation();
|
||||||
open.toggle();
|
open.toggle();
|
||||||
})
|
})
|
||||||
|
.maybe_child(leading)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(selected)
|
.text(selected)
|
||||||
.font_size(12.)
|
.font_size(if outlined { 13. } else { 12. })
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -164,7 +201,7 @@ impl Component for Dropdown {
|
||||||
.offset_y(offset_y)
|
.offset_y(offset_y)
|
||||||
.content(Content::Fit)
|
.content(Content::Fit)
|
||||||
.on_sized(move |e: Event<SizedEventData>| {
|
.on_sized(move |e: Event<SizedEventData>| {
|
||||||
list_size.set_if_modified(Some(e.area.size));
|
list_area.set_if_modified(Some(e.area));
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -181,11 +218,16 @@ impl Component for Dropdown {
|
||||||
.children(options.into_iter().enumerate().map(
|
.children(options.into_iter().enumerate().map(
|
||||||
|(idx, option)| {
|
|(idx, option)| {
|
||||||
let on_select = on_select.clone();
|
let on_select = on_select.clone();
|
||||||
DropdownOption::new(option, move |_| {
|
let checked = checked
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.get(idx).copied().unwrap_or(false));
|
||||||
|
DropdownOption::new(option, checked, move |_| {
|
||||||
if let Some(handler) = &on_select {
|
if let Some(handler) = &on_select {
|
||||||
handler.call(idx);
|
handler.call(idx);
|
||||||
}
|
}
|
||||||
|
if !multi {
|
||||||
open.set(false);
|
open.set(false);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.into_element()
|
.into_element()
|
||||||
},
|
},
|
||||||
|
|
@ -203,16 +245,19 @@ impl Component for Dropdown {
|
||||||
|
|
||||||
struct DropdownOption {
|
struct DropdownOption {
|
||||||
text: String,
|
text: String,
|
||||||
|
checked: Option<bool>,
|
||||||
on_press: EventHandler<Event<PressEventData>>,
|
on_press: EventHandler<Event<PressEventData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DropdownOption {
|
impl DropdownOption {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
text: impl Into<String>,
|
text: impl Into<String>,
|
||||||
|
checked: Option<bool>,
|
||||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
|
checked,
|
||||||
on_press: on_press.into(),
|
on_press: on_press.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +265,7 @@ impl DropdownOption {
|
||||||
|
|
||||||
impl PartialEq for DropdownOption {
|
impl PartialEq for DropdownOption {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.text == other.text
|
self.text == other.text && self.checked == other.checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +278,8 @@ impl Component for DropdownOption {
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.padding(Gaps::new_symmetric(6., 8.))
|
.height(Size::px(OPTION_HEIGHT))
|
||||||
|
.padding(Gaps::new_symmetric(0., 8.))
|
||||||
.corner_radius(CornerRadius::new_all(6.))
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
.a11y_id(a11y_id)
|
.a11y_id(a11y_id)
|
||||||
.a11y_focusable(true)
|
.a11y_focusable(true)
|
||||||
|
|
@ -246,11 +292,27 @@ impl Component for DropdownOption {
|
||||||
.on_pointer_enter(move |_| hovering.set(true))
|
.on_pointer_enter(move |_| hovering.set(true))
|
||||||
.on_pointer_leave(move |_| hovering.set(false))
|
.on_pointer_leave(move |_| hovering.set(false))
|
||||||
.on_press(self.on_press.clone())
|
.on_press(self.on_press.clone())
|
||||||
|
.horizontal()
|
||||||
|
.content(Content::Flex)
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(self.text.clone())
|
.text(self.text.clone())
|
||||||
|
.width(Size::flex(1.))
|
||||||
.font_size(12.)
|
.font_size(12.)
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
|
.maybe_child(self.checked.map(|checked| {
|
||||||
|
rect()
|
||||||
|
.width(Size::px(14.))
|
||||||
|
.height(Size::px(14.))
|
||||||
|
.maybe_child(checked.then(|| {
|
||||||
|
Icon::new(IconType::Check)
|
||||||
|
.size(14.)
|
||||||
|
.color(colors::brand())
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,38 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use freya::animation::{
|
||||||
|
AnimNum, Ease, Function, OnChange, OnCreation, OnFinish, use_animation_with_dependencies,
|
||||||
|
};
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use oneclient_core::clusters::Cluster;
|
|
||||||
use oneclient_core::images::DEFAULT_IMAGE_EDGE;
|
|
||||||
use oneclient_common::domain::GameLoader;
|
use oneclient_common::domain::GameLoader;
|
||||||
use oneclient_common::{VersionKey, parse_mc_version};
|
use oneclient_common::{VersionKey, parse_mc_version};
|
||||||
|
use oneclient_core::clusters::Cluster;
|
||||||
|
use oneclient_core::images::{DEFAULT_IMAGE_EDGE, PREVIEW_IMAGE_EDGE};
|
||||||
|
|
||||||
use crate::AppAssets;
|
use crate::AppAssets;
|
||||||
use crate::hooks::{loaded_image, use_cached_image, use_version_metadata};
|
use crate::hooks::{
|
||||||
|
loaded_image, resolve_art_url, settled_or_loading, use_cached_image, use_local_image,
|
||||||
|
use_version_art, use_version_metadata,
|
||||||
|
};
|
||||||
use crate::layout::HOME_BACKGROUND_ASSET;
|
use crate::layout::HOME_BACKGROUND_ASSET;
|
||||||
|
use crate::theme::colors;
|
||||||
use crate::ui::ImageFallbackExt;
|
use crate::ui::ImageFallbackExt;
|
||||||
|
|
||||||
/// The size cluster cards request so it is usually already cached when anything else wants it
|
pub const ART_PREVIEW_EDGE: u32 = PREVIEW_IMAGE_EDGE;
|
||||||
pub const ART_PREVIEW_EDGE: u32 = 512;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
pub struct DynamicArt {
|
pub struct DynamicArt {
|
||||||
major: Option<u32>,
|
major: Option<u32>,
|
||||||
key: Option<VersionKey>,
|
key: Option<VersionKey>,
|
||||||
loader: Option<GameLoader>,
|
loader: Option<GameLoader>,
|
||||||
|
cover: Option<PathBuf>,
|
||||||
|
cover_picked: bool,
|
||||||
max_edge: u32,
|
max_edge: u32,
|
||||||
preview_edge: Option<u32>,
|
preview_edge: Option<u32>,
|
||||||
|
url: Option<String>,
|
||||||
|
skeleton: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicArt {
|
impl DynamicArt {
|
||||||
|
|
@ -28,8 +41,7 @@ impl DynamicArt {
|
||||||
major: Some(major),
|
major: Some(major),
|
||||||
key,
|
key,
|
||||||
loader,
|
loader,
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
..Self::fallback()
|
||||||
preview_edge: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,8 +51,15 @@ impl DynamicArt {
|
||||||
major: parsed.as_ref().map(|p| p.major),
|
major: parsed.as_ref().map(|p| p.major),
|
||||||
key: parsed.and_then(|p| p.key()),
|
key: parsed.and_then(|p| p.key()),
|
||||||
loader: Some(cluster.mc_loader),
|
loader: Some(cluster.mc_loader),
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
cover: cluster.cover_file(),
|
||||||
preview_edge: None,
|
..Self::fallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_url(url: String) -> Self {
|
||||||
|
Self {
|
||||||
|
url: Some(url),
|
||||||
|
..Self::fallback()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,11 +68,34 @@ impl DynamicArt {
|
||||||
major: None,
|
major: None,
|
||||||
key: None,
|
key: None,
|
||||||
loader: None,
|
loader: None,
|
||||||
|
cover: None,
|
||||||
|
cover_picked: false,
|
||||||
max_edge: DEFAULT_IMAGE_EDGE,
|
max_edge: DEFAULT_IMAGE_EDGE,
|
||||||
preview_edge: None,
|
preview_edge: None,
|
||||||
|
url: None,
|
||||||
|
skeleton: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn cover(mut self, cover: Option<PathBuf>) -> Self {
|
||||||
|
self.cover = cover;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn picked_cover(mut self, cover: Option<PathBuf>) -> Self {
|
||||||
|
self.cover = cover;
|
||||||
|
self.cover_picked = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn skeleton(mut self, skeleton: bool) -> Self {
|
||||||
|
self.skeleton = skeleton;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn max_edge(mut self, max_edge: u32) -> Self {
|
pub fn max_edge(mut self, max_edge: u32) -> Self {
|
||||||
self.max_edge = max_edge;
|
self.max_edge = max_edge;
|
||||||
|
|
@ -68,26 +110,46 @@ impl DynamicArt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_bytes(&self) -> (String, Bytes) {
|
pub fn use_bytes(&self) -> (String, Bytes) {
|
||||||
use_art_bytes(
|
use_art_bytes(self)
|
||||||
self.major,
|
|
||||||
self.key,
|
|
||||||
self.loader,
|
|
||||||
self.max_edge,
|
|
||||||
self.preview_edge,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_art_bytes(
|
pub fn use_art_bytes(art: &DynamicArt) -> (String, Bytes) {
|
||||||
major: Option<u32>,
|
let max_edge = art.max_edge;
|
||||||
key: Option<VersionKey>,
|
use_resolved_art(art)
|
||||||
loader: Option<GameLoader>,
|
.unwrap_or_else(|| (format!("{max_edge}|{HOME_BACKGROUND_ASSET}"), bundled_art()))
|
||||||
max_edge: u32,
|
}
|
||||||
preview_edge: Option<u32>,
|
|
||||||
) -> (String, Bytes) {
|
|
||||||
let fallback = use_memo(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
|
||||||
|
|
||||||
let art_url = use_version_metadata(major, key, loader).and_then(|m| m.art_url);
|
fn bundled_art() -> Bytes {
|
||||||
|
static ART: LazyLock<Bytes> =
|
||||||
|
LazyLock::new(|| AppAssets::get_bytes(HOME_BACKGROUND_ASSET).unwrap_or_default());
|
||||||
|
ART.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_resolved_art(art: &DynamicArt) -> Option<(String, Bytes)> {
|
||||||
|
let DynamicArt {
|
||||||
|
major,
|
||||||
|
key,
|
||||||
|
loader,
|
||||||
|
cover,
|
||||||
|
cover_picked,
|
||||||
|
max_edge,
|
||||||
|
preview_edge,
|
||||||
|
url,
|
||||||
|
skeleton: _,
|
||||||
|
} = art.clone();
|
||||||
|
|
||||||
|
let cover_query = use_local_image(cover.clone().unwrap_or_default(), max_edge, cover_picked);
|
||||||
|
|
||||||
|
let cover_bytes = cover.as_ref().and_then(|path| {
|
||||||
|
settled_or_loading(&cover_query)
|
||||||
|
.filter(|bytes: &Bytes| !bytes.is_empty())
|
||||||
|
.map(|bytes| (format!("{max_edge}|{}", path.display()), bytes))
|
||||||
|
});
|
||||||
|
|
||||||
|
let curated = use_version_metadata(major, key, loader);
|
||||||
|
let candidates = use_version_art(major, key);
|
||||||
|
let art_url = url.or_else(|| resolve_art_url(curated.as_ref(), &candidates));
|
||||||
|
|
||||||
let image_query = use_cached_image(art_url.clone(), max_edge);
|
let image_query = use_cached_image(art_url.clone(), max_edge);
|
||||||
|
|
||||||
|
|
@ -105,17 +167,41 @@ pub fn use_art_bytes(
|
||||||
.map(|(url, bytes)| (format!("{edge}|{url}"), bytes))
|
.map(|(url, bytes)| (format!("{edge}|{url}"), bytes))
|
||||||
};
|
};
|
||||||
|
|
||||||
full.or_else(preview).unwrap_or_else(|| {
|
cover_bytes.or(full).or_else(preview)
|
||||||
(
|
|
||||||
format!("{max_edge}|{HOME_BACKGROUND_ASSET}"),
|
|
||||||
fallback.read().clone(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for DynamicArt {
|
impl Component for DynamicArt {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let (key, bytes) = self.use_bytes();
|
let resolved = use_resolved_art(self);
|
||||||
|
let show_skeleton = self.skeleton && resolved.is_none();
|
||||||
|
|
||||||
|
let pulse = use_animation_with_dependencies(&show_skeleton, |conf, show| {
|
||||||
|
conf.on_change(OnChange::Rerun);
|
||||||
|
|
||||||
|
if *show {
|
||||||
|
conf.on_creation(OnCreation::Run);
|
||||||
|
conf.on_finish(OnFinish::reverse());
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimNum::new(0.4, 0.9)
|
||||||
|
.time(800)
|
||||||
|
.ease(Ease::InOut)
|
||||||
|
.function(Function::Sine)
|
||||||
|
});
|
||||||
|
let pulse_v = pulse.read().value();
|
||||||
|
|
||||||
|
if show_skeleton {
|
||||||
|
return rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.opacity(pulse_v)
|
||||||
|
.into_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_edge = self.max_edge;
|
||||||
|
let (key, bytes) = resolved
|
||||||
|
.unwrap_or_else(|| (format!("{max_edge}|{HOME_BACKGROUND_ASSET}"), bundled_art()));
|
||||||
|
|
||||||
ImageViewer::new((key, bytes))
|
ImageViewer::new((key, bytes))
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
|
|
|
||||||
146
packages/oneclient_app/src/components/essential_confirm.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{
|
||||||
|
Button, Icon, IconType, Markdown, MarkdownStyle, OverlayPopup, ScrollArea,
|
||||||
|
};
|
||||||
|
use crate::hooks::{EssentialGuardKind, use_cluster_mutation, use_essential_guard};
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
|
const BODY_MAX_H: f32 = 400.;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct EssentialConfirmOverlay;
|
||||||
|
|
||||||
|
impl Component for EssentialConfirmOverlay {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut pending = use_essential_guard();
|
||||||
|
let cluster = use_cluster_mutation();
|
||||||
|
|
||||||
|
let Some(request) = pending.read().clone() else {
|
||||||
|
return rect().into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let (title, confirm_label) = match request.kind {
|
||||||
|
EssentialGuardKind::Disable => (format!("Turn off {}?", request.name), "Turn it off"),
|
||||||
|
EssentialGuardKind::Remove => (format!("Remove {}?", request.name), "Remove anyway"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let action = request.action.clone();
|
||||||
|
|
||||||
|
OverlayPopup::new()
|
||||||
|
.on_close(move |_| pending.set(None))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::px(440.))
|
||||||
|
.max_width(Size::window_percent(90.))
|
||||||
|
.spacing(14.)
|
||||||
|
.padding(Gaps::new_all(20.))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.background(CARD_BG)
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(10.)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::AlertTriangle)
|
||||||
|
.size(20.)
|
||||||
|
.color(colors::code_warn()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(title)
|
||||||
|
.font_size(16.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(WarningBody {
|
||||||
|
body: request.body.clone(),
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::End)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.on_press(move |_| {
|
||||||
|
cluster.mutate(action.clone());
|
||||||
|
pending.set(None);
|
||||||
|
})
|
||||||
|
.text(confirm_label),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| pending.set(None))
|
||||||
|
.text("Keep it"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct WarningBody {
|
||||||
|
body: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for WarningBody {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut content_h = use_state(|| 0f32);
|
||||||
|
let height = content_h.read().min(BODY_MAX_H);
|
||||||
|
|
||||||
|
ScrollArea::new()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(height))
|
||||||
|
.scrollbar_gutter(true)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.on_sized(move |e: Event<SizedEventData>| {
|
||||||
|
let h = e.area.height();
|
||||||
|
if (*content_h.read() - h).abs() > 0.5 {
|
||||||
|
content_h.set(h);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.padding(Gaps::new_all(12.))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(Markdown::new(self.body.clone()).width(Size::fill()).style(
|
||||||
|
MarkdownStyle {
|
||||||
|
color: Color::WHITE,
|
||||||
|
color_link: colors::code_info(),
|
||||||
|
color_code: Color::WHITE,
|
||||||
|
background_code: CARD_BG,
|
||||||
|
background_blockquote: CARD_BG,
|
||||||
|
border_blockquote: colors::component_border(),
|
||||||
|
background_divider: colors::component_border(),
|
||||||
|
headings: [16., 14., 13., 12., 12., 12.],
|
||||||
|
paragraph_size: 12.,
|
||||||
|
code_font_size: 11.,
|
||||||
|
..MarkdownStyle::default()
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,17 +3,24 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use freya::animation::{AnimNum, Ease, Function, OnCreation, use_animation};
|
use freya::animation::{AnimNum, Ease, Function, OnCreation, use_animation};
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::router::use_route;
|
use freya::router::use_route;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_content::modpacks::detect_format;
|
||||||
use oneclient_content::packages::ContentType;
|
use oneclient_content::packages::ContentType;
|
||||||
|
use oneclient_core::clusters::{Cluster, ModpackSource};
|
||||||
use oneclient_db::models::ClusterId;
|
use oneclient_db::models::ClusterId;
|
||||||
|
|
||||||
use crate::Route;
|
use crate::Route;
|
||||||
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea};
|
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea};
|
||||||
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch};
|
use crate::hooks::{
|
||||||
|
add_world_datapacks, settled_or_loading, spawn_world_task, try_cluster_worlds,
|
||||||
|
use_active_cluster_id, use_cluster_worlds, use_clusters, use_datapack_world, use_debounced,
|
||||||
|
use_dispatch,
|
||||||
|
};
|
||||||
|
use crate::launcher::off_ui;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
|
@ -23,19 +30,29 @@ const FILE_ROW_H: f32 = 34.;
|
||||||
const FILE_LIST_MAX_ROWS: usize = 5;
|
const FILE_LIST_MAX_ROWS: usize = 5;
|
||||||
const TYPE_DROPDOWN_W: f32 = 108.;
|
const TYPE_DROPDOWN_W: f32 = 108.;
|
||||||
|
|
||||||
/// Worlds are directories and modpacks/datapacks have no view so both are excluded
|
/// Worlds are directories and modpacks have no view so both are excluded
|
||||||
const IMPORTABLE: [ContentType; 3] = [
|
const IMPORTABLE: [ContentType; 4] = [
|
||||||
ContentType::Mod,
|
ContentType::Mod,
|
||||||
ContentType::ResourcePack,
|
ContentType::ResourcePack,
|
||||||
ContentType::Shader,
|
ContentType::Shader,
|
||||||
|
ContentType::DataPack,
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHADER_HINTS: [&str; 5] = ["shader", "bsl", "seus", "complementary", "sildur"];
|
const SHADER_HINTS: [&str; 5] = ["shader", "bsl", "seus", "complementary", "sildur"];
|
||||||
|
const DATAPACK_HINTS: [&str; 4] = ["datapack", "data pack", "data_pack", "data-pack"];
|
||||||
|
|
||||||
|
fn importable(mod_loader: bool) -> Vec<ContentType> {
|
||||||
|
IMPORTABLE
|
||||||
|
.into_iter()
|
||||||
|
.filter(|ct| mod_loader || !ct.needs_mod_loader())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn content_label(content_type: ContentType) -> &'static str {
|
fn content_label(content_type: ContentType) -> &'static str {
|
||||||
match content_type {
|
match content_type {
|
||||||
ContentType::ResourcePack => "Textures",
|
ContentType::ResourcePack => "Textures",
|
||||||
ContentType::Shader => "Shaders",
|
ContentType::Shader => "Shaders",
|
||||||
|
ContentType::DataPack => "Data packs",
|
||||||
_ => "Mods",
|
_ => "Mods",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,26 +68,87 @@ fn file_name(path: &Path) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precedence `.jar` is always a mod then the current route then a name sniff
|
/// Precedence `.jar` is always a mod then the current route then a name sniff
|
||||||
fn infer_content_type(path: &Path, route_type: Option<ContentType>) -> ContentType {
|
fn infer_content_type(
|
||||||
|
path: &Path,
|
||||||
|
route_type: Option<ContentType>,
|
||||||
|
mod_loader: bool,
|
||||||
|
) -> Option<ContentType> {
|
||||||
if extension(path).as_deref() == Some("jar") {
|
if extension(path).as_deref() == Some("jar") {
|
||||||
return ContentType::Mod;
|
return mod_loader.then_some(ContentType::Mod);
|
||||||
}
|
}
|
||||||
if let Some(route_type) = route_type {
|
if let Some(route_type) = route_type {
|
||||||
return route_type;
|
if mod_loader || !route_type.needs_mod_loader() {
|
||||||
|
return Some(route_type);
|
||||||
|
}
|
||||||
|
if route_type == ContentType::Shader {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let name = file_name(path).to_lowercase();
|
let name = file_name(path).to_lowercase();
|
||||||
if SHADER_HINTS.iter().any(|hint| name.contains(hint)) {
|
if DATAPACK_HINTS.iter().any(|hint| name.contains(hint)) {
|
||||||
ContentType::Shader
|
Some(ContentType::DataPack)
|
||||||
|
} else if mod_loader && SHADER_HINTS.iter().any(|hint| name.contains(hint)) {
|
||||||
|
Some(ContentType::Shader)
|
||||||
} else {
|
} else {
|
||||||
ContentType::ResourcePack
|
Some(ContentType::ResourcePack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DROP_SETTLE: Duration = Duration::from_millis(300);
|
||||||
|
|
||||||
|
pub fn accept_drop(
|
||||||
|
paths: &[PathBuf],
|
||||||
|
mut pending: State<Vec<PathBuf>>,
|
||||||
|
mut modpacks: State<Vec<PathBuf>>,
|
||||||
|
) {
|
||||||
|
for path in paths {
|
||||||
|
match extension(path).as_deref() {
|
||||||
|
Some("mrpack") => modpacks.write().push(path.clone()),
|
||||||
|
Some("zip") => {
|
||||||
|
let path = path.clone();
|
||||||
|
spawn(async move {
|
||||||
|
let probe = path.clone();
|
||||||
|
let is_modpack =
|
||||||
|
off_ui(async move { detect_format(&probe).await.is_ok() }).await;
|
||||||
|
if is_modpack {
|
||||||
|
modpacks.write().push(path);
|
||||||
|
} else {
|
||||||
|
pending.write().push(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => pending.write().push(path.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_dropped_modpack_driver(pending: State<Vec<PathBuf>>, modpacks: State<Vec<PathBuf>>) {
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
let settled = use_debounced(
|
||||||
|
(pending.read().is_empty(), modpacks.read().len()),
|
||||||
|
DROP_SETTLE,
|
||||||
|
);
|
||||||
|
let settled = *settled.read();
|
||||||
|
|
||||||
|
use_side_effect_with_deps(&settled, move |&(prompt_closed, queued)| {
|
||||||
|
if !prompt_closed || queued == 0 || !pending.peek().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut modpacks = modpacks;
|
||||||
|
let dropped = std::mem::take(&mut *modpacks.write());
|
||||||
|
for path in dropped {
|
||||||
|
dispatch.install_modpack(ModpackSource::File(path));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn route_target(route: &Route) -> Option<(ClusterId, ContentType)> {
|
fn route_target(route: &Route) -> Option<(ClusterId, ContentType)> {
|
||||||
match route {
|
match route {
|
||||||
Route::ClusterMods { cluster_id } => Some((*cluster_id, ContentType::Mod)),
|
Route::ClusterMods { cluster_id } => Some((*cluster_id, ContentType::Mod)),
|
||||||
Route::ClusterShaders { cluster_id } => Some((*cluster_id, ContentType::Shader)),
|
Route::ClusterShaders { cluster_id } => Some((*cluster_id, ContentType::Shader)),
|
||||||
Route::ClusterTextures { cluster_id } => Some((*cluster_id, ContentType::ResourcePack)),
|
Route::ClusterTextures { cluster_id } => Some((*cluster_id, ContentType::ResourcePack)),
|
||||||
|
Route::ClusterDataPacks { cluster_id } => Some((*cluster_id, ContentType::DataPack)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,10 +158,12 @@ fn route_target(route: &Route) -> Option<(ClusterId, ContentType)> {
|
||||||
pub struct FileDropOverlay {
|
pub struct FileDropOverlay {
|
||||||
pub hovering: State<bool>,
|
pub hovering: State<bool>,
|
||||||
pub pending: State<Vec<PathBuf>>,
|
pub pending: State<Vec<PathBuf>>,
|
||||||
|
pub modpacks: State<Vec<PathBuf>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for FileDropOverlay {
|
impl Component for FileDropOverlay {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
|
use_dropped_modpack_driver(self.pending, self.modpacks);
|
||||||
let files = self.pending.read().clone();
|
let files = self.pending.read().clone();
|
||||||
|
|
||||||
if !files.is_empty() {
|
if !files.is_empty() {
|
||||||
|
|
@ -191,23 +271,39 @@ impl Component for DropPrompt {
|
||||||
let target = route_target(&route);
|
let target = route_target(&route);
|
||||||
let initial_cluster = target.map(|(id, _)| id).or(*active_cluster.peek());
|
let initial_cluster = target.map(|(id, _)| id).or(*active_cluster.peek());
|
||||||
let selected_cluster = use_state(move || initial_cluster);
|
let selected_cluster = use_state(move || initial_cluster);
|
||||||
|
let remembered_world = use_datapack_world();
|
||||||
// Absent entries fall back to the inferred type covering drops landing mid-prompt
|
// Absent entries fall back to the inferred type covering drops landing mid-prompt
|
||||||
let overrides = use_state(HashMap::<PathBuf, ContentType>::new);
|
let overrides = use_state(HashMap::<PathBuf, ContentType>::new);
|
||||||
|
|
||||||
let clusters: Vec<Cluster> = settled_or_loading(&clusters_query).unwrap_or_default();
|
let clusters: Vec<Cluster> = settled_or_loading(&clusters_query).unwrap_or_default();
|
||||||
|
|
||||||
let body = if clusters.is_empty() {
|
// The remembered cluster may have been deleted while the prompt was open
|
||||||
no_clusters_body(pending)
|
let cluster_id = selected_cluster
|
||||||
} else {
|
.read()
|
||||||
prompt_body(
|
.filter(|id| clusters.iter().any(|c| c.id == *id))
|
||||||
|
.or_else(|| clusters.first().map(|c| c.id));
|
||||||
|
|
||||||
|
let worlds_query = use_cluster_worlds(cluster_id.unwrap_or_default());
|
||||||
|
let worlds: Vec<String> = try_cluster_worlds(&worlds_query)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|w| w.folder_name)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let body = match cluster_id {
|
||||||
|
None => no_clusters_body(pending),
|
||||||
|
Some(cluster_id) => prompt_body(
|
||||||
&files,
|
&files,
|
||||||
&clusters,
|
&clusters,
|
||||||
|
cluster_id,
|
||||||
target.map(|(_, ct)| ct),
|
target.map(|(_, ct)| ct),
|
||||||
selected_cluster,
|
selected_cluster,
|
||||||
|
&worlds,
|
||||||
|
remembered_world,
|
||||||
overrides,
|
overrides,
|
||||||
dispatch,
|
dispatch,
|
||||||
pending,
|
pending,
|
||||||
)
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
OverlayPopup::new()
|
OverlayPopup::new()
|
||||||
|
|
@ -297,40 +393,92 @@ fn no_clusters_body(mut pending: State<Vec<PathBuf>>) -> Element {
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn prompt_body(
|
fn prompt_body(
|
||||||
files: &[PathBuf],
|
files: &[PathBuf],
|
||||||
clusters: &[Cluster],
|
clusters: &[Cluster],
|
||||||
|
cluster_id: ClusterId,
|
||||||
route_type: Option<ContentType>,
|
route_type: Option<ContentType>,
|
||||||
selected_cluster: State<Option<ClusterId>>,
|
selected_cluster: State<Option<ClusterId>>,
|
||||||
|
worlds: &[String],
|
||||||
|
remembered_world: State<HashMap<ClusterId, String>>,
|
||||||
overrides: State<HashMap<PathBuf, ContentType>>,
|
overrides: State<HashMap<PathBuf, ContentType>>,
|
||||||
dispatch: crate::Actions,
|
dispatch: crate::Actions,
|
||||||
mut pending: State<Vec<PathBuf>>,
|
mut pending: State<Vec<PathBuf>>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
// The remembered cluster may have been deleted while the prompt was open
|
|
||||||
let cluster_id = selected_cluster
|
|
||||||
.read()
|
|
||||||
.filter(|id| clusters.iter().any(|c| c.id == *id))
|
|
||||||
.unwrap_or(clusters[0].id);
|
|
||||||
let cluster_idx = clusters
|
let cluster_idx = clusters
|
||||||
.iter()
|
.iter()
|
||||||
.position(|c| c.id == cluster_id)
|
.position(|c| c.id == cluster_id)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let destination = clusters.get(cluster_idx);
|
||||||
|
let mod_loader = destination.is_none_or(|c| !c.lacks_mod_loader());
|
||||||
|
|
||||||
|
let mut rejected: Vec<PathBuf> = Vec::new();
|
||||||
let resolved: Vec<(PathBuf, ContentType)> = files
|
let resolved: Vec<(PathBuf, ContentType)> = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|path| {
|
.filter_map(|path| {
|
||||||
let content_type = overrides
|
let content_type = overrides
|
||||||
.read()
|
.read()
|
||||||
.get(path)
|
.get(path)
|
||||||
.copied()
|
.copied()
|
||||||
.unwrap_or_else(|| infer_content_type(path, route_type));
|
.filter(|ct| mod_loader || !ct.needs_mod_loader())
|
||||||
(path.clone(), content_type)
|
.or_else(|| infer_content_type(path, route_type, mod_loader));
|
||||||
|
match content_type {
|
||||||
|
Some(content_type) => Some((path.clone(), content_type)),
|
||||||
|
None => {
|
||||||
|
rejected.push(path.clone());
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let has_datapacks = resolved.iter().any(|(_, ct)| *ct == ContentType::DataPack);
|
||||||
|
let world = remembered_world
|
||||||
|
.read()
|
||||||
|
.get(&cluster_id)
|
||||||
|
.filter(|name| worlds.contains(name))
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| worlds.first().cloned());
|
||||||
|
let can_import = !resolved.is_empty() && (!has_datapacks || world.is_some());
|
||||||
|
let skipped = rejected.len();
|
||||||
|
let destination_name = destination.map(|c| c.name.clone()).unwrap_or_default();
|
||||||
|
|
||||||
let import_list = resolved.clone();
|
let import_list = resolved.clone();
|
||||||
|
let import_world = world.clone();
|
||||||
let import = move |_| {
|
let import = move |_| {
|
||||||
dispatch.import_local_files(cluster_id, import_list.clone());
|
if !can_import {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (datapacks, packages): (Vec<_>, Vec<_>) = import_list
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.partition(|(_, ct)| *ct == ContentType::DataPack);
|
||||||
|
dispatch.import_local_files(cluster_id, packages);
|
||||||
|
if let Some(world) = import_world.clone()
|
||||||
|
&& !datapacks.is_empty()
|
||||||
|
{
|
||||||
|
spawn_world_task(
|
||||||
|
dispatch.clone(),
|
||||||
|
"Couldn't add data packs",
|
||||||
|
add_world_datapacks(
|
||||||
|
cluster_id,
|
||||||
|
world,
|
||||||
|
datapacks.into_iter().map(|(path, _)| path).collect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if skipped > 0 {
|
||||||
|
dispatch
|
||||||
|
.notify("Some files weren't added")
|
||||||
|
.body(format!(
|
||||||
|
"{destination_name} has no mod loader, so {skipped} mod or shader file{} {} skipped.",
|
||||||
|
if skipped == 1 { "" } else { "s" },
|
||||||
|
if skipped == 1 { "was" } else { "were" },
|
||||||
|
))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
pending.set(Vec::new());
|
pending.set(Vec::new());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -347,7 +495,11 @@ fn prompt_body(
|
||||||
"Pick a cluster, then check what each one gets installed as.".to_string(),
|
"Pick a cluster, then check what each one gets installed as.".to_string(),
|
||||||
))
|
))
|
||||||
.child(cluster_field(clusters, cluster_idx, selected_cluster))
|
.child(cluster_field(clusters, cluster_idx, selected_cluster))
|
||||||
.child(file_field(&resolved, overrides))
|
.maybe_child(
|
||||||
|
has_datapacks
|
||||||
|
.then(|| world_field(worlds, world.as_deref(), cluster_id, remembered_world)),
|
||||||
|
)
|
||||||
|
.child(file_field(&resolved, &rejected, mod_loader, overrides))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
@ -364,6 +516,7 @@ fn prompt_body(
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.primary()
|
.primary()
|
||||||
|
.enabled(can_import)
|
||||||
.on_press(import)
|
.on_press(import)
|
||||||
.child(Icon::new(IconType::Plus).size(15.))
|
.child(Icon::new(IconType::Plus).size(15.))
|
||||||
.text(match files.len() {
|
.text(match files.len() {
|
||||||
|
|
@ -415,14 +568,55 @@ fn cluster_field(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn world_field(
|
||||||
|
worlds: &[String],
|
||||||
|
current: Option<&str>,
|
||||||
|
cluster_id: ClusterId,
|
||||||
|
mut remembered: State<HashMap<ClusterId, String>>,
|
||||||
|
) -> Element {
|
||||||
|
let Some(current) = current else {
|
||||||
|
return field(
|
||||||
|
"Destination world",
|
||||||
|
label()
|
||||||
|
.text("This cluster has no worlds yet. Create one in game, then drop the data packs again.")
|
||||||
|
.font_size(12.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let names = worlds.to_vec();
|
||||||
|
field(
|
||||||
|
"Destination world",
|
||||||
|
Dropdown::new(current.to_string(), worlds.to_vec())
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(32.))
|
||||||
|
.on_select(move |idx: usize| {
|
||||||
|
if let Some(name) = names.get(idx) {
|
||||||
|
remembered.write().insert(cluster_id, name.clone());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
fn file_field(
|
fn file_field(
|
||||||
resolved: &[(PathBuf, ContentType)],
|
resolved: &[(PathBuf, ContentType)],
|
||||||
|
rejected: &[PathBuf],
|
||||||
|
mod_loader: bool,
|
||||||
overrides: State<HashMap<PathBuf, ContentType>>,
|
overrides: State<HashMap<PathBuf, ContentType>>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let visible = resolved.len().clamp(1, FILE_LIST_MAX_ROWS);
|
let visible = (resolved.len() + rejected.len()).clamp(1, FILE_LIST_MAX_ROWS);
|
||||||
let rows = resolved
|
let rows = resolved
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(path, content_type)| file_row(path, *content_type, overrides).into_element())
|
.map(|(path, content_type)| {
|
||||||
|
file_row(path, Some(*content_type), mod_loader, overrides).into_element()
|
||||||
|
})
|
||||||
|
.chain(
|
||||||
|
rejected
|
||||||
|
.iter()
|
||||||
|
.map(|path| file_row(path, None, mod_loader, overrides).into_element()),
|
||||||
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
field(
|
field(
|
||||||
|
|
@ -444,14 +638,33 @@ fn file_field(
|
||||||
|
|
||||||
fn file_row(
|
fn file_row(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
content_type: ContentType,
|
content_type: Option<ContentType>,
|
||||||
|
mod_loader: bool,
|
||||||
mut overrides: State<HashMap<PathBuf, ContentType>>,
|
mut overrides: State<HashMap<PathBuf, ContentType>>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let key = path.to_path_buf();
|
let key = path.to_path_buf();
|
||||||
let options: Vec<String> = IMPORTABLE
|
let types = importable(mod_loader);
|
||||||
|
let options: Vec<String> = types
|
||||||
.iter()
|
.iter()
|
||||||
.map(|ct| content_label(*ct).to_string())
|
.map(|ct| content_label(*ct).to_string())
|
||||||
.collect();
|
.collect();
|
||||||
|
let control = match content_type {
|
||||||
|
Some(content_type) => Dropdown::new(content_label(content_type), options)
|
||||||
|
.width(Size::px(TYPE_DROPDOWN_W))
|
||||||
|
.height(Size::px(24.))
|
||||||
|
.on_select(move |idx: usize| {
|
||||||
|
if let Some(content_type) = types.get(idx) {
|
||||||
|
overrides.write().insert(key.clone(), *content_type);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_element(),
|
||||||
|
None => label()
|
||||||
|
.text("Needs a mod loader")
|
||||||
|
.font_size(12.)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(colors::fg_secondary())
|
||||||
|
.into_element(),
|
||||||
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
@ -474,14 +687,5 @@ fn file_row(
|
||||||
.width(Size::flex(1.0))
|
.width(Size::flex(1.0))
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(control)
|
||||||
Dropdown::new(content_label(content_type), options)
|
|
||||||
.width(Size::px(TYPE_DROPDOWN_W))
|
|
||||||
.height(Size::px(24.))
|
|
||||||
.on_select(move |idx: usize| {
|
|
||||||
if let Some(content_type) = IMPORTABLE.get(idx) {
|
|
||||||
overrides.write().insert(key.clone(), *content_type);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
245
packages/oneclient_app/src/components/filter_menu.rs
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
use freya::animation::{AnimNum, Ease, OnCreation, use_animation};
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Button, Icon, IconType};
|
||||||
|
use crate::hooks::use_overlay_claim;
|
||||||
|
use crate::theme::colors;
|
||||||
|
|
||||||
|
const PANEL_W: f32 = 172.;
|
||||||
|
const BUTTON_W: f32 = 34.;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub struct FilterOption {
|
||||||
|
text: String,
|
||||||
|
selected: bool,
|
||||||
|
on_press: EventHandler<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterOption {
|
||||||
|
pub fn new(
|
||||||
|
text: impl Into<String>,
|
||||||
|
selected: bool,
|
||||||
|
on_press: impl Into<EventHandler<()>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
selected,
|
||||||
|
on_press: on_press.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct FilterSection {
|
||||||
|
title: &'static str,
|
||||||
|
options: Vec<FilterOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct FilterMenu {
|
||||||
|
active: bool,
|
||||||
|
sections: Vec<FilterSection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterMenu {
|
||||||
|
pub fn new(active: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
active,
|
||||||
|
sections: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn section(
|
||||||
|
mut self,
|
||||||
|
title: &'static str,
|
||||||
|
options: impl IntoIterator<Item = FilterOption>,
|
||||||
|
) -> Self {
|
||||||
|
self.sections.push(FilterSection {
|
||||||
|
title,
|
||||||
|
options: options.into_iter().collect(),
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for FilterMenu {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut open = use_state(|| false);
|
||||||
|
|
||||||
|
let icon_color = if self.active {
|
||||||
|
colors::brand()
|
||||||
|
} else {
|
||||||
|
colors::fg_secondary()
|
||||||
|
};
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.width(Size::px(BUTTON_W))
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.icon()
|
||||||
|
.tooltip("Sort and filter")
|
||||||
|
.width(Size::px(BUTTON_W))
|
||||||
|
.height(Size::px(34.))
|
||||||
|
.on_press(move |e: Event<PressEventData>| {
|
||||||
|
e.stop_propagation();
|
||||||
|
open.toggle();
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconType::Sliders04).size(16.).color(icon_color)),
|
||||||
|
)
|
||||||
|
.maybe_child(open().then(|| {
|
||||||
|
FilterPanel {
|
||||||
|
sections: self.sections.clone(),
|
||||||
|
on_close: (move |()| open.set(false)).into(),
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct FilterPanel {
|
||||||
|
sections: Vec<FilterSection>,
|
||||||
|
on_close: EventHandler<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for FilterPanel {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
use_overlay_claim();
|
||||||
|
|
||||||
|
let backdrop_close = self.on_close.clone();
|
||||||
|
let key_close = self.on_close.clone();
|
||||||
|
|
||||||
|
let a11y_id = use_a11y();
|
||||||
|
|
||||||
|
let fade = use_animation(|conf| {
|
||||||
|
conf.on_creation(OnCreation::Run);
|
||||||
|
AnimNum::new(0., 1.).time(160).ease(Ease::Out)
|
||||||
|
});
|
||||||
|
let progress = fade.read().value();
|
||||||
|
|
||||||
|
let mut panel = rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(4.)
|
||||||
|
.padding(Gaps::new_all(8.))
|
||||||
|
.opacity(progress)
|
||||||
|
.offset_y((progress - 1.0) * 6.)
|
||||||
|
.corner_radius(CornerRadius::new_all(10.))
|
||||||
|
.background(colors::page_elevated().with_a(230))
|
||||||
|
.backdrop_blur(12.)
|
||||||
|
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||||
|
.shadow(Shadow::from((
|
||||||
|
0.,
|
||||||
|
8.,
|
||||||
|
32.,
|
||||||
|
0.,
|
||||||
|
Color::from_argb(120, 0, 0, 0),
|
||||||
|
)))
|
||||||
|
.a11y_id(a11y_id)
|
||||||
|
.a11y_role(AccessibilityRole::Dialog)
|
||||||
|
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
||||||
|
if e.key == Key::Named(NamedKey::Escape) {
|
||||||
|
key_close.call(());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for section in &self.sections {
|
||||||
|
panel = panel.child(section_label(section.title));
|
||||||
|
for option in §ion.options {
|
||||||
|
panel = panel.child(ChoiceRow {
|
||||||
|
option: option.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.height(Size::px(0.))
|
||||||
|
.width(Size::px(PANEL_W))
|
||||||
|
.layer(Layer::Overlay)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.layer(Layer::OverlayLevel(10))
|
||||||
|
.position(Position::new_global().top(0.).left(0.))
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.on_press(move |_| backdrop_close.call(())),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.layer(Layer::OverlayLevel(12))
|
||||||
|
.margin(Gaps::new(6., 0., 0., -(PANEL_W - BUTTON_W)))
|
||||||
|
.child(panel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn section_label(text: &'static str) -> impl IntoElement {
|
||||||
|
label()
|
||||||
|
.text(text)
|
||||||
|
.font_size(10.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_secondary())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct ChoiceRow {
|
||||||
|
option: FilterOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for ChoiceRow {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let selected = self.option.selected;
|
||||||
|
let on_press = self.option.on_press.clone();
|
||||||
|
|
||||||
|
let a11y_id = use_a11y();
|
||||||
|
let focus = use_focus(a11y_id);
|
||||||
|
let focused = focus().is_focused();
|
||||||
|
|
||||||
|
let color = if selected {
|
||||||
|
colors::fg_primary()
|
||||||
|
} else {
|
||||||
|
colors::fg_secondary()
|
||||||
|
};
|
||||||
|
|
||||||
|
let bg = if selected {
|
||||||
|
colors::component_bg()
|
||||||
|
} else if focused {
|
||||||
|
colors::ghost_overlay_hover()
|
||||||
|
} else {
|
||||||
|
Color::TRANSPARENT
|
||||||
|
};
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(8.)
|
||||||
|
.padding(Gaps::new_symmetric(5., 8.))
|
||||||
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
|
.background(bg)
|
||||||
|
.content(Content::Flex)
|
||||||
|
.a11y_id(a11y_id)
|
||||||
|
.a11y_focusable(true)
|
||||||
|
.a11y_role(AccessibilityRole::MenuItemRadio)
|
||||||
|
.maybe(focused, |el| {
|
||||||
|
el.border(crate::ui::border_all_color(1., colors::brand()))
|
||||||
|
})
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_press(move |_| on_press.call(()))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(self.option.text.clone())
|
||||||
|
.font_size(12.)
|
||||||
|
.width(Size::flex(1.0))
|
||||||
|
.color(color),
|
||||||
|
)
|
||||||
|
.maybe_child(selected.then(|| {
|
||||||
|
Icon::new(IconType::Check)
|
||||||
|
.size(14.)
|
||||||
|
.color(colors::brand())
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
282
packages/oneclient_app/src/components/flagged_install_popup.rs
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
use freya::router::RouterContext;
|
||||||
|
use oneclient_content::packages::ResolvedAlternative;
|
||||||
|
|
||||||
|
use crate::components::{
|
||||||
|
Button, Icon, IconType, Markdown, MarkdownStyle, OverlayPopup, remote_icon,
|
||||||
|
};
|
||||||
|
use crate::hooks::{use_cached_image, use_dispatch, use_installs_snapshot};
|
||||||
|
use crate::routes::Route;
|
||||||
|
use crate::state::FlaggedInstallPrompt;
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
use crate::view::app::browser::encode_package_id;
|
||||||
|
|
||||||
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
|
const DIALOG_W: f32 = 460.;
|
||||||
|
const ROW_H: f32 = 48.;
|
||||||
|
const ICON_SIZE: f32 = 32.;
|
||||||
|
const ROW_GAP: f32 = 6.;
|
||||||
|
const SECTION_GAP: f32 = 14.;
|
||||||
|
const CONTENT_MAX_H: f32 = 400.;
|
||||||
|
const DIALOG_PADDING: f32 = 20.;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct FlaggedInstallPopup;
|
||||||
|
|
||||||
|
impl Component for FlaggedInstallPopup {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let installs = use_installs_snapshot();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
|
||||||
|
let Some(prompt) = installs.flagged.clone() else {
|
||||||
|
return rect().into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let close = dispatch.clone();
|
||||||
|
|
||||||
|
OverlayPopup::new()
|
||||||
|
.on_close(move |_| close.dismiss_flagged_install())
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(dialog(prompt, dispatch)),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog(prompt: FlaggedInstallPrompt, dispatch: crate::Actions) -> impl IntoElement {
|
||||||
|
let description = if prompt.alternatives.is_empty() {
|
||||||
|
format!(
|
||||||
|
"{} is flagged as a problematic mod. Installing it is not recommended.",
|
||||||
|
prompt.name
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} is flagged as a problematic mod. Installing it is not recommended, consider one of these alternatives instead.",
|
||||||
|
prompt.name
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body = rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::px(DIALOG_W))
|
||||||
|
.height(Size::Inner)
|
||||||
|
.max_width(Size::window_percent(90.))
|
||||||
|
.max_height(Size::window_percent(85.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.spacing(SECTION_GAP)
|
||||||
|
.padding(Gaps::new_all(DIALOG_PADDING))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.background(CARD_BG)
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(SECTION_GAP)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(10.)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::AlertTriangle)
|
||||||
|
.size(20.)
|
||||||
|
.color(colors::code_warn()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Flagged mod")
|
||||||
|
.font_size(16.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(description)
|
||||||
|
.font_size(12.)
|
||||||
|
.width(Size::fill())
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if prompt.explanation.is_some() || !prompt.alternatives.is_empty() {
|
||||||
|
body = body.child(flagged_content(&prompt));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel = dispatch.clone();
|
||||||
|
let anyway = dispatch;
|
||||||
|
|
||||||
|
body.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::End)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.on_press(move |_| cancel.dismiss_flagged_install())
|
||||||
|
.text("Cancel"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.danger()
|
||||||
|
.on_press(move |_| anyway.install_flagged_anyway(prompt.clone()))
|
||||||
|
.text("Install anyway"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flagged_content(prompt: &FlaggedInstallPrompt) -> impl IntoElement {
|
||||||
|
let mut content = rect().vertical().width(Size::fill()).spacing(SECTION_GAP);
|
||||||
|
|
||||||
|
if let Some(markdown) = &prompt.explanation {
|
||||||
|
content = content.child(explanation_panel(markdown.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !prompt.alternatives.is_empty() {
|
||||||
|
content = content.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(ROW_GAP)
|
||||||
|
.children(prompt.alternatives.iter().map(|alternative| {
|
||||||
|
AlternativeRow {
|
||||||
|
alternative: alternative.clone(),
|
||||||
|
cluster_id: prompt.cluster_id,
|
||||||
|
mc_version: prompt.mc_version.clone(),
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView::new()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::Inner)
|
||||||
|
.max_height(Size::px(CONTENT_MAX_H))
|
||||||
|
.child(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct AlternativeRow {
|
||||||
|
alternative: ResolvedAlternative,
|
||||||
|
cluster_id: i64,
|
||||||
|
mc_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for AlternativeRow {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
let icon_query = use_cached_image(self.alternative.icon_url.clone(), 256);
|
||||||
|
let icon = remote_icon(self.alternative.icon_url.as_deref(), &icon_query, ICON_SIZE);
|
||||||
|
let version = self
|
||||||
|
.alternative
|
||||||
|
.version_number
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| format!("No version for {}", self.mc_version));
|
||||||
|
alternative_row(
|
||||||
|
self.alternative.clone(),
|
||||||
|
version,
|
||||||
|
self.cluster_id,
|
||||||
|
icon,
|
||||||
|
dispatch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn alternative_row(
|
||||||
|
alternative: ResolvedAlternative,
|
||||||
|
version: String,
|
||||||
|
cluster_id: i64,
|
||||||
|
icon: Element,
|
||||||
|
dispatch: crate::Actions,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let ResolvedAlternative {
|
||||||
|
provider,
|
||||||
|
project_id,
|
||||||
|
name,
|
||||||
|
..
|
||||||
|
} = alternative;
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.content(Content::Flex)
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(ROW_H))
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(10.)
|
||||||
|
.padding(Gaps::new_symmetric(0., 8.))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(icon)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::flex(1.))
|
||||||
|
.spacing(2.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(name)
|
||||||
|
.font_size(13.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(version)
|
||||||
|
.font_size(11.)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.small()
|
||||||
|
.on_press(move |_| {
|
||||||
|
dispatch.dismiss_flagged_install();
|
||||||
|
let _ = RouterContext::get().push(Route::BrowserPackage {
|
||||||
|
cluster_id,
|
||||||
|
package_type: "mod".to_string(),
|
||||||
|
package_id: encode_package_id(provider, &project_id),
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconType::ArrowRight).size(14.))
|
||||||
|
.text("Go to page"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn explanation_panel(markdown: String) -> impl IntoElement {
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.padding(Gaps::new_all(12.))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(
|
||||||
|
Markdown::new(markdown)
|
||||||
|
.width(Size::fill())
|
||||||
|
.style(MarkdownStyle {
|
||||||
|
color: colors::fg_primary(),
|
||||||
|
color_link: colors::code_info(),
|
||||||
|
color_code: colors::fg_primary(),
|
||||||
|
background_code: CARD_BG,
|
||||||
|
background_blockquote: CARD_BG,
|
||||||
|
border_blockquote: colors::component_border(),
|
||||||
|
background_divider: colors::component_border(),
|
||||||
|
headings: [16., 14., 13., 12., 12., 12.],
|
||||||
|
paragraph_size: 12.,
|
||||||
|
code_font_size: 11.,
|
||||||
|
..MarkdownStyle::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -79,9 +79,7 @@ impl Component for GenericPromptOverlay {
|
||||||
.pick_folder()
|
.pick_folder()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
dispatch.answer_prompt(
|
dispatch.answer_prompt(Answer::new(id).with_folder(handle.path()));
|
||||||
Answer::new(id).with_folder(handle.path()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,11 @@ impl Icon {
|
||||||
impl Component for Icon {
|
impl Component for Icon {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let path = self.icon.path();
|
let path = self.icon.path();
|
||||||
let bytes = use_memo(move || AppAssets::get_bytes(path).unwrap_or_default());
|
let load = move || (path, AppAssets::get_bytes(path).unwrap_or_default());
|
||||||
|
let mut bytes = use_state(load);
|
||||||
|
if bytes.peek().0 != path {
|
||||||
|
bytes.set(load());
|
||||||
|
}
|
||||||
let tint = use_hook(try_consume_context::<IconTint>);
|
let tint = use_hook(try_consume_context::<IconTint>);
|
||||||
|
|
||||||
let color = self
|
let color = self
|
||||||
|
|
@ -59,7 +63,7 @@ impl Component for Icon {
|
||||||
.or_else(|| tint.map(|tint| *tint.0.read()))
|
.or_else(|| tint.map(|tint| *tint.0.read()))
|
||||||
.unwrap_or_else(colors::fg_primary);
|
.unwrap_or_else(colors::fg_primary);
|
||||||
|
|
||||||
SvgViewer::new((path, bytes.read().cloned()))
|
SvgViewer::new((path, bytes.peek().1.clone()))
|
||||||
.show_loader(false)
|
.show_loader(false)
|
||||||
.width(Size::px(self.size_px))
|
.width(Size::px(self.size_px))
|
||||||
.height(Size::px(self.size_px))
|
.height(Size::px(self.size_px))
|
||||||
|
|
@ -115,6 +119,7 @@ pub enum IconType {
|
||||||
Folder,
|
Folder,
|
||||||
FolderCheck,
|
FolderCheck,
|
||||||
FolderDownload,
|
FolderDownload,
|
||||||
|
Github,
|
||||||
Globe01,
|
Globe01,
|
||||||
HelpCircle,
|
HelpCircle,
|
||||||
IconLogo,
|
IconLogo,
|
||||||
|
|
|
||||||
155
packages/oneclient_app/src/components/instance_row.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
use oneclient_core::clusters::Cluster;
|
||||||
|
|
||||||
|
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::{border_all_color, last_played_label};
|
||||||
|
use crate::utils::{GridSelection, ReleaseLine, line_art_key, line_title};
|
||||||
|
|
||||||
|
const ROW_HEIGHT_PX: f32 = 56.;
|
||||||
|
const THUMB_PX: f32 = 40.;
|
||||||
|
|
||||||
|
pub struct InstanceRow {
|
||||||
|
key: GridSelection,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
played: String,
|
||||||
|
art: DynamicArt,
|
||||||
|
selected: bool,
|
||||||
|
on_press: EventHandler<Event<PressEventData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstanceRow {
|
||||||
|
pub fn new(
|
||||||
|
cluster: &Cluster,
|
||||||
|
selected: bool,
|
||||||
|
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
key: GridSelection::Instance(cluster.id),
|
||||||
|
title: cluster.name.clone(),
|
||||||
|
subtitle: format!("{} \u{b7} {}", cluster.mc_version, cluster.mc_loader),
|
||||||
|
played: last_played_label(cluster.last_played),
|
||||||
|
art: DynamicArt::for_cluster(cluster).max_edge(ART_PREVIEW_EDGE),
|
||||||
|
selected,
|
||||||
|
on_press: on_press.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_line(
|
||||||
|
line: ReleaseLine,
|
||||||
|
clusters: &[Cluster],
|
||||||
|
caption: String,
|
||||||
|
selected: bool,
|
||||||
|
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
key: GridSelection::Line(line),
|
||||||
|
title: line_title(line, clusters),
|
||||||
|
subtitle: caption,
|
||||||
|
played: last_played_label(clusters.iter().filter_map(|c| c.last_played).max()),
|
||||||
|
art: DynamicArt::for_version(line.major, line_art_key(line, clusters), None)
|
||||||
|
.max_edge(ART_PREVIEW_EDGE),
|
||||||
|
selected,
|
||||||
|
on_press: on_press.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for InstanceRow {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.key == other.key
|
||||||
|
&& self.title == other.title
|
||||||
|
&& self.subtitle == other.subtitle
|
||||||
|
&& self.played == other.played
|
||||||
|
&& self.art == other.art
|
||||||
|
&& self.selected == other.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for InstanceRow {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut hovering = use_state(|| false);
|
||||||
|
|
||||||
|
let a11y_id = use_a11y();
|
||||||
|
let focus = use_focus(a11y_id);
|
||||||
|
|
||||||
|
let selected = self.selected;
|
||||||
|
let hovered = *hovering.read();
|
||||||
|
let focused = focus().is_focused();
|
||||||
|
let on_press = self.on_press.clone();
|
||||||
|
|
||||||
|
let background = if selected {
|
||||||
|
colors::brand().with_a(36)
|
||||||
|
} else if hovered {
|
||||||
|
colors::component_bg_hover()
|
||||||
|
} else {
|
||||||
|
colors::component_bg()
|
||||||
|
};
|
||||||
|
|
||||||
|
let border_color = if selected || focused {
|
||||||
|
colors::brand()
|
||||||
|
} else if hovered {
|
||||||
|
colors::component_border_hover()
|
||||||
|
} else {
|
||||||
|
colors::component_border()
|
||||||
|
};
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.key(self.key)
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(ROW_HEIGHT_PX))
|
||||||
|
.padding(Gaps::new(0., 14., 0., 8.))
|
||||||
|
.spacing(14.)
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.content(Content::Flex)
|
||||||
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
|
.background(background)
|
||||||
|
.border(border_all_color(1., border_color).alignment(BorderAlignment::Inner))
|
||||||
|
.a11y_id(a11y_id)
|
||||||
|
.a11y_focusable(true)
|
||||||
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
.on_press(move |e| on_press.call(e))
|
||||||
|
.on_pointer_enter(move |_| hovering.set(true))
|
||||||
|
.on_pointer_leave(move |_| hovering.set(false))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::px(THUMB_PX))
|
||||||
|
.height(Size::px(THUMB_PX))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.child(self.art.clone()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::flex(1.0))
|
||||||
|
.spacing(3.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(self.title.clone())
|
||||||
|
.font_size(14.)
|
||||||
|
.font_weight(FontWeight::MEDIUM)
|
||||||
|
.max_lines(1)
|
||||||
|
.text_overflow(TextOverflow::Ellipsis)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(self.subtitle.clone())
|
||||||
|
.font_size(12.)
|
||||||
|
.max_lines(1)
|
||||||
|
.text_overflow(TextOverflow::Ellipsis)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(self.played.clone())
|
||||||
|
.font_size(12.)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use oneclient_java::{JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JavaVendor};
|
|
||||||
use oneclient_events::Answer;
|
use oneclient_events::Answer;
|
||||||
|
use oneclient_java::{JAVA_CHOICE_DOWNLOAD, JAVA_CHOICE_FOLDER, JavaVendor};
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, JavaInstallManager, OverlayPopup};
|
use crate::components::{Button, Icon, IconType, JavaInstallManager, OverlayPopup};
|
||||||
use crate::hooks::{use_dispatch, use_notifications_snapshot};
|
use crate::hooks::{use_dispatch, use_notifications_snapshot};
|
||||||
|
|
@ -65,9 +65,8 @@ impl Component for JavaPromptOverlay {
|
||||||
.pick_folder()
|
.pick_folder()
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
dispatch.answer_prompt(
|
dispatch
|
||||||
Answer::new(JAVA_CHOICE_FOLDER).with_folder(handle.path()),
|
.answer_prompt(Answer::new(JAVA_CHOICE_FOLDER).with_folder(handle.path()));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -124,9 +123,7 @@ impl Component for JavaPromptOverlay {
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.secondary()
|
.secondary()
|
||||||
.on_press(move |_| {
|
.on_press(move |_| cancel.dismiss_prompt())
|
||||||
cancel.dismiss_prompt()
|
|
||||||
})
|
|
||||||
.text("Cancel"),
|
.text("Cancel"),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
use crate::components::{
|
||||||
use crate::hooks::use_link_confirm;
|
Button, CodeBlock, Icon, IconType, OVERLAY_BASE_LEVEL, OverlayPopup, copy_button,
|
||||||
|
};
|
||||||
|
use crate::hooks::{use_dispatch, use_link_confirm};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
|
@ -13,13 +15,26 @@ pub struct ConfirmLinkOverlay;
|
||||||
impl Component for ConfirmLinkOverlay {
|
impl Component for ConfirmLinkOverlay {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let mut pending = use_link_confirm();
|
let mut pending = use_link_confirm();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
let Some(url) = pending.read().clone() else {
|
let Some(url) = pending.read().clone() else {
|
||||||
return rect().into_element();
|
return rect().into_element();
|
||||||
};
|
};
|
||||||
|
|
||||||
let open_url = url.clone();
|
let open_url = url.clone();
|
||||||
|
let (title, body) = if is_first_party(&url) {
|
||||||
|
(
|
||||||
|
"Leave the launcher?",
|
||||||
|
"This will open the page in your web browser.",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"Open external link?",
|
||||||
|
"This link was provided by a third party. Only open it if you trust the source.",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
OverlayPopup::new()
|
OverlayPopup::new()
|
||||||
|
.overlay_level(OVERLAY_BASE_LEVEL + 3)
|
||||||
.on_close(move |_| pending.set(None))
|
.on_close(move |_| pending.set(None))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -48,7 +63,7 @@ impl Component for ConfirmLinkOverlay {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text("Open external link?")
|
.text(title)
|
||||||
.font_size(16.)
|
.font_size(16.)
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
.color(colors::fg_primary()),
|
.color(colors::fg_primary()),
|
||||||
|
|
@ -56,25 +71,14 @@ impl Component for ConfirmLinkOverlay {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text("This link was provided by a third party. Only open it if you trust the source.")
|
.text(body)
|
||||||
.font_size(12.)
|
.font_size(12.)
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
CodeBlock::new(url.clone())
|
||||||
.width(Size::fill())
|
.color(colors::code_info())
|
||||||
.padding(Gaps::new_all(10.))
|
.max_lines(4),
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
|
||||||
.background(colors::component_bg())
|
|
||||||
.border(border_all_color(1., colors::component_border()))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(url.clone())
|
|
||||||
.font_size(12.)
|
|
||||||
.max_lines(4)
|
|
||||||
.width(Size::fill())
|
|
||||||
.color(colors::code_info()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -82,6 +86,11 @@ impl Component for ConfirmLinkOverlay {
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.spacing(8.)
|
.spacing(8.)
|
||||||
|
.child(copy_button(
|
||||||
|
url.clone(),
|
||||||
|
"Link copied to your clipboard.",
|
||||||
|
dispatch,
|
||||||
|
))
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.secondary()
|
.secondary()
|
||||||
|
|
@ -104,3 +113,13 @@ impl Component for ConfirmLinkOverlay {
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_first_party(url: &str) -> bool {
|
||||||
|
let host = url
|
||||||
|
.split_once("://")
|
||||||
|
.map_or(url, |(_, rest)| rest)
|
||||||
|
.split(['/', '?', '#', ':'])
|
||||||
|
.next()
|
||||||
|
.unwrap_or_default();
|
||||||
|
host == "polyfrost.org" || host.ends_with(".polyfrost.org")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct LocalImage {
|
||||||
max_edge: u32,
|
max_edge: u32,
|
||||||
cover: bool,
|
cover: bool,
|
||||||
skeleton: bool,
|
skeleton: bool,
|
||||||
|
picked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalImage {
|
impl LocalImage {
|
||||||
|
|
@ -26,9 +27,15 @@ impl LocalImage {
|
||||||
max_edge,
|
max_edge,
|
||||||
cover,
|
cover,
|
||||||
skeleton: false,
|
skeleton: false,
|
||||||
|
picked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn picked(mut self, picked: bool) -> Self {
|
||||||
|
self.picked = picked;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn skeleton(mut self, skeleton: bool) -> Self {
|
pub fn skeleton(mut self, skeleton: bool) -> Self {
|
||||||
self.skeleton = skeleton;
|
self.skeleton = skeleton;
|
||||||
self
|
self
|
||||||
|
|
@ -37,9 +44,9 @@ impl LocalImage {
|
||||||
|
|
||||||
impl Component for LocalImage {
|
impl Component for LocalImage {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let query = use_local_image(self.path.clone(), self.max_edge);
|
let query = use_local_image(self.path.clone(), self.max_edge, self.picked);
|
||||||
|
let bytes: Option<Bytes> =
|
||||||
let bytes: Option<Bytes> = settled_or_loading(&query);
|
settled_or_loading(&query).filter(|bytes: &Bytes| !bytes.is_empty());
|
||||||
|
|
||||||
let mut cache = use_state(|| None::<(usize, ImageHandle)>);
|
let mut cache = use_state(|| None::<(usize, ImageHandle)>);
|
||||||
let holder = bytes.and_then(|bytes| {
|
let holder = bytes.and_then(|bytes| {
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,7 @@ fn scroll_body(args: BodyArgs) -> Element {
|
||||||
.height(Size::px(content_h))
|
.height(Size::px(content_h))
|
||||||
.on_pointer_down(on_body_down)
|
.on_pointer_down(on_body_down)
|
||||||
.on_capture_global_pointer_move(on_global_move)
|
.on_capture_global_pointer_move(on_global_move)
|
||||||
.on_capture_global_pointer_press(on_global_release)
|
.on_capture_global_pointer_up(on_global_release)
|
||||||
.on_global_key_down(on_key_down)
|
.on_global_key_down(on_key_down)
|
||||||
.child(log_body(
|
.child(log_body(
|
||||||
&lines[..],
|
&lines[..],
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ use std::borrow::Cow;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use freya::elements::image::ImageHandle;
|
use freya::elements::image::ImageHandle;
|
||||||
use freya::engine::prelude::{ClipOp, Paint, SkRect};
|
use freya::engine::prelude::{ClipOp, FontMgr, Paint, Picture, PictureRecorder, SkRect, svg};
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya_core::element::{ClipContext, ElementExt, LayoutContext};
|
use freya_core::element::{ClipContext, ElementExt, LayoutContext};
|
||||||
use freya_core::tree::DiffModifies;
|
use freya_core::tree::DiffModifies;
|
||||||
|
|
||||||
use super::super::local_image::decode;
|
use super::super::local_image::decode;
|
||||||
|
use super::svg_compat::prepare_svg;
|
||||||
use crate::hooks::{loaded_image, use_cached_image};
|
use crate::hooks::{loaded_image, use_cached_image};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
||||||
|
|
@ -34,24 +35,24 @@ impl Component for MarkdownImage {
|
||||||
let query = use_cached_image(Some(self.url.clone()), MAX_EDGE);
|
let query = use_cached_image(Some(self.url.clone()), MAX_EDGE);
|
||||||
let loaded = loaded_image(Some(&self.url), &query);
|
let loaded = loaded_image(Some(&self.url), &query);
|
||||||
|
|
||||||
let mut cache = use_state(|| None::<(usize, ImageHandle)>);
|
let mut cache = use_state(|| None::<(usize, ImageContent)>);
|
||||||
let holder = loaded.and_then(|(_, bytes)| {
|
let content = loaded.and_then(|(_, bytes)| {
|
||||||
let ptr = bytes.as_ptr() as usize;
|
let ptr = bytes.as_ptr() as usize;
|
||||||
|
|
||||||
if let Some((cached_ptr, holder)) = cache.read().clone()
|
if let Some((cached_ptr, content)) = cache.read().clone()
|
||||||
&& cached_ptr == ptr
|
&& cached_ptr == ptr
|
||||||
{
|
{
|
||||||
return Some(holder);
|
return Some(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
let holder = decode(&bytes)?;
|
let content = decode_any(&bytes)?;
|
||||||
cache.set(Some((ptr, holder.clone())));
|
cache.set(Some((ptr, content.clone())));
|
||||||
|
|
||||||
Some(holder)
|
Some(content)
|
||||||
});
|
});
|
||||||
|
|
||||||
match holder {
|
match content {
|
||||||
Some(holder) => scaled_image(holder)
|
Some(content) => scaled_content(content)
|
||||||
.a11y_role(AccessibilityRole::Image)
|
.a11y_role(AccessibilityRole::Image)
|
||||||
.a11y_alt(self.alt.clone())
|
.a11y_alt(self.alt.clone())
|
||||||
.into_element(),
|
.into_element(),
|
||||||
|
|
@ -64,6 +65,90 @@ impl Component for MarkdownImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
enum ImageContent {
|
||||||
|
Bitmap(ImageHandle),
|
||||||
|
Vector(SvgPicture),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An svg recorded as drawing commands, so it is drawn straight onto the
|
||||||
|
/// window's pixel grid instead of being resampled from a bitmap
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SvgPicture {
|
||||||
|
picture: Picture,
|
||||||
|
/// Logical size, scaled to physical pixels at layout like any `Size::px`
|
||||||
|
size: Size2D,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for SvgPicture {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.picture.unique_id() == other.picture.unique_id() && self.size == other.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_any(bytes: &Bytes) -> Option<ImageContent> {
|
||||||
|
if let Some(handle) = decode(bytes) {
|
||||||
|
return Some(ImageContent::Bitmap(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skia's codecs can't read svg, which is what shields.io badges are
|
||||||
|
if !looks_like_svg(bytes) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
record_svg(bytes).map(ImageContent::Vector)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn looks_like_svg(bytes: &[u8]) -> bool {
|
||||||
|
let head = &bytes[..bytes.len().min(1024)];
|
||||||
|
let head = head.strip_prefix(&[0xEF, 0xBB, 0xBF]).unwrap_or(head);
|
||||||
|
let start = head
|
||||||
|
.iter()
|
||||||
|
.position(|b| !b.is_ascii_whitespace())
|
||||||
|
.unwrap_or(head.len());
|
||||||
|
let head = &head[start..];
|
||||||
|
|
||||||
|
head.starts_with(b"<svg")
|
||||||
|
|| (head.starts_with(b"<?xml")
|
||||||
|
|| head.starts_with(b"<!--")
|
||||||
|
|| head.starts_with(b"<!DOCTYPE"))
|
||||||
|
&& head.windows(4).any(|w| w == b"<svg")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn svg_size(root: &svg::Svg) -> Option<Size2D> {
|
||||||
|
let intrinsic = root.intrinsic_size();
|
||||||
|
let size = if intrinsic.width > 0. && intrinsic.height > 0. {
|
||||||
|
Size2D::new(intrinsic.width, intrinsic.height)
|
||||||
|
} else {
|
||||||
|
let view_box = root.view_box()?;
|
||||||
|
Size2D::new(view_box.width(), view_box.height())
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid = size.width.is_finite() && size.height.is_finite();
|
||||||
|
(valid && size.width > 0. && size.height > 0.).then_some(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Badges are mostly text, so the dom gets the system fonts rather than an empty manager
|
||||||
|
fn record_svg(bytes: &[u8]) -> Option<SvgPicture> {
|
||||||
|
let mut dom = svg::Dom::from_bytes(&prepare_svg(bytes), FontMgr::default()).ok()?;
|
||||||
|
let declared = svg_size(&dom.root())?;
|
||||||
|
dom.set_container_size((declared.width, declared.height));
|
||||||
|
|
||||||
|
// the dom always draws at its declared size, so an oversized svg is scaled down while recording
|
||||||
|
let shrink = (MAX_EDGE as f32 / declared.width.max(declared.height)).min(1.);
|
||||||
|
let size = declared * shrink;
|
||||||
|
|
||||||
|
let bounds = SkRect::from_wh(size.width, size.height);
|
||||||
|
let mut recorder = PictureRecorder::new();
|
||||||
|
let canvas = recorder.begin_recording(bounds, false);
|
||||||
|
canvas.clip_rect(bounds, None, None);
|
||||||
|
canvas.scale((shrink, shrink));
|
||||||
|
dom.render(canvas);
|
||||||
|
let picture = recorder.finish_recording_as_picture(Some(&bounds))?;
|
||||||
|
|
||||||
|
Some(SvgPicture { picture, size })
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ScaledImage {
|
pub struct ScaledImage {
|
||||||
key: DiffKey,
|
key: DiffKey,
|
||||||
|
|
@ -74,18 +159,18 @@ pub struct ScaledImage {
|
||||||
pub struct ScaledImageElement {
|
pub struct ScaledImageElement {
|
||||||
accessibility: AccessibilityData,
|
accessibility: AccessibilityData,
|
||||||
layout: LayoutData,
|
layout: LayoutData,
|
||||||
image_handle: ImageHandle,
|
content: ImageContent,
|
||||||
sampling_mode: SamplingMode,
|
sampling_mode: SamplingMode,
|
||||||
corner_radius: CornerRadius,
|
corner_radius: CornerRadius,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn scaled_image(image_handle: ImageHandle) -> ScaledImage {
|
fn scaled_content(content: ImageContent) -> ScaledImage {
|
||||||
ScaledImage {
|
ScaledImage {
|
||||||
key: DiffKey::None,
|
key: DiffKey::None,
|
||||||
element: ScaledImageElement {
|
element: ScaledImageElement {
|
||||||
accessibility: AccessibilityData::default(),
|
accessibility: AccessibilityData::default(),
|
||||||
layout: LayoutData::default(),
|
layout: LayoutData::default(),
|
||||||
image_handle,
|
content,
|
||||||
sampling_mode: SamplingMode::default(),
|
sampling_mode: SamplingMode::default(),
|
||||||
corner_radius: CornerRadius::default(),
|
corner_radius: CornerRadius::default(),
|
||||||
},
|
},
|
||||||
|
|
@ -134,6 +219,18 @@ impl From<ScaledImage> for Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ImageContent {
|
||||||
|
/// Size in layout (physical) pixels; bitmaps keep their pixel dimensions
|
||||||
|
fn size(&self, scale_factor: f32) -> Size2D {
|
||||||
|
match self {
|
||||||
|
Self::Bitmap(handle) => {
|
||||||
|
Size2D::new(handle.image.width() as f32, handle.image.height() as f32)
|
||||||
|
}
|
||||||
|
Self::Vector(svg) => svg.size * scale_factor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl ElementExt for ScaledImageElement {
|
impl ElementExt for ScaledImageElement {
|
||||||
fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
|
fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
|
||||||
let Some(other) = (other.as_ref() as &dyn Any).downcast_ref::<ScaledImageElement>() else {
|
let Some(other) = (other.as_ref() as &dyn Any).downcast_ref::<ScaledImageElement>() else {
|
||||||
|
|
@ -157,10 +254,10 @@ impl ElementExt for ScaledImageElement {
|
||||||
diff.insert(DiffModifies::LAYOUT);
|
diff.insert(DiffModifies::LAYOUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.image_handle != other.image_handle {
|
if self.content != other.content {
|
||||||
diff.insert(DiffModifies::STYLE);
|
diff.insert(DiffModifies::STYLE);
|
||||||
|
|
||||||
if self.image_handle.image.dimensions() != other.image_handle.image.dimensions() {
|
if self.content.size(1.) != other.content.size(1.) {
|
||||||
diff.insert(DiffModifies::LAYOUT);
|
diff.insert(DiffModifies::LAYOUT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -192,11 +289,9 @@ impl ElementExt for ScaledImageElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
|
fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
|
||||||
let image = &self.image_handle.image;
|
let natural = self.content.size(context.scale_factor as f32);
|
||||||
let natural = Size2D::new(image.width() as f32, image.height() as f32);
|
|
||||||
|
|
||||||
let available =
|
let available = (*context.area_size - context.torin_node.margin.into()).max(Size2D::zero());
|
||||||
(*context.area_size - context.torin_node.margin.into()).max(Size2D::zero());
|
|
||||||
|
|
||||||
let ratio = (available.width / natural.width).min(1.);
|
let ratio = (available.width / natural.width).min(1.);
|
||||||
let ratio = if ratio.is_finite() && ratio > 0. {
|
let ratio = if ratio.is_finite() && ratio > 0. {
|
||||||
|
|
@ -239,16 +334,28 @@ impl ElementExt for ScaledImageElement {
|
||||||
.canvas
|
.canvas
|
||||||
.clip_rrect(clip_rrect, ClipOp::Intersect, true);
|
.clip_rrect(clip_rrect, ClipOp::Intersect, true);
|
||||||
|
|
||||||
|
match &self.content {
|
||||||
|
ImageContent::Bitmap(handle) => {
|
||||||
let mut paint = Paint::default();
|
let mut paint = Paint::default();
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
|
|
||||||
context.canvas.draw_image_rect_with_sampling_options(
|
context.canvas.draw_image_rect_with_sampling_options(
|
||||||
&self.image_handle.image,
|
&handle.image,
|
||||||
None,
|
None,
|
||||||
rect,
|
rect,
|
||||||
self.sampling_mode.sampling_options(),
|
self.sampling_mode.sampling_options(),
|
||||||
&paint,
|
&paint,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
ImageContent::Vector(svg) => {
|
||||||
|
context.canvas.translate((rect.left, rect.top));
|
||||||
|
context.canvas.scale((
|
||||||
|
rect.width() / svg.size.width,
|
||||||
|
rect.height() / svg.size.height,
|
||||||
|
));
|
||||||
|
context.canvas.draw_picture(&svg.picture, None, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
context.canvas.restore();
|
context.canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
@ -256,8 +363,8 @@ impl ElementExt for ScaledImageElement {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use freya::prelude::Bytes;
|
|
||||||
use freya::engine::prelude::AlphaType;
|
use freya::engine::prelude::AlphaType;
|
||||||
|
use freya::prelude::Bytes;
|
||||||
use freya_testing::prelude::*;
|
use freya_testing::prelude::*;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -272,7 +379,9 @@ mod tests {
|
||||||
rect()
|
rect()
|
||||||
.width(Size::px(panel.0))
|
.width(Size::px(panel.0))
|
||||||
.height(Size::px(panel.1))
|
.height(Size::px(panel.1))
|
||||||
.child(scaled_image(handle(image.0, image.1)))
|
.child(scaled_content(ImageContent::Bitmap(handle(
|
||||||
|
image.0, image.1,
|
||||||
|
))))
|
||||||
};
|
};
|
||||||
|
|
||||||
let test = launch_test(app);
|
let test = launch_test(app);
|
||||||
|
|
@ -292,7 +401,7 @@ mod tests {
|
||||||
rect().width(Size::px(300.)).height(Size::px(400.)).child(
|
rect().width(Size::px(300.)).height(Size::px(400.)).child(
|
||||||
paragraph()
|
paragraph()
|
||||||
.span(Span::new("before "))
|
.span(Span::new("before "))
|
||||||
.child(scaled_image(handle(600, 200)))
|
.child(scaled_content(ImageContent::Bitmap(handle(600, 200))))
|
||||||
.span(Span::new(" after")),
|
.span(Span::new(" after")),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
@ -325,4 +434,175 @@ mod tests {
|
||||||
fn short_panel_does_not_shrink_the_width() {
|
fn short_panel_does_not_shrink_the_width() {
|
||||||
assert_eq!(measured((300., 60.), (600, 200)), Size2D::new(300., 100.));
|
assert_eq!(measured((300., 60.), (600, 200)), Size2D::new(300., 100.));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BADGE: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="88" height="20"><rect width="88" height="20" fill="#007ec6"/><text x="10" y="14" font-family="Verdana" font-size="11">v3.0.1</text></svg>"##;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sniffs_svg_documents() {
|
||||||
|
assert!(looks_like_svg(BADGE.as_bytes()));
|
||||||
|
assert!(looks_like_svg(
|
||||||
|
b"\xEF\xBB\xBF <?xml version=\"1.0\"?>\n<svg width=\"1\" height=\"1\"/>"
|
||||||
|
));
|
||||||
|
assert!(!looks_like_svg(b"\x89PNG\r\n\x1a\n"));
|
||||||
|
assert!(!looks_like_svg(b"<html><body>not found</body></html>"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn records_a_badge_at_its_declared_size() {
|
||||||
|
let svg = record_svg(BADGE.as_bytes()).expect("badge records");
|
||||||
|
assert_eq!(svg.size, Size2D::new(88., 20.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_the_view_box() {
|
||||||
|
let svg = record_svg(
|
||||||
|
br#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 10" width="100%"/>"#,
|
||||||
|
)
|
||||||
|
.expect("view box svg records");
|
||||||
|
|
||||||
|
assert_eq!(svg.size, Size2D::new(40., 10.));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vector_size(scale_factor: f64) -> Size2D {
|
||||||
|
let svg = record_svg(BADGE.as_bytes()).expect("badge records");
|
||||||
|
let app = move || {
|
||||||
|
rect()
|
||||||
|
.width(Size::px(300.))
|
||||||
|
.height(Size::px(400.))
|
||||||
|
.child(scaled_content(ImageContent::Vector(svg.clone())))
|
||||||
|
};
|
||||||
|
|
||||||
|
let (test, _) = TestingRunner::new(app, Size2D::new(500., 500.), |_| {}, scale_factor);
|
||||||
|
test.find(|node, element| {
|
||||||
|
(element as &dyn Any)
|
||||||
|
.downcast_ref::<ScaledImageElement>()
|
||||||
|
.map(|_| node.layout())
|
||||||
|
})
|
||||||
|
.expect("image node")
|
||||||
|
.area
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_vector_follows_the_scale_factor_like_text() {
|
||||||
|
assert_eq!(vector_size(1.), Size2D::new(88., 20.));
|
||||||
|
assert_eq!(vector_size(1.5), Size2D::new(132., 30.));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_vector_at_a_fractional_offset_is_not_resampled() {
|
||||||
|
// grey ends at 39.5, so after the 10.5 offset the colour edge lands on pixel 50
|
||||||
|
let svg = record_svg(
|
||||||
|
br##"<svg xmlns="http://www.w3.org/2000/svg" width="88" height="20"><rect width="39.5" height="20" fill="#555555"/><rect x="39.5" width="48.5" height="20" fill="#007ec6"/></svg>"##,
|
||||||
|
)
|
||||||
|
.expect("svg records");
|
||||||
|
let app = move || {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.child(rect().width(Size::px(10.5)).height(Size::px(1.)))
|
||||||
|
.child(scaled_content(ImageContent::Vector(svg.clone())))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut test = launch_test(app);
|
||||||
|
let png = test.render();
|
||||||
|
let snapshot = image::load_from_memory(png.as_bytes())
|
||||||
|
.expect("snapshot decodes")
|
||||||
|
.to_rgb8();
|
||||||
|
let rgb = |x: u32| {
|
||||||
|
let [r, g, b] = snapshot.get_pixel(x, 10).0;
|
||||||
|
(r, g, b)
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(rgb(49), (0x55, 0x55, 0x55));
|
||||||
|
assert_eq!(rgb(50), (0x00, 0x7e, 0xc6));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A grey shields-style badge with a 14px logo slot, rendered at the origin
|
||||||
|
fn logo_pixel(href: &str) -> (u8, u8, u8) {
|
||||||
|
let badge = format!(
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="20"><rect width="40" height="20" fill="#555555"/><image x="5" y="3" width="14" height="14" href="{href}"/></svg>"##
|
||||||
|
);
|
||||||
|
let svg = record_svg(badge.as_bytes()).expect("badge records");
|
||||||
|
let app = move || rect().child(scaled_content(ImageContent::Vector(svg.clone())));
|
||||||
|
|
||||||
|
let mut test = launch_test(app);
|
||||||
|
let png = test.render();
|
||||||
|
let snapshot = image::load_from_memory(png.as_bytes())
|
||||||
|
.expect("snapshot decodes")
|
||||||
|
.to_rgb8();
|
||||||
|
let [r, g, b] = snapshot.get_pixel(12, 10).0;
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn data_url(mime: &str, bytes: &[u8]) -> String {
|
||||||
|
use base64::Engine;
|
||||||
|
format!(
|
||||||
|
"data:{mime};base64,{}",
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(bytes)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draws_a_png_logo_given_by_href() {
|
||||||
|
let mut png = Vec::new();
|
||||||
|
image::RgbImage::from_pixel(4, 4, image::Rgb([255, 0, 0]))
|
||||||
|
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||||
|
.expect("png encodes");
|
||||||
|
|
||||||
|
assert_eq!(logo_pixel(&data_url("image/png", &png)), (255, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draws_an_svg_logo_given_as_a_data_url() {
|
||||||
|
let logo = br##"<svg fill="#5865F2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"/></svg>"##;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
logo_pixel(&data_url("image/svg+xml", logo)),
|
||||||
|
(0x58, 0x65, 0xf2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replays a recorded svg at its own size on white and reads one pixel
|
||||||
|
fn picture_pixel(svg: &SvgPicture, x: u32, y: u32) -> (u8, u8, u8) {
|
||||||
|
use freya::engine::prelude::{EncodedImageFormat, raster_n32_premul};
|
||||||
|
|
||||||
|
let size = (svg.size.width.ceil() as i32, svg.size.height.ceil() as i32);
|
||||||
|
let mut surface = raster_n32_premul(size).expect("surface");
|
||||||
|
surface.canvas().clear(Color::WHITE);
|
||||||
|
surface.canvas().draw_picture(&svg.picture, None, None);
|
||||||
|
let png = surface
|
||||||
|
.image_snapshot()
|
||||||
|
.encode(None, EncodedImageFormat::PNG, None)
|
||||||
|
.expect("png encodes");
|
||||||
|
let [r, g, b] = image::load_from_memory(png.as_bytes())
|
||||||
|
.expect("png decodes")
|
||||||
|
.to_rgb8()
|
||||||
|
.get_pixel(x, y)
|
||||||
|
.0;
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_oversized_svg_is_scaled_down_rather_than_cropped() {
|
||||||
|
let svg = record_svg(
|
||||||
|
br##"<svg xmlns="http://www.w3.org/2000/svg" width="3200" height="100"><rect width="1600" height="100" fill="#ff0000"/><rect x="1600" width="1600" height="100" fill="#0000ff"/></svg>"##,
|
||||||
|
)
|
||||||
|
.expect("svg records");
|
||||||
|
|
||||||
|
assert_eq!(svg.size, Size2D::new(1600., 50.));
|
||||||
|
assert_eq!(picture_pixel(&svg, 10, 40), (255, 0, 0));
|
||||||
|
assert_eq!(picture_pixel(&svg, 1590, 40), (0, 0, 255));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_logo_reusing_the_badges_ids_leaves_the_badge_intact() {
|
||||||
|
let logo = br##"<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="r"><rect width="1" height="1"/></clipPath></defs><rect width="24" height="24" fill="#00ff00" clip-path="url(#r)"/></svg>"##;
|
||||||
|
let badge = format!(
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg" width="80" height="20"><clipPath id="r"><rect width="80" height="20" rx="3"/></clipPath><g clip-path="url(#r)"><rect width="80" height="20" fill="#555555"/></g><image x="5" y="3" width="14" height="14" href="{}"/></svg>"##,
|
||||||
|
data_url("image/svg+xml", logo)
|
||||||
|
);
|
||||||
|
let svg = record_svg(badge.as_bytes()).expect("badge records");
|
||||||
|
|
||||||
|
assert_eq!(picture_pixel(&svg, 60, 10), (0x55, 0x55, 0x55));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use pulldown_cmark::HeadingLevel;
|
||||||
mod image;
|
mod image;
|
||||||
mod parse;
|
mod parse;
|
||||||
mod render;
|
mod render;
|
||||||
|
mod svg_compat;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct MarkdownStyle {
|
pub struct MarkdownStyle {
|
||||||
|
|
@ -37,7 +38,7 @@ impl Default for MarkdownStyle {
|
||||||
paragraph_size: 16.,
|
paragraph_size: 16.,
|
||||||
code_font_size: 14.,
|
code_font_size: 14.,
|
||||||
table_font_size: 14.,
|
table_font_size: 14.,
|
||||||
code_font_family: Cow::Borrowed("Jetbrains Mono"),
|
code_font_family: Cow::Borrowed(crate::theme::MONO_FONT),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -115,4 +116,3 @@ impl Component for Markdown {
|
||||||
self.key.clone().or(self.default_key())
|
self.key.clone().or(self.default_key())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,10 @@ impl Walker {
|
||||||
} else {
|
} else {
|
||||||
std::mem::take(&mut self.image_alt)
|
std::mem::take(&mut self.image_alt)
|
||||||
};
|
};
|
||||||
self.place(Inline::Image { url, alt }, |url, alt| Block::Image { url, alt });
|
self.place(Inline::Image { url, alt }, |url, alt| Block::Image {
|
||||||
|
url,
|
||||||
|
alt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
TagEnd::Link => {
|
TagEnd::Link => {
|
||||||
self.in_link = false;
|
self.in_link = false;
|
||||||
|
|
@ -488,18 +491,27 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn inline_html_keeps_the_text_it_wrapped() {
|
fn inline_html_keeps_the_text_it_wrapped() {
|
||||||
assert_eq!(paragraph_text(&blocks("a <b>bold</b> word")[0]), "a bold word");
|
assert_eq!(
|
||||||
|
paragraph_text(&blocks("a <b>bold</b> word")[0]),
|
||||||
|
"a bold word"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn block_html_keeps_its_text() {
|
fn block_html_keeps_its_text() {
|
||||||
assert_eq!(paragraph_text(&blocks("<div>\nhello\n</div>")[0]).trim(), "hello");
|
assert_eq!(
|
||||||
|
paragraph_text(&blocks("<div>\nhello\n</div>")[0]).trim(),
|
||||||
|
"hello"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn images_arrive_as_paragraph_content() {
|
fn images_arrive_as_paragraph_content() {
|
||||||
assert_eq!(paragraph_text(&blocks("")[0]), "[img:alt]");
|
assert_eq!(paragraph_text(&blocks("")[0]), "[img:alt]");
|
||||||
assert_eq!(paragraph_text(&blocks("see  here")[0]), "see [img:alt] here");
|
assert_eq!(
|
||||||
|
paragraph_text(&blocks("see  here")[0]),
|
||||||
|
"see [img:alt] here"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -530,7 +542,8 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tables_split_head_from_body() {
|
fn tables_split_head_from_body() {
|
||||||
let Block::Table { headers, rows } = &blocks("| a | b |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |")[0]
|
let Block::Table { headers, rows } =
|
||||||
|
&blocks("| a | b |\n| - | - |\n| 1 | 2 |\n| 3 | 4 |")[0]
|
||||||
else {
|
else {
|
||||||
panic!("expected a table");
|
panic!("expected a table");
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,32 +3,29 @@ use freya::prelude::*;
|
||||||
use super::MarkdownStyle;
|
use super::MarkdownStyle;
|
||||||
use super::image::MarkdownImage;
|
use super::image::MarkdownImage;
|
||||||
use super::parse::{Block, Inline, List, TextSpan};
|
use super::parse::{Block, Inline, List, TextSpan};
|
||||||
|
use crate::components::CodeBlock;
|
||||||
use crate::hooks::LinkConfirmState;
|
use crate::hooks::LinkConfirmState;
|
||||||
|
|
||||||
pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element {
|
pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element {
|
||||||
match block {
|
match block {
|
||||||
Block::Heading { level, spans } => {
|
Block::Heading { level, spans } => render_spans(
|
||||||
render_spans(spans, style.heading_size(*level), style.color, style.color_code)
|
spans,
|
||||||
|
style.heading_size(*level),
|
||||||
|
style.color,
|
||||||
|
style.color_code,
|
||||||
|
)
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(FontWeight::BOLD)
|
||||||
.key(key)
|
.key(key)
|
||||||
.into()
|
.into(),
|
||||||
}
|
|
||||||
Block::Paragraph { content } => render_content(content, style.paragraph_size, style)
|
Block::Paragraph { content } => render_content(content, style.paragraph_size, style)
|
||||||
.key(key)
|
.key(key)
|
||||||
.into(),
|
.into(),
|
||||||
Block::Code { code } => rect()
|
Block::Code { code } => CodeBlock::new(code.clone())
|
||||||
.key(key)
|
.color(style.color_code)
|
||||||
.width(Size::fill())
|
|
||||||
.background(style.background_code)
|
.background(style.background_code)
|
||||||
.corner_radius(CornerRadius::new_all(6.))
|
|
||||||
.padding(Gaps::new_all(12.))
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(code.clone())
|
|
||||||
.font_family(style.code_font_family.clone())
|
.font_family(style.code_font_family.clone())
|
||||||
.font_size(style.code_font_size)
|
.font_size(style.code_font_size)
|
||||||
.color(style.color_code),
|
.key(key)
|
||||||
)
|
|
||||||
.into(),
|
.into(),
|
||||||
Block::List(list) => render_list(list, style).key(key).into(),
|
Block::List(list) => render_list(list, style).key(key).into(),
|
||||||
Block::Image { url, alt } => rect()
|
Block::Image { url, alt } => rect()
|
||||||
|
|
@ -53,7 +50,9 @@ pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element
|
||||||
.alignment(BorderAlignment::Inner),
|
.alignment(BorderAlignment::Inner),
|
||||||
)
|
)
|
||||||
.background(style.background_blockquote)
|
.background(style.background_blockquote)
|
||||||
.child(render_content(content, style.paragraph_size, style).font_slant(FontSlant::Italic))
|
.child(
|
||||||
|
render_content(content, style.paragraph_size, style).font_slant(FontSlant::Italic),
|
||||||
|
)
|
||||||
.into(),
|
.into(),
|
||||||
Block::Rule => rect()
|
Block::Rule => rect()
|
||||||
.key(key)
|
.key(key)
|
||||||
|
|
@ -74,7 +73,9 @@ fn styled_span(span: &TextSpan, text_color: Color, code_color: Color) -> Span<'s
|
||||||
styled = styled.font_slant(FontSlant::Italic);
|
styled = styled.font_slant(FontSlant::Italic);
|
||||||
}
|
}
|
||||||
if span.code {
|
if span.code {
|
||||||
styled.font_family("monospace").color(code_color)
|
styled
|
||||||
|
.font_family(crate::theme::MONO_FONT)
|
||||||
|
.color(code_color)
|
||||||
} else {
|
} else {
|
||||||
styled.color(text_color)
|
styled.color(text_color)
|
||||||
}
|
}
|
||||||
|
|
@ -215,12 +216,7 @@ impl KeyExt for MarkdownLink {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MarkdownLink {
|
impl MarkdownLink {
|
||||||
fn new(
|
fn new(url: String, title: Option<String>, content: Vec<Inline>, style: MarkdownStyle) -> Self {
|
||||||
url: String,
|
|
||||||
title: Option<String>,
|
|
||||||
content: Vec<Inline>,
|
|
||||||
style: MarkdownStyle,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
|
|
@ -242,18 +238,24 @@ impl Component for MarkdownLink {
|
||||||
let mut text = paragraph().font_size(self.style.paragraph_size);
|
let mut text = paragraph().font_size(self.style.paragraph_size);
|
||||||
for item in &self.content {
|
for item in &self.content {
|
||||||
text = match item {
|
text = match item {
|
||||||
Inline::Span(span) => {
|
Inline::Span(span) => text.span(styled_span(
|
||||||
text.span(styled_span(span, self.style.color_link, self.style.color_code))
|
span,
|
||||||
}
|
self.style.color_link,
|
||||||
|
self.style.color_code,
|
||||||
|
)),
|
||||||
Inline::Image { url, alt } => {
|
Inline::Image { url, alt } => {
|
||||||
text.child(MarkdownImage::new(url.clone(), alt.clone()))
|
text.child(MarkdownImage::new(url.clone(), alt.clone()))
|
||||||
}
|
}
|
||||||
Inline::Link { content, .. } => content.iter().fold(text, |text, item| match item {
|
Inline::Link { content, .. } => {
|
||||||
Inline::Span(span) => {
|
content.iter().fold(text, |text, item| match item {
|
||||||
text.span(styled_span(span, self.style.color_link, self.style.color_code))
|
Inline::Span(span) => text.span(styled_span(
|
||||||
}
|
span,
|
||||||
|
self.style.color_link,
|
||||||
|
self.style.color_code,
|
||||||
|
)),
|
||||||
_ => text,
|
_ => text,
|
||||||
}),
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
434
packages/oneclient_app/src/components/markdown/svg_compat.rs
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
//! Rewrites the parts of an svg Skia's svg module can't read.
|
||||||
|
//!
|
||||||
|
//! shields.io embeds badge logos as `<image href="data:...">`. Skia only reads
|
||||||
|
//! the SVG 1.1 `xlink:href`, and its `<image>` only decodes bitmaps, so svg logos
|
||||||
|
//! (`logo=discord`, `logo=modrinth`, ...) are inlined as nested `<svg>` elements instead.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
|
||||||
|
const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
|
||||||
|
|
||||||
|
/// Logos are a few kilobytes, anything bigger is left for Skia to skip
|
||||||
|
const MAX_INLINED_SVG: usize = 256 * 1024;
|
||||||
|
const MAX_INLINED_IMAGES: usize = 16;
|
||||||
|
|
||||||
|
/// The `<image>` attributes that position the picture, carried onto the inlined `<svg>`
|
||||||
|
const PLACEMENT: [&str; 5] = ["x", "y", "width", "height", "preserveAspectRatio"];
|
||||||
|
|
||||||
|
pub(super) fn prepare_svg(bytes: &[u8]) -> Cow<'_, [u8]> {
|
||||||
|
let Ok(text) = std::str::from_utf8(bytes) else {
|
||||||
|
return Cow::Borrowed(bytes);
|
||||||
|
};
|
||||||
|
|
||||||
|
match rewrite_images(text) {
|
||||||
|
Some(rewritten) => Cow::Owned(rewritten.into_bytes()),
|
||||||
|
None => Cow::Borrowed(bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rewrite_images(text: &str) -> Option<String> {
|
||||||
|
let mut out = String::with_capacity(text.len());
|
||||||
|
let mut rest = text;
|
||||||
|
let mut changed = false;
|
||||||
|
let mut uses_xlink = false;
|
||||||
|
let mut inlined = 0;
|
||||||
|
|
||||||
|
while let Some(at) = find_tag(rest, "image") {
|
||||||
|
out.push_str(&rest[..at]);
|
||||||
|
let from_tag = &rest[at..];
|
||||||
|
|
||||||
|
let Some(tag) = Tag::parse(from_tag, "image") else {
|
||||||
|
out.push_str(from_tag);
|
||||||
|
rest = "";
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
rest = &from_tag[tag.len..];
|
||||||
|
|
||||||
|
if inlined < MAX_INLINED_IMAGES
|
||||||
|
&& let Some(nested) = inline_svg_image(&tag, &mut rest, inlined)
|
||||||
|
{
|
||||||
|
out.push_str(&nested);
|
||||||
|
inlined += 1;
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if tag.attr("href").is_some() && tag.attr("xlink:href").is_none() {
|
||||||
|
out.push_str(&tag.renamed("href", "xlink:href").to_source());
|
||||||
|
uses_xlink = true;
|
||||||
|
changed = true;
|
||||||
|
} else {
|
||||||
|
out.push_str(&from_tag[..tag.len]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_str(rest);
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if uses_xlink {
|
||||||
|
declare_xlink(&mut out);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces an `<image>` holding an svg data url with that svg, placed where the image was
|
||||||
|
fn inline_svg_image(tag: &Tag<'_>, rest: &mut &str, index: usize) -> Option<String> {
|
||||||
|
let href = tag.attr("href").or_else(|| tag.attr("xlink:href"))?;
|
||||||
|
// the logo's ids now share the badge's document, where shields.io already uses `r`, `s` and `blur`
|
||||||
|
let document = prefix_ids(&decode_svg_data_url(href)?, &format!("logo{index}-"));
|
||||||
|
|
||||||
|
let start = find_tag(&document, "svg")?;
|
||||||
|
let inner = Tag::parse(&document[start..], "svg")?;
|
||||||
|
let body = &document[start + inner.len..];
|
||||||
|
|
||||||
|
// `<image ...></image>` has a closing tag the nested svg must swallow
|
||||||
|
if !tag.self_closing {
|
||||||
|
let trimmed = rest.trim_start();
|
||||||
|
*rest = trimmed.strip_prefix("</image>")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nested = inner.without(&PLACEMENT);
|
||||||
|
for name in PLACEMENT {
|
||||||
|
if let Some(value) = tag.attr(name) {
|
||||||
|
nested.attrs.push((name, value, '"'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = nested.to_source();
|
||||||
|
out.push_str(body);
|
||||||
|
|
||||||
|
Some(match tag.attr("transform") {
|
||||||
|
Some(transform) => format!("<g transform=\"{transform}\">{out}</g>"),
|
||||||
|
None => out,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_svg_data_url(href: &str) -> Option<String> {
|
||||||
|
let payload = href.trim().strip_prefix("data:image/svg+xml")?;
|
||||||
|
let (params, data) = payload.split_once(',')?;
|
||||||
|
if !params.split(';').any(|param| param == "base64") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data: String = data.chars().filter(|c| !c.is_ascii_whitespace()).collect();
|
||||||
|
// base64 grows by a third, so the encoded length bounds the decoded one
|
||||||
|
if data.len() / 4 * 3 > MAX_INLINED_SVG {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = String::from_utf8(STANDARD.decode(data).ok()?).ok()?;
|
||||||
|
// one level of nesting, an inlined logo can't pull in further svgs
|
||||||
|
(!decoded.contains("data:image/svg+xml")).then_some(decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prefixes every `id` attribute and the `url(#id)` / `href="#id"` references to it
|
||||||
|
fn prefix_ids(svg: &str, prefix: &str) -> String {
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
let mut out = String::with_capacity(svg.len());
|
||||||
|
let mut rest = svg;
|
||||||
|
|
||||||
|
while let Some(at) = rest.find("id=") {
|
||||||
|
let attr_start = at > 0 && rest[..at].ends_with(|c: char| c.is_ascii_whitespace());
|
||||||
|
let (before, after) = rest.split_at(at + "id=".len());
|
||||||
|
out.push_str(before);
|
||||||
|
rest = after;
|
||||||
|
|
||||||
|
let Some(quote) = rest.chars().next().filter(|c| *c == '"' || *c == '\'') else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(len) = rest[1..].find(quote) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if !attr_start || len == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = &rest[1..1 + len];
|
||||||
|
out.push(quote);
|
||||||
|
out.push_str(prefix);
|
||||||
|
out.push_str(id);
|
||||||
|
out.push(quote);
|
||||||
|
ids.push(id);
|
||||||
|
rest = &rest[len + 2..];
|
||||||
|
}
|
||||||
|
out.push_str(rest);
|
||||||
|
|
||||||
|
for id in ids {
|
||||||
|
for (from, to) in [
|
||||||
|
("url(#", ")"),
|
||||||
|
("url('#", "')"),
|
||||||
|
("url(\"#", "\")"),
|
||||||
|
("href=\"#", "\""),
|
||||||
|
("href='#", "'"),
|
||||||
|
] {
|
||||||
|
out = out.replace(
|
||||||
|
&format!("{from}{id}{to}"),
|
||||||
|
&format!("{from}{prefix}{id}{to}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn declare_xlink(svg: &mut String) {
|
||||||
|
let Some(root) = find_tag(svg, "svg") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(tag) = Tag::parse(&svg[root..], "svg") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if tag.attr("xmlns:xlink").is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.insert_str(root + "<svg".len(), &format!(" xmlns:xlink=\"{XLINK_NS}\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte offset of the next `<name` start tag, skipping longer names sharing the prefix
|
||||||
|
fn find_tag(text: &str, name: &str) -> Option<usize> {
|
||||||
|
let open = format!("<{name}");
|
||||||
|
let mut from = 0;
|
||||||
|
|
||||||
|
while let Some(found) = text[from..].find(&open) {
|
||||||
|
let at = from + found;
|
||||||
|
let next = text[at + open.len()..].chars().next();
|
||||||
|
if matches!(next, Some(c) if c.is_ascii_whitespace() || c == '/' || c == '>') {
|
||||||
|
return Some(at);
|
||||||
|
}
|
||||||
|
from = at + open.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A start tag, values kept as written so re-emitting them needs no escaping
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct Tag<'a> {
|
||||||
|
name: &'a str,
|
||||||
|
attrs: Vec<(&'a str, &'a str, char)>,
|
||||||
|
self_closing: bool,
|
||||||
|
/// Bytes of the source the tag spans, `>` included
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Tag<'a> {
|
||||||
|
fn parse(source: &'a str, name: &'a str) -> Option<Self> {
|
||||||
|
let mut at = source.strip_prefix('<')?.strip_prefix(name)?.len();
|
||||||
|
at = source.len() - at;
|
||||||
|
let mut attrs = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
at += whitespace(&source[at..]);
|
||||||
|
let rest = &source[at..];
|
||||||
|
|
||||||
|
if let Some(after) = rest.strip_prefix("/>") {
|
||||||
|
let len = source.len() - after.len();
|
||||||
|
return Some(Self {
|
||||||
|
name,
|
||||||
|
attrs,
|
||||||
|
self_closing: true,
|
||||||
|
len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if rest.starts_with('>') {
|
||||||
|
return Some(Self {
|
||||||
|
name,
|
||||||
|
attrs,
|
||||||
|
self_closing: false,
|
||||||
|
len: at + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let name_len =
|
||||||
|
rest.find(|c: char| c == '=' || c.is_ascii_whitespace() || c == '>' || c == '/')?;
|
||||||
|
if name_len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let attr = &rest[..name_len];
|
||||||
|
at += name_len;
|
||||||
|
at += whitespace(&source[at..]);
|
||||||
|
source[at..].strip_prefix('=')?;
|
||||||
|
at += 1;
|
||||||
|
at += whitespace(&source[at..]);
|
||||||
|
|
||||||
|
let quote = source[at..]
|
||||||
|
.chars()
|
||||||
|
.next()
|
||||||
|
.filter(|c| *c == '"' || *c == '\'')?;
|
||||||
|
at += 1;
|
||||||
|
let value_len = source[at..].find(quote)?;
|
||||||
|
attrs.push((attr, &source[at..at + value_len], quote));
|
||||||
|
at += value_len + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr(&self, name: &str) -> Option<&'a str> {
|
||||||
|
self.attrs
|
||||||
|
.iter()
|
||||||
|
.find(|(attr, _, _)| *attr == name)
|
||||||
|
.map(|(_, value, _)| *value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn renamed(mut self, from: &str, to: &'a str) -> Self {
|
||||||
|
for attr in &mut self.attrs {
|
||||||
|
if attr.0 == from {
|
||||||
|
attr.0 = to;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn without(&self, names: &[&str]) -> Self {
|
||||||
|
Self {
|
||||||
|
name: self.name,
|
||||||
|
attrs: self
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|(attr, _, _)| !names.contains(attr))
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
self_closing: self.self_closing,
|
||||||
|
len: self.len,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_source(&self) -> String {
|
||||||
|
let mut out = format!("<{}", self.name);
|
||||||
|
for (attr, value, quote) in &self.attrs {
|
||||||
|
out.push_str(&format!(" {attr}={quote}{value}{quote}"));
|
||||||
|
}
|
||||||
|
out.push_str(if self.self_closing { "/>" } else { ">" });
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn whitespace(text: &str) -> usize {
|
||||||
|
text.len()
|
||||||
|
- text
|
||||||
|
.trim_start_matches(|c: char| c.is_ascii_whitespace())
|
||||||
|
.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn prepared(svg: &str) -> String {
|
||||||
|
String::from_utf8(prepare_svg(svg.as_bytes()).into_owned()).expect("utf8")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn svg_data_url(svg: &str) -> String {
|
||||||
|
format!("data:image/svg+xml;base64,{}", STANDARD.encode(svg))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leaves_an_svg_without_images_untouched() {
|
||||||
|
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><rect width="1" height="1"/></svg>"#;
|
||||||
|
assert!(matches!(prepare_svg(svg.as_bytes()), Cow::Borrowed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn renames_href_and_declares_xlink_once() {
|
||||||
|
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg"><image x="5" href="data:image/png;base64,AAAA"/><image href="b.png"/></svg>"#;
|
||||||
|
assert_eq!(
|
||||||
|
prepared(svg),
|
||||||
|
r#"<svg xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg"><image x="5" xlink:href="data:image/png;base64,AAAA"/><image xlink:href="b.png"/></svg>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn keeps_an_existing_xlink_declaration() {
|
||||||
|
let svg = r#"<svg xmlns:xlink="http://www.w3.org/1999/xlink"><image href="a.png"/></svg>"#;
|
||||||
|
assert_eq!(
|
||||||
|
prepared(svg),
|
||||||
|
r#"<svg xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="a.png"/></svg>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inlines_an_svg_logo_where_the_image_was() {
|
||||||
|
let logo = r##"<?xml version="1.0"?><svg fill="#5865F2" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M0 0h24v24H0z"/></svg>"##;
|
||||||
|
let svg = format!(
|
||||||
|
r#"<svg xmlns="http://www.w3.org/2000/svg"><image x="5" y="3" width="14" height="14" href="{}"/><text>Discord</text></svg>"#,
|
||||||
|
svg_data_url(logo)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
prepared(&svg),
|
||||||
|
r##"<svg xmlns="http://www.w3.org/2000/svg"><svg fill="#5865F2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" x="5" y="3" width="14" height="14"><path d="M0 0h24v24H0z"/></svg><text>Discord</text></svg>"##
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swallows_the_closing_tag_of_an_open_image() {
|
||||||
|
let logo = r#"<svg viewBox="0 0 1 1"/>"#;
|
||||||
|
let svg = format!(
|
||||||
|
r#"<svg><image width="2" href="{}"> </image></svg>"#,
|
||||||
|
svg_data_url(logo)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
prepared(&svg),
|
||||||
|
r#"<svg><svg viewBox="0 0 1 1" width="2"/></svg>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn malformed_base64_falls_back_to_the_xlink_rename() {
|
||||||
|
let svg = r#"<svg><image href="data:image/svg+xml;base64,@@@"/></svg>"#;
|
||||||
|
assert_eq!(
|
||||||
|
prepared(svg),
|
||||||
|
r#"<svg xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="data:image/svg+xml;base64,@@@"/></svg>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn does_not_inline_a_logo_that_nests_another_svg() {
|
||||||
|
let inner = svg_data_url(r#"<svg viewBox="0 0 1 1"/>"#);
|
||||||
|
let logo = format!(r#"<svg viewBox="0 0 1 1"><image href="{inner}"/></svg>"#);
|
||||||
|
let svg = format!(r#"<svg><image href="{}"/></svg>"#, svg_data_url(&logo));
|
||||||
|
|
||||||
|
assert!(prepared(&svg).contains("<image xlink:href=\"data:image/svg+xml;base64,"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prefixes_the_ids_of_an_inlined_logo() {
|
||||||
|
let logo = r##"<svg viewBox="0 0 1 1"><clipPath id="r"><rect width="1" height="1"/></clipPath><linearGradient id='s'/><rect grid="r" clip-path="url(#r)" fill="url('#s')"/><use href="#r"/><use xlink:href="#s"/><rect fill="url(#rr)"/></svg>"##;
|
||||||
|
let svg = format!(r#"<svg><image href="{}"/></svg>"#, svg_data_url(logo));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
prepared(&svg),
|
||||||
|
r##"<svg><svg viewBox="0 0 1 1"><clipPath id="logo0-r"><rect width="1" height="1"/></clipPath><linearGradient id='logo0-s'/><rect grid="r" clip-path="url(#logo0-r)" fill="url('#logo0-s')"/><use href="#logo0-r"/><use xlink:href="#logo0-s"/><rect fill="url(#rr)"/></svg></svg>"##
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_inlined_logo_gets_its_own_prefix() {
|
||||||
|
let logo = svg_data_url(r#"<svg><g id="a"/></svg>"#);
|
||||||
|
let svg = format!(r#"<svg><image href="{logo}"/><image href="{logo}"/></svg>"#);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
prepared(&svg),
|
||||||
|
r#"<svg><svg><g id="logo0-a"/></svg><svg><g id="logo1-a"/></svg></svg>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ignores_tags_that_only_share_the_prefix() {
|
||||||
|
let svg = r#"<svg><imageset href="a"/></svg>"#;
|
||||||
|
assert!(matches!(prepare_svg(svg.as_bytes()), Cow::Borrowed(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unterminated_tag_is_left_alone() {
|
||||||
|
let svg = r#"<svg><image href="a.png"#;
|
||||||
|
assert!(matches!(prepare_svg(svg.as_bytes()), Cow::Borrowed(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
use crate::state::LoginProgress;
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
use freya::query::{MutationCapability, MutationStateData, UseMutation};
|
||||||
use freya::text_edit::Clipboard;
|
use freya::text_edit::Clipboard;
|
||||||
use oneclient_core::LauncherError;
|
|
||||||
use oneclient_auth::{AuthErrorGuidance, MicrosoftLoginSession};
|
use oneclient_auth::{AuthErrorGuidance, MicrosoftLoginSession};
|
||||||
use crate::state::LoginProgress;
|
use oneclient_core::LauncherError;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
|
|
@ -225,10 +225,7 @@ pub(crate) fn login_dialog(
|
||||||
.spacing(18.)
|
.spacing(18.)
|
||||||
.child(browser_dialog_body(auth_url))
|
.child(browser_dialog_body(auth_url))
|
||||||
.child(dialog_divider())
|
.child(dialog_divider())
|
||||||
.child(device_code_dialog_body(
|
.child(device_code_dialog_body(user_code, verification_uri))
|
||||||
user_code,
|
|
||||||
verification_uri,
|
|
||||||
))
|
|
||||||
.child(status_row(status, error))
|
.child(status_row(status, error))
|
||||||
.maybe_child(guidance.map(guidance_block)),
|
.maybe_child(guidance.map(guidance_block)),
|
||||||
),
|
),
|
||||||
|
|
@ -314,11 +311,7 @@ fn status_row(status: Option<LoginProgress>, error: Option<String>) -> impl Into
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guidance_block(guidance: AuthErrorGuidance) -> impl IntoElement {
|
fn guidance_block(guidance: AuthErrorGuidance) -> impl IntoElement {
|
||||||
let mut steps = rect()
|
let mut steps = rect().vertical().width(Size::fill()).spacing(6.).child(
|
||||||
.vertical()
|
|
||||||
.width(Size::fill())
|
|
||||||
.spacing(6.)
|
|
||||||
.child(
|
|
||||||
label()
|
label()
|
||||||
.text("What you can do:")
|
.text("What you can do:")
|
||||||
.font_size(12.)
|
.font_size(12.)
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,52 @@
|
||||||
mod account_switcher;
|
mod account_switcher;
|
||||||
mod control_center;
|
|
||||||
mod active_cluster_panel;
|
mod active_cluster_panel;
|
||||||
|
mod asset_image;
|
||||||
mod avatar;
|
mod avatar;
|
||||||
|
mod blocked_downloads_popup;
|
||||||
|
mod bundle_choices_popup;
|
||||||
mod button;
|
mod button;
|
||||||
mod charts;
|
mod charts;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
mod cluster_landscape_art;
|
|
||||||
mod cluster_update_popup;
|
mod cluster_update_popup;
|
||||||
|
mod code_block;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
|
mod control_center;
|
||||||
mod date_range_picker;
|
mod date_range_picker;
|
||||||
mod optional_mods_popup;
|
|
||||||
mod dropdown;
|
mod dropdown;
|
||||||
mod dynamic_art;
|
mod dynamic_art;
|
||||||
|
mod essential_confirm;
|
||||||
mod file_drop;
|
mod file_drop;
|
||||||
mod icons;
|
mod filter_menu;
|
||||||
mod java_install_manager;
|
mod flagged_install_popup;
|
||||||
mod generic_prompt;
|
mod generic_prompt;
|
||||||
|
mod icons;
|
||||||
|
mod instance_row;
|
||||||
|
mod java_install_manager;
|
||||||
mod java_prompt;
|
mod java_prompt;
|
||||||
mod microsoft_java_prompt;
|
|
||||||
mod link_confirm;
|
mod link_confirm;
|
||||||
mod local_image;
|
mod local_image;
|
||||||
mod log_viewer;
|
mod log_viewer;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
mod memory_field;
|
mod memory_field;
|
||||||
|
mod microsoft_java_prompt;
|
||||||
mod microsoft_login;
|
mod microsoft_login;
|
||||||
|
mod modpack_confirm_popup;
|
||||||
mod navbar;
|
mod navbar;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
mod optional_mods_popup;
|
||||||
mod overlay_popup;
|
mod overlay_popup;
|
||||||
|
mod package_delete_dialog;
|
||||||
mod package_row;
|
mod package_row;
|
||||||
mod package_update_popup;
|
mod package_update_popup;
|
||||||
mod pagination;
|
mod pagination;
|
||||||
mod player_model;
|
mod player_model;
|
||||||
mod progress_track;
|
mod progress_track;
|
||||||
mod recents_row;
|
mod recents_row;
|
||||||
|
mod release_migration_popup;
|
||||||
mod screenshot_viewer;
|
mod screenshot_viewer;
|
||||||
mod scrollview;
|
mod scrollview;
|
||||||
mod segmented_control;
|
mod segmented_control;
|
||||||
|
mod spinner;
|
||||||
mod splash_curtain;
|
mod splash_curtain;
|
||||||
mod status_bar;
|
mod status_bar;
|
||||||
mod tab_bar;
|
mod tab_bar;
|
||||||
|
|
@ -44,57 +55,76 @@ mod toasts;
|
||||||
mod toggle;
|
mod toggle;
|
||||||
mod tooltip;
|
mod tooltip;
|
||||||
mod update_prompt;
|
mod update_prompt;
|
||||||
mod version_card;
|
|
||||||
pub mod upload_mclogs;
|
pub mod upload_mclogs;
|
||||||
|
mod version_art_gallery;
|
||||||
|
mod version_card;
|
||||||
|
|
||||||
pub use account_switcher::AccountSwitcher;
|
pub use account_switcher::AccountSwitcher;
|
||||||
pub use control_center::ControlCenter;
|
|
||||||
pub use active_cluster_panel::ActiveClusterPanel;
|
pub use active_cluster_panel::ActiveClusterPanel;
|
||||||
|
pub use asset_image::AssetImage;
|
||||||
pub use avatar::Avatar;
|
pub use avatar::Avatar;
|
||||||
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
|
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
||||||
|
pub use bundle_choices_popup::BundleChoicesPopup;
|
||||||
|
pub use button::{Button, ButtonSize, ButtonVariant, copy_button, link_button, open_folder_button};
|
||||||
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
|
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
|
||||||
pub use checkbox::checkbox_labeled;
|
pub use checkbox::checkbox_labeled;
|
||||||
pub use cluster_landscape_art::ClusterLandscapeArt;
|
|
||||||
pub use cluster_update_popup::ClusterUpdatePopup;
|
pub use cluster_update_popup::ClusterUpdatePopup;
|
||||||
|
pub use code_block::CodeBlock;
|
||||||
pub use context_menu::ContextMenu;
|
pub use context_menu::ContextMenu;
|
||||||
|
pub use control_center::ControlCenter;
|
||||||
pub use date_range_picker::{DateRange, DateRangePicker};
|
pub use date_range_picker::{DateRange, DateRangePicker};
|
||||||
pub use optional_mods_popup::OptionalModsPopup;
|
|
||||||
pub use dropdown::Dropdown;
|
pub use dropdown::Dropdown;
|
||||||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||||
pub use file_drop::FileDropOverlay;
|
pub use essential_confirm::EssentialConfirmOverlay;
|
||||||
pub use icons::{Icon, IconTint, IconType};
|
pub use file_drop::{FileDropOverlay, accept_drop};
|
||||||
pub use java_install_manager::JavaInstallManager;
|
pub use filter_menu::{FilterMenu, FilterOption};
|
||||||
|
pub use flagged_install_popup::FlaggedInstallPopup;
|
||||||
pub use generic_prompt::GenericPromptOverlay;
|
pub use generic_prompt::GenericPromptOverlay;
|
||||||
|
pub use icons::{Icon, IconTint, IconType};
|
||||||
|
pub use instance_row::InstanceRow;
|
||||||
|
pub use java_install_manager::JavaInstallManager;
|
||||||
pub use java_prompt::JavaPromptOverlay;
|
pub use java_prompt::JavaPromptOverlay;
|
||||||
pub use microsoft_java_prompt::MicrosoftJavaPromptOverlay;
|
|
||||||
pub use link_confirm::ConfirmLinkOverlay;
|
pub use link_confirm::ConfirmLinkOverlay;
|
||||||
pub use local_image::LocalImage;
|
pub use local_image::LocalImage;
|
||||||
pub use markdown::{Markdown, MarkdownStyle};
|
|
||||||
pub use log_viewer::LogViewer;
|
pub use log_viewer::LogViewer;
|
||||||
|
pub use markdown::{Markdown, MarkdownStyle};
|
||||||
pub use memory_field::memory_field;
|
pub use memory_field::memory_field;
|
||||||
pub use microsoft_login::use_microsoft_login;
|
pub use microsoft_java_prompt::MicrosoftJavaPromptOverlay;
|
||||||
pub(crate) use microsoft_login::login_dialog;
|
pub(crate) use microsoft_login::login_dialog;
|
||||||
|
pub use microsoft_login::{MicrosoftLogin, use_microsoft_login};
|
||||||
|
pub use modpack_confirm_popup::ModpackConfirmPopup;
|
||||||
pub(crate) use navbar::window_controls;
|
pub(crate) use navbar::window_controls;
|
||||||
pub use navbar::{AppNavbar, OnboardingNavbar};
|
pub use navbar::{AppNavbar, OnboardingNavbar};
|
||||||
pub use notifications::NotificationCenter;
|
pub use notifications::{ChevronToggle, NotificationCenter};
|
||||||
|
pub use optional_mods_popup::OptionalModsPopup;
|
||||||
pub use overlay_popup::{OVERLAY_BASE_LEVEL, OVERLAY_MAX_LEVEL, OverlayPopup, overlay_is_topmost};
|
pub use overlay_popup::{OVERLAY_BASE_LEVEL, OVERLAY_MAX_LEVEL, OverlayPopup, overlay_is_topmost};
|
||||||
pub use package_row::{CardLayout, PackageEntry, PackageRow, provider_badge};
|
pub use package_delete_dialog::use_shared_delete;
|
||||||
pub(crate) use package_row::{CARD_GRID_H, GRID_GAP, GRID_MIN_W, grid_card, package_icon};
|
pub(crate) use package_row::{
|
||||||
|
CARD_BG, CARD_GRID_H, CARD_H, CARD_NAME, GRID_GAP, GRID_MIN_W, badge, disable_warning_body,
|
||||||
|
grid_card, icon_box, kebab_button, meta_size, meta_text, on_secondary, package_icon, pill,
|
||||||
|
remote_icon, set_enabled_action, toggle_action,
|
||||||
|
};
|
||||||
|
pub use package_row::{
|
||||||
|
CardLayout, PackageEntry, PackageRow, github_badge, package_context_menu, provider_badge,
|
||||||
|
};
|
||||||
pub use package_update_popup::PackageUpdatePopup;
|
pub use package_update_popup::PackageUpdatePopup;
|
||||||
pub use pagination::Pagination;
|
pub use pagination::Pagination;
|
||||||
pub use player_model::PlayerModel;
|
pub use player_model::PlayerModel;
|
||||||
pub use progress_track::progress_track;
|
pub use progress_track::progress_track;
|
||||||
pub use recents_row::RecentsRow;
|
pub use recents_row::RecentsRow;
|
||||||
|
pub use release_migration_popup::ReleaseMigrationPopup;
|
||||||
pub use screenshot_viewer::{ScreenshotViewer, screenshot_context_menu};
|
pub use screenshot_viewer::{ScreenshotViewer, screenshot_context_menu};
|
||||||
pub(crate) use scrollview::corrected_scroll;
|
pub(crate) use scrollview::corrected_scroll;
|
||||||
pub use scrollview::{ScrollArea, ScrollAreaCtx};
|
pub use scrollview::{LazySection, ScrollArea, ScrollAreaCtx};
|
||||||
pub use segmented_control::{Segment, SegmentedControl};
|
pub use segmented_control::{Segment, SegmentedControl};
|
||||||
|
pub use spinner::centered_spinner;
|
||||||
pub use splash_curtain::SplashCurtain;
|
pub use splash_curtain::SplashCurtain;
|
||||||
pub use status_bar::StatusBar;
|
pub use status_bar::StatusBar;
|
||||||
pub use tab_bar::{TabBar, TabItem};
|
pub use tab_bar::{TabBar, TabItem};
|
||||||
pub use text_input::{TextInput, validate_memory, validate_number};
|
pub use text_input::{TextInput, validate_memory, validate_number};
|
||||||
pub use toasts::Toasts;
|
pub use toasts::Toasts;
|
||||||
pub use toggle::{toggle, toggle_controlled};
|
pub use toggle::{toggle, toggle_controlled};
|
||||||
pub use tooltip::{TooltipHost, TooltipPlacement, use_provide_tooltips};
|
pub use tooltip::{TooltipHost, TooltipPlacement, use_provide_tooltips, use_tooltip_anchor};
|
||||||
pub use update_prompt::UpdatePromptOverlay;
|
pub use update_prompt::UpdatePromptOverlay;
|
||||||
|
pub use version_art_gallery::{GALLERY_COVER_EDGE, VersionArtGallery};
|
||||||
pub use version_card::VersionCard;
|
pub use version_card::VersionCard;
|
||||||
|
|
|
||||||
259
packages/oneclient_app/src/components/modpack_confirm_popup.rs
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
use oneclient_content::modpacks::ModpackSummary;
|
||||||
|
|
||||||
|
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||||
|
use crate::hooks::{use_dispatch, use_notifications_snapshot};
|
||||||
|
use crate::notifications::ModpackConfirm;
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
use crate::utils::format_size;
|
||||||
|
|
||||||
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
|
const DIALOG_W: f32 = 500.;
|
||||||
|
const DIALOG_PAD: f32 = 22.;
|
||||||
|
const FACT_LABEL_W: f32 = 110.;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct ModpackConfirmPopup;
|
||||||
|
|
||||||
|
impl Component for ModpackConfirmPopup {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let snapshot = use_notifications_snapshot();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
|
||||||
|
let Some(confirm) = snapshot.modpack_confirm else {
|
||||||
|
return rect().into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let close = dispatch.clone();
|
||||||
|
OverlayPopup::new()
|
||||||
|
.on_close(move |_| close.cancel_modpack())
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(dialog(&confirm, dispatch)),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_label(count: usize, one: &str, many: &str) -> String {
|
||||||
|
format!("{count} {}", if count == 1 { one } else { many })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content_line(summary: &ModpackSummary) -> String {
|
||||||
|
let parts: Vec<String> = [
|
||||||
|
(summary.mods, "mod", "mods"),
|
||||||
|
(summary.resource_packs, "resource pack", "resource packs"),
|
||||||
|
(summary.shaders, "shader", "shaders"),
|
||||||
|
(summary.other_files, "other file", "other files"),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(count, _, _)| *count > 0)
|
||||||
|
.map(|(count, one, many)| count_label(count, one, many))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if parts.is_empty() {
|
||||||
|
"Configs and settings only".to_string()
|
||||||
|
} else {
|
||||||
|
parts.join(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notes(summary: &ModpackSummary) -> Vec<(String, Color)> {
|
||||||
|
let mut notes = Vec::new();
|
||||||
|
if summary.optional > 0 {
|
||||||
|
notes.push((
|
||||||
|
format!(
|
||||||
|
"{} optional. They are installed turned off, so you can switch them on in the Mods tab.",
|
||||||
|
count_label(summary.optional, "file is", "files are")
|
||||||
|
),
|
||||||
|
colors::fg_secondary(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if summary.manual > 0 {
|
||||||
|
notes.push((
|
||||||
|
format!(
|
||||||
|
"{} must be downloaded by hand from CurseForge. The launcher will show you which ones.",
|
||||||
|
count_label(summary.manual, "file", "files")
|
||||||
|
),
|
||||||
|
colors::code_warn(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if summary.unresolved > 0 {
|
||||||
|
notes.push((
|
||||||
|
format!(
|
||||||
|
"{} could not be found on Modrinth or CurseForge and will be skipped.",
|
||||||
|
count_label(summary.unresolved, "file", "files")
|
||||||
|
),
|
||||||
|
colors::danger(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
notes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fact(name: &'static str, value: String) -> Element {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.content(Content::Flex)
|
||||||
|
.spacing(10.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(name)
|
||||||
|
.width(Size::px(FACT_LABEL_W))
|
||||||
|
.font_size(12.5)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(value)
|
||||||
|
.width(Size::flex(1.))
|
||||||
|
.font_size(12.5)
|
||||||
|
.max_lines(2)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dialog(confirm: &ModpackConfirm, dispatch: crate::Actions) -> impl IntoElement {
|
||||||
|
let summary = &confirm.summary;
|
||||||
|
let version = if confirm.version.trim().is_empty() {
|
||||||
|
"Not stated by the pack".to_string()
|
||||||
|
} else {
|
||||||
|
confirm.version.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut facts = vec![
|
||||||
|
fact("Instance", confirm.instance_name.clone()),
|
||||||
|
fact("Version", version),
|
||||||
|
fact(
|
||||||
|
"Minecraft",
|
||||||
|
format!("{} · {}", confirm.mc_version, confirm.loader),
|
||||||
|
),
|
||||||
|
fact("Content", content_line(summary)),
|
||||||
|
];
|
||||||
|
if summary.download_bytes > 0 {
|
||||||
|
facts.push(fact(
|
||||||
|
"Download",
|
||||||
|
format!(
|
||||||
|
"Up to {}, less for files the launcher already has",
|
||||||
|
format_size(summary.download_bytes)
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancel = dispatch.clone();
|
||||||
|
let add = dispatch;
|
||||||
|
|
||||||
|
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(16.)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(3.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(format!("Add {}?", confirm.pack_name))
|
||||||
|
.font_size(17.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.max_lines(2)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(format!(
|
||||||
|
"From {}. It is added as a new instance with its own folder.",
|
||||||
|
confirm.source
|
||||||
|
))
|
||||||
|
.font_size(12.5)
|
||||||
|
.max_lines(2)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(8.)
|
||||||
|
.padding(Gaps::new_all(12.))
|
||||||
|
.corner_radius(CornerRadius::new_all(10.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.children(facts),
|
||||||
|
)
|
||||||
|
.children(notes(summary).into_iter().map(|(text, color)| {
|
||||||
|
label()
|
||||||
|
.text(text)
|
||||||
|
.font_size(12.)
|
||||||
|
.max_lines(3)
|
||||||
|
.color(color)
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.main_align(Alignment::End)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.ghost()
|
||||||
|
.on_press(move |_| cancel.cancel_modpack())
|
||||||
|
.text("Cancel"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| add.confirm_modpack())
|
||||||
|
.child(Icon::new(IconType::Plus).size(15.))
|
||||||
|
.text("Add"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_categories_are_left_out_of_the_content_line() {
|
||||||
|
let summary = ModpackSummary {
|
||||||
|
mods: 336,
|
||||||
|
shaders: 1,
|
||||||
|
..ModpackSummary::default()
|
||||||
|
};
|
||||||
|
assert_eq!(content_line(&summary), "336 mods, 1 shader");
|
||||||
|
assert_eq!(
|
||||||
|
content_line(&ModpackSummary::default()),
|
||||||
|
"Configs and settings only"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,17 +9,18 @@ use crate::{
|
||||||
components::{Avatar, Icon, IconType},
|
components::{Avatar, Icon, IconType},
|
||||||
hooks::{
|
hooks::{
|
||||||
settled_or_loading, try_default_account, use_active_cluster_id, use_browser_type,
|
settled_or_loading, try_default_account, use_active_cluster_id, use_browser_type,
|
||||||
use_clusters, use_current_account, use_dispatch, use_notifications_snapshot,
|
use_clusters, use_current_account, use_dispatch, use_link_confirm,
|
||||||
|
use_notifications_snapshot,
|
||||||
},
|
},
|
||||||
theme,
|
theme,
|
||||||
utils::sort_clusters_for_home,
|
utils::default_cluster,
|
||||||
};
|
};
|
||||||
|
|
||||||
const NAVBAR_INTRO_MS: u64 = 460;
|
const NAVBAR_INTRO_MS: u64 = 460;
|
||||||
const LOGO_HIDE_NAVBAR_W: f32 = 1100.;
|
const LOGO_HIDE_NAVBAR_W: f32 = 1100.;
|
||||||
const NAVBAR_SIDE_PADDING_PX: f32 = 40.;
|
const NAVBAR_SIDE_PADDING_PX: f32 = 40.;
|
||||||
const NAV_LINK_SPACING_PX: f32 = 36.;
|
const NAV_LINK_SPACING_PX: f32 = 36.;
|
||||||
const COMPACT_LOGO_PX: f32 = 28.;
|
const COMPACT_LOGO_PX: f32 = 32.;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct Navbar;
|
pub struct Navbar;
|
||||||
|
|
@ -84,16 +85,16 @@ fn navbar_left(show_logo: bool) -> impl IntoElement {
|
||||||
Size::auto()
|
Size::auto()
|
||||||
})
|
})
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(NAV_LINK_SPACING_PX / 2.)
|
||||||
.maybe(!show_logo, |el| {
|
.maybe(!show_logo, |el| {
|
||||||
el.padding(Gaps::new(0., NAV_LINK_SPACING_PX, 0., 0.))
|
el.padding(Gaps::new(0., NAV_LINK_SPACING_PX, 0., 0.))
|
||||||
})
|
})
|
||||||
.child(if show_logo {
|
.child(
|
||||||
NavbarLogo.into_element()
|
|
||||||
} else {
|
|
||||||
Icon::new(IconType::IconLogo)
|
Icon::new(IconType::IconLogo)
|
||||||
.size(COMPACT_LOGO_PX)
|
.size(COMPACT_LOGO_PX)
|
||||||
.into_element()
|
.into_element(),
|
||||||
})
|
)
|
||||||
|
.maybe(show_logo, |rect| rect.child(NavbarLogo.into_element()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -124,11 +125,7 @@ fn navbar_center(is_small: bool) -> impl IntoElement {
|
||||||
Alignment::Center
|
Alignment::Center
|
||||||
})
|
})
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(if is_small {
|
.spacing(if is_small { 12. } else { 4. })
|
||||||
NAV_LINK_SPACING_PX
|
|
||||||
} else {
|
|
||||||
NAV_LINK_SPACING_PX / 2.
|
|
||||||
})
|
|
||||||
.child(NavLink {
|
.child(NavLink {
|
||||||
active: route == Route::Home {},
|
active: route == Route::Home {},
|
||||||
target: NavTarget::Route(Route::Home {}),
|
target: NavTarget::Route(Route::Home {}),
|
||||||
|
|
@ -139,11 +136,6 @@ fn navbar_center(is_small: bool) -> impl IntoElement {
|
||||||
target: NavTarget::Route(Route::Clusters {}),
|
target: NavTarget::Route(Route::Clusters {}),
|
||||||
nav_label: "Versions",
|
nav_label: "Versions",
|
||||||
})
|
})
|
||||||
.child(NavLink {
|
|
||||||
active: route == Route::AccountSkins {},
|
|
||||||
target: NavTarget::Route(Route::AccountSkins {}),
|
|
||||||
nav_label: "Skins",
|
|
||||||
})
|
|
||||||
.child(NavLink {
|
.child(NavLink {
|
||||||
active: matches!(
|
active: matches!(
|
||||||
route,
|
route,
|
||||||
|
|
@ -155,6 +147,16 @@ fn navbar_center(is_small: bool) -> impl IntoElement {
|
||||||
target: NavTarget::Route(browse_target),
|
target: NavTarget::Route(browse_target),
|
||||||
nav_label: "Browse",
|
nav_label: "Browse",
|
||||||
})
|
})
|
||||||
|
.child(NavLink {
|
||||||
|
active: matches!(route, Route::AccountSkins {}),
|
||||||
|
target: NavTarget::Route(Route::AccountSkins {}),
|
||||||
|
nav_label: "Skins",
|
||||||
|
})
|
||||||
|
.child(NavLink {
|
||||||
|
active: false,
|
||||||
|
target: NavTarget::External("https://store.polyfrost.org"),
|
||||||
|
nav_label: "Cosmetics",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Falls back active cluster then most recently played then Versions when none exist
|
/// Falls back active cluster then most recently played then Versions when none exist
|
||||||
|
|
@ -163,14 +165,10 @@ fn browse_target() -> Route {
|
||||||
let active = *use_active_cluster_id().read();
|
let active = *use_active_cluster_id().read();
|
||||||
let package_type = use_browser_type().read().clone();
|
let package_type = use_browser_type().read().clone();
|
||||||
|
|
||||||
let cluster_id = active
|
match default_cluster(clusters, active) {
|
||||||
.filter(|id| clusters.iter().any(|cluster| cluster.id == *id))
|
Some(cluster) => Route::Browser {
|
||||||
.or_else(|| sort_clusters_for_home(clusters).first().map(|c| c.id));
|
cluster_id: cluster.id,
|
||||||
|
package_type: crate::view::app::browser::browsable_type(&package_type, &cluster),
|
||||||
match cluster_id {
|
|
||||||
Some(cluster_id) => Route::Browser {
|
|
||||||
cluster_id,
|
|
||||||
package_type,
|
|
||||||
pick_cluster: true,
|
pick_cluster: true,
|
||||||
},
|
},
|
||||||
None => Route::Clusters {},
|
None => Route::Clusters {},
|
||||||
|
|
@ -178,7 +176,6 @@ fn browse_target() -> Route {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Clone)]
|
#[derive(PartialEq, Clone)]
|
||||||
#[allow(dead_code)]
|
|
||||||
enum NavTarget {
|
enum NavTarget {
|
||||||
Route(Route),
|
Route(Route),
|
||||||
External(&'static str),
|
External(&'static str),
|
||||||
|
|
@ -196,6 +193,7 @@ impl Component for NavLink {
|
||||||
let mut hovering = use_state(|| false);
|
let mut hovering = use_state(|| false);
|
||||||
let a11y_id = use_a11y();
|
let a11y_id = use_a11y();
|
||||||
let focused = use_focus(a11y_id);
|
let focused = use_focus(a11y_id);
|
||||||
|
let mut confirm_link = use_link_confirm();
|
||||||
|
|
||||||
let active = self.active;
|
let active = self.active;
|
||||||
let target = self.target.clone();
|
let target = self.target.clone();
|
||||||
|
|
@ -207,22 +205,30 @@ impl Component for NavLink {
|
||||||
theme::colors::fg_secondary()
|
theme::colors::fg_secondary()
|
||||||
};
|
};
|
||||||
|
|
||||||
let underline_width = if active {
|
let background = if active {
|
||||||
27.
|
theme::colors::ghost_overlay()
|
||||||
} else if hovering() || focused().is_focused() {
|
} else if hovering() || focused().is_focused() {
|
||||||
18.
|
theme::colors::ghost_overlay_hover()
|
||||||
} else {
|
} else {
|
||||||
0.
|
Color::TRANSPARENT
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.horizontal()
|
||||||
|
.main_align(Alignment::Center)
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(2.)
|
.height(Size::px(36.))
|
||||||
.width(Size::px(nav_label.len() as f32 * 10. + 10.))
|
.width(Size::px(nav_label.len() as f32 * 10. + 34.))
|
||||||
// TODO workaround for a Freya measurement bug a fully transparent background
|
.corner_radius(CornerRadius::new_all(10.))
|
||||||
// measures wrongly so give it alpha 0 red to keep pointer events working
|
.background(background)
|
||||||
.background(Color::RED.with_a(0))
|
.maybe(active, |el| {
|
||||||
|
el.border(
|
||||||
|
Border::new()
|
||||||
|
.fill(theme::colors::component_border())
|
||||||
|
.width(1.)
|
||||||
|
.alignment(BorderAlignment::Inner),
|
||||||
|
)
|
||||||
|
})
|
||||||
.a11y_id(a11y_id)
|
.a11y_id(a11y_id)
|
||||||
.a11y_focusable(true)
|
.a11y_focusable(true)
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
|
@ -232,7 +238,7 @@ impl Component for NavLink {
|
||||||
NavTarget::Route(route) => {
|
NavTarget::Route(route) => {
|
||||||
let _ = RouterContext::get().push(route.clone());
|
let _ = RouterContext::get().push(route.clone());
|
||||||
}
|
}
|
||||||
NavTarget::External(url) => crate::platform::open_url(url),
|
NavTarget::External(url) => confirm_link.set(Some((*url).to_string())),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on_pointer_over(move |_| hovering.set(true))
|
.on_pointer_over(move |_| hovering.set(true))
|
||||||
|
|
@ -249,17 +255,6 @@ impl Component for NavLink {
|
||||||
})
|
})
|
||||||
.color(color),
|
.color(color),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.height(Size::px(2.))
|
|
||||||
.width(Size::px(underline_width))
|
|
||||||
.corner_radius(CornerRadius::new_all(2.))
|
|
||||||
.background(if active {
|
|
||||||
theme::colors::fg_primary()
|
|
||||||
} else {
|
|
||||||
theme::colors::fg_secondary()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -324,11 +319,7 @@ fn avatar_with_gear(uuid: String) -> impl IntoElement {
|
||||||
.width(Size::px(28.))
|
.width(Size::px(28.))
|
||||||
.height(Size::px(28.))
|
.height(Size::px(28.))
|
||||||
.center()
|
.center()
|
||||||
.child(
|
.child(Avatar::new(uuid).width(Size::px(28.)).height(Size::px(28.)))
|
||||||
Avatar::new(uuid)
|
|
||||||
.width(Size::px(28.))
|
|
||||||
.height(Size::px(28.)),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.position(Position::new_absolute().bottom(-7.).right(-7.))
|
.position(Position::new_absolute().bottom(-7.).right(-7.))
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,7 @@ pub fn window_controls() -> impl IntoElement {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let close = |_| {
|
let close = |_| crate::platform::close();
|
||||||
let platform = Platform::get();
|
|
||||||
Platform::get().with_window(Platform::window_id(), move |window| {
|
|
||||||
platform.close_window(window.id());
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ use freya::{
|
||||||
use oneclient_events::Level;
|
use oneclient_events::Level;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ui::{divider, relative_time},
|
|
||||||
components::{Button, ButtonVariant, Icon, IconType, OverlayPopup, ScrollArea, progress_track},
|
components::{Button, ButtonVariant, Icon, IconType, OverlayPopup, ScrollArea, progress_track},
|
||||||
hooks::{use_dispatch, use_notifications_snapshot},
|
hooks::{use_dispatch, use_notifications_snapshot},
|
||||||
notifications::{InboxEntry, NotificationActionKind},
|
notifications::{InboxEntry, NotificationActionKind},
|
||||||
theme::colors,
|
theme::colors,
|
||||||
transfer::TransferStats,
|
transfer::TransferStats,
|
||||||
|
ui::{divider, relative_time},
|
||||||
utils::{format_durations, format_size},
|
utils::{format_durations, format_size},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,9 +48,13 @@ impl Component for NotificationPanel {
|
||||||
|
|
||||||
let progress = intro.read().value();
|
let progress = intro.read().value();
|
||||||
|
|
||||||
let entries = inbox.len();
|
|
||||||
let mut rows: Vec<Element> = Vec::new();
|
let mut rows: Vec<Element> = Vec::new();
|
||||||
if entries == 0 {
|
// Toast-only notices are ephemeral: never surface them in the center
|
||||||
|
let visible: Vec<InboxEntry> = inbox
|
||||||
|
.into_iter()
|
||||||
|
.filter(|entry| !entry.toast_only)
|
||||||
|
.collect();
|
||||||
|
if visible.is_empty() {
|
||||||
rows.push(
|
rows.push(
|
||||||
label()
|
label()
|
||||||
.text("No notifications")
|
.text("No notifications")
|
||||||
|
|
@ -59,8 +63,8 @@ impl Component for NotificationPanel {
|
||||||
.into_element(),
|
.into_element(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let last = entries - 1;
|
let last = visible.len() - 1;
|
||||||
for (i, entry) in inbox.into_iter().enumerate() {
|
for (i, entry) in visible.into_iter().enumerate() {
|
||||||
let id = entry.id;
|
let id = entry.id;
|
||||||
rows.push(NotifEntryRow::new(entry, i != last).key(id).into_element());
|
rows.push(NotifEntryRow::new(entry, i != last).key(id).into_element());
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +74,7 @@ impl Component for NotificationPanel {
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::flex(1.0))
|
.height(Size::flex(1.0))
|
||||||
.spacing(8.)
|
.spacing(8.)
|
||||||
|
.scrollbar_gutter(true)
|
||||||
.children(rows);
|
.children(rows);
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -82,7 +87,7 @@ impl Component for NotificationPanel {
|
||||||
.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))
|
||||||
.blur(12.)
|
.backdrop_blur(12.)
|
||||||
.corner_radius(CornerRadius::new_all(12.))
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
.border(
|
.border(
|
||||||
Border::new()
|
Border::new()
|
||||||
|
|
@ -212,7 +217,9 @@ fn tasks_section(entry: &InboxEntry, mut expanded: State<bool>) -> impl IntoElem
|
||||||
let now = !*expanded.peek();
|
let now = !*expanded.peek();
|
||||||
expanded.set(now);
|
expanded.set(now);
|
||||||
})
|
})
|
||||||
.child(ChevronToggle { expanded: is_expanded })
|
.child(ChevronToggle {
|
||||||
|
expanded: is_expanded,
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(if is_expanded {
|
.text(if is_expanded {
|
||||||
|
|
@ -279,8 +286,8 @@ fn task_row(task: &crate::notifications::TaskView) -> impl IntoElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct ChevronToggle {
|
pub struct ChevronToggle {
|
||||||
expanded: bool,
|
pub expanded: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Component for ChevronToggle {
|
impl Component for ChevronToggle {
|
||||||
|
|
@ -483,7 +490,10 @@ struct Footer;
|
||||||
impl Component for Footer {
|
impl Component for Footer {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
let is_empty = use_notifications_snapshot().inbox.is_empty();
|
let is_empty = !use_notifications_snapshot()
|
||||||
|
.inbox
|
||||||
|
.iter()
|
||||||
|
.any(|e| e.dismissable());
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
@ -525,4 +535,3 @@ fn level_color(level: &Level) -> Color {
|
||||||
Level::Error => colors::danger(),
|
Level::Error => colors::danger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ use crate::hooks::{
|
||||||
package_meta_batch, use_cached_image, use_dispatch, use_notifications_snapshot,
|
package_meta_batch, use_cached_image, use_dispatch, use_notifications_snapshot,
|
||||||
use_package_meta_batch,
|
use_package_meta_batch,
|
||||||
};
|
};
|
||||||
use crate::notifications::{ClusterUpdateItem, OptionalModRef, OptionalModsGroup};
|
use crate::notifications::{
|
||||||
|
ClusterUpdateItem, OptionalModRef, OptionalModsGroup, OptionalModsOutcome,
|
||||||
|
};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
|
@ -20,7 +22,9 @@ const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
const DIALOG_W: f32 = 720.;
|
const DIALOG_W: f32 = 720.;
|
||||||
const DIALOG_PAD: f32 = 22.;
|
const DIALOG_PAD: f32 = 22.;
|
||||||
const LIST_MAX_H: f32 = 306.;
|
const LIST_MAX_H: f32 = 306.;
|
||||||
const ICON_SIZE: f32 = 52.;
|
const ICON_SIZE: f32 = 40.;
|
||||||
|
const SKIP_NOTE: &str =
|
||||||
|
"Anything you don't pick won't be offered again, but you can add it later from the Mods tab.";
|
||||||
|
|
||||||
type MetaMap = HashMap<(ProviderId, String), CachedPackageMeta>;
|
type MetaMap = HashMap<(ProviderId, String), CachedPackageMeta>;
|
||||||
|
|
||||||
|
|
@ -33,12 +37,16 @@ impl Component for OptionalModsPopup {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let snapshot = use_notifications_snapshot();
|
let snapshot = use_notifications_snapshot();
|
||||||
let dispatch = use_dispatch();
|
let dispatch = use_dispatch();
|
||||||
let picked = use_state(Picked::new);
|
let mut picked = use_state(Picked::new);
|
||||||
|
|
||||||
let groups = snapshot.optional_mods.clone();
|
let groups = snapshot.optional_mods.clone();
|
||||||
|
|
||||||
let mut meta = MetaMap::new();
|
let mut meta = MetaMap::new();
|
||||||
for provider in ProviderId::REMOTE_PROVIDERS.iter().copied() {
|
for provider in ProviderId::REMOTE_PROVIDERS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.chain([ProviderId::Local])
|
||||||
|
{
|
||||||
let ids: Vec<String> = groups
|
let ids: Vec<String> = groups
|
||||||
.iter()
|
.iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
@ -57,11 +65,12 @@ impl Component for OptionalModsPopup {
|
||||||
};
|
};
|
||||||
|
|
||||||
let close = dispatch.clone();
|
let close = dispatch.clone();
|
||||||
let dismissed: Vec<(i64, OptionalModRef)> =
|
|
||||||
groups.iter().flat_map(|group| group.offers()).collect();
|
|
||||||
|
|
||||||
OverlayPopup::new()
|
OverlayPopup::new()
|
||||||
.on_close(move |_| close.decline_optional_mods(dismissed.clone()))
|
.on_close(move |_| {
|
||||||
|
picked.set(Picked::new());
|
||||||
|
close.close_optional_mods(OptionalModsOutcome::Cancel);
|
||||||
|
})
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::window_percent(100.))
|
.width(Size::window_percent(100.))
|
||||||
|
|
@ -92,9 +101,12 @@ fn entry_from_item(
|
||||||
.and_then(|project_id| meta.get(&(item.provider, project_id.clone())));
|
.and_then(|project_id| meta.get(&(item.provider, project_id.clone())));
|
||||||
|
|
||||||
PackageEntry {
|
PackageEntry {
|
||||||
|
essential: crate::essential::lookup(item.provider, &package_id),
|
||||||
package_id,
|
package_id,
|
||||||
bundle_name: item.offer.as_ref().map(|(bundle, _)| bundle.clone()),
|
bundle_name: item.offer.as_ref().map(|(bundle, _)| bundle.clone()),
|
||||||
provider: item.provider,
|
provider: item.provider,
|
||||||
|
github_hosted: item.github_hosted,
|
||||||
|
github_url: None,
|
||||||
name: cached
|
name: cached
|
||||||
.map(|cached| cached.name.clone())
|
.map(|cached| cached.name.clone())
|
||||||
.filter(|name| !name.is_empty())
|
.filter(|name| !name.is_empty())
|
||||||
|
|
@ -103,6 +115,7 @@ fn entry_from_item(
|
||||||
author: cached
|
author: cached
|
||||||
.map(|cached| cached.author.clone())
|
.map(|cached| cached.author.clone())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
version: None,
|
||||||
description: cached
|
description: cached
|
||||||
.map(|cached| cached.summary.clone())
|
.map(|cached| cached.summary.clone())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
|
@ -114,6 +127,10 @@ fn entry_from_item(
|
||||||
hash: None,
|
hash: None,
|
||||||
manifest_default: false,
|
manifest_default: false,
|
||||||
hidden: false,
|
hidden: false,
|
||||||
|
opted_in: true,
|
||||||
|
shadowed: false,
|
||||||
|
outranked: false,
|
||||||
|
advanced: false,
|
||||||
update_available: false,
|
update_available: false,
|
||||||
// Offered, not installed, so there is no recency badge to show
|
// Offered, not installed, so there is no recency badge to show
|
||||||
seen_status: SeenStatus::default(),
|
seen_status: SeenStatus::default(),
|
||||||
|
|
@ -149,7 +166,7 @@ fn dialog(
|
||||||
fn content(
|
fn content(
|
||||||
groups: &[OptionalModsGroup],
|
groups: &[OptionalModsGroup],
|
||||||
meta: &MetaMap,
|
meta: &MetaMap,
|
||||||
picked: State<Picked>,
|
mut picked: State<Picked>,
|
||||||
dispatch: crate::Actions,
|
dispatch: crate::Actions,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let total: usize = groups.iter().map(|group| group.mods.len()).sum();
|
let total: usize = groups.iter().map(|group| group.mods.len()).sum();
|
||||||
|
|
@ -167,24 +184,23 @@ fn content(
|
||||||
let plural = if total == 1 { "" } else { "s" };
|
let plural = if total == 1 { "" } else { "s" };
|
||||||
let subtitle = match groups {
|
let subtitle = match groups {
|
||||||
[only] => format!(
|
[only] => format!(
|
||||||
"{} includes {total} optional mod{plural} that {} off by default. Pick the {} to turn on.",
|
"{} includes {total} optional mod{plural} that {} off by default. Pick the {} to turn on. {SKIP_NOTE}",
|
||||||
only.cluster_name,
|
only.cluster_name,
|
||||||
if total == 1 { "is" } else { "are" },
|
if total == 1 { "is" } else { "are" },
|
||||||
if total == 1 { "one" } else { "ones" }
|
if total == 1 { "one" } else { "ones" }
|
||||||
),
|
),
|
||||||
_ => format!(
|
_ => format!(
|
||||||
"Your bundles include {total} optional mod{plural} across {} clusters that are off by default. Pick the ones to turn on.",
|
"Your bundles include {total} optional mod{plural} across {} clusters that are off by default. Pick the ones to turn on. {SKIP_NOTE}",
|
||||||
groups.len()
|
groups.len()
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let cancel = dispatch.clone();
|
let cancel = dispatch.clone();
|
||||||
let declined = offers.clone();
|
|
||||||
|
|
||||||
let enable_text = if enable.is_empty() {
|
let primary_text = if enable.is_empty() {
|
||||||
"Enable".to_string()
|
"Launch".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("Enable {}", enable.len())
|
format!("Install {} & launch", enable.len())
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -209,7 +225,7 @@ fn content(
|
||||||
label()
|
label()
|
||||||
.text(subtitle)
|
.text(subtitle)
|
||||||
.font_size(12.5)
|
.font_size(12.5)
|
||||||
.max_lines(3)
|
.max_lines(4)
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -224,22 +240,25 @@ fn content(
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_press(move |_| cancel.decline_optional_mods(declined.clone()))
|
.on_press(move |_| {
|
||||||
|
picked.set(Picked::new());
|
||||||
|
cancel.close_optional_mods(OptionalModsOutcome::Cancel);
|
||||||
|
})
|
||||||
.text("Cancel"),
|
.text("Cancel"),
|
||||||
)
|
)
|
||||||
.maybe_child((!offers.is_empty()).then(|| {
|
.child({
|
||||||
let apply = dispatch.clone();
|
let apply = dispatch.clone();
|
||||||
|
let install = !enable.is_empty();
|
||||||
Button::new()
|
Button::new()
|
||||||
.primary()
|
.primary()
|
||||||
.disabled(enable.is_empty())
|
|
||||||
.on_press(move |_| {
|
.on_press(move |_| {
|
||||||
|
picked.set(Picked::new());
|
||||||
apply.record_skipped_optional_mods(skip.clone());
|
apply.record_skipped_optional_mods(skip.clone());
|
||||||
apply.enable_optional_mods(enable.clone());
|
apply.enable_optional_mods(enable.clone());
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::Check).size(15.))
|
.maybe_child(install.then(|| Icon::new(IconType::Check).size(15.)))
|
||||||
.text(enable_text.clone())
|
.text(primary_text)
|
||||||
.into_element()
|
}),
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -350,6 +369,7 @@ impl Component for OptionalModCard {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let icon_query = use_cached_image(self.entry.icon_url.clone(), 256);
|
let icon_query = use_cached_image(self.entry.icon_url.clone(), 256);
|
||||||
let icon = package_icon(&self.entry, &icon_query, ICON_SIZE);
|
let icon = package_icon(&self.entry, &icon_query, ICON_SIZE);
|
||||||
|
let hovered = use_state(|| false);
|
||||||
|
|
||||||
grid_card(
|
grid_card(
|
||||||
&self.entry,
|
&self.entry,
|
||||||
|
|
@ -358,6 +378,8 @@ impl Component for OptionalModCard {
|
||||||
icon,
|
icon,
|
||||||
self.on_toggle.clone(),
|
self.on_toggle.clone(),
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
|
hovered,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
141
packages/oneclient_app/src/components/package_delete_dialog.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
|
|
||||||
|
pub fn use_shared_delete(
|
||||||
|
cluster_id: i64,
|
||||||
|
delete: impl Into<EventHandler<(String, String)>>,
|
||||||
|
) -> (EventHandler<(String, String)>, Option<Element>) {
|
||||||
|
let mut pending = use_state(|| None::<(String, String, usize)>);
|
||||||
|
let delete = delete.into();
|
||||||
|
|
||||||
|
let request = {
|
||||||
|
let delete = delete.clone();
|
||||||
|
EventHandler::new_current(move |(name, hash): (String, String)| {
|
||||||
|
let delete = delete.clone();
|
||||||
|
spawn(async move {
|
||||||
|
let clusters = match crate::launcher::state() {
|
||||||
|
Ok(state) => oneclient_core::clusters_sharing_artifact(
|
||||||
|
cluster_id,
|
||||||
|
&hash,
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
tracing::warn!(%err, "could not check whether this package is shared");
|
||||||
|
None
|
||||||
|
}),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
match clusters {
|
||||||
|
Some(clusters) if clusters > 1 => pending.set(Some((name, hash, clusters))),
|
||||||
|
_ => delete.call((name, hash)),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let dialog = pending.read().clone().map(|(name, hash, clusters)| {
|
||||||
|
SharedPackageDeleteDialog {
|
||||||
|
name: name.clone(),
|
||||||
|
clusters,
|
||||||
|
on_cancel: (move |()| pending.set(None)).into(),
|
||||||
|
on_confirm: (move |()| {
|
||||||
|
delete.call((name.clone(), hash.clone()));
|
||||||
|
pending.set(None);
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
(request, dialog)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct SharedPackageDeleteDialog {
|
||||||
|
name: String,
|
||||||
|
clusters: usize,
|
||||||
|
on_cancel: EventHandler<()>,
|
||||||
|
on_confirm: EventHandler<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for SharedPackageDeleteDialog {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let title = format!("Delete {} from every cluster?", self.name);
|
||||||
|
let body = format!(
|
||||||
|
"Resource packs and shaders are shared, so this deletes it from all {} clusters that use it.",
|
||||||
|
self.clusters
|
||||||
|
);
|
||||||
|
let close = self.on_cancel.clone();
|
||||||
|
let cancel = self.on_cancel.clone();
|
||||||
|
let confirm = self.on_confirm.clone();
|
||||||
|
|
||||||
|
OverlayPopup::new().on_close(move |_| close.call(())).child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::px(440.))
|
||||||
|
.max_width(Size::window_percent(90.))
|
||||||
|
.spacing(14.)
|
||||||
|
.padding(Gaps::new_all(20.))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.background(CARD_BG)
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(10.)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::AlertTriangle)
|
||||||
|
.size(20.)
|
||||||
|
.color(colors::code_warn()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(title)
|
||||||
|
.font_size(16.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(body)
|
||||||
|
.font_size(12.)
|
||||||
|
.width(Size::fill())
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::End)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.on_press(move |_| cancel.call(()))
|
||||||
|
.text("Cancel"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.danger()
|
||||||
|
.on_press(move |_| confirm.call(()))
|
||||||
|
.child(Icon::new(IconType::Trash01).size(14.))
|
||||||
|
.text("Delete"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,12 @@ use freya::router::RouterContext;
|
||||||
use oneclient_content::packages::ProviderId;
|
use oneclient_content::packages::ProviderId;
|
||||||
use oneclient_core::SeenStatus;
|
use oneclient_core::SeenStatus;
|
||||||
|
|
||||||
use crate::components::{Icon, IconType, toggle_controlled};
|
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
||||||
use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation};
|
use crate::essential::EssentialPackage;
|
||||||
|
use crate::hooks::{
|
||||||
|
ClusterAction, EssentialGuardKind, PendingEssential, disable_warnings, loaded_image,
|
||||||
|
use_cached_image, use_cluster_mutation, use_disable_warnings, use_essential_guard,
|
||||||
|
};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::{ImageFallbackExt, border_all_color};
|
use crate::ui::{ImageFallbackExt, border_all_color};
|
||||||
|
|
@ -13,9 +17,12 @@ use crate::utils::format_size;
|
||||||
pub(crate) const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
pub(crate) const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
pub(crate) const CARD_NAME: Color = Color::from_rgb(213, 219, 255);
|
pub(crate) const CARD_NAME: Color = Color::from_rgb(213, 219, 255);
|
||||||
pub(crate) const CARD_H: f32 = 84.;
|
pub(crate) const CARD_H: f32 = 84.;
|
||||||
pub(crate) const CARD_GRID_H: f32 = 148.;
|
pub(crate) const CARD_GRID_H: f32 = 112.;
|
||||||
pub(crate) const GRID_GAP: f32 = 10.;
|
pub(crate) const GRID_GAP: f32 = 12.;
|
||||||
pub(crate) const GRID_MIN_W: f32 = 260.;
|
pub(crate) const GRID_MIN_W: f32 = 290.;
|
||||||
|
const GRID_CARD_PADDING: f32 = 14.;
|
||||||
|
const BADGE_STRIP_H: f32 = 20.;
|
||||||
|
const BADGE_STRIP_LIFT: f32 = 2.;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq)]
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
pub enum CardLayout {
|
pub enum CardLayout {
|
||||||
|
|
@ -37,9 +44,12 @@ pub struct PackageEntry {
|
||||||
pub package_id: String,
|
pub package_id: String,
|
||||||
pub bundle_name: Option<String>,
|
pub bundle_name: Option<String>,
|
||||||
pub provider: ProviderId,
|
pub provider: ProviderId,
|
||||||
|
pub github_hosted: bool,
|
||||||
|
pub github_url: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub file_name: String,
|
pub file_name: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
|
pub version: Option<String>,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub icon_url: Option<String>,
|
pub icon_url: Option<String>,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
|
|
@ -50,10 +60,17 @@ pub struct PackageEntry {
|
||||||
pub manifest_default: bool,
|
pub manifest_default: bool,
|
||||||
/// Private bundle dependency only set for bundle rows
|
/// Private bundle dependency only set for bundle rows
|
||||||
pub hidden: bool,
|
pub hidden: bool,
|
||||||
|
/// False for uninstalled rows of bundles the cluster never took keeps them out of the All tab
|
||||||
|
pub opted_in: bool,
|
||||||
|
pub advanced: bool,
|
||||||
/// Only set for browser-installed content bundle packages use the bundle update flow
|
/// Only set for browser-installed content bundle packages use the bundle update flow
|
||||||
pub update_available: bool,
|
pub update_available: bool,
|
||||||
|
pub shadowed: bool,
|
||||||
|
/// The user's own copy, older than the bundle's the loader picks over it
|
||||||
|
pub outranked: bool,
|
||||||
/// Recency badge state cleared once the user views the list
|
/// Recency badge state cleared once the user views the list
|
||||||
pub seen_status: SeenStatus,
|
pub seen_status: SeenStatus,
|
||||||
|
pub essential: Option<&'static EssentialPackage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageEntry {
|
impl PackageEntry {
|
||||||
|
|
@ -85,6 +102,10 @@ pub struct PackageRow {
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
package_type: &'static str,
|
package_type: &'static str,
|
||||||
layout: CardLayout,
|
layout: CardLayout,
|
||||||
|
on_context: EventHandler<(f32, f32)>,
|
||||||
|
selected: bool,
|
||||||
|
selecting: bool,
|
||||||
|
on_select: EventHandler<()>,
|
||||||
key: DiffKey,
|
key: DiffKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +116,10 @@ impl PackageRow {
|
||||||
cluster_id,
|
cluster_id,
|
||||||
package_type,
|
package_type,
|
||||||
layout: CardLayout::List,
|
layout: CardLayout::List,
|
||||||
|
on_context: (|_| {}).into(),
|
||||||
|
selected: false,
|
||||||
|
selecting: false,
|
||||||
|
on_select: (|()| {}).into(),
|
||||||
key: DiffKey::None,
|
key: DiffKey::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +128,23 @@ impl PackageRow {
|
||||||
self.layout = layout;
|
self.layout = layout;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_context(mut self, on_context: impl Into<EventHandler<(f32, f32)>>) -> Self {
|
||||||
|
self.on_context = on_context.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn selection(
|
||||||
|
mut self,
|
||||||
|
selected: bool,
|
||||||
|
selecting: bool,
|
||||||
|
on_select: impl Into<EventHandler<()>>,
|
||||||
|
) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self.selecting = selecting;
|
||||||
|
self.on_select = on_select.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeyExt for PackageRow {
|
impl KeyExt for PackageRow {
|
||||||
|
|
@ -118,80 +160,227 @@ impl Component for PackageRow {
|
||||||
let package_type = self.package_type;
|
let package_type = self.package_type;
|
||||||
let layout = self.layout;
|
let layout = self.layout;
|
||||||
let cluster = use_cluster_mutation();
|
let cluster = use_cluster_mutation();
|
||||||
let remove_hover = use_state(|| false);
|
let guard = use_essential_guard();
|
||||||
|
let warnings_query = use_disable_warnings();
|
||||||
|
let hovered = use_state(|| false);
|
||||||
|
|
||||||
|
let warning_body = disable_warning_body(&item, disable_warnings(&warnings_query));
|
||||||
|
|
||||||
let icon_size = match layout {
|
let icon_size = match layout {
|
||||||
CardLayout::List => 44.,
|
CardLayout::List => 44.,
|
||||||
CardLayout::Grid => 52.,
|
CardLayout::Grid => 40.,
|
||||||
};
|
};
|
||||||
let icon_query = use_cached_image(item.icon_url.clone(), 256);
|
let icon_query = use_cached_image(item.icon_url.clone(), 256);
|
||||||
let icon = package_icon(&item, &icon_query, icon_size);
|
let icon = package_icon(&item, &icon_query, icon_size);
|
||||||
|
|
||||||
let on_toggle: EventHandler<()> = {
|
let on_toggle: EventHandler<()> = {
|
||||||
let hash = item.hash.clone();
|
let action = toggle_action(&item, cluster_id, !item.enabled);
|
||||||
let bundle_name = item.bundle_name.clone();
|
|
||||||
let package_id = item.package_id.clone();
|
|
||||||
let enabled_now = item.enabled;
|
let enabled_now = item.enabled;
|
||||||
let manifest_default = item.manifest_default;
|
let name = item.name.clone();
|
||||||
|
let mut guard = guard;
|
||||||
(move |()| {
|
(move |()| {
|
||||||
if let Some(h) = &hash {
|
let Some(action) = action.clone() else {
|
||||||
cluster.mutate(ClusterAction::SetArtifactEnabled {
|
return;
|
||||||
cluster_id,
|
};
|
||||||
hash: h.clone(),
|
|
||||||
enabled: !enabled_now,
|
match warning_body.clone().filter(|_| enabled_now) {
|
||||||
});
|
Some(body) => guard.set(Some(PendingEssential {
|
||||||
} else if let Some(bundle) = &bundle_name {
|
name: name.clone(),
|
||||||
cluster.mutate(ClusterAction::SetBundlePackageEnabled {
|
body,
|
||||||
cluster_id,
|
kind: EssentialGuardKind::Disable,
|
||||||
bundle_name: bundle.clone(),
|
action,
|
||||||
package_id: package_id.clone(),
|
})),
|
||||||
enabled: !enabled_now,
|
None => cluster.mutate(action),
|
||||||
manifest_default,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
match layout {
|
let on_context = Some(self.on_context.clone());
|
||||||
CardLayout::List => {
|
|
||||||
let removable = !item.in_bundle();
|
let (card, radius) = match layout {
|
||||||
let can_remove = removable && item.installed;
|
CardLayout::List => (
|
||||||
let rm_hash = item.hash.clone();
|
|
||||||
let on_remove = move || {
|
|
||||||
if let Some(h) = &rm_hash {
|
|
||||||
cluster.mutate(ClusterAction::RemoveArtifact {
|
|
||||||
cluster_id,
|
|
||||||
hash: h.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
list_card(
|
list_card(
|
||||||
&item,
|
&item,
|
||||||
package_type,
|
package_type,
|
||||||
cluster_id,
|
cluster_id,
|
||||||
icon,
|
icon,
|
||||||
on_toggle,
|
on_toggle,
|
||||||
can_remove,
|
on_context.clone(),
|
||||||
on_remove,
|
),
|
||||||
remove_hover,
|
8.,
|
||||||
|
),
|
||||||
|
CardLayout::Grid => (
|
||||||
|
grid_card(
|
||||||
|
&item,
|
||||||
|
package_type,
|
||||||
|
cluster_id,
|
||||||
|
icon,
|
||||||
|
on_toggle,
|
||||||
|
true,
|
||||||
|
on_context.clone(),
|
||||||
|
hovered,
|
||||||
|
),
|
||||||
|
6.,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_select = self.on_select.clone();
|
||||||
|
let overlay = self.selecting.then(|| {
|
||||||
|
rect()
|
||||||
|
.position(Position::new_absolute().top(0.).left(0.))
|
||||||
|
.width(Size::percent(100.))
|
||||||
|
.height(Size::percent(100.))
|
||||||
|
.layer(Layer::Relative(16))
|
||||||
|
.corner_radius(CornerRadius::new_all(radius))
|
||||||
|
.maybe(self.selected, |el| {
|
||||||
|
el.background(colors::brand().with_a(28)).border(
|
||||||
|
border_all_color(2., colors::brand()).alignment(BorderAlignment::Inner),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_press(move |_| on_select.call(()))
|
||||||
|
.on_secondary_down(on_secondary(on_context))
|
||||||
|
.into_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.child(card)
|
||||||
|
.maybe_child(overlay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn toggle_action(
|
||||||
|
item: &PackageEntry,
|
||||||
|
cluster_id: i64,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Option<ClusterAction> {
|
||||||
|
set_enabled_action(
|
||||||
|
cluster_id,
|
||||||
|
item.hash.as_deref(),
|
||||||
|
item.bundle_name.as_deref(),
|
||||||
|
&item.package_id,
|
||||||
|
item.manifest_default,
|
||||||
|
enabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CardLayout::Grid => grid_card(&item, package_type, cluster_id, icon, on_toggle, true),
|
|
||||||
|
pub(crate) fn set_enabled_action(
|
||||||
|
cluster_id: i64,
|
||||||
|
hash: Option<&str>,
|
||||||
|
bundle_name: Option<&str>,
|
||||||
|
package_id: &str,
|
||||||
|
manifest_default: bool,
|
||||||
|
enabled: bool,
|
||||||
|
) -> Option<ClusterAction> {
|
||||||
|
if let Some(hash) = hash {
|
||||||
|
Some(ClusterAction::SetArtifactEnabled {
|
||||||
|
cluster_id,
|
||||||
|
hash: hash.to_string(),
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
bundle_name.map(|bundle_name| ClusterAction::SetBundlePackageEnabled {
|
||||||
|
cluster_id,
|
||||||
|
bundle_name: bundle_name.to_string(),
|
||||||
|
package_id: package_id.to_string(),
|
||||||
|
enabled,
|
||||||
|
manifest_default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable_warning_body(
|
||||||
|
item: &PackageEntry,
|
||||||
|
warnings: Option<oneclient_core::DisableWarnings>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let bundled = item.in_bundle() && item.provider == ProviderId::Modrinth;
|
||||||
|
|
||||||
|
match warnings {
|
||||||
|
Some(warnings) if bundled => warnings.body_for(&item.package_id).map(str::to_string),
|
||||||
|
_ => item
|
||||||
|
.essential
|
||||||
|
.map(|package| package.disable_body.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn package_context_menu(
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
item: &PackageEntry,
|
||||||
|
cluster_id: i64,
|
||||||
|
package_type: &'static str,
|
||||||
|
on_delete: EventHandler<(String, String)>,
|
||||||
|
on_select: EventHandler<()>,
|
||||||
|
) -> ContextMenu {
|
||||||
|
let mut menu = ContextMenu::new(x, y).title(item.name.clone()).action(
|
||||||
|
IconType::Check,
|
||||||
|
"Select",
|
||||||
|
on_select,
|
||||||
|
);
|
||||||
|
|
||||||
|
if item.is_remote() {
|
||||||
|
let provider = item.provider;
|
||||||
|
let package_id = item.package_id.clone();
|
||||||
|
let package_type = package_type.to_string();
|
||||||
|
menu = menu.action(IconType::LinkExternal01, "View in browser", move |()| {
|
||||||
|
let _ = RouterContext::get().push(Route::BrowserPackage {
|
||||||
|
cluster_id,
|
||||||
|
package_type: package_type.clone(),
|
||||||
|
package_id: format!("{}:{}", provider as u8, package_id),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(url) = item.github_url.clone() {
|
||||||
|
menu = menu.action(IconType::LinkExternal01, "View in browser", move |()| {
|
||||||
|
crate::platform::open_url(&url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hash) = item.hash.clone() {
|
||||||
|
menu = menu.action(
|
||||||
|
IconType::Folder,
|
||||||
|
"View in folder",
|
||||||
|
EventHandler::new_current(move |()| reveal_in_store(hash.clone())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.installed && !item.in_bundle() {
|
||||||
|
let hash = item.hash.clone();
|
||||||
|
let name = item.name.clone();
|
||||||
|
menu = menu
|
||||||
|
.separator()
|
||||||
|
.danger_action(IconType::Trash01, "Delete", move |()| {
|
||||||
|
if let Some(hash) = &hash {
|
||||||
|
on_delete.call((name.clone(), hash.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menu
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn on_secondary(
|
||||||
|
handler: Option<EventHandler<(f32, f32)>>,
|
||||||
|
) -> impl FnMut(Event<PressEventData>) {
|
||||||
|
move |e: Event<PressEventData>| {
|
||||||
|
if let (Some(handler), PressEventData::Mouse(m)) = (handler.as_ref(), e.data()) {
|
||||||
|
e.stop_propagation();
|
||||||
|
handler.call((m.global_location.x as f32, m.global_location.y as f32));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn list_card(
|
fn list_card(
|
||||||
item: &PackageEntry,
|
item: &PackageEntry,
|
||||||
package_type: &'static str,
|
package_type: &'static str,
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
icon: impl IntoElement,
|
icon: impl IntoElement,
|
||||||
on_toggle: EventHandler<()>,
|
on_toggle: EventHandler<()>,
|
||||||
can_remove: bool,
|
on_context: Option<EventHandler<(f32, f32)>>,
|
||||||
on_remove: impl FnMut() + 'static,
|
|
||||||
hovering: State<bool>,
|
|
||||||
) -> Element {
|
) -> Element {
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
|
|
@ -203,13 +392,15 @@ fn list_card(
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
.background(CARD_BG)
|
.background(CARD_BG)
|
||||||
.content(Content::Flex)
|
.content(Content::Flex)
|
||||||
|
.on_secondary_down(on_secondary(on_context.clone()))
|
||||||
.child(package_info(item, package_type, cluster_id, icon))
|
.child(package_info(item, package_type, cluster_id, icon))
|
||||||
.child(meta_size(item.size))
|
.child(meta_size(item.size))
|
||||||
.child(toggle_controlled(item.enabled, on_toggle))
|
.child(toggle_controlled(item.enabled, on_toggle))
|
||||||
.child(remove_button(can_remove, on_remove, hovering))
|
.maybe_child(on_context.map(kebab_button))
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn grid_card(
|
pub(crate) fn grid_card(
|
||||||
item: &PackageEntry,
|
item: &PackageEntry,
|
||||||
package_type: &'static str,
|
package_type: &'static str,
|
||||||
|
|
@ -217,46 +408,34 @@ pub(crate) fn grid_card(
|
||||||
icon: impl IntoElement,
|
icon: impl IntoElement,
|
||||||
on_toggle: EventHandler<()>,
|
on_toggle: EventHandler<()>,
|
||||||
navigable: bool, // if true, it takes the user to the mod page
|
navigable: bool, // if true, it takes the user to the mod page
|
||||||
|
on_context: Option<EventHandler<(f32, f32)>>,
|
||||||
|
mut hovered: State<bool>,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let remote = item.is_remote();
|
|
||||||
let enabled = item.enabled;
|
let enabled = item.enabled;
|
||||||
let title = item.name.clone();
|
let title = item.name.clone();
|
||||||
|
let hovering = *hovered.read();
|
||||||
|
|
||||||
let (bg, border, content_alpha) = if enabled {
|
let bg = match (enabled, hovering) {
|
||||||
(colors::brand().with_a(38), colors::brand(), 255)
|
(_, true) => colors::component_bg_hover(),
|
||||||
} else {
|
(true, false) => colors::component_bg(),
|
||||||
(CARD_BG, colors::component_border(), 140)
|
(false, false) => colors::component_bg_disabled(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let badge = if remote && navigable {
|
let alpha = if enabled { 255u8 } else { 115u8 };
|
||||||
let provider = item.provider;
|
|
||||||
let package_id = item.package_id.clone();
|
let border = if !hovering {
|
||||||
let package_type_owned = package_type.to_string();
|
colors::component_border()
|
||||||
rect()
|
|
||||||
.cursor(CursorIcon::Pointer)
|
|
||||||
.on_press(move |e: Event<PressEventData>| {
|
|
||||||
e.stop_propagation();
|
|
||||||
let _ = RouterContext::get().push(Route::BrowserPackage {
|
|
||||||
cluster_id,
|
|
||||||
package_type: package_type_owned.clone(),
|
|
||||||
package_id: format!("{}:{}", provider as u8, package_id),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.child(provider_badge(item.provider))
|
|
||||||
.into_element()
|
|
||||||
} else if remote {
|
|
||||||
provider_badge(item.provider)
|
|
||||||
} else {
|
} else {
|
||||||
local_badge()
|
colors::component_border_hover()
|
||||||
};
|
};
|
||||||
|
|
||||||
let header = rect()
|
let header = rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.cross_align(Alignment::Center)
|
// .cross_align(Alignment::Center)
|
||||||
.spacing(10.)
|
.spacing(11.)
|
||||||
.content(Content::Flex)
|
.content(Content::Flex)
|
||||||
.child(icon)
|
.child(rect().opacity(alpha as f32 / 255.).child(icon))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
@ -266,62 +445,68 @@ pub(crate) fn grid_card(
|
||||||
label()
|
label()
|
||||||
.text(title)
|
.text(title)
|
||||||
.font_size(14.)
|
.font_size(14.)
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
.max_lines(1)
|
.max_lines(1)
|
||||||
.text_overflow(TextOverflow::Ellipsis)
|
.text_overflow(TextOverflow::Ellipsis)
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.color(CARD_NAME.with_a(content_alpha)),
|
.color(if enabled {
|
||||||
|
Color::WHITE
|
||||||
|
} else {
|
||||||
|
CARD_NAME.with_a(alpha)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.maybe(!item.author.is_empty(), |el| {
|
.child(grid_meta(item, package_type, cluster_id, navigable, alpha)),
|
||||||
el.child(
|
|
||||||
label()
|
|
||||||
.text(format!("by {}", item.author))
|
|
||||||
.font_size(10.)
|
|
||||||
.max_lines(1)
|
|
||||||
.text_overflow(TextOverflow::Ellipsis)
|
|
||||||
.width(Size::fill())
|
|
||||||
.color(colors::fg_secondary().with_a(content_alpha)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.horizontal()
|
|
||||||
.width(Size::fill())
|
|
||||||
.cross_align(Alignment::Center)
|
|
||||||
.spacing(6.)
|
|
||||||
.content(Content::wrap_spacing(4.))
|
|
||||||
.child(badge)
|
|
||||||
.maybe_child(item.is_outdated().then(outdated_badge))
|
|
||||||
.maybe_child(item.recency_badge()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.maybe_child(on_context.clone().map(kebab_button))
|
||||||
.into_element();
|
.into_element();
|
||||||
|
|
||||||
let description = if item.description.is_empty() {
|
let description = (!item.description.is_empty()).then(|| {
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(
|
|
||||||
label()
|
label()
|
||||||
.text(item.description.clone())
|
.text(item.description.clone())
|
||||||
.font_size(11.)
|
.font_size(11.)
|
||||||
.max_lines(3)
|
.line_height(1.45)
|
||||||
|
.max_lines(2)
|
||||||
|
.text_overflow(TextOverflow::Ellipsis)
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.color(colors::fg_secondary().with_a(content_alpha))
|
.color(colors::fg_primary().with_a(scale_a(alpha, 0.72)))
|
||||||
.into_element(),
|
.into_element()
|
||||||
|
});
|
||||||
|
|
||||||
|
let badged =
|
||||||
|
item.is_outdated() || item.shadowed || item.outranked || item.recency_badge().is_some();
|
||||||
|
let floating = badged.then(|| {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(BADGE_STRIP_H))
|
||||||
|
.position(
|
||||||
|
Position::new_absolute()
|
||||||
|
.bottom(-(GRID_CARD_PADDING + BADGE_STRIP_H / 2.) + BADGE_STRIP_LIFT),
|
||||||
)
|
)
|
||||||
};
|
.main_align(Alignment::End)
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(4.)
|
||||||
|
.maybe_child(item.is_outdated().then(outdated_badge))
|
||||||
|
.maybe_child(item.shadowed.then(shadowed_badge))
|
||||||
|
.maybe_child(item.outranked.then(outranked_badge))
|
||||||
|
.maybe_child(item.recency_badge())
|
||||||
|
.into_element()
|
||||||
|
});
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.spacing(8.)
|
.spacing(9.)
|
||||||
.padding(Gaps::new_all(12.))
|
.padding(Gaps::new_all(GRID_CARD_PADDING))
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
.background(bg)
|
.background(bg.with_a(alpha))
|
||||||
.border(border_all_color(1.5, border))
|
.border(border_all_color(1., border))
|
||||||
.content(Content::Flex)
|
.content(Content::Flex)
|
||||||
.cursor(CursorIcon::Pointer)
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_pointer_enter(move |_| hovered.set(true))
|
||||||
|
.on_pointer_leave(move |_| hovered.set(false))
|
||||||
|
.on_secondary_down(on_secondary(on_context))
|
||||||
.on_press(move |_| on_toggle.call(()))
|
.on_press(move |_| on_toggle.call(()))
|
||||||
.child(header)
|
.child(header)
|
||||||
.child(
|
.child(
|
||||||
|
|
@ -329,11 +514,200 @@ pub(crate) fn grid_card(
|
||||||
.vertical()
|
.vertical()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::flex(1.0))
|
.height(Size::flex(1.0))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
.maybe_child(description),
|
.maybe_child(description),
|
||||||
)
|
)
|
||||||
|
.maybe_child(floating)
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scale_a(alpha: u8, factor: f32) -> u8 {
|
||||||
|
(alpha as f32 * factor) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
fn grid_meta(
|
||||||
|
item: &PackageEntry,
|
||||||
|
package_type: &'static str,
|
||||||
|
cluster_id: i64,
|
||||||
|
navigable: bool,
|
||||||
|
alpha: u8,
|
||||||
|
) -> Element {
|
||||||
|
let muted = CARD_NAME.with_a(scale_a(alpha, 0.5));
|
||||||
|
|
||||||
|
let source = if let Some(url) = item.github_url.clone() {
|
||||||
|
SourceLink {
|
||||||
|
text: "GitHub".to_string(),
|
||||||
|
on_press: EventHandler::new(move |()| crate::platform::open_url(&url)),
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
} else if item.github_hosted {
|
||||||
|
meta_text("GitHub".to_string(), muted)
|
||||||
|
} else if item.is_remote() && navigable {
|
||||||
|
let provider = item.provider;
|
||||||
|
let package_id = item.package_id.clone();
|
||||||
|
SourceLink {
|
||||||
|
text: provider.to_string(),
|
||||||
|
on_press: EventHandler::new(move |()| {
|
||||||
|
let _ = RouterContext::get().push(Route::BrowserPackage {
|
||||||
|
cluster_id,
|
||||||
|
package_type: package_type.to_string(),
|
||||||
|
package_id: format!("{}:{}", provider as u8, package_id),
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
alpha,
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
} else if item.is_remote() {
|
||||||
|
meta_text(item.provider.to_string(), muted)
|
||||||
|
} else {
|
||||||
|
meta_text("Local file".to_string(), muted)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut parts: Vec<Element> = Vec::new();
|
||||||
|
if !item.author.is_empty() {
|
||||||
|
parts.push(meta_text(format!("by {}", item.author), muted));
|
||||||
|
}
|
||||||
|
if let Some(version) = &item.version {
|
||||||
|
parts.push(meta_text(version.clone(), muted));
|
||||||
|
}
|
||||||
|
parts.push(source);
|
||||||
|
|
||||||
|
let mut row = rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(5.)
|
||||||
|
.overflow(Overflow::Clip);
|
||||||
|
|
||||||
|
for (i, part) in parts.into_iter().enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
row = row.child(meta_text("\u{b7}".to_string(), muted));
|
||||||
|
}
|
||||||
|
row = row.child(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
row.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct SourceLink {
|
||||||
|
text: String,
|
||||||
|
on_press: EventHandler<()>,
|
||||||
|
alpha: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for SourceLink {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut hovered = use_state(|| false);
|
||||||
|
let mut pressed = use_state(|| false);
|
||||||
|
|
||||||
|
let color = if *pressed.read() {
|
||||||
|
colors::fg_primary_pressed()
|
||||||
|
} else if *hovered.read() {
|
||||||
|
colors::fg_primary_hover()
|
||||||
|
} else {
|
||||||
|
CARD_NAME.with_a(scale_a(self.alpha, 0.68))
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_press = self.on_press.clone();
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_pointer_enter(move |_| hovered.set(true))
|
||||||
|
.on_pointer_leave(move |_| {
|
||||||
|
hovered.set(false);
|
||||||
|
pressed.set(false);
|
||||||
|
})
|
||||||
|
.on_pointer_down(move |_| pressed.set(true))
|
||||||
|
.on_press(move |e: Event<PressEventData>| {
|
||||||
|
e.stop_propagation();
|
||||||
|
pressed.set(false);
|
||||||
|
on_press.call(());
|
||||||
|
})
|
||||||
|
.child(meta_text(self.text.clone(), color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn meta_text(text: String, color: Color) -> Element {
|
||||||
|
label()
|
||||||
|
.text(text)
|
||||||
|
.font_size(11.)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(color)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn kebab_button(on_context: EventHandler<(f32, f32)>) -> Element {
|
||||||
|
KebabButton { on_context }.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct KebabButton {
|
||||||
|
on_context: EventHandler<(f32, f32)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for KebabButton {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut hovered = use_state(|| false);
|
||||||
|
let on_context = self.on_context.clone();
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.center()
|
||||||
|
.width(Size::px(26.))
|
||||||
|
.height(Size::px(26.))
|
||||||
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
|
.background(if *hovered.read() {
|
||||||
|
colors::ghost_overlay_hover()
|
||||||
|
} else {
|
||||||
|
Color::TRANSPARENT
|
||||||
|
})
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
.on_pointer_enter(move |_| hovered.set(true))
|
||||||
|
.on_pointer_leave(move |_| hovered.set(false))
|
||||||
|
.on_press(move |e: Event<PressEventData>| {
|
||||||
|
e.stop_propagation();
|
||||||
|
if let PressEventData::Mouse(m) = e.data() {
|
||||||
|
on_context.call((m.global_location.x as f32, m.global_location.y as f32));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::DotsVertical)
|
||||||
|
.size(16.)
|
||||||
|
.color(if *hovered.read() {
|
||||||
|
colors::fg_primary()
|
||||||
|
} else {
|
||||||
|
colors::fg_secondary()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reveal_in_store(hash: String) {
|
||||||
|
spawn(async move {
|
||||||
|
let Ok(state) = crate::launcher::state() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let row = match oneclient_db::dao::artifact::get_artifact_by_hash(&state.services.db, &hash)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Some(row)) => row,
|
||||||
|
Ok(None) => return,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(%err, "failed to look up artifact for reveal");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Ok(path) = oneclient_content::packages::store::artifact_absolute_path(&row.path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
crate::platform::open_path(&dir.to_string_lossy());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn package_info(
|
fn package_info(
|
||||||
item: &PackageEntry,
|
item: &PackageEntry,
|
||||||
package_type: &'static str,
|
package_type: &'static str,
|
||||||
|
|
@ -383,12 +757,25 @@ fn package_info(
|
||||||
.max_width(Size::percent(60.))
|
.max_width(Size::percent(60.))
|
||||||
.color(CARD_NAME),
|
.color(CARD_NAME),
|
||||||
)
|
)
|
||||||
.child(if remote {
|
.child(if let Some(url) = item.github_url.clone() {
|
||||||
|
rect()
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_press(move |e: Event<PressEventData>| {
|
||||||
|
e.stop_propagation();
|
||||||
|
crate::platform::open_url(&url);
|
||||||
|
})
|
||||||
|
.child(github_badge())
|
||||||
|
.into_element()
|
||||||
|
} else if item.github_hosted {
|
||||||
|
github_badge()
|
||||||
|
} else if remote {
|
||||||
provider_badge(item.provider)
|
provider_badge(item.provider)
|
||||||
} else {
|
} else {
|
||||||
local_badge()
|
local_badge()
|
||||||
})
|
})
|
||||||
.maybe_child(item.is_outdated().then(outdated_badge))
|
.maybe_child(item.is_outdated().then(outdated_badge))
|
||||||
|
.maybe_child(item.shadowed.then(shadowed_badge))
|
||||||
|
.maybe_child(item.outranked.then(outranked_badge))
|
||||||
.maybe_child(item.recency_badge()),
|
.maybe_child(item.recency_badge()),
|
||||||
)
|
)
|
||||||
.maybe(!item.author.is_empty(), |el| {
|
.maybe(!item.author.is_empty(), |el| {
|
||||||
|
|
@ -422,8 +809,15 @@ pub(crate) fn package_icon(
|
||||||
icon_query: &freya::query::UseQuery<crate::hooks::CachedImageQuery>,
|
icon_query: &freya::query::UseQuery<crate::hooks::CachedImageQuery>,
|
||||||
size: f32,
|
size: f32,
|
||||||
) -> Element {
|
) -> Element {
|
||||||
let icon_url = &item.icon_url;
|
remote_icon(item.icon_url.as_deref(), icon_query, size)
|
||||||
let loaded = loaded_image(icon_url.as_deref(), icon_query);
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remote_icon(
|
||||||
|
icon_url: Option<&str>,
|
||||||
|
icon_query: &freya::query::UseQuery<crate::hooks::CachedImageQuery>,
|
||||||
|
size: f32,
|
||||||
|
) -> Element {
|
||||||
|
let loaded = loaded_image(icon_url, icon_query);
|
||||||
|
|
||||||
match loaded {
|
match loaded {
|
||||||
Some((url, bytes)) => ImageViewer::new((url, bytes))
|
Some((url, bytes)) => ImageViewer::new((url, bytes))
|
||||||
|
|
@ -439,7 +833,7 @@ pub(crate) fn package_icon(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_box(icon: IconType, size: f32) -> Element {
|
pub(crate) fn icon_box(icon: IconType, size: f32) -> Element {
|
||||||
rect()
|
rect()
|
||||||
.center()
|
.center()
|
||||||
.width(Size::px(size))
|
.width(Size::px(size))
|
||||||
|
|
@ -454,7 +848,7 @@ fn icon_box(icon: IconType, size: f32) -> Element {
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn meta_size(size: u64) -> impl IntoElement {
|
pub(crate) fn meta_size(size: u64) -> impl IntoElement {
|
||||||
rect()
|
rect()
|
||||||
.maybe_child((size > 0).then(|| {
|
.maybe_child((size > 0).then(|| {
|
||||||
label()
|
label()
|
||||||
|
|
@ -472,37 +866,56 @@ pub fn provider_badge(provider: ProviderId) -> Element {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn outdated_badge() -> Element {
|
pub fn github_badge() -> Element {
|
||||||
accent_badge(
|
badge(
|
||||||
Icon::new(IconType::RefreshCw01)
|
Icon::new(IconType::Github).size(12.).into_element(),
|
||||||
.size(12.)
|
"GitHub".to_string(),
|
||||||
.color(colors::brand())
|
|
||||||
.into_element(),
|
|
||||||
"Update available".to_string(),
|
|
||||||
colors::brand(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn outdated_badge() -> Element {
|
||||||
|
status_tag("Update available", colors::brand())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shadowed_badge() -> Element {
|
||||||
|
status_tag("Using your copy", colors::brand())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn outranked_badge() -> Element {
|
||||||
|
status_tag("Using bundle's newer copy", colors::brand())
|
||||||
|
}
|
||||||
|
|
||||||
fn new_badge() -> Element {
|
fn new_badge() -> Element {
|
||||||
accent_badge(
|
status_tag("New", colors::success())
|
||||||
Icon::new(IconType::Plus)
|
|
||||||
.size(12.)
|
|
||||||
.color(colors::success())
|
|
||||||
.into_element(),
|
|
||||||
"New".to_string(),
|
|
||||||
colors::success(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn updated_badge() -> Element {
|
fn updated_badge() -> Element {
|
||||||
accent_badge(
|
status_tag("Updated", colors::success())
|
||||||
Icon::new(IconType::RefreshCcw02)
|
}
|
||||||
.size(12.)
|
|
||||||
.color(colors::success())
|
fn status_tag(text: &'static str, accent: Color) -> Element {
|
||||||
.into_element(),
|
rect()
|
||||||
"Updated".to_string(),
|
.horizontal()
|
||||||
colors::success(),
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(5.)
|
||||||
|
.padding(Gaps::new(2., 6., 2., 6.))
|
||||||
|
.corner_radius(CornerRadius::new_all(4.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::px(6.))
|
||||||
|
.height(Size::px(6.))
|
||||||
|
.corner_radius(CornerRadius::new_all(3.))
|
||||||
|
.background(accent),
|
||||||
)
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(text)
|
||||||
|
.font_size(11.)
|
||||||
|
.font_weight(FontWeight::MEDIUM)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_badge() -> Element {
|
fn local_badge() -> Element {
|
||||||
|
|
@ -515,11 +928,15 @@ fn local_badge() -> Element {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn badge(icon: impl IntoElement, text: String) -> Element {
|
pub(crate) fn badge(icon: impl IntoElement, text: String) -> Element {
|
||||||
accent_badge(icon, text, colors::fg_secondary())
|
accent_badge(icon, text, colors::fg_secondary())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn accent_badge(icon: impl IntoElement, text: String, accent: Color) -> Element {
|
fn accent_badge(icon: impl IntoElement, text: String, accent: Color) -> Element {
|
||||||
|
pill(Some(icon.into_element()), text, accent)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pill(icon: Option<Element>, text: String, accent: Color) -> Element {
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
|
|
@ -528,7 +945,7 @@ fn accent_badge(icon: impl IntoElement, text: String, accent: Color) -> Element
|
||||||
.corner_radius(CornerRadius::new_all(999.))
|
.corner_radius(CornerRadius::new_all(999.))
|
||||||
.border(border_all_color(1., colors::component_border()))
|
.border(border_all_color(1., colors::component_border()))
|
||||||
.background(colors::component_bg())
|
.background(colors::component_bg())
|
||||||
.child(icon)
|
.maybe_child(icon)
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(text)
|
.text(text)
|
||||||
|
|
@ -538,37 +955,3 @@ fn accent_badge(icon: impl IntoElement, text: String, accent: Color) -> Element
|
||||||
)
|
)
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_button(
|
|
||||||
enabled: bool,
|
|
||||||
mut on_remove: impl FnMut() + 'static,
|
|
||||||
mut hovering: State<bool>,
|
|
||||||
) -> impl IntoElement {
|
|
||||||
let hot = enabled && *hovering.read();
|
|
||||||
let color = if !enabled {
|
|
||||||
colors::fg_secondary().with_a(90)
|
|
||||||
} else if hot {
|
|
||||||
colors::danger()
|
|
||||||
} else {
|
|
||||||
colors::fg_secondary()
|
|
||||||
};
|
|
||||||
|
|
||||||
rect()
|
|
||||||
.center()
|
|
||||||
.width(Size::px(30.))
|
|
||||||
.height(Size::px(30.))
|
|
||||||
.corner_radius(CornerRadius::new_all(7.))
|
|
||||||
.background(if hot {
|
|
||||||
colors::danger().with_a(30)
|
|
||||||
} else {
|
|
||||||
Color::TRANSPARENT
|
|
||||||
})
|
|
||||||
.maybe(enabled, |el| {
|
|
||||||
el.cursor(CursorIcon::Pointer)
|
|
||||||
.on_pointer_enter(move |_| hovering.set(true))
|
|
||||||
.on_pointer_leave(move |_| hovering.set(false))
|
|
||||||
.on_press(move |_| on_remove())
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Trash01).size(14.).color(color))
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,9 @@ use oneclient_content::packages::{CachedPackageMeta, ProviderId};
|
||||||
use oneclient_core::{BrowserPackageUpdate, ProfileUpdate};
|
use oneclient_core::{BrowserPackageUpdate, ProfileUpdate};
|
||||||
use oneclient_db::models::ClusterId;
|
use oneclient_db::models::ClusterId;
|
||||||
|
|
||||||
use crate::components::{Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea, checkbox_labeled};
|
use crate::components::{
|
||||||
|
Button, Dropdown, Icon, IconType, OverlayPopup, ScrollArea, checkbox_labeled,
|
||||||
|
};
|
||||||
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,
|
||||||
};
|
};
|
||||||
|
|
@ -210,9 +212,7 @@ fn content(
|
||||||
.main_align(Alignment::Start)
|
.main_align(Alignment::Start)
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(6.)
|
.spacing(6.)
|
||||||
.child(
|
.child(checkbox_labeled(dont_show_again, "Don't show again?")),
|
||||||
checkbox_labeled(dont_show_again, "Don't show again?")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -267,7 +267,7 @@ fn content(
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::DownloadCloud02).size(15.))
|
.child(Icon::new(IconType::DownloadCloud02).size(15.))
|
||||||
.text("Proceed"),
|
.text("Proceed"),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,8 @@ impl Component for Pagination {
|
||||||
let current = *page.read();
|
let current = *page.read();
|
||||||
let last = total_pages.saturating_sub(1);
|
let last = total_pages.saturating_sub(1);
|
||||||
|
|
||||||
let icon_btn = move |target: usize,
|
let icon_btn =
|
||||||
available: bool,
|
move |target: usize, available: bool, icon: IconType, tooltip: &'static str| {
|
||||||
icon: IconType,
|
|
||||||
tooltip: &'static str| {
|
|
||||||
let mut page = page;
|
let mut page = page;
|
||||||
let enabled = available && interactive;
|
let enabled = available && interactive;
|
||||||
Button::new()
|
Button::new()
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ impl Component for PlayerModel {
|
||||||
|
|
||||||
let effect = player_effect();
|
let effect = player_effect();
|
||||||
|
|
||||||
let render_cb = RenderCallback::new(move |ctx: &mut CanvasContext| {
|
let render_cb = RenderCallback::new(move |ctx: &mut FillRenderContext| {
|
||||||
let (Some(effect), Some(skin)) = (&effect, &skin_shader) else {
|
let (Some(effect), Some(skin)) = (&effect, &skin_shader) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -159,18 +159,12 @@ impl Component for PlayerModel {
|
||||||
ctx.canvas.draw_rect(SkRect::new(0.0, 0.0, w, h), &paint);
|
ctx.canvas.draw_rect(SkRect::new(0.0, 0.0, w, h), &paint);
|
||||||
});
|
});
|
||||||
|
|
||||||
// `canvas` carries no style data, so the cursor rides on a wrapper `rect` that
|
|
||||||
// covers exactly the same area
|
|
||||||
rect()
|
rect()
|
||||||
|
.key(src_ptr as u64)
|
||||||
.width(self.width.clone())
|
.width(self.width.clone())
|
||||||
.height(self.height.clone())
|
.height(self.height.clone())
|
||||||
.cursor(CursorIcon::Grab)
|
.cursor(CursorIcon::Grab)
|
||||||
.child(
|
.background(render_cb)
|
||||||
canvas(render_cb)
|
|
||||||
.key(src_ptr as u64)
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill()),
|
|
||||||
)
|
|
||||||
.on_pointer_down(move |e: Event<PointerEventData>| {
|
.on_pointer_down(move |e: Event<PointerEventData>| {
|
||||||
let loc = e.global_location();
|
let loc = e.global_location();
|
||||||
drag.set(Some((loc.x as f32, loc.y as f32, yaw(), pitch())));
|
drag.set(Some((loc.x as f32, loc.y as f32, yaw(), pitch())));
|
||||||
|
|
@ -193,7 +187,7 @@ impl Component for PlayerModel {
|
||||||
Platform::get().send(UserEvent::RequestRedraw);
|
Platform::get().send(UserEvent::RequestRedraw);
|
||||||
})
|
})
|
||||||
// Global so a release outside the widget still ends the drag
|
// Global so a release outside the widget still ends the drag
|
||||||
.on_global_pointer_press(move |_: Event<PointerEventData>| {
|
.on_global_pointer_up(move |_: Event<PointerEventData>| {
|
||||||
if drag.peek().is_some() {
|
if drag.peek().is_some() {
|
||||||
drag.set(None);
|
drag.set(None);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use freya::animation::*;
|
use freya::animation::*;
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use freya::router::RouterContext;
|
use freya::router::RouterContext;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
|
|
||||||
use crate::components::{ART_PREVIEW_EDGE, ContextMenu, DynamicArt, Icon, IconType};
|
use freya::elements::image::{AspectRatio, ImageCover, ImageHandle, image};
|
||||||
|
|
||||||
|
use super::dynamic_art::use_art_bytes;
|
||||||
|
use super::local_image::decode;
|
||||||
|
use crate::components::{ART_PREVIEW_EDGE, ContextMenu, DynamicArt, Icon, IconType, LocalImage};
|
||||||
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters};
|
use crate::hooks::{settled_or_loading, use_active_cluster_id, use_clusters};
|
||||||
|
use crate::motion::use_animations_enabled;
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme;
|
use crate::theme;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
@ -20,6 +27,18 @@ const CARD_MS: u64 = 460;
|
||||||
const STAGGER_MS: u64 = 85;
|
const STAGGER_MS: u64 = 85;
|
||||||
const CARD_RISE_PX: f32 = 48.0;
|
const CARD_RISE_PX: f32 = 48.0;
|
||||||
const INTRO_ITEMS: usize = 6;
|
const INTRO_ITEMS: usize = 6;
|
||||||
|
const MODPACK_ICON_PX: f32 = 72.0;
|
||||||
|
const MODPACK_ICON_EDGE: u32 = 192;
|
||||||
|
const MODPACK_BLUR: f32 = 4.0;
|
||||||
|
const MODPACK_TINT: Color = Color::from_argb(40, 21, 28, 34);
|
||||||
|
const MODPACK_BACKDROP: Color = Color::from_rgb(21, 28, 34);
|
||||||
|
const MODPACK_TITLE_INSET: f32 = 8.0;
|
||||||
|
const MODPACK_TITLE_LEFT: f32 = 10.5;
|
||||||
|
const MARQUEE_DELAY: Duration = Duration::from_millis(200);
|
||||||
|
const MARQUEE_HOLD: Duration = Duration::from_millis(700);
|
||||||
|
const MARQUEE_FRAME: Duration = Duration::from_millis(16);
|
||||||
|
const MARQUEE_SPEED_PX_PER_SEC: f32 = 60.0;
|
||||||
|
const MARQUEE_TRACK_PX: f32 = 4096.0;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
pub struct RecentsRow;
|
pub struct RecentsRow;
|
||||||
|
|
@ -35,10 +54,8 @@ impl Component for RecentsRow {
|
||||||
|
|
||||||
let sorted: Vec<Cluster> = sort_clusters_for_home(clusters);
|
let sorted: Vec<Cluster> = sort_clusters_for_home(clusters);
|
||||||
|
|
||||||
let slots = recent_card_slots_for_window(
|
let slots =
|
||||||
root_size.read().width,
|
recent_card_slots_for_window(root_size.read().width, *scale_factor.read() as f32);
|
||||||
*scale_factor.read() as f32,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Budget off cluster count not slot count an offset-only change makes the layout
|
// Budget off cluster count not slot count an offset-only change makes the layout
|
||||||
// engine reuse a card's cached area freezing survivors at pre-resize widths
|
// engine reuse a card's cached area freezing survivors at pre-resize widths
|
||||||
|
|
@ -153,6 +170,10 @@ impl KeyExt for ClusterCard {
|
||||||
impl PartialEq for ClusterCard {
|
impl PartialEq for ClusterCard {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.cluster.id == other.cluster.id
|
self.cluster.id == other.cluster.id
|
||||||
|
&& self.cluster.name == other.cluster.name
|
||||||
|
&& self.cluster.cover_path == other.cluster.cover_path
|
||||||
|
&& self.cluster.linked_modpack_hash == other.cluster.linked_modpack_hash
|
||||||
|
&& self.cluster.mc_loader == other.cluster.mc_loader
|
||||||
&& self.index == other.index
|
&& self.index == other.index
|
||||||
&& self.items == other.items
|
&& self.items == other.items
|
||||||
&& self.progress == other.progress
|
&& self.progress == other.progress
|
||||||
|
|
@ -173,9 +194,19 @@ impl Component for ClusterCard {
|
||||||
let eased = stagger_eased(self.progress, self.index, self.items);
|
let eased = stagger_eased(self.progress, self.index, self.items);
|
||||||
let rise = (1.0 - eased) * CARD_RISE_PX;
|
let rise = (1.0 - eased) * CARD_RISE_PX;
|
||||||
|
|
||||||
let title = format!("{} {}", self.cluster.mc_version, self.cluster.mc_loader);
|
let title = if self.cluster.user_created {
|
||||||
|
self.cluster.name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{} {}", self.cluster.mc_version, self.cluster.mc_loader)
|
||||||
|
};
|
||||||
|
|
||||||
let cluster_id = self.cluster.id;
|
let cluster_id = self.cluster.id;
|
||||||
|
let mod_tabs = !self.cluster.lacks_mod_loader();
|
||||||
|
let modpack = self.cluster.linked_modpack_hash.is_some();
|
||||||
|
let modpack_icon = self
|
||||||
|
.cluster
|
||||||
|
.modpack_icon_file()
|
||||||
|
.filter(|icon| icon.is_file());
|
||||||
let on_press = move |_| {
|
let on_press = move |_| {
|
||||||
*active_id.write() = Some(cluster_id);
|
*active_id.write() = Some(cluster_id);
|
||||||
};
|
};
|
||||||
|
|
@ -185,7 +216,17 @@ impl Component for ClusterCard {
|
||||||
.open_upwards()
|
.open_upwards()
|
||||||
.title(title.clone())
|
.title(title.clone())
|
||||||
.on_close(move |_| menu.set(None));
|
.on_close(move |_| menu.set(None));
|
||||||
for (icon, label, route) in cluster_menu_entries(cluster_id) {
|
for (icon, label, route) in
|
||||||
|
cluster_menu_entries(cluster_id)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(_, _, route)| {
|
||||||
|
mod_tabs
|
||||||
|
|| !matches!(
|
||||||
|
route,
|
||||||
|
Route::ClusterMods { .. } | Route::ClusterShaders { .. }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
context = context.action(icon, label, move |()| {
|
context = context.action(icon, label, move |()| {
|
||||||
let _ = RouterContext::get().push(route.clone());
|
let _ = RouterContext::get().push(route.clone());
|
||||||
});
|
});
|
||||||
|
|
@ -249,20 +290,44 @@ impl Component for ClusterCard {
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.child(
|
.child(if modpack {
|
||||||
|
BlurredArt {
|
||||||
|
art: DynamicArt::for_cluster(&self.cluster)
|
||||||
|
.max_edge(ART_PREVIEW_EDGE),
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
} else {
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.layer(Layer::Relative(1))
|
.layer(Layer::Relative(1))
|
||||||
.child(DynamicArt::for_cluster(&self.cluster).max_edge(ART_PREVIEW_EDGE)),
|
.child(
|
||||||
|
DynamicArt::for_cluster(&self.cluster)
|
||||||
|
.max_edge(ART_PREVIEW_EDGE),
|
||||||
)
|
)
|
||||||
|
.into_element()
|
||||||
|
})
|
||||||
|
.maybe_child(modpack_icon.map(|icon| {
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.position(Position::new_absolute())
|
||||||
|
.center()
|
||||||
|
.layer(Layer::Relative(5))
|
||||||
|
.child(modpack_badge(icon))
|
||||||
|
.into_element()
|
||||||
|
}))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.padding(Gaps::new_symmetric(12., 24.))
|
.padding(if modpack {
|
||||||
|
Gaps::new(12., MODPACK_TITLE_INSET, 12., MODPACK_TITLE_LEFT)
|
||||||
|
} else {
|
||||||
|
Gaps::new_symmetric(12., 24.)
|
||||||
|
})
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.cross_align(Alignment::Start)
|
.cross_align(Alignment::Start)
|
||||||
.layer(Layer::Relative(4))
|
.layer(Layer::Relative(4))
|
||||||
|
|
@ -272,13 +337,15 @@ impl Component for ClusterCard {
|
||||||
.stop((Color::from_argb(0, 25, 25, 25), 24.519))
|
.stop((Color::from_argb(0, 25, 25, 25), 24.519))
|
||||||
.stop((Color::from_af32rgb(0.75, 17, 17, 21), 65.)),
|
.stop((Color::from_af32rgb(0.75, 17, 17, 21), 65.)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(if modpack {
|
||||||
label()
|
MarqueeTitle {
|
||||||
.text(title)
|
text: title,
|
||||||
.font_size(32.)
|
hovered: *hovering.read(),
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
}
|
||||||
.color(colors::fg_primary()),
|
.into_element()
|
||||||
),
|
} else {
|
||||||
|
title_label(title).into_element()
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -302,6 +369,175 @@ impl Component for ClusterCard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn title_label(text: String) -> Label {
|
||||||
|
label()
|
||||||
|
.text(text)
|
||||||
|
.font_size(32.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.max_lines(1)
|
||||||
|
.color(colors::fg_primary())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct MarqueeTitle {
|
||||||
|
text: String,
|
||||||
|
hovered: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for MarqueeTitle {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let animations = use_animations_enabled();
|
||||||
|
let mut box_width = use_state(|| 0f32);
|
||||||
|
let mut text_width = use_state(|| 0f32);
|
||||||
|
let offset = use_state(|| 0f32);
|
||||||
|
let run = use_state(|| 0u64);
|
||||||
|
|
||||||
|
let overflow = (*text_width.read() - *box_width.read()).max(0.);
|
||||||
|
let deps = (self.hovered && animations, overflow);
|
||||||
|
use_side_effect_with_deps(&deps, move |&(hovered, overflow)| {
|
||||||
|
let mut run = run;
|
||||||
|
let mut offset = offset;
|
||||||
|
let current = *run.peek() + 1;
|
||||||
|
run.set(current);
|
||||||
|
|
||||||
|
if !hovered || overflow < 1. {
|
||||||
|
offset.set(0.);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spawn(async move {
|
||||||
|
let alive = move || *run.peek() == current;
|
||||||
|
tokio::time::sleep(MARQUEE_DELAY).await;
|
||||||
|
if !alive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !slide(offset, 0., -overflow, &alive).await {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(MARQUEE_HOLD).await;
|
||||||
|
if !alive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slide(offset, -overflow, 0., &alive).await;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.on_sized(move |event: Event<SizedEventData>| {
|
||||||
|
let width = event.data().area.width();
|
||||||
|
if (width - *box_width.peek()).abs() > 0.5 {
|
||||||
|
box_width.set(width);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
rect().width(Size::px(MARQUEE_TRACK_PX)).child(
|
||||||
|
rect()
|
||||||
|
.offset_x(*offset.read())
|
||||||
|
.on_sized(move |event: Event<SizedEventData>| {
|
||||||
|
let width = event.data().area.width();
|
||||||
|
if (width - *text_width.peek()).abs() > 0.5 {
|
||||||
|
text_width.set(width);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.child(title_label(self.text.clone())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn slide(mut offset: State<f32>, from: f32, to: f32, alive: &impl Fn() -> bool) -> bool {
|
||||||
|
let distance = (to - from).abs();
|
||||||
|
let total = Duration::from_secs_f32(distance / MARQUEE_SPEED_PX_PER_SEC);
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if !alive() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let progress = (started.elapsed().as_secs_f32() / total.as_secs_f32().max(0.001)).min(1.);
|
||||||
|
offset.set(from + (to - from) * progress);
|
||||||
|
if progress >= 1. {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(MARQUEE_FRAME).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
struct BlurredArt {
|
||||||
|
art: DynamicArt,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for BlurredArt {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let (key, bytes) = use_art_bytes(&self.art);
|
||||||
|
let mut cache = use_state(|| None::<(String, ImageHandle)>);
|
||||||
|
|
||||||
|
let cached = cache
|
||||||
|
.read()
|
||||||
|
.clone()
|
||||||
|
.filter(|(cached_key, _)| *cached_key == key)
|
||||||
|
.map(|(_, holder)| holder);
|
||||||
|
let holder = cached.or_else(|| {
|
||||||
|
let holder = decode(&bytes)?;
|
||||||
|
cache.set(Some((key.clone(), holder.clone())));
|
||||||
|
Some(holder)
|
||||||
|
});
|
||||||
|
|
||||||
|
let backdrop = rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.position(Position::new_absolute())
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.background(MODPACK_BACKDROP);
|
||||||
|
|
||||||
|
let Some(holder) = holder else {
|
||||||
|
return backdrop.into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
backdrop
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.position(Position::new_absolute())
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.layer(Layer::Relative(1))
|
||||||
|
.child(
|
||||||
|
image(holder)
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.aspect_ratio(AspectRatio::Max)
|
||||||
|
.image_cover(ImageCover::Center),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.position(Position::new_absolute())
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.layer(Layer::Relative(3))
|
||||||
|
.backdrop_blur(MODPACK_BLUR)
|
||||||
|
.background(MODPACK_TINT),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modpack_badge(icon: std::path::PathBuf) -> Element {
|
||||||
|
rect()
|
||||||
|
.width(Size::px(MODPACK_ICON_PX))
|
||||||
|
.height(Size::px(MODPACK_ICON_PX))
|
||||||
|
.min_width(Size::px(MODPACK_ICON_PX))
|
||||||
|
.corner_radius(CornerRadius::new_all(14.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.child(LocalImage::new(icon, MODPACK_ICON_EDGE, false))
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct OtherVersionsTile {
|
struct OtherVersionsTile {
|
||||||
index: usize,
|
index: usize,
|
||||||
|
|
@ -339,7 +575,7 @@ impl Component for OtherVersionsTile {
|
||||||
} else {
|
} else {
|
||||||
colors::ghost_overlay_hover()
|
colors::ghost_overlay_hover()
|
||||||
})
|
})
|
||||||
.blur(32.)
|
.backdrop_blur(32.)
|
||||||
.a11y_id(a11y_id)
|
.a11y_id(a11y_id)
|
||||||
.a11y_focusable(true)
|
.a11y_focusable(true)
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
|
|
||||||
1363
packages/oneclient_app/src/components/release_migration_popup.rs
Normal file
|
|
@ -45,6 +45,8 @@ impl Component for ScreenshotViewer {
|
||||||
move || start
|
move || start
|
||||||
});
|
});
|
||||||
let mut menu = use_state(|| None::<(f32, f32)>);
|
let mut menu = use_state(|| None::<(f32, f32)>);
|
||||||
|
let action = use_screenshot_action();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return rect().into_element();
|
return rect().into_element();
|
||||||
|
|
@ -52,9 +54,6 @@ impl Component for ScreenshotViewer {
|
||||||
let idx = (*index.read()).min(len - 1);
|
let idx = (*index.read()).min(len - 1);
|
||||||
let info = shots[idx].clone();
|
let info = shots[idx].clone();
|
||||||
|
|
||||||
let action = use_screenshot_action();
|
|
||||||
let dispatch = use_dispatch();
|
|
||||||
|
|
||||||
let close = self.on_close.clone();
|
let close = self.on_close.clone();
|
||||||
let scrim_close = self.on_close.clone();
|
let scrim_close = self.on_close.clone();
|
||||||
let delete_close = self.on_close.clone();
|
let delete_close = self.on_close.clone();
|
||||||
|
|
@ -81,6 +80,7 @@ impl Component for ScreenshotViewer {
|
||||||
action,
|
action,
|
||||||
dispatch.clone(),
|
dispatch.clone(),
|
||||||
move |()| menu_close.call(()),
|
move |()| menu_close.call(()),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.overlay_level(OVERLAY_MAX_LEVEL)
|
.overlay_level(OVERLAY_MAX_LEVEL)
|
||||||
.on_close(move |_| menu.set(None))
|
.on_close(move |_| menu.set(None))
|
||||||
|
|
@ -182,6 +182,7 @@ impl Component for ScreenshotViewer {
|
||||||
.body("Screenshot copied to your clipboard.")
|
.body("Screenshot copied to your clipboard.")
|
||||||
.info()
|
.info()
|
||||||
.icon(IconType::ClipboardCheck)
|
.icon(IconType::ClipboardCheck)
|
||||||
|
.toast_only()
|
||||||
.send();
|
.send();
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::Copy01).size(14.))
|
.child(Icon::new(IconType::Copy01).size(14.))
|
||||||
|
|
@ -216,13 +217,20 @@ pub fn screenshot_context_menu(
|
||||||
action: UseScreenshotAction,
|
action: UseScreenshotAction,
|
||||||
dispatch: Actions,
|
dispatch: Actions,
|
||||||
on_deleted: impl Into<EventHandler<()>>,
|
on_deleted: impl Into<EventHandler<()>>,
|
||||||
|
on_select: Option<EventHandler<()>>,
|
||||||
) -> ContextMenu {
|
) -> ContextMenu {
|
||||||
let open_path = path.clone();
|
let open_path = path.clone();
|
||||||
let copy_path = path.clone();
|
let copy_path = path.clone();
|
||||||
let delete_path = path;
|
let delete_path = path;
|
||||||
let on_deleted = on_deleted.into();
|
let on_deleted = on_deleted.into();
|
||||||
|
|
||||||
ContextMenu::new(x, y)
|
let context_menu = match on_select {
|
||||||
|
Some(on_select) => ContextMenu::new(x, y)
|
||||||
|
.action(IconType::Check, "Select", on_select)
|
||||||
|
.separator(),
|
||||||
|
None => ContextMenu::new(x, y),
|
||||||
|
};
|
||||||
|
context_menu
|
||||||
.action(IconType::Folder, "Open in folder", move |()| {
|
.action(IconType::Folder, "Open in folder", move |()| {
|
||||||
if let Some(dir) = open_path.parent() {
|
if let Some(dir) = open_path.parent() {
|
||||||
crate::platform::open_path(&dir.to_string_lossy());
|
crate::platform::open_path(&dir.to_string_lossy());
|
||||||
|
|
@ -235,6 +243,7 @@ pub fn screenshot_context_menu(
|
||||||
.body("Screenshot copied to your clipboard.")
|
.body("Screenshot copied to your clipboard.")
|
||||||
.info()
|
.info()
|
||||||
.icon(IconType::ClipboardCheck)
|
.icon(IconType::ClipboardCheck)
|
||||||
|
.toast_only()
|
||||||
.send();
|
.send();
|
||||||
})
|
})
|
||||||
.separator()
|
.separator()
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ pub(crate) fn scroll_pos_from_wheel(wheel: f32, inner: f32, viewport: f32, curre
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if new_pos <= -(inner - viewport) && wheel < 0.0 {
|
if new_pos <= -(inner - viewport) && wheel < 0.0 {
|
||||||
return -(inner - viewport) as i32;
|
return -(inner - viewport).ceil() as i32;
|
||||||
}
|
}
|
||||||
new_pos as i32
|
new_pos as i32
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ pub(crate) fn corrected_scroll(inner: f32, viewport: f32, pos: f32) -> f32 {
|
||||||
}
|
}
|
||||||
if (-pos + viewport) > inner {
|
if (-pos + viewport) > inner {
|
||||||
return if viewport < inner {
|
return if viewport < inner {
|
||||||
-(inner - viewport)
|
-(inner - viewport).ceil()
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
@ -81,6 +81,22 @@ pub struct ScrollAreaCtx {
|
||||||
pub viewport_left: f32,
|
pub viewport_left: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct LazySection {
|
||||||
|
pub header: bool,
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LazyRow {
|
||||||
|
Header(usize),
|
||||||
|
Items {
|
||||||
|
section: usize,
|
||||||
|
row: usize,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ScrollArea {
|
pub struct ScrollArea {
|
||||||
width: Size,
|
width: Size,
|
||||||
height: Size,
|
height: Size,
|
||||||
|
|
@ -197,6 +213,11 @@ impl ScrollArea {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn append_children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
|
||||||
|
self.children.extend(children);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn content(mut self, builder: impl Fn(ScrollAreaCtx) -> Element + 'static) -> Self {
|
pub fn content(mut self, builder: impl Fn(ScrollAreaCtx) -> Element + 'static) -> Self {
|
||||||
self.builder = Some(Box::new(builder));
|
self.builder = Some(Box::new(builder));
|
||||||
self
|
self
|
||||||
|
|
@ -246,6 +267,173 @@ impl ScrollArea {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lazy_grid(
|
||||||
|
mut self,
|
||||||
|
count: usize,
|
||||||
|
item_height: f32,
|
||||||
|
gap: f32,
|
||||||
|
min_width: f32,
|
||||||
|
max_cols: usize,
|
||||||
|
render: impl Fn(usize) -> Element + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let slot = (item_height + gap).max(1.);
|
||||||
|
self.builder = Some(Box::new(move |ctx: ScrollAreaCtx| {
|
||||||
|
let cols = (((ctx.viewport_w + gap) / (min_width + gap)).floor() as usize)
|
||||||
|
.clamp(1, max_cols.max(1));
|
||||||
|
let rows_total = count.div_ceil(cols);
|
||||||
|
|
||||||
|
let first =
|
||||||
|
(((-ctx.corrected_y) / slot).floor() as i64 - LAZY_OVERSCAN).max(0) as usize;
|
||||||
|
let span = ((ctx.viewport_h / slot).ceil() as i64 + 2 * LAZY_OVERSCAN).max(0) as usize;
|
||||||
|
let last = (first + span).min(rows_total);
|
||||||
|
|
||||||
|
let top_pad = first as f32 * slot;
|
||||||
|
let bottom_pad = rows_total.saturating_sub(last) as f32 * slot;
|
||||||
|
|
||||||
|
let mut container = rect().vertical().width(Size::fill());
|
||||||
|
if top_pad > 0. {
|
||||||
|
container = container.child(rect().width(Size::fill()).height(Size::px(top_pad)));
|
||||||
|
}
|
||||||
|
for r in first..last {
|
||||||
|
let mut row = rect()
|
||||||
|
.key(r)
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(slot))
|
||||||
|
.spacing(gap)
|
||||||
|
.content(Content::Flex);
|
||||||
|
for c in 0..cols {
|
||||||
|
let idx = r * cols + c;
|
||||||
|
let cell = rect().width(Size::flex(1.0)).height(Size::px(item_height));
|
||||||
|
row = row.child(if idx < count {
|
||||||
|
cell.child(render(idx))
|
||||||
|
} else {
|
||||||
|
cell
|
||||||
|
});
|
||||||
|
}
|
||||||
|
container = container.child(row);
|
||||||
|
}
|
||||||
|
if bottom_pad > 0. {
|
||||||
|
container =
|
||||||
|
container.child(rect().width(Size::fill()).height(Size::px(bottom_pad)));
|
||||||
|
}
|
||||||
|
container.into_element()
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn lazy_sections(
|
||||||
|
mut self,
|
||||||
|
sections: Vec<LazySection>,
|
||||||
|
item_height: f32,
|
||||||
|
gap: f32,
|
||||||
|
min_width: f32,
|
||||||
|
max_cols: usize,
|
||||||
|
header_height: f32,
|
||||||
|
render: impl Fn(usize) -> Element + 'static,
|
||||||
|
render_header: impl Fn(usize) -> Element + 'static,
|
||||||
|
) -> Self {
|
||||||
|
let slot = (item_height + gap).max(1.);
|
||||||
|
let header_slot = header_height + gap;
|
||||||
|
self.builder = Some(Box::new(move |ctx: ScrollAreaCtx| {
|
||||||
|
let cols = (((ctx.viewport_w + gap) / (min_width + gap)).floor() as usize)
|
||||||
|
.clamp(1, max_cols.max(1));
|
||||||
|
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut offset = 0;
|
||||||
|
for (s, section) in sections.iter().enumerate() {
|
||||||
|
if section.header {
|
||||||
|
rows.push(LazyRow::Header(s));
|
||||||
|
}
|
||||||
|
for r in 0..section.count.div_ceil(cols) {
|
||||||
|
let start = offset + r * cols;
|
||||||
|
rows.push(LazyRow::Items {
|
||||||
|
section: s,
|
||||||
|
row: r,
|
||||||
|
start,
|
||||||
|
end: (start + cols).min(offset + section.count),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
offset += section.count;
|
||||||
|
}
|
||||||
|
|
||||||
|
let row_slot = |row: &LazyRow| match row {
|
||||||
|
LazyRow::Header(_) => header_slot,
|
||||||
|
LazyRow::Items { .. } => slot,
|
||||||
|
};
|
||||||
|
|
||||||
|
let overscan = LAZY_OVERSCAN as f32 * slot;
|
||||||
|
let visible_top = -ctx.corrected_y - overscan;
|
||||||
|
let visible_bottom = -ctx.corrected_y + ctx.viewport_h + overscan;
|
||||||
|
|
||||||
|
let mut top_pad = 0.;
|
||||||
|
let mut bottom_pad = 0.;
|
||||||
|
let mut visible = Vec::new();
|
||||||
|
let mut y = 0.;
|
||||||
|
for row in &rows {
|
||||||
|
let h = row_slot(row);
|
||||||
|
if y + h <= visible_top {
|
||||||
|
top_pad += h;
|
||||||
|
} else if y >= visible_bottom {
|
||||||
|
bottom_pad += h;
|
||||||
|
} else {
|
||||||
|
visible.push(row);
|
||||||
|
}
|
||||||
|
y += h;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut container = rect().vertical().width(Size::fill());
|
||||||
|
if top_pad > 0. {
|
||||||
|
container = container.child(rect().width(Size::fill()).height(Size::px(top_pad)));
|
||||||
|
}
|
||||||
|
for row in visible {
|
||||||
|
let el = match *row {
|
||||||
|
LazyRow::Header(section) => rect()
|
||||||
|
.key(("h", section))
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(header_slot))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(header_height))
|
||||||
|
.child(render_header(section)),
|
||||||
|
),
|
||||||
|
LazyRow::Items {
|
||||||
|
section,
|
||||||
|
row,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
} => {
|
||||||
|
let mut items = rect()
|
||||||
|
.key(("r", section, row))
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(slot))
|
||||||
|
.spacing(gap)
|
||||||
|
.content(Content::Flex);
|
||||||
|
for idx in start..start + cols {
|
||||||
|
let cell = rect().width(Size::flex(1.0)).height(Size::px(item_height));
|
||||||
|
items = items.child(if idx < end {
|
||||||
|
cell.child(render(idx))
|
||||||
|
} else {
|
||||||
|
cell
|
||||||
|
});
|
||||||
|
}
|
||||||
|
items
|
||||||
|
}
|
||||||
|
};
|
||||||
|
container = container.child(el);
|
||||||
|
}
|
||||||
|
if bottom_pad > 0. {
|
||||||
|
container =
|
||||||
|
container.child(rect().width(Size::fill()).height(Size::px(bottom_pad)));
|
||||||
|
}
|
||||||
|
container.into_element()
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn view(&self) -> impl IntoElement {
|
fn view(&self) -> impl IntoElement {
|
||||||
let horizontal = self.horizontal;
|
let horizontal = self.horizontal;
|
||||||
let content_w = self.content_width;
|
let content_w = self.content_width;
|
||||||
|
|
@ -472,7 +660,7 @@ impl ScrollArea {
|
||||||
})
|
})
|
||||||
.on_wheel(on_wheel)
|
.on_wheel(on_wheel)
|
||||||
.on_capture_global_pointer_move(on_global_move)
|
.on_capture_global_pointer_move(on_global_move)
|
||||||
.on_capture_global_pointer_press(on_global_release)
|
.on_capture_global_pointer_up(on_global_release)
|
||||||
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
||||||
let held = e.modifiers.contains(Modifiers::SHIFT)
|
let held = e.modifiers.contains(Modifiers::SHIFT)
|
||||||
|| matches!(e.code, Code::ShiftLeft | Code::ShiftRight);
|
|| matches!(e.code, Code::ShiftLeft | Code::ShiftRight);
|
||||||
|
|
@ -520,7 +708,7 @@ impl ScrollArea {
|
||||||
content_h.set(h);
|
content_h.set(h);
|
||||||
if stick_bottom {
|
if stick_bottom {
|
||||||
let vp = *viewport_h.read();
|
let vp = *viewport_h.read();
|
||||||
let target = -((h - vp).max(0.));
|
let target = -((h - vp).max(0.).ceil());
|
||||||
controller.scroll_to_y(target as i32);
|
controller.scroll_to_y(target as i32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
53
packages/oneclient_app/src/components/spinner.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
use freya::animation::{AnimNum, Function, OnCreation, OnFinish, use_animation};
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use super::{Icon, IconType};
|
||||||
|
use crate::theme::colors;
|
||||||
|
|
||||||
|
const SPIN_TIME: u64 = 900;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct Spinner {
|
||||||
|
size: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Spinner {
|
||||||
|
pub fn new(size: f32) -> Self {
|
||||||
|
Self { size }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for Spinner {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let spin = use_animation(|conf| {
|
||||||
|
conf.on_creation(OnCreation::Run);
|
||||||
|
conf.on_finish(OnFinish::restart());
|
||||||
|
AnimNum::new(0., 360.)
|
||||||
|
.time(SPIN_TIME)
|
||||||
|
.function(Function::Linear)
|
||||||
|
});
|
||||||
|
|
||||||
|
rect().rotate(spin.get().value()).child(
|
||||||
|
Icon::new(IconType::Loading02)
|
||||||
|
.size(self.size)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn centered_spinner(message: &str) -> Element {
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.center()
|
||||||
|
.spacing(12.)
|
||||||
|
.child(Spinner::new(22.))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(message.to_string())
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,12 @@ use std::collections::HashSet;
|
||||||
|
|
||||||
use freya::animation::{AnimNum, Ease, Function, OnCreation, use_animation};
|
use freya::animation::{AnimNum, Ease, Function, OnCreation, use_animation};
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
use freya::router::RouterContext;
|
||||||
use oneclient_net::status::{self, ServiceStatus};
|
use oneclient_net::status::{self, ServiceStatus};
|
||||||
|
|
||||||
use crate::components::{Icon, IconType};
|
use crate::components::{Icon, IconType};
|
||||||
|
use crate::hooks::{try_default_account, use_current_account};
|
||||||
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
||||||
const BAR_HEIGHT: f32 = 34.;
|
const BAR_HEIGHT: f32 = 34.;
|
||||||
|
|
@ -18,32 +21,48 @@ const BAR_LAYER: u8 = 14;
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
enum Issue {
|
enum Issue {
|
||||||
NoInternet,
|
NoInternet,
|
||||||
|
SignedOut,
|
||||||
McAuthDown,
|
McAuthDown,
|
||||||
PolyfrostDown,
|
PolyfrostDown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the bar reacts to the network probe plus whether the active account
|
||||||
|
/// lost its sign-in
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
struct Conditions {
|
||||||
|
status: ServiceStatus,
|
||||||
|
signed_out_as: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Issue {
|
impl Issue {
|
||||||
fn is_active(self, s: &ServiceStatus) -> bool {
|
fn is_active(self, c: &Conditions) -> bool {
|
||||||
|
let s = &c.status;
|
||||||
match self {
|
match self {
|
||||||
Self::NoInternet => !s.online,
|
Self::NoInternet => !s.online,
|
||||||
|
Self::SignedOut => c.signed_out_as.is_some(),
|
||||||
Self::McAuthDown => s.online && !s.mc_auth_up,
|
Self::McAuthDown => s.online && !s.mc_auth_up,
|
||||||
Self::PolyfrostDown => s.online && !s.polyfrost_up,
|
Self::PolyfrostDown => s.online && !s.polyfrost_up,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message(self) -> &'static str {
|
fn message(self, c: &Conditions) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::NoInternet => "No internet connection.",
|
Self::NoInternet => "No internet connection.".to_string(),
|
||||||
|
Self::SignedOut => format!(
|
||||||
|
"You have been signed out of {}. Sign in again to keep playing.",
|
||||||
|
c.signed_out_as.as_deref().unwrap_or("your account")
|
||||||
|
),
|
||||||
Self::McAuthDown => {
|
Self::McAuthDown => {
|
||||||
"Minecraft authentication servers are unreachable. Logging in may fail."
|
"Minecraft authentication servers are unreachable. Logging in may fail.".to_string()
|
||||||
}
|
}
|
||||||
Self::PolyfrostDown => "Polyfrost services are experiencing issues.",
|
Self::PolyfrostDown => "Polyfrost services are experiencing issues.".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon(self) -> IconType {
|
fn icon(self) -> IconType {
|
||||||
match self {
|
match self {
|
||||||
Self::NoInternet => IconType::Globe01,
|
Self::NoInternet => IconType::Globe01,
|
||||||
|
Self::SignedOut => IconType::Users01,
|
||||||
Self::McAuthDown => IconType::AlertTriangle,
|
Self::McAuthDown => IconType::AlertTriangle,
|
||||||
Self::PolyfrostDown => IconType::AlertCircle,
|
Self::PolyfrostDown => IconType::AlertCircle,
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +70,7 @@ impl Issue {
|
||||||
|
|
||||||
fn background(self) -> Color {
|
fn background(self) -> Color {
|
||||||
match self {
|
match self {
|
||||||
Self::NoInternet => colors::danger(),
|
Self::NoInternet | Self::SignedOut => colors::danger(),
|
||||||
Self::McAuthDown | Self::PolyfrostDown => AMBER,
|
Self::McAuthDown | Self::PolyfrostDown => AMBER,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,12 +78,22 @@ impl Issue {
|
||||||
fn closeable(self) -> bool {
|
fn closeable(self) -> bool {
|
||||||
!matches!(self, Self::NoInternet)
|
!matches!(self, Self::NoInternet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Where pressing the message takes the user if anywhere
|
||||||
|
fn route(self) -> Option<Route> {
|
||||||
|
matches!(self, Self::SignedOut).then_some(Route::SettingsAccounts {})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn active_issues(s: &ServiceStatus) -> Vec<Issue> {
|
fn active_issues(c: &Conditions) -> Vec<Issue> {
|
||||||
[Issue::NoInternet, Issue::McAuthDown, Issue::PolyfrostDown]
|
[
|
||||||
|
Issue::NoInternet,
|
||||||
|
Issue::SignedOut,
|
||||||
|
Issue::McAuthDown,
|
||||||
|
Issue::PolyfrostDown,
|
||||||
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|i| i.is_active(s))
|
.filter(|i| i.is_active(c))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +104,7 @@ impl Component for StatusBar {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let mut status = use_state(status::current);
|
let mut status = use_state(status::current);
|
||||||
let mut dismissed = use_state(HashSet::<Issue>::new);
|
let mut dismissed = use_state(HashSet::<Issue>::new);
|
||||||
|
let current_account = use_current_account();
|
||||||
|
|
||||||
use_hook(move || {
|
use_hook(move || {
|
||||||
status::request_recheck();
|
status::request_recheck();
|
||||||
|
|
@ -86,21 +116,35 @@ impl Component for StatusBar {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let conditions = move || Conditions {
|
||||||
|
status: *status.read(),
|
||||||
|
signed_out_as: try_default_account(¤t_account)
|
||||||
|
.filter(|a| a.needs_sign_in())
|
||||||
|
.map(|a| a.username),
|
||||||
|
};
|
||||||
|
|
||||||
|
// A dismissal lasts only while its issue does so the next sign-out
|
||||||
|
// shows the bar again
|
||||||
use_side_effect(move || {
|
use_side_effect(move || {
|
||||||
let s = *status.read();
|
let c = conditions();
|
||||||
let cur = dismissed.peek().clone();
|
let cur = dismissed.peek().clone();
|
||||||
let next: HashSet<Issue> = cur.iter().copied().filter(|i| i.is_active(&s)).collect();
|
let next: HashSet<Issue> = cur.iter().copied().filter(|i| i.is_active(&c)).collect();
|
||||||
if next != cur {
|
if next != cur {
|
||||||
dismissed.set(next);
|
dismissed.set(next);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let s = *status.read();
|
let c = conditions();
|
||||||
let dset = dismissed.read();
|
let dset = dismissed.read();
|
||||||
let visible = active_issues(&s).into_iter().find(|i| !dset.contains(i));
|
let visible = active_issues(&c).into_iter().find(|i| !dset.contains(i));
|
||||||
|
|
||||||
match visible {
|
match visible {
|
||||||
Some(issue) => StatusBanner { issue, dismissed }.into_element(),
|
Some(issue) => StatusBanner {
|
||||||
|
issue,
|
||||||
|
text: issue.message(&c),
|
||||||
|
dismissed,
|
||||||
|
}
|
||||||
|
.into_element(),
|
||||||
None => rect().into_element(),
|
None => rect().into_element(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +153,7 @@ impl Component for StatusBar {
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
struct StatusBanner {
|
struct StatusBanner {
|
||||||
issue: Issue,
|
issue: Issue,
|
||||||
|
text: String,
|
||||||
dismissed: State<HashSet<Issue>>,
|
dismissed: State<HashSet<Issue>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -153,6 +198,35 @@ impl Component for StatusBanner {
|
||||||
// to the top of the content box inside the bar's padding
|
// to the top of the content box inside the bar's padding
|
||||||
let leading = rect().width(Size::flex(1.0)).height(Size::fill());
|
let leading = rect().width(Size::flex(1.0)).height(Size::fill());
|
||||||
|
|
||||||
|
let mut action_hover = use_state(|| false);
|
||||||
|
|
||||||
|
let action = issue.route().map(|route| {
|
||||||
|
rect()
|
||||||
|
.center()
|
||||||
|
.height(Size::px(22.))
|
||||||
|
.padding(Gaps::new_symmetric(0., 8.))
|
||||||
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
|
.background(if *action_hover.read() {
|
||||||
|
Color::WHITE.with_a(46)
|
||||||
|
} else {
|
||||||
|
Color::WHITE.with_a(26)
|
||||||
|
})
|
||||||
|
.cursor(CursorIcon::Pointer)
|
||||||
|
.on_pointer_enter(move |_| action_hover.set(true))
|
||||||
|
.on_pointer_leave(move |_| action_hover.set(false))
|
||||||
|
.on_press(move |_| {
|
||||||
|
let _ = RouterContext::get().push(route.clone());
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Sign in")
|
||||||
|
.font_size(12.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(Color::WHITE),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
});
|
||||||
|
|
||||||
let message = rect()
|
let message = rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
|
|
@ -162,12 +236,13 @@ impl Component for StatusBanner {
|
||||||
.child(Icon::new(issue.icon()).size(15.).color(Color::WHITE))
|
.child(Icon::new(issue.icon()).size(15.).color(Color::WHITE))
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(issue.message())
|
.text(self.text.clone())
|
||||||
.font_size(12.)
|
.font_size(12.)
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.max_lines(1)
|
.max_lines(1)
|
||||||
.color(Color::WHITE),
|
.color(Color::WHITE),
|
||||||
);
|
)
|
||||||
|
.maybe_child(action);
|
||||||
|
|
||||||
let trailing = rect()
|
let trailing = rect()
|
||||||
.width(Size::flex(1.0))
|
.width(Size::flex(1.0))
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,11 @@ fn tab_label(text: &str, font_size: f32, active: bool) -> impl IntoElement {
|
||||||
FontWeight::NORMAL
|
FontWeight::NORMAL
|
||||||
})
|
})
|
||||||
.max_lines(1)
|
.max_lines(1)
|
||||||
.color(colors::fg_primary())
|
.color(if active {
|
||||||
|
colors::fg_primary()
|
||||||
|
} else {
|
||||||
|
colors::fg_secondary()
|
||||||
|
})
|
||||||
.text_align(TextAlign::Center),
|
.text_align(TextAlign::Center),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -221,7 +225,7 @@ impl Component for TabButton {
|
||||||
.child(tab_label(&self.label, font_size, active))
|
.child(tab_label(&self.label, font_size, active))
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.height(Size::px(1.5))
|
.height(Size::px(2.))
|
||||||
.width(Size::fill_minimum())
|
.width(Size::fill_minimum())
|
||||||
.margin(Gaps::new_symmetric(0., 4.0))
|
.margin(Gaps::new_symmetric(0., 4.0))
|
||||||
.corner_radius(CornerRadius::new_all(2.))
|
.corner_radius(CornerRadius::new_all(2.))
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
use crate::ui::clamp_to_window;
|
||||||
|
|
||||||
const GAP_VERT: f32 = 8.0;
|
const GAP_VERT: f32 = 8.0;
|
||||||
const GAP_HORI: f32 = 12.0;
|
const GAP_HORI: f32 = 12.0;
|
||||||
|
|
||||||
|
const EXPANDED_WIDTH: f32 = 440.;
|
||||||
|
const EXPANDED_HEIGHT: f32 = 180.;
|
||||||
|
|
||||||
fn text_input(value: impl Into<Writable<String>>) -> Input {
|
fn text_input(value: impl Into<Writable<String>>) -> Input {
|
||||||
Input::new(value)
|
Input::new(value)
|
||||||
.inner_margin(Gaps::new_symmetric(GAP_VERT, GAP_HORI))
|
.padding(Gaps::new_symmetric(GAP_VERT, GAP_HORI))
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
.background(colors::component_bg())
|
.background(colors::component_bg())
|
||||||
.focus_background(colors::component_bg_pressed())
|
.focus_background(colors::component_bg_pressed())
|
||||||
|
|
@ -40,6 +45,10 @@ pub fn validate_memory(validator: InputValidator) {
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct TextInput {
|
pub struct TextInput {
|
||||||
input: Input,
|
input: Input,
|
||||||
|
value: Writable<String>,
|
||||||
|
on_submit: Option<EventHandler<String>>,
|
||||||
|
multiline: bool,
|
||||||
|
expandable: bool,
|
||||||
layout: LayoutData,
|
layout: LayoutData,
|
||||||
style: StyleState,
|
style: StyleState,
|
||||||
text_style: TextStyleData,
|
text_style: TextStyleData,
|
||||||
|
|
@ -50,12 +59,13 @@ pub struct TextInput {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl TextInput {
|
impl TextInput {
|
||||||
pub fn new(value: impl Into<Writable<String>>) -> Self {
|
pub fn new(value: impl Into<Writable<String>>) -> Self {
|
||||||
Self::from_input(text_input(value))
|
let value = value.into();
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_input(input: Input) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
input,
|
input: text_input(value.clone()),
|
||||||
|
value,
|
||||||
|
on_submit: None,
|
||||||
|
multiline: false,
|
||||||
|
expandable: false,
|
||||||
layout: LayoutData::default(),
|
layout: LayoutData::default(),
|
||||||
style: StyleState::default(),
|
style: StyleState::default(),
|
||||||
text_style: TextStyleData::default(),
|
text_style: TextStyleData::default(),
|
||||||
|
|
@ -81,7 +91,27 @@ impl TextInput {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
|
pub fn on_submit(mut self, on_submit: impl Into<EventHandler<String>>) -> Self {
|
||||||
self.input = self.input.on_submit(on_submit);
|
let on_submit = on_submit.into();
|
||||||
|
self.input = self.input.on_submit(on_submit.clone());
|
||||||
|
self.on_submit = Some(on_submit);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compact(mut self) -> Self {
|
||||||
|
self.input = self
|
||||||
|
.input
|
||||||
|
.padding(Gaps::new_symmetric(GAP_VERT / 2., GAP_HORI / 1.5));
|
||||||
|
self.font_size(12.)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn multiline(mut self, multiline: bool) -> Self {
|
||||||
|
self.input = self.input.multiline(multiline);
|
||||||
|
self.multiline = multiline;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expandable(mut self, expandable: bool) -> Self {
|
||||||
|
self.expandable = expandable;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,23 +177,118 @@ impl StyleExt for TextInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn submit_on_modifier_enter(
|
||||||
|
value: Writable<String>,
|
||||||
|
on_submit: EventHandler<String>,
|
||||||
|
) -> Callback<Event<KeyboardEventData>, bool> {
|
||||||
|
Callback::new(move |e: Event<KeyboardEventData>| match &e.key {
|
||||||
|
Key::Named(NamedKey::Enter) if e.modifiers.contains(Modifiers::ctrl_or_meta()) => {
|
||||||
|
e.stop_propagation();
|
||||||
|
e.prevent_default();
|
||||||
|
on_submit.call(value.read().clone());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Key::Named(NamedKey::Escape) | Key::Named(NamedKey::Shift) => true,
|
||||||
|
Key::Named(NamedKey::Tab) => false,
|
||||||
|
_ => {
|
||||||
|
e.stop_propagation();
|
||||||
|
e.prevent_default();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl Component for TextInput {
|
impl Component for TextInput {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let mut expanded = use_state(|| false);
|
||||||
|
let mut area = use_state(Area::default);
|
||||||
|
|
||||||
let mut card = rect()
|
let mut card = rect()
|
||||||
.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())
|
||||||
|
.maybe(self.expandable, |el| {
|
||||||
|
el.on_sized(move |e: Event<SizedEventData>| area.set_if_modified(e.area))
|
||||||
|
});
|
||||||
|
|
||||||
card.get_style().clone_from(&self.style);
|
card.get_style().clone_from(&self.style);
|
||||||
|
|
||||||
card.child(
|
let mut input = self.input.clone();
|
||||||
|
if self.multiline
|
||||||
|
&& let Some(on_submit) = self.on_submit.clone()
|
||||||
|
{
|
||||||
|
input = input.on_pre_key_down(submit_on_modifier_enter(self.value.clone(), on_submit));
|
||||||
|
}
|
||||||
|
if !matches!(self.layout.height, Size::Inner) {
|
||||||
|
input = input.height(Size::fill());
|
||||||
|
}
|
||||||
|
|
||||||
|
let expanded_input = (self.expandable && expanded()).then(|| {
|
||||||
|
let area = area();
|
||||||
|
let (left, top) = clamp_to_window(
|
||||||
|
area.max_x() - EXPANDED_WIDTH,
|
||||||
|
area.min_y(),
|
||||||
|
EXPANDED_WIDTH,
|
||||||
|
EXPANDED_HEIGHT,
|
||||||
|
);
|
||||||
|
|
||||||
|
OverlayPopup::new()
|
||||||
|
.backdrop(false)
|
||||||
|
.position(Position::new_global().left(left).top(top))
|
||||||
|
.on_close(move |_| expanded.set(false))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.shadow(Shadow::from((
|
||||||
|
0.,
|
||||||
|
8.,
|
||||||
|
32.,
|
||||||
|
0.,
|
||||||
|
Color::from_argb(120, 0, 0, 0),
|
||||||
|
)))
|
||||||
|
.child(
|
||||||
self.input
|
self.input
|
||||||
.clone()
|
.clone()
|
||||||
|
.multiline(true)
|
||||||
|
.auto_focus(true)
|
||||||
|
.width(Size::px(EXPANDED_WIDTH))
|
||||||
|
.height(Size::px(EXPANDED_HEIGHT)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if self.expandable {
|
||||||
|
input = input.trailing(
|
||||||
|
rect()
|
||||||
|
.on_pointer_down(|e: Event<PointerEventData>| e.stop_propagation())
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.ghost()
|
||||||
|
.icon()
|
||||||
|
.width(Size::px(22.))
|
||||||
|
.height(Size::px(22.))
|
||||||
|
.padding(3.)
|
||||||
|
.tooltip("Expand")
|
||||||
|
.on_press(move |e: Event<PressEventData>| {
|
||||||
|
e.stop_propagation();
|
||||||
|
expanded.set(true);
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Icon::new(IconType::Maximize01)
|
||||||
|
.size(14.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.maybe_child(expanded_input).child(
|
||||||
|
input
|
||||||
.map(self.text_style.text_align, |mut el, text_align| {
|
.map(self.text_style.text_align, |mut el, text_align| {
|
||||||
el = el.text_align(text_align);
|
el = el.text_align(text_align);
|
||||||
|
|
||||||
if text_align == TextAlign::Center {
|
if text_align == TextAlign::Center {
|
||||||
el = el.inner_margin(Gaps::new_symmetric(GAP_VERT, 0.));
|
el = el.padding(Gaps::new_symmetric(GAP_VERT, 0.));
|
||||||
}
|
}
|
||||||
|
|
||||||
el
|
el
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ use freya::{
|
||||||
use oneclient_events::Level;
|
use oneclient_events::Level;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ui::{divider},
|
|
||||||
components::{Button, ButtonSize, Icon, IconType, TooltipPlacement, progress_track},
|
components::{Button, ButtonSize, Icon, IconType, TooltipPlacement, progress_track},
|
||||||
hooks::{use_dispatch, use_notifications_snapshot},
|
hooks::{use_dispatch, use_notifications_snapshot},
|
||||||
motion::use_animations_enabled,
|
motion::use_animations_enabled,
|
||||||
notifications::{InboxEntry, NotificationActionKind},
|
notifications::{InboxEntry, NotificationActionKind},
|
||||||
theme::colors,
|
theme::colors,
|
||||||
|
ui::divider,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TOAST_W: f32 = 300.;
|
const TOAST_W: f32 = 300.;
|
||||||
|
|
@ -339,12 +339,7 @@ fn close_button(dispatch: crate::Actions, id: u64) -> impl IntoElement {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message_card(
|
fn message_card(entry: &InboxEntry, dispatch: crate::Actions, id: u64, ttl_pct: f32) -> Rect {
|
||||||
entry: &InboxEntry,
|
|
||||||
dispatch: crate::Actions,
|
|
||||||
id: u64,
|
|
||||||
ttl_pct: f32,
|
|
||||||
) -> Rect {
|
|
||||||
toast_shell(entry)
|
toast_shell(entry)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
|
||||||
|
|
@ -287,11 +287,19 @@ fn place(anchor: AnchorRect, placement: TooltipPlacement, size: Size2D) -> (f32,
|
||||||
let (x, y) = match placement {
|
let (x, y) = match placement {
|
||||||
TooltipPlacement::Top => (
|
TooltipPlacement::Top => (
|
||||||
centered_x,
|
centered_x,
|
||||||
if fits_above || !fits_below { above } else { below },
|
if fits_above || !fits_below {
|
||||||
|
above
|
||||||
|
} else {
|
||||||
|
below
|
||||||
|
},
|
||||||
),
|
),
|
||||||
TooltipPlacement::Bottom => (
|
TooltipPlacement::Bottom => (
|
||||||
centered_x,
|
centered_x,
|
||||||
if fits_below || !fits_above { below } else { above },
|
if fits_below || !fits_above {
|
||||||
|
below
|
||||||
|
} else {
|
||||||
|
above
|
||||||
|
},
|
||||||
),
|
),
|
||||||
TooltipPlacement::Left => (
|
TooltipPlacement::Left => (
|
||||||
if fits_before || !fits_after {
|
if fits_before || !fits_after {
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,7 @@ pub struct UploadToMclogs {
|
||||||
|
|
||||||
impl UploadToMclogs {
|
impl UploadToMclogs {
|
||||||
pub fn new(has_log: bool, confirm: State<Option<Confirm>>) -> Self {
|
pub fn new(has_log: bool, confirm: State<Option<Confirm>>) -> Self {
|
||||||
Self {
|
Self { has_log, confirm }
|
||||||
has_log,
|
|
||||||
confirm
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,20 +31,45 @@ impl Component for UploadToMclogs {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_mclogs_feedback(upload: UseUploadLog) {
|
pub fn notify_log_truncated(dispatch: &crate::Actions) {
|
||||||
let dispatch = use_dispatch();
|
dispatch
|
||||||
let mut handled = use_state(|| None::<String>);
|
.notify("Log was shortened")
|
||||||
|
.body("The log was too large for mclo.gs, so only its most recent part was uploaded.")
|
||||||
use_side_effect(move || match &*upload.read().state() {
|
.info()
|
||||||
MutationStateData::Settled {
|
.icon(IconType::AlertTriangle)
|
||||||
res: Ok(result), ..
|
.send();
|
||||||
} => {
|
|
||||||
if handled.peek().as_deref() == Some(result.url.as_str()) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handled.set(Some(result.url.clone()));
|
pub fn use_mclogs_feedback(upload: UseUploadLog) {
|
||||||
let _ = Clipboard::set(result.url.clone());
|
let dispatch = use_dispatch();
|
||||||
|
let mut handled = use_state(|| match &*upload.peek().state() {
|
||||||
|
MutationStateData::Settled {
|
||||||
|
settlement_instant, ..
|
||||||
|
} => Some(*settlement_instant),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
use_side_effect(move || {
|
||||||
|
let reader = upload.read();
|
||||||
|
let state = reader.state();
|
||||||
|
let MutationStateData::Settled {
|
||||||
|
res,
|
||||||
|
settlement_instant,
|
||||||
|
} = &*state
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if *handled.peek() == Some(*settlement_instant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handled.set(Some(*settlement_instant));
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(result) => {
|
||||||
|
if let Err(err) = Clipboard::set(result.url.clone()) {
|
||||||
|
tracing::warn!("clipboard copy failed: {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
dispatch
|
dispatch
|
||||||
.notify("Uploaded to mclo.gs")
|
.notify("Uploaded to mclo.gs")
|
||||||
|
|
@ -55,16 +77,18 @@ pub fn use_mclogs_feedback(upload: UseUploadLog) {
|
||||||
.info()
|
.info()
|
||||||
.icon(IconType::LinkExternal01)
|
.icon(IconType::LinkExternal01)
|
||||||
.send();
|
.send();
|
||||||
}
|
|
||||||
MutationStateData::Settled { res: Err(err), .. } => {
|
|
||||||
let msg = err.to_string();
|
|
||||||
if handled.peek().as_deref() == Some(msg.as_str()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handled.set(Some(msg.clone()));
|
if result.truncated {
|
||||||
dispatch.notify("Upload failed").body(msg).error().send();
|
notify_log_truncated(&dispatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
dispatch
|
||||||
|
.notify("Upload failed")
|
||||||
|
.body(err.to_string())
|
||||||
|
.error()
|
||||||
|
.send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
306
packages/oneclient_app/src/components/version_art_gallery.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
||||||
|
use freya::prelude::*;
|
||||||
|
use oneclient_core::images::DEFAULT_IMAGE_EDGE;
|
||||||
|
|
||||||
|
use crate::components::overlay_popup::OVERLAY_MAX_LEVEL;
|
||||||
|
use crate::components::spinner::Spinner;
|
||||||
|
use crate::components::{
|
||||||
|
ART_PREVIEW_EDGE, Button, DynamicArt, Icon, IconType, OverlayPopup, ScrollArea,
|
||||||
|
};
|
||||||
|
use crate::hooks::{refresh_version_art_gallery, use_version_art_gallery};
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
const DIALOG_WIDTH: f32 = 880.;
|
||||||
|
const DIALOG_HEIGHT: f32 = 620.;
|
||||||
|
const PANE_PADDING: f32 = 24.;
|
||||||
|
const TILE_W: f32 = 248.;
|
||||||
|
const TILE_H: f32 = 140.;
|
||||||
|
const TILE_GAP: f32 = 14.;
|
||||||
|
const COLUMNS: usize = 3;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub struct VersionArtGallery {
|
||||||
|
pending: State<Option<String>>,
|
||||||
|
error: State<Option<String>>,
|
||||||
|
on_pick: EventHandler<String>,
|
||||||
|
on_close: EventHandler<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VersionArtGallery {
|
||||||
|
pub fn new(
|
||||||
|
pending: State<Option<String>>,
|
||||||
|
error: State<Option<String>>,
|
||||||
|
on_pick: impl Into<EventHandler<String>>,
|
||||||
|
on_close: impl Into<EventHandler<()>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pending,
|
||||||
|
error,
|
||||||
|
on_pick: on_pick.into(),
|
||||||
|
on_close: on_close.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for VersionArtGallery {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let urls = use_version_art_gallery();
|
||||||
|
let mut pending = self.pending;
|
||||||
|
let mut error = self.error;
|
||||||
|
let in_flight = pending.read().clone();
|
||||||
|
let failure = error.read().clone();
|
||||||
|
|
||||||
|
let scrim_close = self.on_close.clone();
|
||||||
|
let header_close = self.on_close.clone();
|
||||||
|
let footer_close = self.on_close.clone();
|
||||||
|
let picked = self.on_pick.clone();
|
||||||
|
let on_pick: EventHandler<String> = EventHandler::from(move |url: String| {
|
||||||
|
if pending.read().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
error.set(None);
|
||||||
|
pending.set(Some(url.clone()));
|
||||||
|
picked.call(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
let rows = urls.len().div_ceil(COLUMNS);
|
||||||
|
|
||||||
|
let scroll = ScrollArea::new()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::flex(1.0))
|
||||||
|
.padding(Gaps::new(0., PANE_PADDING, 8., PANE_PADDING))
|
||||||
|
.scrollbar_gutter(true);
|
||||||
|
|
||||||
|
let scroll = if urls.is_empty() {
|
||||||
|
scroll.child(empty_state())
|
||||||
|
} else {
|
||||||
|
scroll.lazy(rows, TILE_H, TILE_GAP, move |index| {
|
||||||
|
let start = index * COLUMNS;
|
||||||
|
let end = (start + COLUMNS).min(urls.len());
|
||||||
|
tile_row(&urls[start..end], &on_pick, in_flight.as_deref())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
OverlayPopup::new()
|
||||||
|
.overlay_level(OVERLAY_MAX_LEVEL)
|
||||||
|
.on_close(move |()| scrim_close.call(()))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::px(DIALOG_WIDTH))
|
||||||
|
.height(Size::px(DIALOG_HEIGHT))
|
||||||
|
.max_width(Size::window_percent(94.))
|
||||||
|
.max_height(Size::window_percent(92.))
|
||||||
|
.content(Content::Flex)
|
||||||
|
.corner_radius(CornerRadius::new_all(16.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.background(colors::page_elevated())
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(header(move |_| header_close.call(())))
|
||||||
|
.child(scroll)
|
||||||
|
.child(footer(failure, move |_| footer_close.call(()))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tile_row(row: &[String], on_pick: &EventHandler<String>, in_flight: Option<&str>) -> Element {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.spacing(TILE_GAP)
|
||||||
|
.children(
|
||||||
|
row.iter()
|
||||||
|
.map(|url| {
|
||||||
|
GalleryTile {
|
||||||
|
busy: in_flight == Some(url.as_str()),
|
||||||
|
dimmed: in_flight.is_some(),
|
||||||
|
url: url.clone(),
|
||||||
|
on_pick: on_pick.clone(),
|
||||||
|
}
|
||||||
|
.into_element()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
struct GalleryTile {
|
||||||
|
url: String,
|
||||||
|
busy: bool,
|
||||||
|
dimmed: bool,
|
||||||
|
on_pick: EventHandler<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for GalleryTile {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let a11y_id = use_a11y();
|
||||||
|
let mut hovering = use_state(|| false);
|
||||||
|
let hovered = *hovering.read();
|
||||||
|
|
||||||
|
let on_pick = self.on_pick.clone();
|
||||||
|
let picked = self.url.clone();
|
||||||
|
|
||||||
|
let border_color = if hovered || self.busy {
|
||||||
|
colors::brand()
|
||||||
|
} else {
|
||||||
|
colors::component_border()
|
||||||
|
};
|
||||||
|
|
||||||
|
let opacity = if self.dimmed && !self.busy { 0.45 } else { 1.0 };
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.width(Size::px(TILE_W))
|
||||||
|
.height(Size::px(TILE_H))
|
||||||
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.opacity(opacity)
|
||||||
|
.a11y_id(a11y_id)
|
||||||
|
.a11y_focusable(true)
|
||||||
|
.a11y_role(AccessibilityRole::Button)
|
||||||
|
.on_press(move |_| on_pick.call(picked.clone()))
|
||||||
|
.on_pointer_enter(move |_| hovering.set(true))
|
||||||
|
.on_pointer_leave(move |_| hovering.set(false))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.position(Position::new_absolute())
|
||||||
|
.child(
|
||||||
|
DynamicArt::for_url(self.url.clone())
|
||||||
|
.max_edge(ART_PREVIEW_EDGE)
|
||||||
|
.skeleton(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::fill())
|
||||||
|
.layer(Layer::Relative(3))
|
||||||
|
.corner_radius(CornerRadius::new_all(12.))
|
||||||
|
.border(border_all_color(2., border_color).alignment(BorderAlignment::Inner))
|
||||||
|
.center()
|
||||||
|
.maybe_child(self.busy.then(|| Spinner::new(22.).into_element())),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state() -> Element {
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::px(220.))
|
||||||
|
.center()
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(12.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("The art gallery could not be loaded. Check your connection.")
|
||||||
|
.font_size(13.)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.secondary()
|
||||||
|
.small()
|
||||||
|
.on_press(move |_| {
|
||||||
|
spawn(async move {
|
||||||
|
refresh_version_art_gallery().await;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.text("Try again"),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(on_close: impl FnMut(Event<PressEventData>) + 'static) -> Element {
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.content(Content::Flex)
|
||||||
|
.cross_align(Alignment::Start)
|
||||||
|
.spacing(16.)
|
||||||
|
.padding(Gaps::new(22., PANE_PADDING, 16., PANE_PADDING))
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::flex(1.0))
|
||||||
|
.spacing(5.)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Art gallery")
|
||||||
|
.font_size(20.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Pick any of the official version artworks as the background for this instance.")
|
||||||
|
.font_size(13.)
|
||||||
|
.line_height(1.35)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.ghost()
|
||||||
|
.icon()
|
||||||
|
.alt("Close")
|
||||||
|
.on_press(on_close)
|
||||||
|
.child(Icon::new(IconType::XClose).size(16.)),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn footer(
|
||||||
|
failure: Option<String>,
|
||||||
|
on_close: impl FnMut(Event<PressEventData>) + 'static,
|
||||||
|
) -> Element {
|
||||||
|
let (note, note_color) = match failure {
|
||||||
|
Some(message) => (message, colors::danger()),
|
||||||
|
None => (
|
||||||
|
"Choosing an artwork pins it to this instance, so it stays put if you change the version later."
|
||||||
|
.to_string(),
|
||||||
|
colors::fg_secondary(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.content(Content::Flex)
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.spacing(20.)
|
||||||
|
.padding(Gaps::new(16., PANE_PADDING, 16., PANE_PADDING))
|
||||||
|
.border(
|
||||||
|
Border::new()
|
||||||
|
.fill(colors::component_border())
|
||||||
|
.width(BorderWidth {
|
||||||
|
top: 1.,
|
||||||
|
right: 0.,
|
||||||
|
bottom: 0.,
|
||||||
|
left: 0.,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(note)
|
||||||
|
.width(Size::flex(1.0))
|
||||||
|
.font_size(12.)
|
||||||
|
.line_height(1.35)
|
||||||
|
.max_lines(2)
|
||||||
|
.color(note_color),
|
||||||
|
)
|
||||||
|
.child(Button::new().ghost().on_press(on_close).text("Cancel"))
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const GALLERY_COVER_EDGE: u32 = DEFAULT_IMAGE_EDGE;
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
use oneclient_common::VersionKey;
|
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
|
|
||||||
use crate::components::ClusterLandscapeArt;
|
use crate::components::{ART_PREVIEW_EDGE, DynamicArt};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
use crate::utils::{ReleaseLine, line_art_key, line_title, loader_tags};
|
use crate::utils::{GridSelection, ReleaseLine, line_art_key, line_title};
|
||||||
|
|
||||||
const CARD_HEIGHT_PX: f32 = 240.;
|
const CARD_HEIGHT_PX: f32 = 150.;
|
||||||
|
|
||||||
pub struct VersionCard {
|
pub struct VersionCard {
|
||||||
pub line: ReleaseLine,
|
pub key: GridSelection,
|
||||||
pub art_key: Option<VersionKey>,
|
pub art: DynamicArt,
|
||||||
pub tags: Vec<String>,
|
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub caption: String,
|
||||||
|
pub count: usize,
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub on_press: EventHandler<Event<PressEventData>>,
|
pub on_press: EventHandler<Event<PressEventData>>,
|
||||||
}
|
}
|
||||||
|
|
@ -22,14 +22,33 @@ impl VersionCard {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
line: ReleaseLine,
|
line: ReleaseLine,
|
||||||
clusters: &[Cluster],
|
clusters: &[Cluster],
|
||||||
|
caption: String,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
line,
|
key: GridSelection::Line(line),
|
||||||
art_key: line_art_key(line, clusters),
|
art: DynamicArt::for_version(line.major, line_art_key(line, clusters), None)
|
||||||
tags: loader_tags(clusters),
|
.max_edge(ART_PREVIEW_EDGE),
|
||||||
title: line_title(line, clusters),
|
title: line_title(line, clusters),
|
||||||
|
caption,
|
||||||
|
count: clusters.len(),
|
||||||
|
selected,
|
||||||
|
on_press: on_press.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn for_instance(
|
||||||
|
cluster: &Cluster,
|
||||||
|
selected: bool,
|
||||||
|
on_press: impl Into<EventHandler<Event<PressEventData>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
key: GridSelection::Instance(cluster.id),
|
||||||
|
art: DynamicArt::for_cluster(cluster).max_edge(ART_PREVIEW_EDGE),
|
||||||
|
title: cluster.name.clone(),
|
||||||
|
caption: format!("{} \u{b7} {}", cluster.mc_version, cluster.mc_loader),
|
||||||
|
count: 1,
|
||||||
selected,
|
selected,
|
||||||
on_press: on_press.into(),
|
on_press: on_press.into(),
|
||||||
}
|
}
|
||||||
|
|
@ -38,10 +57,11 @@ impl VersionCard {
|
||||||
|
|
||||||
impl PartialEq for VersionCard {
|
impl PartialEq for VersionCard {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.line == other.line
|
self.key == other.key
|
||||||
&& self.art_key == other.art_key
|
&& self.art == other.art
|
||||||
&& self.tags == other.tags
|
|
||||||
&& self.title == other.title
|
&& self.title == other.title
|
||||||
|
&& self.caption == other.caption
|
||||||
|
&& self.count == other.count
|
||||||
&& self.selected == other.selected
|
&& self.selected == other.selected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -58,101 +78,91 @@ impl Component for VersionCard {
|
||||||
let focused = focus().is_focused();
|
let focused = focus().is_focused();
|
||||||
let on_press = self.on_press.clone();
|
let on_press = self.on_press.clone();
|
||||||
|
|
||||||
let opacity = if selected || hovered || focused {
|
let art_opacity = if selected {
|
||||||
if selected { 1.0 } else { 0.85 }
|
1.0
|
||||||
|
} else if hovered || focused {
|
||||||
|
0.85
|
||||||
} else {
|
} else {
|
||||||
0.6
|
0.6
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_color = if selected || focused {
|
let border = if selected || focused {
|
||||||
colors::brand()
|
border_all_color(2., colors::brand())
|
||||||
} else if hovered {
|
} else if hovered {
|
||||||
colors::component_border_hover()
|
border_all_color(1., colors::component_border_hover())
|
||||||
} else {
|
} else {
|
||||||
colors::component_border()
|
border_all_color(1., colors::component_border())
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.key(self.line)
|
.key(self.key)
|
||||||
.width(Size::flex(1.0))
|
.width(Size::fill())
|
||||||
.height(Size::px(CARD_HEIGHT_PX))
|
.height(Size::px(CARD_HEIGHT_PX))
|
||||||
|
.corner_radius(CornerRadius::new_all(16.))
|
||||||
|
.overflow(Overflow::Clip)
|
||||||
|
.background(colors::page_elevated())
|
||||||
.a11y_id(a11y_id)
|
.a11y_id(a11y_id)
|
||||||
.a11y_focusable(true)
|
.a11y_focusable(true)
|
||||||
.a11y_role(AccessibilityRole::Button)
|
.a11y_role(AccessibilityRole::Button)
|
||||||
.on_press(move |e| on_press.call(e))
|
.on_press(move |e| on_press.call(e))
|
||||||
.on_pointer_enter(move |_| {
|
.on_pointer_enter(move |_| hovering.set(true))
|
||||||
*hovering.write() = true;
|
.on_pointer_leave(move |_| hovering.set(false))
|
||||||
})
|
|
||||||
.on_pointer_leave(move |_| {
|
|
||||||
*hovering.write() = false;
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
rect()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill())
|
|
||||||
.overflow(Overflow::Clip)
|
|
||||||
.corner_radius(CornerRadius::new_all(12.))
|
|
||||||
.opacity(opacity)
|
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.position(Position::new_absolute())
|
.position(Position::new_absolute())
|
||||||
.child(ClusterLandscapeArt::for_version(
|
.opacity(art_opacity)
|
||||||
self.line.major,
|
.child(self.art.clone()),
|
||||||
self.art_key,
|
|
||||||
None,
|
|
||||||
false,
|
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.height(Size::fill())
|
.height(Size::fill())
|
||||||
.padding(Gaps::new_symmetric(12., 16.))
|
.padding(12.)
|
||||||
.main_align(Alignment::SpaceBetween)
|
.spacing(5.)
|
||||||
.corner_radius(CornerRadius::new_all(12.))
|
.main_align(Alignment::End)
|
||||||
.cross_align(Alignment::Start)
|
.corner_radius(CornerRadius::new_all(16.))
|
||||||
.border(
|
.border(border.alignment(BorderAlignment::Inner))
|
||||||
border_all_color(1., border_color)
|
|
||||||
.alignment(BorderAlignment::Inner),
|
|
||||||
)
|
|
||||||
.layer(Layer::Relative(3))
|
.layer(Layer::Relative(3))
|
||||||
.background(
|
.background(
|
||||||
LinearGradient::new()
|
LinearGradient::new()
|
||||||
.angle(0.)
|
.angle(0.)
|
||||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 0.))
|
.stop((Color::from_af32rgb(0.1, 11, 16, 19), 0.))
|
||||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 20.))
|
.stop((Color::from_af32rgb(0.6, 11, 16, 19), 55.))
|
||||||
.stop((Color::from_af32rgb(0.3, 0, 0, 0), 60.))
|
.stop((Color::from_af32rgb(0.95, 11, 16, 19), 100.)),
|
||||||
.stop((Color::from_af32rgb(0.8, 0, 0, 0), 100.)),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
label()
|
||||||
.horizontal()
|
.text(self.title.clone())
|
||||||
.spacing(8.)
|
.font_size(22.)
|
||||||
.children(self.tags.iter().map(|tag| {
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
rect()
|
.color(Color::WHITE),
|
||||||
.padding(Gaps::new_symmetric(4., 8.))
|
)
|
||||||
.corner_radius(CornerRadius::new_all(999.))
|
|
||||||
.background(colors::fg_primary())
|
|
||||||
.child(
|
.child(
|
||||||
label()
|
label()
|
||||||
.text(tag.clone())
|
.text(self.caption.clone())
|
||||||
.font_size(12.)
|
.font_size(11.)
|
||||||
|
.max_lines(1)
|
||||||
|
.text_overflow(TextOverflow::Ellipsis)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.maybe_child((self.count > 1).then(|| {
|
||||||
|
rect()
|
||||||
|
.position(Position::new_absolute().top(8.).right(8.))
|
||||||
|
.layer(Layer::Relative(4))
|
||||||
|
.padding(Gaps::new_symmetric(4., 8.))
|
||||||
|
.corner_radius(CornerRadius::new_all(6.))
|
||||||
|
.background(Color::from_af32rgb(0.66, 11, 16, 19))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(format!("{} instances", self.count))
|
||||||
|
.font_size(11.)
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.color(colors::brand()),
|
.color(colors::fg_primary()),
|
||||||
)
|
)
|
||||||
.into_element()
|
.into_element()
|
||||||
})),
|
}))
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(format!("Version {}", self.title))
|
|
||||||
.font_size(32.)
|
|
||||||
.font_weight(FontWeight::SEMI_BOLD)
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||