feat: update base to official upstream 2.5.0 with all cracked features intact (2.5.0-c1)
Some checks are pending
Release Multiplatform Binaries / build (deb,appimage, linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-arm64, macos-14, aarch64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-x86_64, macos-14, x86_64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (nsis, windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Release Multiplatform Binaries / create-release (push) Blocked by required conditions
Some checks are pending
Release Multiplatform Binaries / build (deb,appimage, linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-arm64, macos-14, aarch64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-x86_64, macos-14, x86_64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (nsis, windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Release Multiplatform Binaries / create-release (push) Blocked by required conditions
- Rebased base onto official Polyfrost OneLauncher 2.5.0 (release v2.5.0 + latest bundle fixes). - Integrated new Upstream Control Center quick settings & system info overlay and navbar avatar-with-gear trigger. - Preserved top navbar 'Skins' direct route navigation. - Maintained guaranteed offline mode launch without requiring any Microsoft accounts. - Maintained custom 3D offline skin changer, player model switch (Classic/Slim), and targeted Minecraft Java UUID character slot isolation. - Maintained CustomSkinLoader auto-configuration for local skins, Ely.by, and SkinRestorer. - Integrated direct GitHub Releases auto-updater pointing to oiupoyt/OneLauncher-Cracked. - Preserved inline username renaming modal and Change Skin navigation in account settings and onboarding. - Maintained mods filter popover blur/text ghosting fix in package manager. - Preserved fast dev profile and multiplatform release workflow for v2.5.0-c1. - All workspace tests and clippy checks pass with 0 errors.
This commit is contained in:
parent
618731da7d
commit
d214afe99b
21 changed files with 917 additions and 62 deletions
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
|
|
@ -7,9 +7,9 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release Tag (e.g. v2.4.2)'
|
||||
description: 'Release Tag (e.g. v2.5.0)'
|
||||
required: false
|
||||
default: 'v2.4.2-c1'
|
||||
default: 'v2.5.0-c1'
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
|
|
@ -138,7 +138,7 @@ jobs:
|
|||
elif [ -n "${{ inputs.tag }}" ]; then
|
||||
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "tag=v2.4.2-c1" >> $GITHUB_OUTPUT
|
||||
echo "tag=v2.5.0-c1" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Clean up previous release if exists
|
||||
|
|
@ -155,9 +155,9 @@ jobs:
|
|||
name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}"
|
||||
body: |
|
||||
### What's New in ${{ steps.release_tag.outputs.tag }}
|
||||
- **Rebased onto Official Upstream 2.4.2**: Includes all upstream 2.4.0, 2.4.1, and 2.4.2 updates (Linux discrete GPU offloading, shared/cluster mod symlinks, Microsoft Java provider, UI tooltips, analytics graphs, Ornithe loader, bundle fixes).
|
||||
- **Rebased onto Official Upstream 2.5.0**: Includes all upstream 2.5.0 updates (New Control Center quick settings & system info, refined navbar styling, bundle version-tag matching and compatibility filtering, version metadata self-healing).
|
||||
- **Guaranteed Offline Game Launch**: Completely eliminated the upstream check in `account_for_launch` that blocked offline accounts from launching without Microsoft accounts. Offline accounts can launch directly with 0 Microsoft accounts required.
|
||||
- **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation.
|
||||
- **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, slim/classic model toggle, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation.
|
||||
- **Offline / Cracked Accounts**: Full offline account support in onboarding and settings with inline username editing.
|
||||
- **Integrated Auto-Updater**: Native background update checks and direct GitHub Releases binary downloads.
|
||||
- **UI Bug Fixes**: Fixed filter popover blur/text ghosting in the package manager.
|
||||
|
|
|
|||
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -4480,7 +4480,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_app"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
|
@ -4530,7 +4530,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_auth"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
|
@ -4554,7 +4554,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_cluster"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"bytes 1.11.1",
|
||||
"chrono",
|
||||
|
|
@ -4577,7 +4577,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_common"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"interfrost",
|
||||
|
|
@ -4590,7 +4590,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_content"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
|
|
@ -4618,7 +4618,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_core"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"async-stream",
|
||||
|
|
@ -4683,7 +4683,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_db"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"async-tempfile",
|
||||
"chrono",
|
||||
|
|
@ -4698,7 +4698,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_discord"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"discord-rich-presence",
|
||||
|
|
@ -4710,7 +4710,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_events"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
|
|
@ -4720,7 +4720,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_java"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-tempfile",
|
||||
|
|
@ -4743,7 +4743,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_macro"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -4752,7 +4752,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_mc"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
|
@ -4774,7 +4774,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_net"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -4796,7 +4796,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_polyplus"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"async-tungstenite",
|
||||
"bytes 1.11.1",
|
||||
|
|
@ -5094,7 +5094,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
|||
|
||||
[[package]]
|
||||
name = "polyio"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-compression",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ license = "GPL-3.0-only"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/oiupoyt/OneLauncher-Cracked"
|
||||
rust-version = "1.97"
|
||||
version = "2.4.2-c1"
|
||||
version = "2.5.0-c1"
|
||||
|
||||
[workspace.dependencies]
|
||||
# core
|
||||
|
|
@ -172,8 +172,5 @@ lto = false
|
|||
strip = false
|
||||
|
||||
[profile.dev]
|
||||
debug = 1
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
debug = false
|
||||
debug = 0
|
||||
strip = "debuginfo"
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ makedepends=(
|
|||
)
|
||||
provides=('oneclient')
|
||||
conflicts=('oneclient-bin')
|
||||
options=('!lto')
|
||||
options=('!lto', '!debug')
|
||||
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 15V17M12 11V17M16 7V17M6.8 21H17.2C18.88 21 19.72 21 20.36 20.67C20.93 20.39 21.39 19.93 21.67 19.36C22 18.72 22 17.88 22 16.2V7.8C22 6.12 22 5.28 21.67 4.64C21.39 4.07 20.93 3.61 20.36 3.33C19.72 3 18.88 3 17.2 3H6.8C5.12 3 4.28 3 3.64 3.33C3.07 3.61 2.61 4.07 2.33 4.64C2 5.28 2 6.12 2 7.8V16.2C2 17.88 2 18.72 2.33 19.36C2.61 19.93 3.07 20.39 3.64 20.67C4.28 21 5.12 21 6.8 21Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 590 B |
1
packages/oneclient_app/assets/icons/discord.svg
Normal file
1
packages/oneclient_app/assets/icons/discord.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg role="img" viewBox="-11.2 -8.5 149 113" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
packages/oneclient_app/assets/icons/log-out-01.svg
Normal file
3
packages/oneclient_app/assets/icons/log-out-01.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 17L21 12M21 12L16 7M21 12H9M9 21H7.8C6.12 21 5.28 21 4.64 20.67C4.07 20.39 3.61 19.93 3.33 19.36C3 18.72 3 17.88 3 16.2V7.8C3 6.12 3 5.28 3.33 4.64C3.61 4.07 4.07 3.61 4.64 3.33C5.28 3 6.12 3 7.8 3H9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
3
packages/oneclient_app/assets/icons/moon-01.svg
Normal file
3
packages/oneclient_app/assets/icons/moon-01.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="100%" height="100%" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2 12.63A7.2 7.2 0 1 1 11.37 4.8A5.6 5.6 0 0 0 19.2 12.63Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 270 B |
|
|
@ -106,12 +106,20 @@ impl Component for AccountPanel {
|
|||
0.,
|
||||
Color::from_argb(120, 0, 0, 0),
|
||||
)))
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.child(BackToControlCenter)
|
||||
.child(
|
||||
label()
|
||||
.text("Accounts")
|
||||
.font_size(18.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.a11y_role(AccessibilityRole::TitleBar),
|
||||
),
|
||||
)
|
||||
.child(divider())
|
||||
.child(rows)
|
||||
|
|
@ -120,6 +128,42 @@ impl Component for AccountPanel {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct BackToControlCenter;
|
||||
|
||||
impl Component for BackToControlCenter {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let dispatch = use_dispatch();
|
||||
let mut hovered = use_state(|| false);
|
||||
|
||||
let back = move |_| {
|
||||
dispatch.close_account_switcher();
|
||||
dispatch.open_control_center();
|
||||
};
|
||||
|
||||
rect()
|
||||
.width(Size::px(28.))
|
||||
.height(Size::px(28.))
|
||||
.center()
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(Color::RED.with_a(0))
|
||||
.maybe(*hovered.read(), |el| {
|
||||
el.background(colors::ghost_overlay_hover())
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.a11y_alt("Back to control center")
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
.on_pointer_leave(move |_| hovered.set(false))
|
||||
.on_press(back)
|
||||
.child(
|
||||
Icon::new(IconType::ArrowLeft)
|
||||
.size(18.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountRow {
|
||||
account: MinecraftAccount,
|
||||
active: bool,
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ pub struct Button {
|
|||
tooltip: Option<Box<str>>,
|
||||
tooltip_placement: TooltipPlacement,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
overflow: Overflow,
|
||||
|
||||
elements: Vec<Element>,
|
||||
key: DiffKey,
|
||||
|
|
@ -117,6 +118,7 @@ impl Button {
|
|||
tooltip_placement: TooltipPlacement::default(),
|
||||
on_press: None,
|
||||
elements: Vec::new(),
|
||||
overflow: Overflow::Clip,
|
||||
key: DiffKey::None,
|
||||
cursor_icon: CursorIcon::Pointer,
|
||||
}
|
||||
|
|
@ -156,6 +158,11 @@ impl Button {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn overflow(mut self, overflow: Overflow) -> Self {
|
||||
self.overflow = overflow;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn font_size(mut self, font_size: impl Into<f32>) -> Self {
|
||||
self.text_style.font_size = Some(font_size.into().into());
|
||||
self
|
||||
|
|
@ -314,7 +321,7 @@ impl Component for Button {
|
|||
.a11y_focusable(enabled() && self.focusable)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.map(self.alt.clone(), |rect, alt| rect.a11y_alt(alt))
|
||||
.overflow(Overflow::Clip)
|
||||
.overflow(self.overflow)
|
||||
.layout(self.layout.clone())
|
||||
.text_style(self.text_style.clone())
|
||||
.children(self.elements.clone());
|
||||
|
|
|
|||
634
packages/oneclient_app/src/components/control_center.rs
Normal file
634
packages/oneclient_app/src/components/control_center.rs
Normal file
|
|
@ -0,0 +1,634 @@
|
|||
use freya::{
|
||||
animation::{AnimNum, Ease, OnCreation, use_animation},
|
||||
prelude::*,
|
||||
router::RouterContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Route,
|
||||
components::{Avatar, Button, Icon, IconType, OVERLAY_MAX_LEVEL, OverlayPopup},
|
||||
hooks::{
|
||||
RemoveAccountKeys, query_error, query_is_loading, try_accounts, try_default_account,
|
||||
use_accounts, use_control_center_open, use_current_account, use_dispatch,
|
||||
use_remove_account, use_settings_snapshot,
|
||||
},
|
||||
theme::colors,
|
||||
ui::{border_all_color, divider},
|
||||
};
|
||||
|
||||
const PANEL_WIDTH: f32 = 344.;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct ControlCenter;
|
||||
|
||||
impl Component for ControlCenter {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let open = use_control_center_open();
|
||||
let dispatch = use_dispatch();
|
||||
|
||||
if !open {
|
||||
return rect().into_element();
|
||||
}
|
||||
|
||||
OverlayPopup::new()
|
||||
.position(Position::new_global().top(72.).right(40.))
|
||||
.on_close(move |()| dispatch.close_control_center())
|
||||
.child(ControlPanel)
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct ControlPanel;
|
||||
|
||||
impl Component for ControlPanel {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let intro = use_animation(|conf| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
AnimNum::new(0., 1.).time(200).ease(Ease::Out)
|
||||
});
|
||||
let progress = intro.read().value();
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(PANEL_WIDTH))
|
||||
.padding(Gaps::new_all(12.))
|
||||
.spacing(10.)
|
||||
.opacity(progress)
|
||||
.margin(Gaps::new((1.0 - progress) * -8.0, 0., 0., 0.))
|
||||
.background(colors::page_elevated().with_a(220))
|
||||
.blur(12.)
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.border(
|
||||
Border::new()
|
||||
.fill(colors::component_border())
|
||||
.width(1.)
|
||||
.alignment(BorderAlignment::Inner),
|
||||
)
|
||||
.shadow(Shadow::from((
|
||||
0.,
|
||||
8.,
|
||||
32.,
|
||||
0.,
|
||||
Color::from_argb(120, 0, 0, 0),
|
||||
)))
|
||||
.child(AccountHeader)
|
||||
.child(divider())
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(2.)
|
||||
.child(NavRow {
|
||||
icon: IconType::BarChartSquare02,
|
||||
row_label: "Stats",
|
||||
target: Route::Stats {},
|
||||
})
|
||||
.child(NavRow {
|
||||
icon: IconType::Settings02,
|
||||
row_label: "Settings",
|
||||
target: Route::SettingsLauncher {},
|
||||
}),
|
||||
)
|
||||
.child(section_label("QUICK SETTINGS"))
|
||||
.child(QuickSettings)
|
||||
.child(divider())
|
||||
.child(PanelFooter)
|
||||
}
|
||||
}
|
||||
|
||||
fn section_label(text: &'static str) -> impl IntoElement {
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(11.)
|
||||
.letter_spacing(0.8)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_secondary())
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct AccountHeader;
|
||||
|
||||
impl Component for AccountHeader {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let current = use_current_account();
|
||||
let accounts_query = use_accounts();
|
||||
let dispatch = use_dispatch();
|
||||
let mut hovered = use_state(|| false);
|
||||
|
||||
let loading = query_is_loading(¤t);
|
||||
let unusable = query_error(¤t).is_some();
|
||||
let account = try_default_account(¤t).or_else(|| {
|
||||
unusable
|
||||
.then(|| try_accounts(&accounts_query).unwrap_or_default().into_iter().next())
|
||||
.flatten()
|
||||
});
|
||||
|
||||
let uuid = account
|
||||
.as_ref()
|
||||
.map(|account| account.id.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::nil().to_string());
|
||||
let username = match account.as_ref() {
|
||||
Some(account) => account.username.clone(),
|
||||
None if loading => "Loading…".to_string(),
|
||||
None => "Not signed in".to_string(),
|
||||
};
|
||||
let subtitle = match (account.as_ref(), unusable) {
|
||||
(_, true) => "Microsoft Account required",
|
||||
(Some(account), false) if account.is_microsoft() => "Microsoft account",
|
||||
(Some(_), false) => "Offline account",
|
||||
(None, false) if loading => "Checking accounts",
|
||||
(None, false) => "No account selected",
|
||||
};
|
||||
let subtitle_color = if unusable {
|
||||
colors::danger()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
let signed_out = account.is_none() && !loading;
|
||||
let action_label = if signed_out { "Sign In" } else { "Switch" };
|
||||
let open_switcher = move |_| {
|
||||
dispatch.close_control_center();
|
||||
if signed_out {
|
||||
let _ = RouterContext::get().push(Route::SettingsAccounts {});
|
||||
} else {
|
||||
dispatch.open_account_switcher();
|
||||
}
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.content(Content::Flex)
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.)
|
||||
.padding(Gaps::new_all(4.))
|
||||
.child(
|
||||
Avatar::new(uuid)
|
||||
.width(Size::px(40.))
|
||||
.height(Size::px(40.)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.spacing(2.)
|
||||
.child(
|
||||
label()
|
||||
.text(username)
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(subtitle)
|
||||
.font_size(12.)
|
||||
.max_lines(1)
|
||||
.color(subtitle_color),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(6.)
|
||||
.padding(Gaps::new_symmetric(6., 8.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(Color::RED.with_a(0))
|
||||
.maybe(*hovered.read(), |el| {
|
||||
el.background(colors::ghost_overlay_hover())
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
.on_pointer_leave(move |_| hovered.set(false))
|
||||
.on_press(open_switcher)
|
||||
.child(
|
||||
label()
|
||||
.text(action_label)
|
||||
.font_size(13.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconType::ArrowRight)
|
||||
.size(14.)
|
||||
.color(colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct NavRow {
|
||||
icon: IconType,
|
||||
row_label: &'static str,
|
||||
target: Route,
|
||||
}
|
||||
|
||||
impl Component for NavRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let dispatch = use_dispatch();
|
||||
let mut hovered = use_state(|| false);
|
||||
|
||||
let target = self.target.clone();
|
||||
let go = move |_| {
|
||||
dispatch.close_control_center();
|
||||
let _ = RouterContext::get().push(target.clone());
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(42.))
|
||||
.content(Content::Flex)
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(12.)
|
||||
.padding(Gaps::new_symmetric(0., 8.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(Color::RED.with_a(0))
|
||||
.maybe(*hovered.read(), |el| {
|
||||
el.background(colors::ghost_overlay_hover())
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
.on_pointer_leave(move |_| hovered.set(false))
|
||||
.on_press(go)
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
.size(20.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.row_label)
|
||||
.font_size(15.)
|
||||
.width(Size::flex(1.0))
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconType::ArrowRight)
|
||||
.size(16.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct QuickSettings;
|
||||
|
||||
impl Component for QuickSettings {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let settings = use_settings_snapshot().settings;
|
||||
let dispatch = use_dispatch();
|
||||
|
||||
let discord_on = settings.discord_enabled;
|
||||
let toggle_discord = move |_| {
|
||||
dispatch.edit_settings(|s| s.discord_enabled = !s.discord_enabled);
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.content(Content::Flex)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
QuickTile::new(IconType::Discord, "Discord RPC", discord_on)
|
||||
.on_press(toggle_discord),
|
||||
)
|
||||
.child(QuickTile::new(IconType::Moon01, "Close on launch", false).disabled())
|
||||
}
|
||||
}
|
||||
|
||||
struct QuickTile {
|
||||
icon: IconType,
|
||||
title: &'static str,
|
||||
on: bool,
|
||||
enabled: bool,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for QuickTile {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.icon == other.icon
|
||||
&& self.title == other.title
|
||||
&& self.on == other.on
|
||||
&& self.enabled == other.enabled
|
||||
}
|
||||
}
|
||||
|
||||
impl QuickTile {
|
||||
fn new(icon: IconType, title: &'static str, on: bool) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
title,
|
||||
on,
|
||||
enabled: true,
|
||||
on_press: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled(mut self) -> Self {
|
||||
self.enabled = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
|
||||
self.on_press = Some(on_press.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for QuickTile {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut hovered = use_state(|| false);
|
||||
|
||||
let on = self.on;
|
||||
let enabled = self.enabled;
|
||||
let handler = self.on_press.clone();
|
||||
|
||||
let background = if !enabled {
|
||||
colors::component_bg().with_a(120)
|
||||
} else if on {
|
||||
if *hovered.read() {
|
||||
colors::brand_hover()
|
||||
} else {
|
||||
colors::brand()
|
||||
}
|
||||
} else if *hovered.read() {
|
||||
colors::component_bg_hover()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
};
|
||||
|
||||
let title_color = if enabled {
|
||||
colors::fg_primary()
|
||||
} else {
|
||||
colors::fg_primary_disabled()
|
||||
};
|
||||
|
||||
let sub_color = if !enabled {
|
||||
colors::fg_primary_disabled()
|
||||
} else if on {
|
||||
colors::fg_primary().with_a(200)
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::flex(1.0))
|
||||
.height(Size::px(64.))
|
||||
.content(Content::Flex)
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.)
|
||||
.padding(Gaps::new_symmetric(0., 12.))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(background)
|
||||
.maybe(!enabled, |el| el.opacity(0.45))
|
||||
.maybe(!on || !enabled, |el| {
|
||||
el.border(
|
||||
Border::new()
|
||||
.fill(colors::component_border())
|
||||
.width(1.)
|
||||
.alignment(BorderAlignment::Inner),
|
||||
)
|
||||
})
|
||||
.maybe(enabled, |el| {
|
||||
el.cursor(CursorIcon::Pointer)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
.on_pointer_leave(move |_| hovered.set(false))
|
||||
})
|
||||
.map(handler, |el, handler| el.on_press(move |e| handler.call(e)))
|
||||
.child(Icon::new(self.icon).size(24.).color(title_color))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.child(
|
||||
label()
|
||||
.text(self.title)
|
||||
.font_size(12.)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.max_lines(1)
|
||||
.color(title_color),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(if on { "On" } else { "Off" })
|
||||
.font_size(11.)
|
||||
.color(sub_color),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct PanelFooter;
|
||||
|
||||
impl Component for PanelFooter {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let settings = use_settings_snapshot().settings;
|
||||
let current = use_current_account();
|
||||
let remove = use_remove_account();
|
||||
|
||||
let mut confirming = use_state(|| false);
|
||||
|
||||
let folder = settings
|
||||
.data_dir
|
||||
.clone()
|
||||
.or_else(|| oneclient_core::settings::data_dir::default_path().ok());
|
||||
|
||||
let account = try_default_account(¤t);
|
||||
let account_id = account.as_ref().map(|account| account.id);
|
||||
let username = account
|
||||
.as_ref()
|
||||
.map(|account| account.username.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let confirm_sign_out = move |_| {
|
||||
if let Some(id) = account_id {
|
||||
remove.mutate(RemoveAccountKeys { id });
|
||||
}
|
||||
confirming.set(false);
|
||||
};
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.content(Content::Flex)
|
||||
.cross_align(Alignment::Center)
|
||||
.child(
|
||||
footer_action(
|
||||
IconType::Folder,
|
||||
"Open launcher folder",
|
||||
colors::fg_primary(),
|
||||
)
|
||||
.on_press(move |_| {
|
||||
if let Some(folder) = folder.clone() {
|
||||
std::fs::create_dir_all(&folder).ok();
|
||||
crate::platform::open_path(&folder.to_string_lossy());
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.map(account_id, |el, _| {
|
||||
el.child(
|
||||
footer_action(
|
||||
IconType::LogOut01,
|
||||
"Sign out",
|
||||
colors::fg_primary(),
|
||||
)
|
||||
.on_press(move |_| confirming.set(true)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.maybe(*confirming.read(), |el| {
|
||||
el.child(
|
||||
OverlayPopup::new()
|
||||
.overlay_level(OVERLAY_MAX_LEVEL)
|
||||
.on_close(move |_| confirming.set(false))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(400.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.spacing(14.)
|
||||
.padding(Gaps::new_all(20.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(colors::page_elevated())
|
||||
.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("Sign out?")
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(format!(
|
||||
"{username} will be removed from OneClient. You will need to sign in again to use this account."
|
||||
))
|
||||
.font_size(12.)
|
||||
.max_lines(4)
|
||||
.width(Size::fill())
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.text("Cancel")
|
||||
.on_press(move |_| {
|
||||
confirming.set(false)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.danger()
|
||||
.text("Sign out")
|
||||
.on_press(confirm_sign_out),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn footer_action(icon: IconType, text: &'static str, color: Color) -> FooterAction {
|
||||
FooterAction {
|
||||
icon,
|
||||
text,
|
||||
color,
|
||||
on_press: None,
|
||||
}
|
||||
}
|
||||
|
||||
struct FooterAction {
|
||||
icon: IconType,
|
||||
text: &'static str,
|
||||
color: Color,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for FooterAction {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.icon == other.icon && self.text == other.text && self.color == other.color
|
||||
}
|
||||
}
|
||||
|
||||
impl FooterAction {
|
||||
fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
|
||||
self.on_press = Some(on_press.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for FooterAction {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut hovered = use_state(|| false);
|
||||
let handler = self.on_press.clone();
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.height(Size::px(34.))
|
||||
.padding(Gaps::new_symmetric(0., 8.))
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(Color::RED.with_a(0))
|
||||
.maybe(*hovered.read(), |el| {
|
||||
el.background(colors::ghost_overlay_hover())
|
||||
})
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
.on_pointer_leave(move |_| hovered.set(false))
|
||||
.map(handler, |el, handler| {
|
||||
el.on_press(move |e| handler.call(e))
|
||||
})
|
||||
.child(Icon::new(self.icon).size(16.).color(self.color))
|
||||
.child(
|
||||
label()
|
||||
.text(self.text)
|
||||
.font_size(13.)
|
||||
.max_lines(1)
|
||||
.color(self.color),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ pub enum IconType {
|
|||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Brush01,
|
||||
BarChartSquare02,
|
||||
Bell01,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
|
|
@ -102,6 +103,7 @@ pub enum IconType {
|
|||
Copy01,
|
||||
Curseforge,
|
||||
Database01,
|
||||
Discord,
|
||||
DotsGrid,
|
||||
DotsVertical,
|
||||
Download01,
|
||||
|
|
@ -123,10 +125,12 @@ pub enum IconType {
|
|||
Link03,
|
||||
LinkExternal01,
|
||||
Loading02,
|
||||
LogOut01,
|
||||
Maximize01,
|
||||
MessageTextSquare01,
|
||||
Minus,
|
||||
Modrinth,
|
||||
Moon01,
|
||||
OnboardingAccount,
|
||||
OnboardingComplete,
|
||||
OnboardingLanguage,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod account_switcher;
|
||||
mod control_center;
|
||||
mod active_cluster_panel;
|
||||
mod avatar;
|
||||
mod button;
|
||||
|
|
@ -47,6 +48,7 @@ mod version_card;
|
|||
pub mod upload_mclogs;
|
||||
|
||||
pub use account_switcher::AccountSwitcher;
|
||||
pub use control_center::ControlCenter;
|
||||
pub use active_cluster_panel::ActiveClusterPanel;
|
||||
pub use avatar::Avatar;
|
||||
pub use button::{Button, ButtonSize, ButtonVariant, link_button, open_folder_button};
|
||||
|
|
|
|||
|
|
@ -281,12 +281,8 @@ impl Component for NavbarRight {
|
|||
notif_dispatch.toggle_notification_center();
|
||||
};
|
||||
|
||||
let open_account_switcher = move |_| {
|
||||
dispatch.toggle_account_switcher();
|
||||
};
|
||||
|
||||
let open_settings = |_| {
|
||||
let _ = RouterContext::get().push(Route::SettingsLauncher {});
|
||||
let open_control_center = move |_| {
|
||||
dispatch.toggle_control_center();
|
||||
};
|
||||
|
||||
let open_stats = |_| {
|
||||
|
|
@ -313,25 +309,49 @@ impl Component for NavbarRight {
|
|||
)
|
||||
.child(
|
||||
super::navbar_button()
|
||||
.tooltip("Settings")
|
||||
.child(Icon::new(IconType::Settings02).size(20.))
|
||||
.on_press(open_settings),
|
||||
)
|
||||
.child(
|
||||
super::navbar_button()
|
||||
.overflow(Overflow::None)
|
||||
.tooltip("Control Center")
|
||||
.padding(0.0)
|
||||
.tooltip("Switch account")
|
||||
.on_press(open_account_switcher)
|
||||
.child(
|
||||
Avatar::new(account_uuid)
|
||||
.width(Size::px(24.))
|
||||
.height(Size::px(24.)),
|
||||
),
|
||||
.on_press(open_control_center)
|
||||
.child(avatar_with_gear(account_uuid)),
|
||||
)
|
||||
.child(super::window_controls())
|
||||
}
|
||||
}
|
||||
|
||||
fn avatar_with_gear(uuid: String) -> impl IntoElement {
|
||||
rect()
|
||||
.width(Size::px(28.))
|
||||
.height(Size::px(28.))
|
||||
.center()
|
||||
.child(
|
||||
Avatar::new(uuid)
|
||||
.width(Size::px(28.))
|
||||
.height(Size::px(28.)),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.position(Position::new_absolute().bottom(-7.).right(-7.))
|
||||
.width(Size::px(16.))
|
||||
.height(Size::px(16.))
|
||||
.corner_radius(CornerRadius::from(7.))
|
||||
.background(theme::colors::page_elevated())
|
||||
.border(
|
||||
Border::new()
|
||||
.fill(theme::colors::component_border())
|
||||
.width(1.)
|
||||
.alignment(BorderAlignment::Inner),
|
||||
)
|
||||
.layer(Layer::Relative(3))
|
||||
.center()
|
||||
.child(
|
||||
Icon::new(IconType::Settings02)
|
||||
.size(12.)
|
||||
.color(theme::colors::fg_secondary()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn notification_bell(unread: usize) -> impl IntoElement {
|
||||
rect()
|
||||
.width(Size::px(20.))
|
||||
|
|
|
|||
|
|
@ -255,6 +255,16 @@ impl Actions {
|
|||
});
|
||||
}
|
||||
|
||||
pub fn edit_settings(&self, edit: impl FnOnce(&mut LauncherSettings)) {
|
||||
let Some(updated) = self.mutate_settings(edit) else {
|
||||
return;
|
||||
};
|
||||
if let Ok(state) = launcher::state() {
|
||||
state.discord.set_enabled(updated.discord_enabled);
|
||||
}
|
||||
self.persist(updated);
|
||||
}
|
||||
|
||||
pub fn set_settings(&self, settings: LauncherSettings) {
|
||||
let Some(updated) = self.mutate_settings(|s| *s = settings) else {
|
||||
return;
|
||||
|
|
@ -634,6 +644,13 @@ impl Actions {
|
|||
.account_switcher_open = !open;
|
||||
}
|
||||
|
||||
pub fn open_account_switcher(&self) {
|
||||
self.station
|
||||
.clone()
|
||||
.write_channel(AppChannel::AccountSwitcher)
|
||||
.account_switcher_open = true;
|
||||
}
|
||||
|
||||
pub fn close_account_switcher(&self) {
|
||||
self.station
|
||||
.clone()
|
||||
|
|
@ -641,6 +658,28 @@ impl Actions {
|
|||
.account_switcher_open = false;
|
||||
}
|
||||
|
||||
pub fn toggle_control_center(&self) {
|
||||
let open = self.station.peek().control_center_open;
|
||||
self.station
|
||||
.clone()
|
||||
.write_channel(AppChannel::ControlCenter)
|
||||
.control_center_open = !open;
|
||||
}
|
||||
|
||||
pub fn open_control_center(&self) {
|
||||
self.station
|
||||
.clone()
|
||||
.write_channel(AppChannel::ControlCenter)
|
||||
.control_center_open = true;
|
||||
}
|
||||
|
||||
pub fn close_control_center(&self) {
|
||||
self.station
|
||||
.clone()
|
||||
.write_channel(AppChannel::ControlCenter)
|
||||
.control_center_open = false;
|
||||
}
|
||||
|
||||
pub fn close_notification_center(&self) {
|
||||
self.with_engine(|state| state.center_open = false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,10 @@ pub fn use_account_switcher_open() -> bool {
|
|||
.account_switcher_open
|
||||
}
|
||||
|
||||
pub fn use_control_center_open() -> bool {
|
||||
use_radio(AppChannel::ControlCenter).read().control_center_open
|
||||
}
|
||||
|
||||
pub fn use_game_snapshot() -> GameState {
|
||||
use_radio(AppChannel::Game).read().game.clone()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use freya::prelude::*;
|
|||
use freya::router::*;
|
||||
|
||||
use crate::components::{
|
||||
AccountSwitcher, ClusterUpdatePopup, GenericPromptOverlay, JavaPromptOverlay, OptionalModsPopup, MicrosoftJavaPromptOverlay,
|
||||
AccountSwitcher, ClusterUpdatePopup, ControlCenter, GenericPromptOverlay, JavaPromptOverlay, OptionalModsPopup, MicrosoftJavaPromptOverlay,
|
||||
NotificationCenter, PackageUpdatePopup, SplashCurtain, StatusBar, Toasts, TooltipHost,
|
||||
UpdatePromptOverlay, use_provide_tooltips,
|
||||
};
|
||||
|
|
@ -74,6 +74,7 @@ impl Component for RootLayout {
|
|||
)
|
||||
.child(NotificationCenter)
|
||||
.child(AccountSwitcher)
|
||||
.child(ControlCenter)
|
||||
.child(Toasts)
|
||||
.child(UpdatePromptOverlay)
|
||||
.child(JavaPromptOverlay)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ pub enum AppChannel {
|
|||
Notifications,
|
||||
Game,
|
||||
AccountSwitcher,
|
||||
ControlCenter,
|
||||
MicrosoftLogin,
|
||||
Installs,
|
||||
StorageScan,
|
||||
|
|
@ -42,6 +43,7 @@ pub struct AppState {
|
|||
pub center_open: bool,
|
||||
pub game: GameState,
|
||||
pub account_switcher_open: bool,
|
||||
pub control_center_open: bool,
|
||||
pub microsoft_login: Option<LoginProgress>,
|
||||
pub installs: InstallState,
|
||||
pub storage_scan: Option<StorageScanProgress>,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,17 @@
|
|||
//! of one package are legal rows
|
||||
//! the newest stays enabled the rest are switched off rather than unlinked
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use oneclient_common::domain::{ContentType, ProviderId};
|
||||
use oneclient_db::dao::applied_migration as migration_dao;
|
||||
use oneclient_db::dao::artifact as artifact_dao;
|
||||
use oneclient_db::dao::bundle as bundle_catalog_dao;
|
||||
use oneclient_db::dao::cluster_bundle as bundle_dao;
|
||||
|
||||
use crate::ctx::ContentCtx;
|
||||
use crate::error::ContentResult;
|
||||
use crate::packages::dependencies::base_game_version;
|
||||
use crate::packages::store::PackageStore;
|
||||
use crate::packages::types::LinkedArtifactInfo;
|
||||
|
||||
|
|
@ -95,6 +98,7 @@ fn group_duplicates(
|
|||
pub async fn disable_foreign_game_versions(
|
||||
cluster_id: i64,
|
||||
mc_version: &str,
|
||||
mc_loader: i64,
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<Vec<String>> {
|
||||
if mc_version.is_empty() {
|
||||
|
|
@ -102,12 +106,31 @@ pub async fn disable_foreign_game_versions(
|
|||
}
|
||||
|
||||
let linked = PackageStore::list_linked_artifacts(cluster_id, ctx).await?;
|
||||
|
||||
// bundle installs skip the compatibility check so launch must trust them too
|
||||
let live_bundles: HashSet<String> =
|
||||
bundle_catalog_dao::list_visible_for_version_loader(&ctx.db, mc_version, mc_loader)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|row| row.name)
|
||||
.collect();
|
||||
|
||||
let from_bundle: HashSet<String> = bundle_dao::list_bundle_tracked(&ctx.db, cluster_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|row| {
|
||||
row.bundle_name
|
||||
.as_deref()
|
||||
.is_some_and(|name| live_bundles.contains(name))
|
||||
})
|
||||
.map(|row| row.hash)
|
||||
.collect();
|
||||
|
||||
let mut switched_off = Vec::new();
|
||||
|
||||
for info in linked
|
||||
.iter()
|
||||
.filter(|info| info.enabled && info.content_type == ContentType::Mod)
|
||||
{
|
||||
for info in linked.iter().filter(|info| {
|
||||
info.enabled && info.content_type == ContentType::Mod && !from_bundle.contains(&info.hash)
|
||||
}) {
|
||||
match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await {
|
||||
Ok(true) => switched_off.push(
|
||||
info.display_name
|
||||
|
|
@ -167,11 +190,13 @@ fn built_for_another_game_version(stated: &[String], mc_version: &str) -> bool {
|
|||
}
|
||||
|
||||
fn covers_game_version(stated: &[String], mc_version: &str) -> bool {
|
||||
let wanted = base_game_version(mc_version);
|
||||
stated.is_empty()
|
||||
|| stated.iter().any(|version| {
|
||||
version == mc_version
|
||||
|| mc_version
|
||||
.strip_prefix(version.as_str())
|
||||
let built_for = base_game_version(version);
|
||||
built_for == wanted
|
||||
|| wanted
|
||||
.strip_prefix(built_for)
|
||||
.is_some_and(|rest| rest.starts_with('.'))
|
||||
})
|
||||
}
|
||||
|
|
@ -287,6 +312,34 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pre_release_tag_is_the_same_game_version() {
|
||||
let foreign = |rows: &[&str], mc_version: &str| {
|
||||
built_for_another_game_version(
|
||||
&rows.iter().map(|r| (*r).to_string()).collect::<Vec<_>>(),
|
||||
mc_version,
|
||||
)
|
||||
};
|
||||
|
||||
assert!(
|
||||
!foreign(&["[\"26.3-rc-1\"]"], "26.3"),
|
||||
"a jar built against the release candidate runs on the release"
|
||||
);
|
||||
assert!(!foreign(&["[\"26.3-snapshot-7\",\"26.3-pre-2\"]"], "26.3"));
|
||||
assert!(
|
||||
!foreign(&["[\"26.3\"]"], "26.3-snapshot-10"),
|
||||
"and a snapshot cluster keeps the release build"
|
||||
);
|
||||
assert!(
|
||||
!foreign(&["[\"26.3-rc-1\"]"], "26.3.1"),
|
||||
"the component prefix still applies once the tag is off"
|
||||
);
|
||||
assert!(
|
||||
foreign(&["[\"26.2-rc-1\"]"], "26.3"),
|
||||
"stripping the tag must not blur two game versions together"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_projects_with_several_copies_are_reconciled() {
|
||||
let groups = group_duplicates(&[
|
||||
|
|
|
|||
|
|
@ -193,9 +193,18 @@ fn choose_version(
|
|||
.cloned()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn base_game_version(version: &str) -> &str {
|
||||
version
|
||||
.split_once('-')
|
||||
.filter(|(base, _)| base.contains('.'))
|
||||
.map_or(version, |(base, _)| base)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn supports_game_version(game_versions: &[String], mc_version: &str) -> bool {
|
||||
game_versions.is_empty() || game_versions.iter().any(|v| v == mc_version)
|
||||
let wanted = base_game_version(mc_version);
|
||||
game_versions.is_empty() || game_versions.iter().any(|v| base_game_version(v) == wanted)
|
||||
}
|
||||
|
||||
fn fits_cluster(version: &VersionSummary, cluster: &ClusterRow, loader: GameLoader) -> bool {
|
||||
|
|
@ -230,6 +239,34 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_version_is_offered_only_for_the_game_version_it_names() {
|
||||
let offered = |stated: &[&str], mc_version: &str| {
|
||||
supports_game_version(
|
||||
&stated.iter().map(|v| (*v).to_string()).collect::<Vec<_>>(),
|
||||
mc_version,
|
||||
)
|
||||
};
|
||||
|
||||
assert!(offered(&[], "26.3"), "an undescribed version is offered");
|
||||
assert!(offered(&["26.3"], "26.3"));
|
||||
assert!(
|
||||
offered(&["26.3-rc-1"], "26.3"),
|
||||
"a pre-release tag names the same game version"
|
||||
);
|
||||
assert!(offered(&["26.3"], "26.3-snapshot-10"));
|
||||
assert!(
|
||||
!offered(&["26.1"], "26.1.2"),
|
||||
"strict where the launch-time check is lenient: #830 stopped 26.1 \
|
||||
builds being offered to a 26.1.2 cluster"
|
||||
);
|
||||
assert!(!offered(&["1.21.11"], "26.1.2"));
|
||||
assert!(
|
||||
!offered(&["rd-132211"], "rd-160052"),
|
||||
"pre-classic names the build after the dash, so there is no tag to strip"
|
||||
);
|
||||
}
|
||||
|
||||
fn summary(
|
||||
version_id: &str,
|
||||
release_type: ReleaseType,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ pub async fn materialize_content(
|
|||
match oneclient_content::packages::disable_foreign_game_versions(
|
||||
cluster.id,
|
||||
&cluster.mc_version,
|
||||
cluster.mc_loader as i64,
|
||||
&services.content(),
|
||||
)
|
||||
.await
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue