feat: added resource packs and shaders sections inside browse view (#819)

This commit is contained in:
Maciek 2026-09-13 00:36:46 +02:00 • committed by GitHub
parent 53b27a1c7b
commit a6c03c6f6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 153 additions and 30 deletions

View file

@ -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 {},

View file

@ -55,6 +55,17 @@ pub fn use_browser_compat() -> State<bool> {
consume_root_context::<BrowserCompatState>().0
}
#[derive(Clone)]
pub struct BrowserTypeState(pub State<String>);
pub fn use_provide_browser_type(state: BrowserTypeState) {
use_hook(move || provide_root_context(state));
}
pub fn use_browser_type() -> State<String> {
consume_root_context::<BrowserTypeState>().0
}
#[derive(Clone)]
pub struct LinkConfirmState(pub State<Option<String>>);

View file

@ -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,
};

View file

@ -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::<PathBuf>::new);

View file

@ -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<String> = 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<ProviderId>,
query: State<String>,
view_mode: State<ViewLayout>,
grid_columns: State<u8>,
mut controls_width: State<f32>,
) -> 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<SizedEventData>| {
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)

View file

@ -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(