feat(ui): hidden bundle packages filter (#692)
This commit is contained in:
parent
a838e1c96b
commit
8511048293
3 changed files with 152 additions and 25 deletions
|
|
@ -44,6 +44,9 @@ pub struct PackageEntry {
|
|||
pub installed: bool,
|
||||
pub hash: Option<String>,
|
||||
pub manifest_default: bool,
|
||||
/// The bundle ships this as a private dependency. Only ever set for bundle
|
||||
/// rows, and only surfaced when the user asks for hidden packages.
|
||||
pub hidden: bool,
|
||||
/// The launch-time check found a newer version for this artifact. Only ever
|
||||
/// set for browser-installed content: bundle packages report their staleness
|
||||
/// through the bundle update flow instead.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use crate::components::{CardLayout, PackageEntry};
|
|||
use crate::hooks::{package_meta_batch, use_game_snapshot, use_package_meta_batch, use_view_state};
|
||||
|
||||
mod views;
|
||||
use views::{ContentBox, ContentKind, EnabledFilter, SortMode, toolbar_bar};
|
||||
use views::{ContentBox, ContentKind, EnabledFilter, HiddenFilter, SortMode, toolbar_bar};
|
||||
|
||||
const CARD_H: f32 = 84.;
|
||||
const CARD_SPACING: f32 = 8.;
|
||||
|
|
@ -75,6 +75,9 @@ pub fn use_content_meta(
|
|||
/// `stale` is the set of artifact hashes the last browser update check found a
|
||||
/// newer version for. Bundle-provided rows are built before it is consulted and
|
||||
/// never carry the flag, so the two update flows cannot both claim a package.
|
||||
///
|
||||
/// Rows for a bundle's hidden files are included and flagged; whether they
|
||||
/// reach the screen is [`HiddenFilter`]'s call.
|
||||
pub fn bundle_packages(
|
||||
content: Vec<LinkedArtifactInfo>,
|
||||
bundles: &[BundleWithUpdateStatus],
|
||||
|
|
@ -123,13 +126,6 @@ pub fn bundle_packages(
|
|||
if !seen.insert(pid.clone()) {
|
||||
continue;
|
||||
}
|
||||
// Hidden files are dependencies the bundle manages on the user's
|
||||
// behalf: never listed, never toggleable. The id still counts as
|
||||
// seen so the loose-content pass below does not resurrect the
|
||||
// installed artifact as an unmanaged local file.
|
||||
if file.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let provider = match &file.kind {
|
||||
BundleFileKind::Managed { provider, .. } => *provider,
|
||||
|
|
@ -165,6 +161,11 @@ pub fn bundle_packages(
|
|||
meta,
|
||||
file.display_name(),
|
||||
false,
|
||||
// Emitted as an ordinary row carrying the flag rather than
|
||||
// dropped here: the row is what `HiddenFilter` filters on, and
|
||||
// the id counting as seen keeps the loose-content pass below
|
||||
// from resurrecting the installed artifact as a local file.
|
||||
file.hidden,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +193,7 @@ pub fn bundle_packages(
|
|||
.clone()
|
||||
.unwrap_or_else(|| info.file_name.clone()),
|
||||
outdated,
|
||||
false,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +213,7 @@ fn make_row(
|
|||
meta: &PackageMetaMap,
|
||||
fallback_name: String,
|
||||
update_available: bool,
|
||||
hidden: bool,
|
||||
) -> PackageEntry {
|
||||
let m = meta.get(&(provider, package_id.clone()));
|
||||
let name = m
|
||||
|
|
@ -250,6 +253,7 @@ fn make_row(
|
|||
installed: installed_info.is_some(),
|
||||
hash: installed_info.map(|i| i.hash.clone()),
|
||||
update_available,
|
||||
hidden,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -300,11 +304,12 @@ fn visible_packages(
|
|||
tab: Option<&Tab>,
|
||||
query: &str,
|
||||
show: EnabledFilter,
|
||||
hidden: HiddenFilter,
|
||||
) -> Vec<PackageEntry> {
|
||||
items
|
||||
.iter()
|
||||
.filter(|p| tab.is_none_or(|t| t.matches(p)) && matches_query(p, query))
|
||||
.filter(|p| show.keep(p))
|
||||
.filter(|p| show.keep(p) && hidden.keep(p))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -320,9 +325,11 @@ pub fn bundle_categories(bundles: &[BundleWithUpdateStatus]) -> Vec<String> {
|
|||
cats
|
||||
}
|
||||
|
||||
fn build_tabs(categories: &[String], items: &[PackageEntry]) -> Vec<Tab> {
|
||||
/// `hidden` is applied here too, so a category whose only members are hidden
|
||||
/// bundle dependencies does not offer a tab that leads to an empty list.
|
||||
fn build_tabs(categories: &[String], items: &[PackageEntry], hidden: HiddenFilter) -> Vec<Tab> {
|
||||
let mut cats: Vec<String> = categories.to_vec();
|
||||
for item in items {
|
||||
for item in items.iter().filter(|p| hidden.keep(p)) {
|
||||
for c in &item.categories {
|
||||
if !cats.contains(c) {
|
||||
cats.push(c.clone());
|
||||
|
|
@ -335,7 +342,7 @@ fn build_tabs(categories: &[String], items: &[PackageEntry]) -> Vec<Tab> {
|
|||
tabs.extend(
|
||||
cats.into_iter()
|
||||
.map(Tab::Category)
|
||||
.filter(|t| items.iter().any(|p| t.matches(p))),
|
||||
.filter(|t| items.iter().any(|p| hidden.keep(p) && t.matches(p))),
|
||||
);
|
||||
|
||||
tabs.push(Tab::Browser);
|
||||
|
|
@ -384,16 +391,15 @@ impl Component for PackageManager {
|
|||
let cluster_id = self.cluster_id;
|
||||
let content_type = self.content_type;
|
||||
|
||||
let tabs = build_tabs(&self.categories, &items);
|
||||
// Minecraft reads its content once at startup, so a toggle made now is
|
||||
// recorded but cannot reach the session already playing. Say so rather
|
||||
// than letting the switch look like it did nothing.
|
||||
let session_live = use_game_snapshot().is_active(cluster_id);
|
||||
let active = use_state(|| 0usize);
|
||||
let active_idx = (*active.read()).min(tabs.len().saturating_sub(1));
|
||||
|
||||
let search = use_state(String::new);
|
||||
let enabled_filter = use_state(|| EnabledFilter::All);
|
||||
let hidden_filter = use_state(|| HiddenFilter::Hide);
|
||||
let view = use_view_state("cluster.packages");
|
||||
let sort = view.sort;
|
||||
let layout = view.layout;
|
||||
|
|
@ -405,11 +411,14 @@ impl Component for PackageManager {
|
|||
.unwrap_or(SortMode::NameAsc);
|
||||
|
||||
let show = *enabled_filter.read();
|
||||
let hidden = *hidden_filter.read();
|
||||
let card_layout = CardLayout::from(*layout.read());
|
||||
|
||||
let tabs = build_tabs(&self.categories, &items, hidden);
|
||||
let active_idx = (*active.read()).min(tabs.len().saturating_sub(1));
|
||||
let tab_filter = tabs.get(active_idx);
|
||||
|
||||
let mut filtered = visible_packages(&items, tab_filter, &query, show);
|
||||
let mut filtered = visible_packages(&items, tab_filter, &query, show, hidden);
|
||||
sort_mode.sort(&mut filtered);
|
||||
|
||||
// Coming up empty during a search is about the query, not about the tab
|
||||
|
|
@ -442,6 +451,7 @@ impl Component for PackageManager {
|
|||
sort,
|
||||
sort_mode,
|
||||
enabled_filter,
|
||||
hidden_filter,
|
||||
layout,
|
||||
cluster_id,
|
||||
package_type,
|
||||
|
|
@ -511,21 +521,59 @@ mod tests {
|
|||
rows.iter().map(|r| r.package_id.clone()).collect()
|
||||
}
|
||||
|
||||
fn shown(rows: &[PackageEntry], hidden: HiddenFilter) -> Vec<String> {
|
||||
ids(&visible_packages(
|
||||
rows,
|
||||
None,
|
||||
"",
|
||||
EnabledFilter::All,
|
||||
hidden,
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_bundle_files_are_not_listed() {
|
||||
fn hidden_bundle_files_are_not_listed_by_default() {
|
||||
let bundles = vec![bundle(
|
||||
"Core",
|
||||
vec![file("visible", true, false), file("dep", true, true)],
|
||||
)];
|
||||
assert_eq!(ids(&rows_for(Vec::new(), &bundles)), ["visible"]);
|
||||
let rows = rows_for(Vec::new(), &bundles);
|
||||
assert_eq!(shown(&rows, HiddenFilter::Hide), ["visible"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turning_the_filter_off_reveals_hidden_bundle_files() {
|
||||
let bundles = vec![bundle(
|
||||
"Core",
|
||||
vec![file("visible", true, false), file("dep", true, true)],
|
||||
)];
|
||||
let rows = rows_for(Vec::new(), &bundles);
|
||||
assert_eq!(shown(&rows, HiddenFilter::Show), ["visible", "dep"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_installed_hidden_file_does_not_come_back_as_loose_content() {
|
||||
// The dependency really is in the cluster, so the pass over installed
|
||||
// content would happily list it as an unmanaged local file.
|
||||
// content would happily list it as a second, unmanaged local row.
|
||||
let bundles = vec![bundle("Core", vec![file("dep", true, true)])];
|
||||
assert!(rows_for(vec![installed("dep")], &bundles).is_empty());
|
||||
let rows = rows_for(vec![installed("dep")], &bundles);
|
||||
assert_eq!(ids(&rows), ["dep"]);
|
||||
assert!(rows[0].hidden && rows[0].in_bundle());
|
||||
assert!(shown(&rows, HiddenFilter::Hide).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_category_of_only_hidden_files_gets_no_tab() {
|
||||
let bundles = vec![bundle("Core", vec![file("dep", true, true)])];
|
||||
let rows = rows_for(Vec::new(), &bundles);
|
||||
let labels = |hidden| -> Vec<String> {
|
||||
build_tabs(&bundle_categories(&bundles), &rows, hidden)
|
||||
.iter()
|
||||
.map(Tab::label)
|
||||
.collect()
|
||||
};
|
||||
assert!(!labels(HiddenFilter::Hide).contains(&"Core".to_string()));
|
||||
assert!(labels(HiddenFilter::Show).contains(&"Core".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -559,6 +607,7 @@ mod tests {
|
|||
installed: true,
|
||||
hash: None,
|
||||
update_available: false,
|
||||
hidden: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -578,21 +627,39 @@ mod tests {
|
|||
fn search_stays_inside_the_active_category() {
|
||||
let items = items();
|
||||
let tab = Tab::Category("Performance".to_string());
|
||||
let rows = visible_packages(&items, Some(&tab), "sodium", EnabledFilter::All);
|
||||
let rows = visible_packages(
|
||||
&items,
|
||||
Some(&tab),
|
||||
"sodium",
|
||||
EnabledFilter::All,
|
||||
HiddenFilter::Hide,
|
||||
);
|
||||
assert_eq!(names(&rows), ["Sodium"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_stays_inside_the_local_tab() {
|
||||
let items = items();
|
||||
let rows = visible_packages(&items, Some(&Tab::Local), "sodium", EnabledFilter::All);
|
||||
let rows = visible_packages(
|
||||
&items,
|
||||
Some(&Tab::Local),
|
||||
"sodium",
|
||||
EnabledFilter::All,
|
||||
HiddenFilter::Hide,
|
||||
);
|
||||
assert_eq!(names(&rows), ["Sodium Local"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tab_searches_every_package() {
|
||||
let items = items();
|
||||
let rows = visible_packages(&items, Some(&Tab::All), "sodium", EnabledFilter::All);
|
||||
let rows = visible_packages(
|
||||
&items,
|
||||
Some(&Tab::All),
|
||||
"sodium",
|
||||
EnabledFilter::All,
|
||||
HiddenFilter::Hide,
|
||||
);
|
||||
assert_eq!(names(&rows), ["Sodium", "Sodium Extra", "Sodium Local"]);
|
||||
}
|
||||
|
||||
|
|
@ -600,7 +667,13 @@ mod tests {
|
|||
fn empty_query_keeps_the_whole_tab() {
|
||||
let items = items();
|
||||
let tab = Tab::Category("Visuals".to_string());
|
||||
let rows = visible_packages(&items, Some(&tab), "", EnabledFilter::All);
|
||||
let rows = visible_packages(
|
||||
&items,
|
||||
Some(&tab),
|
||||
"",
|
||||
EnabledFilter::All,
|
||||
HiddenFilter::Hide,
|
||||
);
|
||||
assert_eq!(names(&rows), ["Sodium Extra"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,32 @@ impl EnabledFilter {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether a bundle's hidden files get a row. They are dependencies the bundle
|
||||
/// manages on the user's behalf, so the list leaves them out until asked.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(super) enum HiddenFilter {
|
||||
Hide,
|
||||
Show,
|
||||
}
|
||||
|
||||
impl HiddenFilter {
|
||||
const ALL: [HiddenFilter; 2] = [HiddenFilter::Hide, HiddenFilter::Show];
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
HiddenFilter::Hide => "Hide",
|
||||
HiddenFilter::Show => "Show",
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn keep(self, p: &PackageEntry) -> bool {
|
||||
match self {
|
||||
HiddenFilter::Hide => !p.hidden,
|
||||
HiddenFilter::Show => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FILTER_PANEL_W: f32 = 172.;
|
||||
const FILTER_BTN_W: f32 = 34.;
|
||||
|
||||
|
|
@ -113,6 +139,7 @@ pub(super) fn toolbar_bar(
|
|||
sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
layout: State<ViewLayout>,
|
||||
cluster_id: i64,
|
||||
package_type: &'static str,
|
||||
|
|
@ -165,6 +192,7 @@ pub(super) fn toolbar_bar(
|
|||
sort,
|
||||
current_sort,
|
||||
enabled_filter,
|
||||
hidden_filter,
|
||||
})
|
||||
.child(
|
||||
SegmentedControl::new(layout)
|
||||
|
|
@ -223,6 +251,7 @@ struct FilterButton {
|
|||
sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
}
|
||||
|
||||
impl Component for FilterButton {
|
||||
|
|
@ -232,10 +261,14 @@ impl Component for FilterButton {
|
|||
let sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
let enabled_filter = self.enabled_filter;
|
||||
let hidden_filter = self.hidden_filter;
|
||||
|
||||
// Hiding hidden packages is the default, so it does not count as the
|
||||
// filters being touched.
|
||||
let is_open = open();
|
||||
let active =
|
||||
current_sort != SortMode::NameAsc || *enabled_filter.read() != EnabledFilter::All;
|
||||
let active = current_sort != SortMode::NameAsc
|
||||
|| *enabled_filter.read() != EnabledFilter::All
|
||||
|| *hidden_filter.read() != HiddenFilter::Hide;
|
||||
|
||||
let icon_color = if active {
|
||||
colors::brand()
|
||||
|
|
@ -263,6 +296,7 @@ impl Component for FilterButton {
|
|||
sort,
|
||||
current_sort,
|
||||
enabled_filter,
|
||||
hidden_filter,
|
||||
on_close,
|
||||
}
|
||||
.into_element()
|
||||
|
|
@ -275,6 +309,7 @@ struct FilterPopover {
|
|||
sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
hidden_filter: State<HiddenFilter>,
|
||||
on_close: EventHandler<()>,
|
||||
}
|
||||
|
||||
|
|
@ -283,6 +318,7 @@ impl Component for FilterPopover {
|
|||
let mut sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
let mut enabled_filter = self.enabled_filter;
|
||||
let mut hidden_filter = self.hidden_filter;
|
||||
let backdrop_close = self.on_close.clone();
|
||||
let key_close = self.on_close.clone();
|
||||
|
||||
|
|
@ -295,6 +331,7 @@ impl Component for FilterPopover {
|
|||
let progress = fade.read().value();
|
||||
|
||||
let show = *enabled_filter.read();
|
||||
let hidden = *hidden_filter.read();
|
||||
|
||||
let mut panel = rect()
|
||||
.vertical()
|
||||
|
|
@ -350,6 +387,20 @@ impl Component for FilterPopover {
|
|||
});
|
||||
}
|
||||
|
||||
panel = panel.child(section_label("Hidden packages"));
|
||||
for filter in HiddenFilter::ALL {
|
||||
let selected = filter == hidden;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
hidden_filter.set(filter);
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: filter.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
|
||||
rect()
|
||||
.height(Size::px(0.))
|
||||
.width(Size::px(FILTER_PANEL_W))
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue