- Rebased base onto official Polyfrost OneLauncher 2.4.2 (release v2.4.2). - Ensured offline mode launches with 0 Microsoft accounts required (no OfflineRequiresMicrosoft error). - Maintained custom 3D offline skin changer, player model switch (Classic/Slim), and targeted Minecraft Java UUID character slot isolation to prevent multiplayer skin cloning. - Maintained CustomSkinLoader auto-configuration for local skins, Ely.by, and SkinRestorer. - Integrated direct GitHub Releases auto-updater pointing to oiupoyt/OneLauncher-Cracked. - Ported 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.4.2-c1. - All workspace tests and clippy checks pass with 0 errors.
281 lines
8.4 KiB
Rust
281 lines
8.4 KiB
Rust
use freya::prelude::*;
|
|
|
|
use oneclient_core::game::{Analytics, Persona};
|
|
|
|
use crate::components::{Icon, IconType};
|
|
use crate::theme::colors;
|
|
use crate::ui::{border_all_color, columns_for, flow_grid};
|
|
use crate::utils::{format_duration, plural};
|
|
|
|
use super::card;
|
|
|
|
pub(super) fn tiles_row(analytics: &Analytics, _force_all: bool) -> Element {
|
|
TilesRow {
|
|
analytics: analytics.clone(),
|
|
}
|
|
.into_element()
|
|
}
|
|
|
|
const TILE_GAP: f32 = 14.;
|
|
const TILE_MIN_W: f32 = 190.;
|
|
const TILE_H: f32 = 104.;
|
|
|
|
#[derive(PartialEq)]
|
|
struct TilesRow {
|
|
analytics: Analytics,
|
|
}
|
|
|
|
impl Component for TilesRow {
|
|
fn render(&self) -> impl IntoElement {
|
|
let width = use_state(|| 0f32);
|
|
let cols = columns_for(*width.read(), TILE_MIN_W, 4, TILE_GAP);
|
|
|
|
let stats = &self.analytics.playtime;
|
|
let avg_session = if stats.session_count > 0 {
|
|
stats.total_secs / stats.session_count as i64
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let hero = hero_tile(
|
|
IconType::ClockRewind,
|
|
"Total playtime",
|
|
format_duration(stats.total_secs),
|
|
format!(
|
|
"over {} session{}",
|
|
stats.session_count,
|
|
plural(stats.session_count as i64)
|
|
),
|
|
);
|
|
|
|
let mut rest = vec![
|
|
stat_tile(IconType::Play, "Sessions", stats.session_count.to_string()),
|
|
stat_tile(
|
|
IconType::Sliders04,
|
|
"Avg / session",
|
|
format_duration(avg_session),
|
|
),
|
|
stat_tile(
|
|
IconType::Maximize01,
|
|
"Longest session",
|
|
format_duration(stats.longest_session_secs),
|
|
),
|
|
stat_tile(
|
|
IconType::Rocket02,
|
|
"Day streak",
|
|
streak_value(stats.current_streak),
|
|
),
|
|
stat_tile(
|
|
IconType::CheckCircle,
|
|
"Best streak",
|
|
streak_value(stats.longest_streak),
|
|
),
|
|
stat_tile(
|
|
IconType::Calendar,
|
|
"Days played",
|
|
stats.active_days.to_string(),
|
|
),
|
|
];
|
|
|
|
let mut root = rect().vertical().width(Size::fill()).spacing(TILE_GAP);
|
|
|
|
if cols >= 3 {
|
|
let alongside: Vec<Element> = rest.drain(..(cols - 2).min(rest.len())).collect();
|
|
let mut lead = rect()
|
|
.horizontal()
|
|
.content(Content::Flex)
|
|
.width(Size::fill())
|
|
.height(Size::px(TILE_H))
|
|
.spacing(TILE_GAP)
|
|
.child(
|
|
rect()
|
|
.width(Size::flex(2.0))
|
|
.height(Size::fill())
|
|
.child(hero),
|
|
);
|
|
for tile in alongside {
|
|
lead = lead.child(
|
|
rect()
|
|
.width(Size::flex(1.0))
|
|
.height(Size::fill())
|
|
.child(tile),
|
|
);
|
|
}
|
|
root = root.child(lead);
|
|
} else {
|
|
root = root.child(
|
|
rect()
|
|
.width(Size::fill())
|
|
.height(Size::px(TILE_H))
|
|
.child(hero),
|
|
);
|
|
}
|
|
|
|
let rows: Vec<Element> = rest
|
|
.into_iter()
|
|
.map(|tile| {
|
|
rect()
|
|
.height(Size::px(TILE_H))
|
|
.width(Size::flex(1.0))
|
|
.child(tile)
|
|
.into_element()
|
|
})
|
|
.collect();
|
|
|
|
root.child(flow_grid(rows, cols, width, TILE_GAP))
|
|
}
|
|
}
|
|
|
|
fn streak_value(days: usize) -> String {
|
|
format!("{days}d")
|
|
}
|
|
|
|
fn icon_chip(icon: IconType, size: f32, tint: Color) -> Element {
|
|
rect()
|
|
.width(Size::px(size))
|
|
.height(Size::px(size))
|
|
.corner_radius(CornerRadius::new_all(size * 0.5))
|
|
.background(tint.with_a(38))
|
|
.center()
|
|
.child(Icon::new(icon).size(size * 0.52).color(tint))
|
|
.into_element()
|
|
}
|
|
|
|
fn hero_tile(icon: IconType, caption: &str, value: String, note: String) -> Element {
|
|
card()
|
|
.width(Size::fill())
|
|
.height(Size::fill())
|
|
.padding(Gaps::new_symmetric(16., 20.))
|
|
.spacing(10.)
|
|
.background(colors::brand().with_a(18))
|
|
.border(border_all_color(1., colors::brand().with_a(90)))
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(8.)
|
|
.child(icon_chip(icon, 24., colors::brand()))
|
|
.child(
|
|
label()
|
|
.text(caption.to_string())
|
|
.font_size(12.)
|
|
.font_weight(FontWeight::MEDIUM)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::End)
|
|
.spacing(8.)
|
|
.child(
|
|
label()
|
|
.text(value)
|
|
.font_size(30.)
|
|
.font_weight(FontWeight::BOLD)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
rect().margin(Gaps::new(0., 0., 5., 0.)).child(
|
|
label()
|
|
.text(note)
|
|
.font_size(12.)
|
|
.max_lines(1)
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
fn stat_tile(icon: IconType, caption: &str, value: String) -> Element {
|
|
card()
|
|
.width(Size::fill())
|
|
.height(Size::fill())
|
|
.spacing(10.)
|
|
.child(
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(8.)
|
|
.child(icon_chip(icon, 24., colors::fg_secondary()))
|
|
.child(
|
|
label()
|
|
.text(caption.to_string())
|
|
.font_size(12.)
|
|
.max_lines(1)
|
|
.width(Size::fill())
|
|
.color(colors::fg_secondary()),
|
|
),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(value)
|
|
.font_size(24.)
|
|
.font_weight(FontWeight::BOLD)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
pub(super) fn personas_row(personas: &[Persona]) -> Element {
|
|
rect()
|
|
.horizontal()
|
|
.content(Content::Wrap {
|
|
wrap_spacing: Some(10.),
|
|
})
|
|
.width(Size::fill())
|
|
.spacing(10.)
|
|
.children(personas.iter().map(|p| persona_badge(*p)))
|
|
.into_element()
|
|
}
|
|
|
|
fn persona_badge(persona: Persona) -> Element {
|
|
rect()
|
|
.horizontal()
|
|
.cross_align(Alignment::Center)
|
|
.spacing(7.)
|
|
.padding(Gaps::new_symmetric(6., 11.))
|
|
.corner_radius(CornerRadius::new_all(8.))
|
|
.background(colors::component_bg())
|
|
.border(border_all_color(1., colors::component_border()))
|
|
.child(
|
|
Icon::new(persona_icon(persona))
|
|
.size(13.)
|
|
.color(colors::brand()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(persona.title().to_string())
|
|
.font_size(13.)
|
|
.font_weight(FontWeight::SEMI_BOLD)
|
|
.max_lines(1)
|
|
.color(colors::fg_primary()),
|
|
)
|
|
.child(
|
|
label()
|
|
.text(persona.description().to_string())
|
|
.font_size(11.)
|
|
.max_lines(1)
|
|
.color(colors::fg_secondary()),
|
|
)
|
|
.into_element()
|
|
}
|
|
|
|
fn persona_icon(persona: Persona) -> IconType {
|
|
match persona {
|
|
Persona::Veteran => IconType::ClockRewind,
|
|
Persona::Marathoner => IconType::Maximize01,
|
|
Persona::Regular => IconType::CheckCircle,
|
|
Persona::Loyalist => IconType::Key01,
|
|
Persona::Explorer => IconType::Globe01,
|
|
Persona::NightOwl => IconType::Eye,
|
|
Persona::EarlyBird => IconType::Bell01,
|
|
Persona::WeekendWarrior => IconType::Calendar,
|
|
Persona::Gamer => IconType::Rocket02,
|
|
Persona::Sprinter => IconType::Play,
|
|
}
|
|
}
|
|
|