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

- 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:
katR 2026-09-17 07:15:57 +05:30
parent 618731da7d
commit d214afe99b
21 changed files with 917 additions and 62 deletions

View file

@ -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
View file

@ -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",

View file

@ -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"

View file

@ -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')

View 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="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

View 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

View 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

View 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

View file

@ -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,

View file

@ -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());

View 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(&current);
let unusable = query_error(&current).is_some();
let account = try_default_account(&current).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(&current);
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),
)
}
}

View file

@ -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,

View file

@ -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};

View file

@ -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.))

View file

@ -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);
}

View file

@ -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()
}

View file

@ -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)

View file

@ -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>,

View file

@ -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(&[

View file

@ -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,

View file

@ -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