OneLauncher-Cracked/packages/oneclient_app/src/layout/cluster_shell.rs
Julian Chang 0f04462863
Folder button
Automatically add stuff added to the mods folder to the next-ran cluster if in a shared directory
Add a txt explanation as to why certain folders are empty in shared directory
2026-07-06 05:02:47 -04:00

345 lines
11 KiB
Rust

use freya::animation::*;
use freya::prelude::*;
use freya::query::QueryStateData;
use freya::router::*;
use oneclient_core::clusters::Cluster;
use oneclient_core::parse_mc_version;
use crate::components::{Button, Icon, IconType, TabBar, TabItem};
use crate::hooks::{use_clusters, use_dispatch, use_game_snapshot, use_version_metadata};
use crate::view::app::launch_button_state;
use crate::routes::Route;
use crate::theme::colors;
use crate::ui::entrance_motion_layer;
const HEADER_HEIGHT: f32 = 64.;
const TABS_HEIGHT: f32 = 44.;
const BAR_SPACING: f32 = 16.;
#[derive(PartialEq, Clone, Copy)]
pub enum ClusterViewShellTab {
Overview,
Logs,
GameLog,
Screenshots,
Mods,
Shaders,
Textures,
Settings,
}
impl ClusterViewShellTab {
fn label(&self) -> &'static str {
match self {
Self::Overview => "Overview",
Self::Logs => "Logs",
Self::GameLog => "Game Log",
Self::Screenshots => "Screenshots",
Self::Mods => "Mods",
Self::Shaders => "Shaders",
Self::Textures => "Textures",
Self::Settings => "Settings",
}
}
fn route(&self, cluster_id: i64) -> Option<Route> {
match self {
Self::Overview => Some(Route::ClusterOverview { cluster_id }),
Self::Logs => Some(Route::ClusterLogs { cluster_id }),
Self::GameLog => Some(Route::ProcessLogs { cluster_id }),
Self::Screenshots => Some(Route::ClusterScreenshots { cluster_id }),
Self::Mods => Some(Route::ClusterMods { cluster_id }),
Self::Shaders => Some(Route::ClusterShaders { cluster_id }),
Self::Textures => Some(Route::ClusterTextures { cluster_id }),
Self::Settings => Some(Route::ClusterSettings { cluster_id }),
}
}
fn index(self) -> i32 {
match self {
Self::Overview => 0,
Self::Logs => 1,
Self::Screenshots => 2,
Self::Mods => 3,
Self::Shaders => 4,
Self::Textures => 5,
Self::Settings => 6,
Self::GameLog => 7,
}
}
}
const SIDE_PADDING: f32 = 40.;
pub fn cluster_content() -> Rect {
rect().vertical().width(Size::fill()).height(Size::fill())
}
fn route_cluster(route: &Route) -> Option<(i64, ClusterViewShellTab)> {
Some(match route {
Route::ClusterOverview { cluster_id } => (*cluster_id, ClusterViewShellTab::Overview),
Route::ClusterLogs { cluster_id } => (*cluster_id, ClusterViewShellTab::Logs),
Route::ProcessLogs { cluster_id } => (*cluster_id, ClusterViewShellTab::GameLog),
Route::ClusterScreenshots { cluster_id } => {
(*cluster_id, ClusterViewShellTab::Screenshots)
}
Route::ClusterMods { cluster_id } => (*cluster_id, ClusterViewShellTab::Mods),
Route::ClusterShaders { cluster_id } => (*cluster_id, ClusterViewShellTab::Shaders),
Route::ClusterTextures { cluster_id } => (*cluster_id, ClusterViewShellTab::Textures),
Route::ClusterSettings { cluster_id } => (*cluster_id, ClusterViewShellTab::Settings),
_ => return None,
})
}
fn load_cluster(cluster_id: i64) -> Option<Cluster> {
let clusters_query = use_clusters();
let reader = clusters_query.read();
let list = match &*reader.state() {
QueryStateData::Settled { res: Ok(list), .. }
| QueryStateData::Loading {
res: Some(Ok(list)),
} => list.clone(),
_ => Vec::new(),
};
list.into_iter().find(|c| c.id == cluster_id)
}
#[derive(PartialEq)]
pub struct ClusterShell;
impl Component for ClusterShell {
fn render(&self) -> impl IntoElement {
let route = use_route::<Route>();
let (cluster_id, active_tab) =
route_cluster(&route).unwrap_or((0, ClusterViewShellTab::Overview));
let dispatch = use_dispatch();
let game = use_game_snapshot();
let cluster = load_cluster(cluster_id);
let show_game_log = game.is_active(cluster_id);
let launch_state = launch_button_state(&game, cluster_id);
let header = cluster.as_ref().map(|cluster| {
let title = format!("{} {}", cluster.mc_loader, cluster.mc_version);
let parsed = parse_mc_version(&cluster.mc_version);
let metadata = use_version_metadata(
parsed.as_ref().map(|p| p.major),
parsed.and_then(|p| p.minor),
Some(cluster.mc_loader),
);
let subtitle = metadata
.and_then(|m| m.long_description)
.unwrap_or_else(|| cluster.name.clone());
cluster_header(
title,
subtitle,
cluster_id,
dispatch,
launch_state,
game.is_running(cluster_id),
cluster.game_dir().ok(),
)
.into_element()
});
rect()
.vertical()
.width(Size::fill())
.height(Size::fill())
.overflow(Overflow::Clip)
.child(
rect()
.vertical()
.width(Size::fill())
.padding(Gaps::new(BAR_SPACING, SIDE_PADDING, BAR_SPACING, SIDE_PADDING))
.spacing(BAR_SPACING)
.maybe_child(header)
.child(cluster_tabs(active_tab, cluster_id, show_game_log)),
)
.child(
rect()
.width(Size::fill())
.height(Size::flex(1.0))
.overflow(Overflow::Clip)
.padding(Gaps::new(0., SIDE_PADDING, SIDE_PADDING, SIDE_PADDING))
.child(AnimatedRouter::<Route>::new(ClusterContentOutlet)),
)
}
}
fn tab_dir(from: &Route, to: &Route) -> f32 {
match (route_cluster(from), route_cluster(to)) {
(Some((_, a)), Some((_, b))) => (b.index() - a.index()).signum() as f32,
_ => 0.,
}
}
#[derive(PartialEq)]
struct ClusterContentOutlet;
impl Component for ClusterContentOutlet {
fn render(&self) -> impl IntoElement {
let mut router = use_animated_router::<Route>();
let anim = use_animation(|_conf| {
AnimNum::new(0., 1.)
.time(300)
.ease(Ease::Out)
.function(Function::Cubic)
});
let mut last_to = use_state(|| None::<Route>);
let (dir, to, is_transition) = match &*router.read() {
AnimatedRouterContext::FromTo(from, to) => (tab_dir(from, to), to.clone(), true),
AnimatedRouterContext::In(to) => (0., to.clone(), false),
};
if last_to.peek().as_ref() != Some(&to) {
last_to.set(Some(to.clone()));
if is_transition {
anim.run(AnimDirection::Forward);
}
}
let anim_finished = *anim.has_run_yet().read() && !*anim.is_running().read();
use_side_effect_with_deps(&anim_finished, move |&finished| {
if finished {
Platform::get().send(UserEvent::RequestRedraw);
}
});
if anim_finished {
if matches!(&*router.peek(), AnimatedRouterContext::FromTo(_, _)) {
router.write().settle();
}
}
const CONTENT_DX: f32 = 44.;
let p = if anim_finished { 1.0 } else { anim.get().value() };
let slide_x = if anim_finished || dir == 0. {
0.
} else {
dir * (1.0 - p) * CONTENT_DX
};
let opacity = if anim_finished || dir == 0. { 1. } else { p };
entrance_motion_layer(slide_x, 0., opacity, Outlet::<Route>::new())
}
}
fn cluster_header(
title: String,
subtitle: String,
cluster_id: i64,
dispatch: crate::BridgeDispatch,
launch_state: (&'static str, bool),
running: bool,
folder: Option<std::path::PathBuf>,
) -> impl IntoElement {
let (launch_label, launch_enabled) = launch_state;
let kill_dispatch = dispatch.clone();
rect()
.horizontal()
.content(Content::Flex)
.width(Size::fill())
.height(Size::px(HEADER_HEIGHT))
.cross_align(Alignment::Center)
.spacing(12.)
.child(
rect()
.vertical()
.width(Size::flex(1.0))
.height(Size::fill())
.main_align(Alignment::Center)
.spacing(6.)
.child(
label()
.text(title)
.font_size(28.)
.font_weight(FontWeight::BOLD)
.color(colors::fg_primary()),
)
.child(
label()
.text(subtitle)
.font_size(12.)
.font_weight(FontWeight::MEDIUM)
.color(colors::fg_secondary()),
),
)
.child(
rect()
.horizontal()
.cross_align(Alignment::Center)
.spacing(10.)
.maybe_child(folder.map(crate::components::open_folder_button))
.maybe(running, |el| {
el.child(
Button::new()
.danger()
.icon()
.on_press(move |_| kill_dispatch.kill_cluster(cluster_id))
.child(Icon::new(IconType::Square).size(16.)),
)
})
.child(
Button::new()
.primary()
.large()
.enabled(launch_enabled)
.on_press(move |_| {
if launch_enabled {
dispatch.launch_cluster(cluster_id);
}
})
.text(launch_label),
),
)
}
fn cluster_tabs(
active: ClusterViewShellTab,
cluster_id: i64,
show_game_log: bool,
) -> impl IntoElement {
let tabs = [
Some(ClusterViewShellTab::Overview),
Some(ClusterViewShellTab::Logs),
Some(ClusterViewShellTab::Screenshots),
Some(ClusterViewShellTab::Mods),
Some(ClusterViewShellTab::Shaders),
Some(ClusterViewShellTab::Textures),
Some(ClusterViewShellTab::Settings),
show_game_log.then_some(ClusterViewShellTab::GameLog),
];
let tab_items = tabs.into_iter().flatten().map(|tab| {
let route = tab.route(cluster_id);
let mut item = TabItem::new(tab.label(), tab == active);
if let Some(route) = route {
item = item.on_press(move |_| {
let _ = RouterContext::get().push(route.clone());
});
}
item
});
rect()
.horizontal()
.width(Size::fill())
.height(Size::px(TABS_HEIGHT))
.cross_align(Alignment::Center)
.padding(Gaps::new(0., 24., 0., 24.))
.background(colors::page_elevated())
.corner_radius(CornerRadius::new_all(12.))
.child(
TabBar::new()
.width(Size::fill())
.height(Size::fill())
.spacing(24.)
.font_size(12.)
.tabs(tab_items),
)
}