Merge branch 'main' of https://github.com/Polyfrost/OneLauncher
This commit is contained in:
commit
7a6a7cbc0b
29 changed files with 468 additions and 279 deletions
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# Code blocks are selectable, and the open link confirmation has a copy button
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# Game logs hide account tokens, and the game output starts with the launch command ready to paste into a terminal
|
||||||
5
.changeset/github_release_changelog.md
Normal file
5
.changeset/github_release_changelog.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: minor
|
||||||
|
---
|
||||||
|
|
||||||
|
# The changelog page now shows each GitHub release's notes
|
||||||
35
.github/workflows/changelog_publish.yml
vendored
Normal file
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
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
|
||||||
103
.github/workflows/oneclient_release.yml
vendored
103
.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:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -4995,6 +4995,7 @@ dependencies = [
|
||||||
"oneclient_db",
|
"oneclient_db",
|
||||||
"oneclient_net",
|
"oneclient_net",
|
||||||
"polyio",
|
"polyio",
|
||||||
|
"regex",
|
||||||
"reqwest 0.13.5",
|
"reqwest 0.13.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ cargo packager --release -p oneclient_app --formats <targets>
|
||||||
|
|
||||||
The workspace shares a single version, defined in the root [`Cargo.toml`](./Cargo.toml) under `[workspace.package]`.
|
The workspace shares a single version, defined in the root [`Cargo.toml`](./Cargo.toml) under `[workspace.package]`.
|
||||||
|
|
||||||
|
Versions and release notes come from [Knope](https://knope.tech) change files in [`.changeset/`](./.changeset).
|
||||||
|
Add one per user-facing change with `knope document-change` (installed via `cargo install knope`). The
|
||||||
|
`OneClient Release Build` workflow consumes them, bumps the version, writes [`CHANGELOG.md`](./CHANGELOG.md),
|
||||||
|
and uses the new entry as the GitHub release body, which is what the launcher's changelog page shows.
|
||||||
|
|
||||||
|
|
||||||
## Code signing
|
## Code signing
|
||||||
|
|
||||||
|
|
|
||||||
28
knope.toml
Normal file
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"
|
||||||
|
|
@ -478,3 +478,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")
|
||||||
|
}
|
||||||
|
|
|
||||||
107
packages/oneclient_app/src/components/code_block.rs
Normal file
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Button, Icon, IconType, OVERLAY_BASE_LEVEL, 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,6 +15,7 @@ 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();
|
||||||
};
|
};
|
||||||
|
|
@ -73,20 +76,9 @@ impl Component for ConfirmLinkOverlay {
|
||||||
.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()
|
||||||
|
|
@ -94,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()
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ 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 {
|
||||||
|
|
@ -19,19 +20,12 @@ pub fn render_block(block: &Block, key: usize, style: &MarkdownStyle) -> Element
|
||||||
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.))
|
.font_family(style.code_font_family.clone())
|
||||||
.padding(Gaps::new_all(12.))
|
.font_size(style.code_font_size)
|
||||||
.child(
|
.key(key)
|
||||||
label()
|
|
||||||
.text(code.clone())
|
|
||||||
.font_family(style.code_font_family.clone())
|
|
||||||
.font_size(style.code_font_size)
|
|
||||||
.color(style.color_code),
|
|
||||||
)
|
|
||||||
.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()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ mod button;
|
||||||
mod charts;
|
mod charts;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
mod cluster_update_popup;
|
mod cluster_update_popup;
|
||||||
|
mod code_block;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod control_center;
|
mod control_center;
|
||||||
mod date_range_picker;
|
mod date_range_picker;
|
||||||
|
|
@ -62,10 +63,11 @@ pub use active_cluster_panel::ActiveClusterPanel;
|
||||||
pub use asset_image::AssetImage;
|
pub use asset_image::AssetImage;
|
||||||
pub use avatar::Avatar;
|
pub use avatar::Avatar;
|
||||||
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
||||||
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
|
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_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 control_center::ControlCenter;
|
||||||
pub use date_range_picker::{DateRange, DateRangePicker};
|
pub use date_range_picker::{DateRange, DateRangePicker};
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ pub use queries::{
|
||||||
UseDiscardLeftovers, UseLogAction, UseRefreshAccount, UseRemoveAccount, UseScreenshotAction,
|
UseDiscardLeftovers, UseLogAction, UseRefreshAccount, UseRemoveAccount, UseScreenshotAction,
|
||||||
UseSetDefaultAccount, UseStorageAction, UseUploadLog, VERSIONS_PAGE_SIZE,
|
UseSetDefaultAccount, UseStorageAction, UseUploadLog, VERSIONS_PAGE_SIZE,
|
||||||
accounts_have_microsoft, available_bundles, bundle_overrides_map, bundles_with_status_items,
|
accounts_have_microsoft, available_bundles, bundle_overrides_map, bundles_with_status_items,
|
||||||
category_list, changelog_error, changelog_groups, changelog_is_loading, cluster_art_url,
|
category_list, changelog_entries, changelog_error, changelog_is_loading, cluster_art_url,
|
||||||
cluster_content_items, content_type_for_slug, disable_warnings, game_versions,
|
cluster_content_items, content_type_for_slug, disable_warnings, game_versions,
|
||||||
has_migration_data, invalidate_cluster_content_queries, invalidate_cluster_queries,
|
has_migration_data, invalidate_cluster_content_queries, invalidate_cluster_queries,
|
||||||
invalidate_java_queries, invalidate_leftovers_queries, invalidate_logs_queries,
|
invalidate_java_queries, invalidate_leftovers_queries, invalidate_logs_queries,
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,30 @@
|
||||||
use freya::query::{Query, QueryCapability, UseQuery, use_query};
|
use freya::query::{Query, QueryCapability, UseQuery, use_query};
|
||||||
use oneclient_core::{ChangelogGroup, LauncherError, fetch_changelog, parse_changelog};
|
use oneclient_core::{ChangelogEntry, LauncherError, fetch_changelog};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ChangelogKeys {
|
|
||||||
pub meta_url_base: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct ChangelogQuery;
|
pub struct ChangelogQuery;
|
||||||
|
|
||||||
impl QueryCapability for ChangelogQuery {
|
impl QueryCapability for ChangelogQuery {
|
||||||
type Ok = Vec<ChangelogGroup>;
|
type Ok = Vec<ChangelogEntry>;
|
||||||
type Err = LauncherError;
|
type Err = LauncherError;
|
||||||
type Keys = ChangelogKeys;
|
type Keys = ();
|
||||||
|
|
||||||
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||||
let state = crate::launcher::state()?;
|
let state = crate::launcher::state()?;
|
||||||
let markdown = fetch_changelog(&state.services.requester).await?;
|
fetch_changelog(&state.services.requester).await
|
||||||
Ok(parse_changelog(&markdown))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn use_changelog() -> UseQuery<ChangelogQuery> {
|
pub fn use_changelog() -> UseQuery<ChangelogQuery> {
|
||||||
let meta_url_base = super::use_meta_url_key();
|
use_query(Query::new((), ChangelogQuery))
|
||||||
|
|
||||||
use_query(Query::new(ChangelogKeys { meta_url_base }, ChangelogQuery))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn changelog_groups(query: &UseQuery<ChangelogQuery>) -> Option<Vec<ChangelogGroup>> {
|
pub fn changelog_entries(query: &UseQuery<ChangelogQuery>) -> Option<Vec<ChangelogEntry>> {
|
||||||
super::state::settled_or_loading(query)
|
super::state::settled_or_loading(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn latest_changelog_version(query: &UseQuery<ChangelogQuery>) -> Option<String> {
|
pub fn latest_changelog_version(query: &UseQuery<ChangelogQuery>) -> Option<String> {
|
||||||
changelog_groups(query).and_then(|groups| groups.first().map(|group| group.version.clone()))
|
changelog_entries(query).and_then(|entries| entries.first().map(|entry| entry.version.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn changelog_error(query: &UseQuery<ChangelogQuery>) -> Option<String> {
|
pub fn changelog_error(query: &UseQuery<ChangelogQuery>) -> Option<String> {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ pub use bundles::{
|
||||||
use_onboarding_bundles,
|
use_onboarding_bundles,
|
||||||
};
|
};
|
||||||
pub use changelog::{
|
pub use changelog::{
|
||||||
changelog_error, changelog_groups, changelog_is_loading, latest_changelog_version,
|
changelog_entries, changelog_error, changelog_is_loading, latest_changelog_version,
|
||||||
use_changelog,
|
use_changelog,
|
||||||
};
|
};
|
||||||
pub use cluster_content::{cluster_content_items, use_cluster_content, use_migratable_routes};
|
pub use cluster_content::{cluster_content_items, use_cluster_content, use_migratable_routes};
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,10 @@ use skia_safe::sampling_options::CubicResampler;
|
||||||
use crate::Route;
|
use crate::Route;
|
||||||
use crate::components::Button;
|
use crate::components::Button;
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
ART_PREVIEW_EDGE, AppNavbar, DynamicArt, FileDropOverlay, Icon, IconType, OverlayPopup,
|
ART_PREVIEW_EDGE, AppNavbar, CodeBlock, DynamicArt, FileDropOverlay, Icon, IconType,
|
||||||
ScrollArea, accept_drop,
|
OverlayPopup, accept_drop, copy_button,
|
||||||
};
|
};
|
||||||
use crate::layout::AnimatedAppOutlet;
|
use crate::layout::AnimatedAppOutlet;
|
||||||
use crate::theme;
|
|
||||||
use crate::use_settings_snapshot;
|
use crate::use_settings_snapshot;
|
||||||
use oneclient_core::clusters::Cluster;
|
use oneclient_core::clusters::Cluster;
|
||||||
use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
|
use oneclient_core::images::BACKGROUND_IMAGE_EDGE;
|
||||||
|
|
@ -121,20 +120,6 @@ struct LaunchErrorDialog {
|
||||||
impl Component for LaunchErrorDialog {
|
impl Component for LaunchErrorDialog {
|
||||||
fn render(&self) -> impl IntoElement {
|
fn render(&self) -> impl IntoElement {
|
||||||
let dispatch = crate::hooks::use_dispatch();
|
let dispatch = crate::hooks::use_dispatch();
|
||||||
let lines: Vec<Element> = self
|
|
||||||
.message
|
|
||||||
.lines()
|
|
||||||
.map(|line| {
|
|
||||||
label()
|
|
||||||
.text(line.to_string())
|
|
||||||
.font_family(theme::MONO_FONT)
|
|
||||||
.font_size(12.)
|
|
||||||
.width(Size::fill())
|
|
||||||
.color(colors::fg_primary())
|
|
||||||
.into_element()
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let close = dispatch.clone();
|
let close = dispatch.clone();
|
||||||
let outside_close = dispatch.clone();
|
let outside_close = dispatch.clone();
|
||||||
let copy_dispatch = dispatch.clone();
|
let copy_dispatch = dispatch.clone();
|
||||||
|
|
@ -181,19 +166,9 @@ impl Component for LaunchErrorDialog {
|
||||||
.color(colors::fg_secondary()),
|
.color(colors::fg_secondary()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
CodeBlock::new(self.message.clone())
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::px(260.))
|
|
||||||
.background(CODE_BG)
|
.background(CODE_BG)
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.height(260.),
|
||||||
.padding(Gaps::new_all(12.))
|
|
||||||
.overflow(Overflow::Clip)
|
|
||||||
.child(
|
|
||||||
ScrollArea::new()
|
|
||||||
.width(Size::fill())
|
|
||||||
.height(Size::fill())
|
|
||||||
.children(lines),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
|
|
@ -201,7 +176,11 @@ impl Component for LaunchErrorDialog {
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
.main_align(Alignment::End)
|
.main_align(Alignment::End)
|
||||||
.spacing(10.)
|
.spacing(10.)
|
||||||
.child(copy_error_button(&self.message, copy_dispatch))
|
.child(copy_button(
|
||||||
|
self.message.clone(),
|
||||||
|
"Error message copied to your clipboard.",
|
||||||
|
copy_dispatch,
|
||||||
|
))
|
||||||
.child(
|
.child(
|
||||||
Button::new()
|
Button::new()
|
||||||
.primary()
|
.primary()
|
||||||
|
|
@ -214,34 +193,6 @@ impl Component for LaunchErrorDialog {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn copy_error_button(message: &str, dispatch: crate::Actions) -> impl IntoElement {
|
|
||||||
let message = message.to_string();
|
|
||||||
Button::new()
|
|
||||||
.secondary()
|
|
||||||
.on_press(move |_| {
|
|
||||||
if let Err(err) = freya::text_edit::Clipboard::set(message.clone()) {
|
|
||||||
tracing::warn!("clipboard copy failed: {err:?}");
|
|
||||||
dispatch
|
|
||||||
.notify("Copy failed")
|
|
||||||
.body("Could not copy the error to the clipboard.")
|
|
||||||
.error()
|
|
||||||
.toast_only()
|
|
||||||
.send();
|
|
||||||
} else {
|
|
||||||
dispatch
|
|
||||||
.notify("Copied to clipboard")
|
|
||||||
.body("Error message copied to your clipboard.")
|
|
||||||
.info()
|
|
||||||
.icon(IconType::ClipboardCheck)
|
|
||||||
.toast_only()
|
|
||||||
.send();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(Icon::new(IconType::Copy01).size(14.))
|
|
||||||
.text("Copy")
|
|
||||||
.into_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn appshell_overlay(alpha: f32) -> Rect {
|
pub(crate) fn appshell_overlay(alpha: f32) -> Rect {
|
||||||
let alpha = alpha.clamp(0., 1.);
|
let alpha = alpha.clamp(0., 1.);
|
||||||
let scale = |a: u8| (f32::from(a) * alpha).round() as u8;
|
let scale = |a: u8| (f32::from(a) * alpha).round() as u8;
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ use freya::animation::{
|
||||||
use freya::prelude::*;
|
use freya::prelude::*;
|
||||||
|
|
||||||
use super::settings_page;
|
use super::settings_page;
|
||||||
use crate::components::{Icon, IconType};
|
use crate::components::{Icon, IconType, Markdown, MarkdownStyle};
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
changelog_error, changelog_groups, changelog_is_loading, latest_changelog_version,
|
changelog_entries, changelog_error, changelog_is_loading, latest_changelog_version,
|
||||||
use_changelog, use_dispatch, use_settings_snapshot,
|
use_changelog, use_dispatch, use_settings_snapshot,
|
||||||
};
|
};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
@ -55,15 +55,15 @@ impl Component for SettingsChangelog {
|
||||||
.into_element();
|
.into_element();
|
||||||
}
|
}
|
||||||
|
|
||||||
let groups = changelog_groups(&query).unwrap_or_default();
|
let entries = changelog_entries(&query).unwrap_or_default();
|
||||||
|
|
||||||
settings_page()
|
settings_page()
|
||||||
.children(groups.into_iter().enumerate().map(|(i, group)| {
|
.children(entries.into_iter().enumerate().map(|(i, entry)| {
|
||||||
let current = group.version == installed_version;
|
let current = entry.version == installed_version;
|
||||||
ReleaseCard {
|
ReleaseCard {
|
||||||
version: group.version,
|
version: entry.version,
|
||||||
current,
|
current,
|
||||||
changes: group.changes,
|
body: entry.body,
|
||||||
initially_open: i == 0,
|
initially_open: i == 0,
|
||||||
}
|
}
|
||||||
.into_element()
|
.into_element()
|
||||||
|
|
@ -79,7 +79,7 @@ const CHEVRON_CLOSED_DEG: f32 = -90.;
|
||||||
struct ReleaseCard {
|
struct ReleaseCard {
|
||||||
version: String,
|
version: String,
|
||||||
current: bool,
|
current: bool,
|
||||||
changes: Vec<String>,
|
body: String,
|
||||||
initially_open: bool,
|
initially_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,8 +110,6 @@ impl Component for ReleaseCard {
|
||||||
self.version.clone()
|
self.version.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let changes = self.changes.clone();
|
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
|
|
@ -144,48 +142,30 @@ impl Component for ReleaseCard {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.maybe_child(is_open.then(|| {
|
.maybe_child(is_open.then(|| {
|
||||||
rect()
|
if self.body.trim().is_empty() {
|
||||||
.vertical()
|
label()
|
||||||
.width(Size::fill())
|
.text("No changes recorded for this version.")
|
||||||
.spacing(4.)
|
.font_size(12.)
|
||||||
.padding(Gaps::new(0., 0., 0., 6.))
|
.color(colors::fg_secondary())
|
||||||
.children(if changes.is_empty() {
|
.into_element()
|
||||||
vec![
|
} else {
|
||||||
rect()
|
Markdown::new(self.body.clone())
|
||||||
.child(
|
.width(Size::fill())
|
||||||
label()
|
.style(MarkdownStyle {
|
||||||
.text("No changes recorded for this version.")
|
color: colors::fg_primary(),
|
||||||
.font_size(12.)
|
color_link: colors::code_info(),
|
||||||
.color(colors::fg_secondary()),
|
color_code: colors::fg_primary(),
|
||||||
)
|
background_code: colors::component_bg(),
|
||||||
.into_element(),
|
background_blockquote: colors::component_bg(),
|
||||||
]
|
border_blockquote: colors::brand(),
|
||||||
} else {
|
background_divider: colors::component_border(),
|
||||||
changes
|
headings: [18., 16., 14., 13., 12., 12.],
|
||||||
.into_iter()
|
paragraph_size: 12.,
|
||||||
.map(|change| {
|
code_font_size: 11.,
|
||||||
rect()
|
..MarkdownStyle::default()
|
||||||
.horizontal()
|
})
|
||||||
.width(Size::fill())
|
.into_element()
|
||||||
.spacing(8.)
|
}
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text("•")
|
|
||||||
.font_size(12.)
|
|
||||||
.color(colors::fg_secondary()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
label()
|
|
||||||
.text(change)
|
|
||||||
.font_size(12.)
|
|
||||||
.width(Size::flex(1.0))
|
|
||||||
.color(colors::fg_primary()),
|
|
||||||
)
|
|
||||||
.into_element()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.into_element()
|
|
||||||
}))
|
}))
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ bytes.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
regex.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_repr.workspace = true
|
serde_repr.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ pub(super) fn lines_from(content: &str, opts: &ReadOptions) -> Vec<LogLine> {
|
||||||
lines.push(LogLine {
|
lines.push(LogLine {
|
||||||
number: idx + 1,
|
number: idx + 1,
|
||||||
level,
|
level,
|
||||||
text: raw.to_string(),
|
text: super::censor(raw).into_owned(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub async fn upload_log_at(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
) -> ClusterResult<MclogsUploadResponse> {
|
) -> ClusterResult<MclogsUploadResponse> {
|
||||||
let path = ensure_allowed(path)?;
|
let path = ensure_allowed(path)?;
|
||||||
let mut content = read_file_string(&path).await?;
|
let mut content = super::censor(&read_file_string(&path).await?).into_owned();
|
||||||
|
|
||||||
let line_count = content.lines().count();
|
let line_count = content.lines().count();
|
||||||
if line_count > MAX_LINES {
|
if line_count > MAX_LINES {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use chrono::{DateTime, Utc};
|
||||||
pub use error::LogsError;
|
pub use error::LogsError;
|
||||||
pub use manage::{cluster_output_log, delete_log_at, list_cluster_logs, read_log_at};
|
pub use manage::{cluster_output_log, delete_log_at, list_cluster_logs, read_log_at};
|
||||||
pub use mclogs::upload_log_at;
|
pub use mclogs::upload_log_at;
|
||||||
pub use parse::parse_level;
|
pub use parse::{censor, parse_level};
|
||||||
|
|
||||||
/// No launcher variant on purpose the launcher's own logs live in the log console
|
/// No launcher variant on purpose the launcher's own logs live in the log console
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,21 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use super::LogLevel;
|
use super::LogLevel;
|
||||||
|
|
||||||
|
static SECRET: LazyLock<Regex> = LazyLock::new(|| {
|
||||||
|
Regex::new(
|
||||||
|
r#"(--accessToken|--session(?:Id)?)([\s,=]+)[^\s,\[\]'"]+|(token:)[^:\s]+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*"#,
|
||||||
|
)
|
||||||
|
.expect("secret pattern")
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn censor(line: &str) -> Cow<'_, str> {
|
||||||
|
SECRET.replace_all(line, "${1}${2}${3}[censored]")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_level(line: &str) -> Option<LogLevel> {
|
pub fn parse_level(line: &str) -> Option<LogLevel> {
|
||||||
let bytes = line.as_bytes();
|
let bytes = line.as_bytes();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
|
||||||
|
|
@ -3,60 +3,24 @@ use oneclient_common::paths;
|
||||||
use oneclient_net::RequestClient;
|
use oneclient_net::RequestClient;
|
||||||
use oneclient_net::{EtagPolicy, fetch_cached};
|
use oneclient_net::{EtagPolicy, fetch_cached};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
const CHANGELOG_URL: &str =
|
||||||
pub struct ChangelogGroup {
|
"https://github.com/Polyfrost/OneLauncher/releases/latest/download/changelog.json";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
||||||
|
pub struct ChangelogEntry {
|
||||||
pub version: String,
|
pub version: String,
|
||||||
pub changes: Vec<String>,
|
pub body: String,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_changelog(data: &str) -> Vec<ChangelogGroup> {
|
|
||||||
let mut groups = Vec::new();
|
|
||||||
|
|
||||||
for line in data.lines() {
|
|
||||||
if let Some(version) = line.strip_prefix("# ") {
|
|
||||||
groups.push(ChangelogGroup {
|
|
||||||
version: version.trim().to_string(),
|
|
||||||
changes: Vec::new(),
|
|
||||||
});
|
|
||||||
} else if let Some(change) = line.strip_prefix("- ") {
|
|
||||||
if let Some(group) = groups.last_mut() {
|
|
||||||
group.changes.push(change.trim().to_string());
|
|
||||||
}
|
|
||||||
} else if line.trim() == "###" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
groups
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip(net))]
|
#[tracing::instrument(level = "debug", skip(net))]
|
||||||
pub async fn fetch_changelog(net: &RequestClient) -> LauncherResult<String> {
|
pub async fn fetch_changelog(net: &RequestClient) -> LauncherResult<Vec<ChangelogEntry>> {
|
||||||
let url = format!("{}/oneclient/CHANGE_LOG.md", net.config().meta_url_base);
|
let cache_path = paths::caches_dir()?.join("changelog.json");
|
||||||
let cache_path = paths::caches_dir()?.join("CHANGE_LOG.md");
|
|
||||||
|
|
||||||
let fetched = fetch_cached(net, &url, &cache_path, EtagPolicy::CommitNow)
|
let fetched = fetch_cached(net, CHANGELOG_URL, &cache_path, EtagPolicy::CommitNow)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| LauncherError::InvalidSettingsProfile {
|
.ok_or_else(|| LauncherError::InvalidSettingsProfile {
|
||||||
reason: "changelog is unavailable and not cached".to_string(),
|
reason: "changelog is unavailable and not cached".to_string(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(fetched.text())
|
Ok(fetched.json()?)
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_changelog_groups_versions_and_bullets() {
|
|
||||||
let data = "# 2.0.0\n\n- Faster UI\n- Bug fixes\n# 1.9.0\n- Older change\n###\nignored\n";
|
|
||||||
let groups = parse_changelog(data);
|
|
||||||
|
|
||||||
assert_eq!(groups.len(), 2);
|
|
||||||
assert_eq!(groups[0].version, "2.0.0");
|
|
||||||
assert_eq!(groups[0].changes, vec!["Faster UI", "Bug fixes"]);
|
|
||||||
assert_eq!(groups[1].version, "1.9.0");
|
|
||||||
assert_eq!(groups[1].changes, vec!["Older change"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::fmt::Write as _;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -5,7 +6,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use interfrost::api::minecraft::ArgumentType;
|
use interfrost::api::minecraft::ArgumentType;
|
||||||
use tokio::io::AsyncReadExt;
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::ClusterStage;
|
use crate::ClusterStage;
|
||||||
|
|
@ -393,6 +394,14 @@ async fn start(
|
||||||
.args(mc_args)
|
.args(mc_args)
|
||||||
.current_dir(&cwd);
|
.current_dir(&cwd);
|
||||||
|
|
||||||
|
let header = format!(
|
||||||
|
"OneClient {} · {} · {version_name} · Java {}\n\n{}\n\n",
|
||||||
|
env!("CARGO_PKG_VERSION"),
|
||||||
|
cluster.name,
|
||||||
|
java.major,
|
||||||
|
command_line(command.as_std(), &version_info.main_class),
|
||||||
|
);
|
||||||
|
|
||||||
let log_path = oneclient_cluster::logs::cluster_output_log(&cluster)?;
|
let log_path = oneclient_cluster::logs::cluster_output_log(&cluster)?;
|
||||||
if let Some(parent) = log_path.parent() {
|
if let Some(parent) = log_path.parent() {
|
||||||
polyio::create_dir_all(parent).await.ok();
|
polyio::create_dir_all(parent).await.ok();
|
||||||
|
|
@ -403,10 +412,15 @@ async fn start(
|
||||||
// The cloned handle shares the file offset
|
// The cloned handle shares the file offset
|
||||||
// so stdout and stderr interleave instead of overwriting
|
// so stdout and stderr interleave instead of overwriting
|
||||||
let handles = match tokio::fs::File::create(&log_path).await {
|
let handles = match tokio::fs::File::create(&log_path).await {
|
||||||
Ok(out) => match out.try_clone().await {
|
Ok(mut out) => {
|
||||||
Ok(err) => Ok((out.into_std().await, err.into_std().await)),
|
if let Err(err) = out.write_all(header.as_bytes()).await {
|
||||||
Err(err) => Err(err),
|
tracing::warn!(cluster_id, error = %err, "failed to write the launch header");
|
||||||
},
|
}
|
||||||
|
match out.try_clone().await {
|
||||||
|
Ok(err) => Ok((out.into_std().await, err.into_std().await)),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -750,6 +764,66 @@ fn base_command(profile: &GameSettingsProfile, java_path: &str) -> (Command, Opt
|
||||||
(command, Some(program.to_string()))
|
(command, Some(program.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn command_line(command: &std::process::Command, main_class: &str) -> String {
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
const CONTINUATION: &str = " \\\n ";
|
||||||
|
#[cfg(windows)]
|
||||||
|
const CONTINUATION: &str = " `\n ";
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
if let Some(dir) = command.get_current_dir() {
|
||||||
|
let _ = writeln!(out, "cd {}", shell_quote(&dir.to_string_lossy()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in command.get_envs() {
|
||||||
|
let (key, Some(value)) = (key.to_string_lossy(), value) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value = shell_quote(&value.to_string_lossy());
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
let _ = write!(out, "{key}={value} ");
|
||||||
|
#[cfg(windows)]
|
||||||
|
let _ = writeln!(out, "$env:{key} = {value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
out += "& ";
|
||||||
|
}
|
||||||
|
out += &shell_quote(&command.get_program().to_string_lossy());
|
||||||
|
|
||||||
|
for arg in command.get_args() {
|
||||||
|
let arg = arg.to_string_lossy();
|
||||||
|
let arg = oneclient_cluster::logs::censor(&arg);
|
||||||
|
out += if arg.starts_with('-') || arg == main_class {
|
||||||
|
CONTINUATION
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
out += &shell_quote(&arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
fn shell_quote(arg: &str) -> String {
|
||||||
|
let plain = !arg.is_empty()
|
||||||
|
&& arg
|
||||||
|
.bytes()
|
||||||
|
.all(|b| b.is_ascii_alphanumeric() || b"_-./=:,+@%".contains(&b));
|
||||||
|
if plain {
|
||||||
|
arg.to_string()
|
||||||
|
} else {
|
||||||
|
format!("'{}'", arg.replace('\'', r"'\''"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn shell_quote(arg: &str) -> String {
|
||||||
|
format!("'{}'", arg.replace('\'', "''"))
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
|
||||||
command.env_remove("_JAVA_OPTIONS");
|
command.env_remove("_JAVA_OPTIONS");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ async fn emit(
|
||||||
crash_watch: &CrashWatch,
|
crash_watch: &CrashWatch,
|
||||||
) {
|
) {
|
||||||
// Blank lines are kept so the console reads the way the log does
|
// Blank lines are kept so the console reads the way the log does
|
||||||
let text = line.trim_end_matches(['\n', '\r']).to_string();
|
let text = oneclient_cluster::logs::censor(line.trim_end_matches(['\n', '\r'])).into_owned();
|
||||||
if let Some(recorder) = recorder {
|
if let Some(recorder) = recorder {
|
||||||
recorder.observe(&text).await;
|
recorder.observe(&text).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ pub mod tos;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
pub mod versions;
|
pub mod versions;
|
||||||
|
|
||||||
pub use changelog::{ChangelogGroup, fetch_changelog, parse_changelog};
|
pub use changelog::{ChangelogEntry, fetch_changelog};
|
||||||
pub use clusters::{
|
pub use clusters::{
|
||||||
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
|
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
|
||||||
apply_bundle_java_override, ensure_from_bundles, ensure_from_versions,
|
apply_bundle_java_override, ensure_from_bundles, ensure_from_versions,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue