OneLauncher-Cracked/packages/oneclient_app/src/utils.rs
2026-07-28 12:11:02 +02:00

414 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::cmp::Ordering;
use std::collections::BTreeMap;
use chrono::{Datelike, NaiveDate};
use oneclient_core::clusters::Cluster;
use oneclient_common::domain::GameLoader;
use oneclient_common::{ParsedMcVersion, VersionKey, format_mc_version, parse_mc_version};
pub type ClusterGroups = BTreeMap<ReleaseLine, Vec<Cluster>>;
/// One entry on the versions page. The modern scheme puts a full release in the
/// first two components (`26.1`, `26.2`), so each minor is its own line; legacy
/// `1.x` versions keep the whole major (`1.21`) as one line. A line that holds a
/// single concrete version is presented as that version (see [`line_title`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ReleaseLine {
pub major: u32,
/// `Some` only for the modern scheme, where the minor is part of the line.
pub minor: Option<u32>,
}
impl ReleaseLine {
fn from_parsed(parsed: &ParsedMcVersion) -> Option<Self> {
let minor = if parsed.major >= 26 {
Some(parsed.minor?)
} else {
None
};
Some(Self {
major: parsed.major,
minor,
})
}
pub fn from_version(version: &str) -> Option<Self> {
Self::from_parsed(&parse_mc_version(version)?)
}
pub fn for_cluster(cluster: &Cluster) -> Option<Self> {
Self::from_version(&cluster.mc_version)
}
/// The generic name of the line: `26.1`, `1.21`.
pub fn pretty_name(&self) -> String {
match self.minor {
Some(minor) => format!("{}.{minor}", self.major),
None => format!("1.{}", self.major),
}
}
/// Metadata/art lookup key for the line itself, when no single version
/// stands in for it.
fn art_key(&self) -> Option<VersionKey> {
self.minor.map(|minor| (minor, None))
}
}
fn cluster_key(cluster: &Cluster) -> Option<VersionKey> {
parse_mc_version(&cluster.mc_version).and_then(|parsed| parsed.key())
}
/// The one concrete version behind `clusters`, if they all share it.
fn sole_version_key(clusters: &[Cluster]) -> Option<VersionKey> {
match version_keys(clusters).as_slice() {
[only] => Some(*only),
_ => None,
}
}
/// A line with a single version is shown as that full version (`26.1.2`);
/// otherwise the generic line name (`1.21`), with the versions in a dropdown.
pub fn line_title(line: ReleaseLine, clusters: &[Cluster]) -> String {
match sole_version_key(clusters) {
Some(key) => version_label(line.major, key),
None => line.pretty_name(),
}
}
/// Metadata/art key matching [`line_title`].
pub fn line_art_key(line: ReleaseLine, clusters: &[Cluster]) -> Option<VersionKey> {
sole_version_key(clusters).or_else(|| line.art_key())
}
pub fn group_clusters_by_release(clusters: &[Cluster]) -> ClusterGroups {
let mut groups: ClusterGroups = BTreeMap::new();
for cluster in clusters {
let Some(line) = ReleaseLine::for_cluster(cluster) else {
continue;
};
groups.entry(line).or_default().push(cluster.clone());
}
for list in groups.values_mut() {
list.sort_by_key(|b| std::cmp::Reverse(cluster_key(b)));
}
groups
}
pub fn loader_tags(clusters: &[Cluster]) -> Vec<String> {
let mut tags = Vec::new();
for cluster in clusters {
if cluster.mc_loader.is_modded() {
let label = cluster.mc_loader.to_string();
if !tags.iter().any(|t| t == &label) {
tags.push(label);
}
}
}
tags.sort();
tags
}
pub fn version_keys(clusters: &[Cluster]) -> Vec<VersionKey> {
let mut keys = Vec::new();
for cluster in clusters {
if let Some(key) = cluster_key(cluster)
&& !keys.contains(&key)
{
keys.push(key);
}
}
keys.sort();
keys
}
pub fn loaders_for_line(clusters: &[Cluster]) -> Vec<GameLoader> {
let mut loaders = Vec::new();
for cluster in clusters {
if cluster.mc_loader.is_modded() && !loaders.contains(&cluster.mc_loader) {
loaders.push(cluster.mc_loader);
}
}
loaders.sort_by_key(|loader| loader.to_string());
loaders
}
pub fn resolve_cluster(
clusters: &[Cluster],
key: Option<VersionKey>,
loader: Option<GameLoader>,
) -> Option<Cluster> {
clusters
.iter()
.find(|cluster| {
let version_ok = key.is_none_or(|key| cluster_key(cluster) == Some(key));
let loader_ok = loader.is_none_or(|loader| cluster.mc_loader == loader);
version_ok && loader_ok
})
.cloned()
}
/// The line the page opens on: the active cluster's line, else the newest.
pub fn default_line(groups: &ClusterGroups, active: Option<Cluster>) -> Option<ReleaseLine> {
if let Some(cluster) = active
&& let Some(line) = ReleaseLine::for_cluster(&cluster)
{
return Some(line);
}
groups.keys().next_back().copied()
}
pub fn default_version_key(
clusters: &[Cluster],
preferred: Option<VersionKey>,
) -> Option<VersionKey> {
let keys = version_keys(clusters);
if keys.is_empty() {
return None;
}
if let Some(key) = preferred
&& keys.contains(&key)
{
return Some(key);
}
keys.last().copied()
}
pub fn default_loader(clusters: &[Cluster], preferred: Option<GameLoader>) -> Option<GameLoader> {
let loaders = loaders_for_line(clusters);
if loaders.is_empty() {
return clusters.first().map(|c| c.mc_loader);
}
if let Some(loader) = preferred
&& loaders.contains(&loader)
{
return Some(loader);
}
Some(loaders[0])
}
pub fn version_label(major: u32, (minor, patch): VersionKey) -> String {
format_mc_version(major, minor, patch)
}
/// Human-readable byte size (B / KB / MB).
pub fn format_size(bytes: u64) -> String {
const KB: f64 = 1024.;
const MB: f64 = KB * 1024.;
let b = bytes as f64;
if b >= MB {
format!("{:.1} MB", b / MB)
} else if b >= KB {
format!("{:.0} KB", b / KB)
} else {
format!("{bytes} B")
}
}
pub fn format_res((w, h): (u32, u32)) -> String {
format!("{w}×{h}")
}
/// `7384` -> `2h 3m`, `540` -> `9m`, `0` -> `0m`.
pub fn format_duration_hm(secs: i64) -> String {
if secs <= 0 {
return "0m".to_string();
}
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
if hours > 0 {
format!("{hours}h {minutes}m")
} else {
format!("{minutes}m")
}
}
/// `3723` -> `1h 2m`, `83` -> `1m 23s`, `45` -> `45s`.
pub fn format_duration_hms(secs: i64) -> String {
if secs <= 0 {
return "0s".to_string();
}
if secs >= 3600 {
format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
} else if secs >= 60 {
format!("{}m {}s", secs / 60, secs % 60)
} else {
format!("{secs}s")
}
}
/// Compact large counts: `1500` -> `1.5K`, `2_400_000` -> `2.4M`.
pub fn abbreviate_number(n: u64) -> String {
let f = n as f64;
if f >= 1_000_000.0 {
format!("{:.1}M", f / 1_000_000.0)
} else if f >= 1_000.0 {
format!("{:.1}K", f / 1_000.0)
} else {
n.to_string()
}
}
/// English plural suffix: `""` for 1, `"s"` otherwise.
pub fn plural(n: i64) -> &'static str {
if n == 1 { "" } else { "s" }
}
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
/// 24-hour clock label: `9` -> `09:00`.
pub fn format_hour(hour: usize) -> String {
format!("{hour:02}:00")
}
pub fn parse_day(date: &str) -> Option<NaiveDate> {
NaiveDate::parse_from_str(date, "%Y-%m-%d").ok()
}
/// `2026-07-05` -> `Jul 5`.
pub fn format_day(date: NaiveDate) -> String {
const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
format!("{} {}", MONTHS[date.month0() as usize], date.day())
}
pub fn sort_clusters_for_home(mut clusters: Vec<Cluster>) -> Vec<Cluster> {
clusters.sort_by(compare_last_played);
clusters
}
fn compare_last_played(a: &Cluster, b: &Cluster) -> Ordering {
match (a.last_played, b.last_played) {
// Most recently played first.
(Some(a), Some(b)) => b.cmp(&a),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
// Never played: latest version first (major, then minor).
(None, None) => version_sort_key(b).cmp(&version_sort_key(a)),
}
}
fn version_sort_key(cluster: &Cluster) -> (u32, u32, u32) {
parse_mc_version(&cluster.mc_version)
.map(|v| (v.major, v.minor.unwrap_or(0), v.patch.unwrap_or(0)))
.unwrap_or((0, 0, 0))
}
#[cfg(not(target_os = "linux"))]
pub fn is_wayland() -> bool {
false
}
#[cfg(target_os = "linux")]
pub fn is_wayland() -> bool {
static IS_WAYLAND: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*IS_WAYLAND.get_or_init(|| {
if cfg!(target_os = "linux") {
std::env::var("XDG_SESSION_TYPE")
.map(|v| v == "wayland")
.unwrap_or(false)
} else {
false
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::view::onboarding::test_support::cluster;
fn versioned(id: i64, mc_version: &str) -> Cluster {
Cluster {
mc_version: mc_version.to_string(),
..cluster(id)
}
}
fn line(major: u32, minor: Option<u32>) -> ReleaseLine {
ReleaseLine { major, minor }
}
fn lines_of(clusters: &[Cluster]) -> Vec<ReleaseLine> {
group_clusters_by_release(clusters)
.keys()
.copied()
.collect()
}
#[test]
fn modern_minors_are_separate_lines() {
let clusters = [
versioned(1, "26.1"),
versioned(2, "26.2"),
versioned(3, "26.2.1"),
];
assert_eq!(
lines_of(&clusters),
vec![line(26, Some(1)), line(26, Some(2))]
);
}
#[test]
fn legacy_minors_share_one_line() {
let clusters = [versioned(1, "1.21.5"), versioned(2, "1.21.11")];
assert_eq!(lines_of(&clusters), vec![line(21, None)]);
}
#[test]
fn a_lone_version_is_shown_in_full() {
let clusters = [versioned(1, "26.1.2")];
let line = line(26, Some(1));
assert_eq!(line_title(line, &clusters), "26.1.2");
assert_eq!(line_art_key(line, &clusters), Some((1, Some(2))));
}
#[test]
fn several_loaders_of_one_version_still_count_as_one() {
let clusters = [
versioned(1, "26.1.2"),
Cluster {
mc_loader: GameLoader::Vanilla,
..versioned(2, "26.1.2")
},
];
assert_eq!(line_title(line(26, Some(1)), &clusters), "26.1.2");
}
#[test]
fn several_versions_fall_back_to_the_generic_name() {
let legacy = [
versioned(1, "1.21.1"),
versioned(2, "1.21.10"),
versioned(3, "1.21.11"),
];
assert_eq!(line_title(line(21, None), &legacy), "1.21");
assert_eq!(line_art_key(line(21, None), &legacy), None);
let modern = [versioned(4, "26.1"), versioned(5, "26.1.2")];
assert_eq!(line_title(line(26, Some(1)), &modern), "26.1");
assert_eq!(line_art_key(line(26, Some(1)), &modern), Some((1, None)));
}
#[test]
fn a_bare_legacy_version_keeps_its_line_name() {
let clusters = [versioned(1, "1.21")];
assert_eq!(line_title(line(21, None), &clusters), "1.21");
}
}