From fc3046d60e84d02f20e76760a2b432e99f0fa5ef Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Thu, 1 Oct 2026 00:48:50 -0400 Subject: [PATCH] feat: list/grid layout toggle on clusters page --- .../src/components/instance_row.rs | 28 ++++++-- .../src/components/version_card.rs | 39 ++++++---- .../src/view/app/clusters/page.rs | 72 ++++++++++++++----- 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/packages/oneclient_app/src/components/instance_row.rs b/packages/oneclient_app/src/components/instance_row.rs index 2709d084..fa44ceeb 100644 --- a/packages/oneclient_app/src/components/instance_row.rs +++ b/packages/oneclient_app/src/components/instance_row.rs @@ -4,12 +4,13 @@ use oneclient_core::clusters::Cluster; use crate::components::{ART_PREVIEW_EDGE, DynamicArt}; use crate::theme::colors; use crate::ui::{border_all_color, last_played_label}; +use crate::utils::{GridSelection, ReleaseLine, line_art_key, line_title}; const ROW_HEIGHT_PX: f32 = 56.; const THUMB_PX: f32 = 40.; pub struct InstanceRow { - id: i64, + key: GridSelection, title: String, subtitle: String, played: String, @@ -25,7 +26,7 @@ impl InstanceRow { on_press: impl Into>>, ) -> Self { Self { - id: cluster.id, + key: GridSelection::Instance(cluster.id), title: cluster.name.clone(), subtitle: format!("{} \u{b7} {}", cluster.mc_version, cluster.mc_loader), played: last_played_label(cluster.last_played), @@ -34,11 +35,30 @@ impl InstanceRow { on_press: on_press.into(), } } + + pub fn for_line( + line: ReleaseLine, + clusters: &[Cluster], + caption: String, + selected: bool, + on_press: impl Into>>, + ) -> Self { + Self { + key: GridSelection::Line(line), + title: line_title(line, clusters), + subtitle: caption, + played: last_played_label(clusters.iter().filter_map(|c| c.last_played).max()), + art: DynamicArt::for_version(line.major, line_art_key(line, clusters), None) + .max_edge(ART_PREVIEW_EDGE), + selected, + on_press: on_press.into(), + } + } } impl PartialEq for InstanceRow { fn eq(&self, other: &Self) -> bool { - self.id == other.id + self.key == other.key && self.title == other.title && self.subtitle == other.subtitle && self.played == other.played @@ -76,7 +96,7 @@ impl Component for InstanceRow { }; rect() - .key(self.id) + .key(self.key) .horizontal() .width(Size::fill()) .height(Size::px(ROW_HEIGHT_PX)) diff --git a/packages/oneclient_app/src/components/version_card.rs b/packages/oneclient_app/src/components/version_card.rs index 96c7b0d0..12c00424 100644 --- a/packages/oneclient_app/src/components/version_card.rs +++ b/packages/oneclient_app/src/components/version_card.rs @@ -1,17 +1,16 @@ use freya::prelude::*; -use oneclient_common::VersionKey; use oneclient_core::clusters::Cluster; use crate::components::{ART_PREVIEW_EDGE, DynamicArt}; use crate::theme::colors; use crate::ui::border_all_color; -use crate::utils::{ReleaseLine, line_art_key, line_title}; +use crate::utils::{GridSelection, ReleaseLine, line_art_key, line_title}; const CARD_HEIGHT_PX: f32 = 150.; pub struct VersionCard { - pub line: ReleaseLine, - pub art_key: Option, + pub key: GridSelection, + pub art: DynamicArt, pub title: String, pub caption: String, pub count: usize, @@ -28,8 +27,9 @@ impl VersionCard { on_press: impl Into>>, ) -> Self { Self { - line, - art_key: line_art_key(line, clusters), + key: GridSelection::Line(line), + art: DynamicArt::for_version(line.major, line_art_key(line, clusters), None) + .max_edge(ART_PREVIEW_EDGE), title: line_title(line, clusters), caption, count: clusters.len(), @@ -37,12 +37,28 @@ impl VersionCard { on_press: on_press.into(), } } + + pub fn for_instance( + cluster: &Cluster, + selected: bool, + on_press: impl Into>>, + ) -> Self { + Self { + key: GridSelection::Instance(cluster.id), + art: DynamicArt::for_cluster(cluster).max_edge(ART_PREVIEW_EDGE), + title: cluster.name.clone(), + caption: format!("{} \u{b7} {}", cluster.mc_version, cluster.mc_loader), + count: 1, + selected, + on_press: on_press.into(), + } + } } impl PartialEq for VersionCard { fn eq(&self, other: &Self) -> bool { - self.line == other.line - && self.art_key == other.art_key + self.key == other.key + && self.art == other.art && self.title == other.title && self.caption == other.caption && self.count == other.count @@ -79,7 +95,7 @@ impl Component for VersionCard { }; rect() - .key(self.line) + .key(self.key) .width(Size::fill()) .height(Size::px(CARD_HEIGHT_PX)) .corner_radius(CornerRadius::new_all(16.)) @@ -97,10 +113,7 @@ impl Component for VersionCard { .height(Size::fill()) .position(Position::new_absolute()) .opacity(art_opacity) - .child( - DynamicArt::for_version(self.line.major, self.art_key, None) - .max_edge(ART_PREVIEW_EDGE), - ), + .child(self.art.clone()), ) .child( rect() diff --git a/packages/oneclient_app/src/view/app/clusters/page.rs b/packages/oneclient_app/src/view/app/clusters/page.rs index 75db20bf..e7bff3d9 100644 --- a/packages/oneclient_app/src/view/app/clusters/page.rs +++ b/packages/oneclient_app/src/view/app/clusters/page.rs @@ -5,14 +5,16 @@ use freya::router::RouterContext; use oneclient_common::domain::GameLoader; use oneclient_common::{VersionKey, parse_mc_version}; use oneclient_core::clusters::Cluster; +use oneclient_core::settings::ViewLayout; use crate::components::{ ART_PREVIEW_EDGE, Button, Dropdown, DynamicArt, FilterMenu, FilterOption, Icon, IconType, - InstanceRow, ScrollArea, TabBar, TabItem, TextInput, VersionCard, open_folder_button, + InstanceRow, ScrollArea, Segment, SegmentedControl, TabBar, TabItem, TextInput, VersionCard, + open_folder_button, }; use crate::hooks::{ settled_or_loading, use_active_cluster_id, use_clusters, use_dispatch, use_game_snapshot, - use_launcher, use_version_metadata, + use_launcher, use_version_metadata, use_view_state, }; use crate::routes::Route; use crate::theme::colors; @@ -97,6 +99,7 @@ impl Component for Clusters { let query = use_state(String::new); let loaders = use_state(Vec::::new); let sort = use_state(|| Sort::RecentFirst); + let layout = use_view_state("clusters").layout; let mut body_width = use_state(|| window_logical_size().width - PAGE_PADDING.left() - PAGE_PADDING.right()); @@ -197,8 +200,20 @@ impl Component for Clusters { SIDEBAR_WIDTH_PX }; let main_w = body_w - sidebar_w - COLUMN_GAP_PX; - let card_columns = grid_columns_for_width(main_w, MAX_CARD_WIDTH_PX, CARD_GAP_PX); - let row_columns = grid_columns_for_width(main_w, MAX_ROW_WIDTH_PX, ROW_GAP_PX); + let grid = *layout.read() == ViewLayout::Grid; + let (columns, item_height, gap) = if grid { + ( + grid_columns_for_width(main_w, MAX_CARD_WIDTH_PX, CARD_GAP_PX), + CARD_HEIGHT_PX, + CARD_GAP_PX, + ) + } else { + ( + grid_columns_for_width(main_w, MAX_ROW_WIDTH_PX, ROW_GAP_PX), + ROW_HEIGHT_PX, + ROW_GAP_PX, + ) + }; let inline_search = main_w >= INLINE_SEARCH_MIN_PX; let active_filter = *filter.read(); @@ -229,7 +244,12 @@ impl Component for Clusters { clusters.iter().map(|c| c.mc_loader.to_string()).collect(); available_loaders.sort(); available_loaders.dedup(); - let filters = filter_menu(&available_loaders, loaders, sort); + let filters = rect() + .horizontal() + .spacing(8.) + .child(filter_menu(&available_loaders, loaders, sort)) + .child(layout_toggle(layout)) + .into_element(); let nothing_shown = shown_lines.is_empty() && shown_custom.is_empty() && shown_packs.is_empty(); @@ -255,11 +275,15 @@ impl Component for Clusters { (true, Some(c), _) | (_, _, [c]) => cluster_caption(c), _ => format!("{} instances", list.len()), }; - VersionCard::new(line, list, caption, is_selected, move |_| { + let on_press = move |_| { selected.set(Some(item)); selected_cluster.set(None); - }) - .into_element() + }; + if grid { + VersionCard::new(line, list, caption, is_selected, on_press).into_element() + } else { + InstanceRow::for_line(line, list, caption, is_selected, on_press).into_element() + } }) .collect(); @@ -267,8 +291,13 @@ impl Component for Clusters { list.iter() .map(|c| { let item = GridSelection::Instance(c.id); - InstanceRow::new(c, current == Some(item), move |_| selected.set(Some(item))) - .into_element() + let is_selected = current == Some(item); + let on_press = move |_| selected.set(Some(item)); + if grid { + VersionCard::for_instance(c, is_selected, on_press).into_element() + } else { + InstanceRow::new(c, is_selected, on_press).into_element() + } }) .collect() }; @@ -341,12 +370,7 @@ impl Component for Clusters { section( "OneClient", "Grouped by Minecraft version", - fixed_grid( - line_cards, - card_columns, - CARD_HEIGHT_PX, - CARD_GAP_PX, - ), + fixed_grid(line_cards, columns, item_height, gap), ) })) .append_children( @@ -367,9 +391,9 @@ impl Component for Clusters { caption, fixed_grid( instance_rows(list), - row_columns, - ROW_HEIGHT_PX, - ROW_GAP_PX, + columns, + item_height, + gap, ), ) }, @@ -543,6 +567,16 @@ fn filter_menu( .into_element() } +fn layout_toggle(layout: State) -> Element { + SegmentedControl::new(layout) + .height(34.) + .icon_size(15.) + .equal_width(34.) + .segment(Segment::new(ViewLayout::List).icon(IconType::ParagraphWrap)) + .segment(Segment::new(ViewLayout::Grid).icon(IconType::DotsGrid)) + .into_element() +} + fn search_input(query: State, width: Size) -> Element { TextInput::new(query) .width(width)