diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs index a49a39fa..fca7e697 100644 --- a/packages/oneclient_app/src/components/navbar/app_navbar.rs +++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs @@ -8,8 +8,8 @@ use crate::{ Route, components::{Avatar, Icon, IconType}, hooks::{ - settled_or_loading, try_default_account, use_active_cluster_id, use_clusters, - use_current_account, use_dispatch, use_notifications_snapshot, + settled_or_loading, try_default_account, use_active_cluster_id, use_browser_type, + use_clusters, use_current_account, use_dispatch, use_notifications_snapshot, }, theme, utils::sort_clusters_for_home, @@ -126,6 +126,7 @@ fn navbar_center() -> impl IntoElement { fn browse_target() -> Route { let clusters = settled_or_loading(&use_clusters()).unwrap_or_default(); let active = *use_active_cluster_id().read(); + let package_type = use_browser_type().read().clone(); let cluster_id = active .filter(|id| clusters.iter().any(|cluster| cluster.id == *id)) @@ -134,7 +135,7 @@ fn browse_target() -> Route { match cluster_id { Some(cluster_id) => Route::Browser { cluster_id, - package_type: "mod".to_string(), + package_type, pick_cluster: true, }, None => Route::Clusters {}, diff --git a/packages/oneclient_app/src/hooks/active_cluster.rs b/packages/oneclient_app/src/hooks/active_cluster.rs index 46b5cba7..6608f6ff 100644 --- a/packages/oneclient_app/src/hooks/active_cluster.rs +++ b/packages/oneclient_app/src/hooks/active_cluster.rs @@ -55,6 +55,17 @@ pub fn use_browser_compat() -> State { consume_root_context::().0 } +#[derive(Clone)] +pub struct BrowserTypeState(pub State); + +pub fn use_provide_browser_type(state: BrowserTypeState) { + use_hook(move || provide_root_context(state)); +} + +pub fn use_browser_type() -> State { + consume_root_context::().0 +} + #[derive(Clone)] pub struct LinkConfirmState(pub State>); diff --git a/packages/oneclient_app/src/hooks/mod.rs b/packages/oneclient_app/src/hooks/mod.rs index 2a119519..bd37cd6c 100644 --- a/packages/oneclient_app/src/hooks/mod.rs +++ b/packages/oneclient_app/src/hooks/mod.rs @@ -14,10 +14,11 @@ pub use overlay_claims::{ pub use view_state::{PersistedView, use_view_state}; pub use active_cluster::{ - ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserUiState, LinkConfirmState, - OnboardingSelectionState, SplashState, StartMaximizedState, use_active_cluster_id, - use_browser_compat, use_browser_state_store, use_link_confirm, use_onboarding_selection, - use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state, + ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState, BrowserUiState, + LinkConfirmState, OnboardingSelectionState, SplashState, StartMaximizedState, + use_active_cluster_id, use_browser_compat, use_browser_state_store, use_browser_type, + use_link_confirm, use_onboarding_selection, use_provide_active_cluster, + use_provide_browser_compat, use_provide_browser_state, use_provide_browser_type, use_provide_link_confirm, use_provide_onboarding_selection, use_provide_splash, use_provide_start_maximized, use_splash, use_start_maximized, }; diff --git a/packages/oneclient_app/src/layout/app_shell.rs b/packages/oneclient_app/src/layout/app_shell.rs index a7ef69d6..f1ed300f 100644 --- a/packages/oneclient_app/src/layout/app_shell.rs +++ b/packages/oneclient_app/src/layout/app_shell.rs @@ -20,9 +20,10 @@ use oneclient_core::clusters::Cluster; use oneclient_db::models::ClusterId; use crate::hooks::{ - ActiveClusterState, BrowserCompatState, BrowserStateStore, use_active_cluster_id, use_clusters, - use_game_snapshot, use_launcher, use_provide_active_cluster, use_provide_browser_compat, - use_provide_browser_state, use_splash, + ActiveClusterState, BrowserCompatState, BrowserStateStore, BrowserTypeState, + use_active_cluster_id, use_clusters, use_game_snapshot, use_launcher, + use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state, + use_provide_browser_type, use_splash, }; use crate::theme::colors; use oneclient_events::LaunchStage; @@ -43,6 +44,9 @@ impl Component for AppShell { let browser_state = use_state(HashMap::new); use_provide_browser_state(BrowserStateStore(browser_state)); + let browser_type = use_state(|| "mod".to_string()); + use_provide_browser_type(BrowserTypeState(browser_type)); + // `FileDrop` bubbles so anything a drop zone doesn't `stop_propagation()` lands here let mut drop_hovering = use_state(|| false); let mut drop_pending = use_state(Vec::::new); diff --git a/packages/oneclient_app/src/view/app/browser/index/mod.rs b/packages/oneclient_app/src/view/app/browser/index/mod.rs index 53b30f17..f7d91111 100644 --- a/packages/oneclient_app/src/view/app/browser/index/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/index/mod.rs @@ -23,7 +23,7 @@ use crate::hooks::{ BROWSE_PAGE_SIZE, BrowserUiState, bundles_with_status_items, category_list, cluster_content_items, content_type_for_slug, pick_version_metadata, search_items, search_pending, search_total, settled_or_loading, use_browser_compat, use_browser_state_store, - use_bundles_with_status, use_cluster_content, use_clusters, use_debounced, + use_browser_type, use_bundles_with_status, use_cluster_content, use_clusters, use_debounced, use_package_categories, use_package_search, use_versions, use_view_state, versions_metadata, }; use crate::routes::Route; @@ -45,6 +45,11 @@ use skeletons::{SkeletonListRow, skeleton_grid_row}; const CARD_BG: Color = Color::from_rgb(26, 34, 41); const CARD_NAME: Color = Color::from_rgb(213, 219, 255); const SCROLLBAR_GUTTER: f32 = 18.; +const CATEGORY_SIDEBAR_W: f32 = 190.; +const PROVIDER_LABELS_W: f32 = 900.; +const WIDE_SEARCH_W: f32 = 780.; +const SEARCH_W: f32 = 260.; +const SEARCH_COMPACT_W: f32 = 170.; const CARD_H: f32 = 240.; const BANNER_H: f32 = 100.; const CARD_TEXT_H: f32 = CARD_H - BANNER_H; @@ -62,12 +67,17 @@ pub(super) struct GridMetrics { pub banner_h: f32, } +const BROWSE_TYPES: [(&str, &str); 3] = [ + ("mod", "Mods"), + ("texture", "Textures"), + ("shader", "Shaders"), +]; + fn type_title(package_type: &str) -> &'static str { - match package_type { - "shader" => "Shaders", - "texture" => "Textures", - _ => "Mods", - } + BROWSE_TYPES + .iter() + .find(|(slug, _)| *slug == package_type) + .map_or("Mods", |(_, title)| *title) } fn encode_package_id(provider: ProviderId, id: &str) -> String { @@ -83,6 +93,37 @@ pub struct Browser { } impl Component for Browser { + fn render(&self) -> impl IntoElement { + let mut last_type = use_browser_type(); + use_side_effect_with_deps(&self.package_type, move |package_type| { + last_type.set_if_modified(package_type.clone()); + }); + + BrowserBody { + cluster_id: self.cluster_id, + package_type: self.package_type.clone(), + pick_cluster: self.pick_cluster, + key: DiffKey::None, + } + .key((self.cluster_id, self.package_type.as_str())) + } +} + +#[derive(PartialEq)] +struct BrowserBody { + cluster_id: i64, + package_type: String, + pick_cluster: bool, + key: DiffKey, +} + +impl KeyExt for BrowserBody { + fn write_key(&mut self) -> &mut DiffKey { + &mut self.key + } +} + +impl Component for BrowserBody { fn render(&self) -> impl IntoElement { let cluster_id = self.cluster_id; let package_type = self.package_type.clone(); @@ -191,6 +232,7 @@ impl Component for Browser { let mode = *view_mode.read(); let mut grid_width = use_state(|| 0f32); + let controls_width = use_state(|| 0f32); let cols = resolved_columns(*grid_columns.read()); let card_w = cell_width( (*grid_width.read() - SCROLLBAR_GUTTER).max(0.), @@ -278,7 +320,18 @@ impl Component for Browser { package_type: package_type.clone(), }), )) - .child(controls(provider, query, view_mode, grid_columns)) + .child(controls( + TypePicker { + cluster_id, + package_type: package_type.clone(), + pick_cluster: self.pick_cluster, + }, + provider, + query, + view_mode, + grid_columns, + controls_width, + )) .child( rect() .horizontal() @@ -319,6 +372,10 @@ impl Component for Browser { ), ) } + + fn render_key(&self) -> DiffKey { + self.key.clone().or(self.default_key()) + } } fn page_title( @@ -341,7 +398,7 @@ fn page_title( // Without the picker the cluster is fixed so it's just a subtitle None => cluster_name.map(|name| { label() - .text(format!("for {name}")) + .text(name) .font_size(14.) .color(colors::fg_secondary()) .into_element() @@ -364,6 +421,41 @@ fn version_name(metadata: &[VersionMetadata], cluster: &Cluster) -> String { .unwrap_or_else(|| cluster.mc_version.clone()) } +#[derive(PartialEq)] +struct TypePicker { + cluster_id: i64, + package_type: String, + pick_cluster: bool, +} + +impl Component for TypePicker { + fn render(&self) -> impl IntoElement { + let cluster_id = self.cluster_id; + let pick_cluster = self.pick_cluster; + let current = self.package_type.clone(); + + let labels: Vec = BROWSE_TYPES + .iter() + .map(|(_, title)| (*title).to_string()) + .collect(); + let selected = type_title(&self.package_type).to_string(); + + Dropdown::new(selected, labels) + .width(Size::px(CATEGORY_SIDEBAR_W)) + .height(Size::px(30.)) + .on_select(move |idx: usize| { + let picked = BROWSE_TYPES.get(idx).filter(|(slug, _)| *slug != current); + if let Some((slug, _)) = picked { + let _ = RouterContext::get().push(Route::Browser { + cluster_id, + package_type: (*slug).to_string(), + pick_cluster, + }); + } + }) + } +} + /// The choice lives in the route so back and forward keep the cluster being browsed #[derive(PartialEq)] struct ClusterPicker { @@ -396,12 +488,6 @@ impl Component for ClusterPicker { .cross_align(Alignment::Center) .spacing(8.) .margin(Gaps::new(4., 0., 0., 0.)) - .child( - label() - .text("for") - .font_size(14.) - .color(colors::fg_secondary()), - ) .child( Dropdown::new(selected, labels) .width(Size::px(240.)) @@ -420,17 +506,34 @@ impl Component for ClusterPicker { } fn controls( + type_picker: TypePicker, provider: State, query: State, view_mode: State, grid_columns: State, + mut controls_width: State, ) -> impl IntoElement { + let measured = *controls_width.read(); + let show_provider_labels = measured == 0. || measured >= PROVIDER_LABELS_W; + let search_width = if measured == 0. || measured >= WIDE_SEARCH_W { + SEARCH_W + } else { + SEARCH_COMPACT_W + }; + rect() .horizontal() .width(Size::fill()) .cross_align(Alignment::Center) .spacing(12.) .content(Content::Flex) + .on_sized(move |event: Event| { + let w = event.data().area.width(); + if (w - *controls_width.peek()).abs() > 0.5 { + *controls_width.write() = w; + } + }) + .child(type_picker) .child(rect().width(Size::flex(1.0))) .maybe_child( (*view_mode.read() == ViewLayout::Grid) @@ -446,16 +549,19 @@ fn controls( .child( SegmentedControl::new(provider) .no_tint() - .segments(ProviderId::REMOTE_PROVIDERS.iter().map(|provider| { - Segment::new(*provider) - .icon(IconType::from(*provider)) - .label(provider.to_string()) + .segments(ProviderId::REMOTE_PROVIDERS.iter().map(move |provider| { + let segment = Segment::new(*provider).icon(IconType::from(*provider)); + if show_provider_labels { + segment.label(provider.to_string()) + } else { + segment + } })) .into_element(), ) .child( TextInput::new(query) - .width(Size::px(260.)) + .width(Size::px(search_width)) .placeholder("Search for content") .leading( Icon::new(IconType::SearchMd) diff --git a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs index ad814631..f7e25dbb 100644 --- a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs +++ b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs @@ -41,7 +41,7 @@ impl Component for CategorySidebar { rect() .vertical() - .width(Size::px(190.)) + .width(Size::px(CATEGORY_SIDEBAR_W)) .height(Size::fill()) .spacing(8.) .child(