diff --git a/packages/oneclient_app/src/components/mod.rs b/packages/oneclient_app/src/components/mod.rs index 79b66f95..f55cf182 100644 --- a/packages/oneclient_app/src/components/mod.rs +++ b/packages/oneclient_app/src/components/mod.rs @@ -95,7 +95,7 @@ pub use progress_track::progress_track; pub use recents_row::RecentsRow; pub use screenshot_viewer::{ScreenshotViewer, screenshot_context_menu}; pub(crate) use scrollview::corrected_scroll; -pub use scrollview::{ScrollArea, ScrollAreaCtx}; +pub use scrollview::{LazySection, ScrollArea, ScrollAreaCtx}; pub use segmented_control::{Segment, SegmentedControl}; pub use splash_curtain::SplashCurtain; pub use status_bar::StatusBar; diff --git a/packages/oneclient_app/src/components/optional_mods_popup.rs b/packages/oneclient_app/src/components/optional_mods_popup.rs index 1a53f157..53c7e554 100644 --- a/packages/oneclient_app/src/components/optional_mods_popup.rs +++ b/packages/oneclient_app/src/components/optional_mods_popup.rs @@ -125,6 +125,7 @@ fn entry_from_item( hash: None, manifest_default: false, hidden: false, + advanced: false, update_available: false, // Offered, not installed, so there is no recency badge to show seen_status: SeenStatus::default(), diff --git a/packages/oneclient_app/src/components/package_row.rs b/packages/oneclient_app/src/components/package_row.rs index ffcc83ca..c99c332f 100644 --- a/packages/oneclient_app/src/components/package_row.rs +++ b/packages/oneclient_app/src/components/package_row.rs @@ -52,6 +52,7 @@ pub struct PackageEntry { pub manifest_default: bool, /// Private bundle dependency only set for bundle rows pub hidden: bool, + pub advanced: bool, /// Only set for browser-installed content bundle packages use the bundle update flow pub update_available: bool, /// Recency badge state cleared once the user views the list diff --git a/packages/oneclient_app/src/components/scrollview.rs b/packages/oneclient_app/src/components/scrollview.rs index 3623b8f6..96045d3c 100644 --- a/packages/oneclient_app/src/components/scrollview.rs +++ b/packages/oneclient_app/src/components/scrollview.rs @@ -81,6 +81,22 @@ pub struct ScrollAreaCtx { pub viewport_left: f32, } +#[derive(Clone, Copy)] +pub struct LazySection { + pub header: bool, + pub count: usize, +} + +enum LazyRow { + Header(usize), + Items { + section: usize, + row: usize, + start: usize, + end: usize, + }, +} + pub struct ScrollArea { width: Size, height: Size, @@ -246,51 +262,108 @@ impl ScrollArea { self } - pub fn lazy_grid( + #[allow(clippy::too_many_arguments)] + pub fn lazy_sections( mut self, - count: usize, + sections: Vec, item_height: f32, gap: f32, min_width: f32, max_cols: usize, + header_height: f32, render: impl Fn(usize) -> Element + 'static, + render_header: impl Fn(usize) -> Element + 'static, ) -> Self { let slot = (item_height + gap).max(1.); + let header_slot = header_height + gap; self.builder = Some(Box::new(move |ctx: ScrollAreaCtx| { let cols = (((ctx.viewport_w + gap) / (min_width + gap)).floor() as usize) .clamp(1, max_cols.max(1)); - let rows_total = count.div_ceil(cols); - let first = - (((-ctx.corrected_y) / slot).floor() as i64 - LAZY_OVERSCAN).max(0) as usize; - let span = ((ctx.viewport_h / slot).ceil() as i64 + 2 * LAZY_OVERSCAN).max(0) as usize; - let last = (first + span).min(rows_total); + let mut rows = Vec::new(); + let mut offset = 0; + for (s, section) in sections.iter().enumerate() { + if section.header { + rows.push(LazyRow::Header(s)); + } + for r in 0..section.count.div_ceil(cols) { + let start = offset + r * cols; + rows.push(LazyRow::Items { + section: s, + row: r, + start, + end: (start + cols).min(offset + section.count), + }); + } + offset += section.count; + } - let top_pad = first as f32 * slot; - let bottom_pad = rows_total.saturating_sub(last) as f32 * slot; + let row_slot = |row: &LazyRow| match row { + LazyRow::Header(_) => header_slot, + LazyRow::Items { .. } => slot, + }; + + let overscan = LAZY_OVERSCAN as f32 * slot; + let visible_top = -ctx.corrected_y - overscan; + let visible_bottom = -ctx.corrected_y + ctx.viewport_h + overscan; + + let mut top_pad = 0.; + let mut bottom_pad = 0.; + let mut visible = Vec::new(); + let mut y = 0.; + for row in &rows { + let h = row_slot(row); + if y + h <= visible_top { + top_pad += h; + } else if y >= visible_bottom { + bottom_pad += h; + } else { + visible.push(row); + } + y += h; + } let mut container = rect().vertical().width(Size::fill()); if top_pad > 0. { container = container.child(rect().width(Size::fill()).height(Size::px(top_pad))); } - for r in first..last { - let mut row = rect() - .key(r) - .horizontal() - .width(Size::fill()) - .height(Size::px(slot)) - .spacing(gap) - .content(Content::Flex); - for c in 0..cols { - let idx = r * cols + c; - let cell = rect().width(Size::flex(1.0)).height(Size::px(item_height)); - row = row.child(if idx < count { - cell.child(render(idx)) - } else { - cell - }); - } - container = container.child(row); + for row in visible { + let el = match *row { + LazyRow::Header(section) => rect() + .key(("h", section)) + .width(Size::fill()) + .height(Size::px(header_slot)) + .child( + rect() + .width(Size::fill()) + .height(Size::px(header_height)) + .child(render_header(section)), + ), + LazyRow::Items { + section, + row, + start, + end, + } => { + let mut items = rect() + .key(("r", section, row)) + .horizontal() + .width(Size::fill()) + .height(Size::px(slot)) + .spacing(gap) + .content(Content::Flex); + for idx in start..start + cols { + let cell = rect().width(Size::flex(1.0)).height(Size::px(item_height)); + items = items.child(if idx < end { + cell.child(render(idx)) + } else { + cell + }); + } + items + } + }; + container = container.child(el); } if bottom_pad > 0. { container = diff --git a/packages/oneclient_app/src/view/app/browser/mod.rs b/packages/oneclient_app/src/view/app/browser/mod.rs index 31d4dcc4..ae4a90cb 100644 --- a/packages/oneclient_app/src/view/app/browser/mod.rs +++ b/packages/oneclient_app/src/view/app/browser/mod.rs @@ -402,6 +402,7 @@ mod tests { hidden: false, path: format!("mods/{project_id}.jar"), size: 1, + file_type: oneclient_core::BundleFileType::Normal, kind: BundleFileKind::Managed { provider: ProviderId::Modrinth, project_id: project_id.to_string(), diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs index 81a9c5b0..1e54d768 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/mod.rs @@ -4,7 +4,7 @@ use freya::prelude::*; use oneclient_common::search::{MatchScore, SearchQuery}; use oneclient_content::packages::{CachedPackageMeta, ContentType, ProviderId}; use oneclient_core::{ - BundleFileKind, BundleWithUpdateStatus, FileUpdateStatus, LinkedArtifactInfo, + BundleFileKind, BundleFileType, BundleWithUpdateStatus, FileUpdateStatus, LinkedArtifactInfo, }; use oneclient_db::models::OverrideType; @@ -12,7 +12,9 @@ use crate::components::{CARD_GRID_H, CardLayout, GRID_GAP, GRID_MIN_W, PackageEn use crate::hooks::{package_meta_batch, use_game_snapshot, use_package_meta_batch, use_view_state}; mod views; -use views::{ContentBox, ContentKind, EnabledFilter, HiddenFilter, SortMode, toolbar_bar}; +use views::{ + AdvancedSection, ContentBox, ContentKind, EnabledFilter, HiddenFilter, SortMode, toolbar_bar, +}; const CARD_H: f32 = 84.; const CARD_SPACING: f32 = 8.; @@ -119,10 +121,14 @@ pub fn bundle_packages( // Hidden is per-bundle so one bundle carrying a mod as a private dependency must not suppress a bundle that offers it openly let mut shown_elsewhere: HashSet = HashSet::new(); + let mut normal_elsewhere: HashSet = HashSet::new(); for bundle in bundles { for (file, _status) in &bundle.files { if !file.hidden && file.content_type() == content_type { shown_elsewhere.insert(file.kind.package_id()); + if file.file_type == BundleFileType::Normal { + normal_elsewhere.insert(file.kind.package_id()); + } } } } @@ -172,13 +178,17 @@ pub fn bundle_packages( None => oneclient_core::effective_enabled(file, ov.and_then(OverrideType::parse)), }; + let advanced = content_type == ContentType::Mod + && file.file_type == BundleFileType::Advanced + && !normal_elsewhere.contains(&pid); + let categories = if category.is_empty() { Vec::new() } else { vec![category.clone()] }; - rows.push(make_row( + let mut row = make_row( pid, Some(bundle_name.clone()), provider, @@ -193,7 +203,9 @@ pub fn bundle_packages( false, // Flagged rather than dropped `HiddenFilter` filters on the row and the seen id stops the loose-content pass resurrecting it as a local file file.hidden, - )); + ); + row.advanced = advanced; + rows.push(row); } } @@ -281,6 +293,7 @@ fn make_row( hash: installed_info.map(|i| i.hash.clone()), update_available, hidden, + advanced: false, seen_status: installed_info.map(|i| i.seen_status).unwrap_or_default(), } } @@ -444,6 +457,7 @@ impl Component for PackageManager { let search = use_state(String::new); let enabled_filter = use_state(|| EnabledFilter::All); let hidden_filter = use_state(|| HiddenFilter::Hide); + let advanced_open = use_state(|| false); let toolbar_width = use_state(|| 0f32); let view = use_view_state("cluster.packages"); let sort = view.sort; @@ -486,6 +500,8 @@ impl Component for PackageManager { } }; + let (advanced, filtered): (Vec<_>, Vec<_>) = filtered.into_iter().partition(|p| p.advanced); + rect() .vertical() .width(Size::fill()) @@ -512,6 +528,11 @@ impl Component for PackageManager { .maybe_child(session_live.then(|| views::running_notice(noun_plural, content_type))) .child(ContentBox::new( filtered, + advanced, + AdvancedSection { + open: advanced_open, + forced: !query.is_empty(), + }, noun_plural, package_type, content_type, diff --git a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs index 54c80139..ce8bc239 100644 --- a/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs +++ b/packages/oneclient_app/src/view/app/cluster/package_manager/views.rs @@ -6,7 +6,7 @@ use oneclient_content::packages::ContentType; use oneclient_core::settings::ViewLayout; use crate::components::{ - Button, CardLayout, Icon, IconType, PackageEntry, PackageRow, ScrollArea, Segment, + Button, CardLayout, Icon, IconType, LazySection, PackageEntry, PackageRow, ScrollArea, Segment, SegmentedControl, TextInput, package_context_menu, use_shared_delete, }; use crate::hooks::{ClusterAction, use_cluster_mutation, use_dispatch, use_overlay_claim}; @@ -695,9 +695,84 @@ pub(super) enum ContentKind { }, } +const SECTION_HEADER_H: f32 = 36.; + +#[derive(Clone, Copy, PartialEq)] +pub(super) struct AdvancedSection { + pub(super) open: State, + pub(super) forced: bool, +} + +impl AdvancedSection { + fn expanded(self) -> bool { + self.forced || *self.open.read() + } +} + +#[derive(PartialEq)] +struct SectionHeader { + label: &'static str, + count: usize, + section: AdvancedSection, +} + +impl Component for SectionHeader { + fn render(&self) -> impl IntoElement { + let mut hovered = use_state(|| false); + let section = self.section; + let mut open = section.open; + let expanded = section.expanded(); + let interactive = !section.forced; + + rect() + .horizontal() + .width(Size::fill()) + .height(Size::fill()) + .cross_align(Alignment::Center) + .spacing(8.) + .padding(Gaps::new_symmetric(0., 12.)) + .corner_radius(CornerRadius::new_all(10.)) + .background(if interactive && *hovered.read() { + colors::ghost_overlay_hover() + } else { + Color::TRANSPARENT + }) + .maybe(interactive, |el| { + el.cursor(CursorIcon::Pointer) + .on_pointer_enter(move |_| hovered.set(true)) + .on_pointer_leave(move |_| hovered.set(false)) + .on_press(move |_| open.toggle()) + }) + .child( + Icon::new(if expanded { + IconType::ChevronDown + } else { + IconType::ChevronRight + }) + .size(14.) + .color(colors::fg_secondary()), + ) + .child( + label() + .text(self.label) + .font_size(13.) + .font_weight(FontWeight::SEMI_BOLD) + .color(colors::fg_primary()), + ) + .child( + label() + .text(self.count.to_string()) + .font_size(12.) + .color(colors::fg_secondary()), + ) + } +} + #[derive(PartialEq)] pub(super) struct ContentBox { items: Vec, + advanced: Vec, + section: AdvancedSection, noun_plural: &'static str, package_type: &'static str, content_type: ContentType, @@ -710,6 +785,8 @@ impl ContentBox { #[allow(clippy::too_many_arguments)] pub(super) fn new( items: Vec, + advanced: Vec, + section: AdvancedSection, noun_plural: &'static str, package_type: &'static str, content_type: ContentType, @@ -719,6 +796,8 @@ impl ContentBox { ) -> Self { Self { items, + advanced, + section, noun_plural, package_type, content_type, @@ -731,7 +810,14 @@ impl ContentBox { impl Component for ContentBox { fn render(&self) -> impl IntoElement { - let items = self.items.clone(); + let section = self.section; + let expanded = section.expanded(); + let advanced_count = self.advanced.len(); + let mut items = self.items.clone(); + let normal_count = items.len(); + if expanded { + items.extend(self.advanced.iter().cloned()); + } let package_type = self.package_type; let content_type = self.content_type; let cluster_id = self.cluster_id; @@ -760,19 +846,44 @@ impl Component for ContentBox { } }; - let count = items.len(); + let count = normal_count + advanced_count; let scroll = (count > 0).then(|| { - let area = ScrollArea::new() + let mut sections = vec![LazySection { + header: false, + count: normal_count, + }]; + if advanced_count > 0 { + sections.push(LazySection { + header: true, + count: if expanded { advanced_count } else { 0 }, + }); + } + let (item_height, gap, min_width, max_cols) = match layout { + CardLayout::List => (CARD_H, CARD_SPACING, 0., 1), + CardLayout::Grid => (CARD_GRID_H, GRID_GAP, GRID_MIN_W, GRID_MAX_COLS), + }; + ScrollArea::new() .width(Size::fill()) .height(Size::fill()) - .scrollbar_gutter(true); - match layout { - CardLayout::List => area.lazy(count, CARD_H, CARD_SPACING, row), - CardLayout::Grid => { - area.lazy_grid(count, CARD_GRID_H, GRID_GAP, GRID_MIN_W, GRID_MAX_COLS, row) - } - } - .into_element() + .scrollbar_gutter(true) + .lazy_sections( + sections, + item_height, + gap, + min_width, + max_cols, + SECTION_HEADER_H, + row, + move |_| { + SectionHeader { + label: "Advanced", + count: advanced_count, + section, + } + .into_element() + }, + ) + .into_element() }); let menu_overlay = menu.read().clone().map(|(x, y, item)| { diff --git a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs index 208fd882..0107ae49 100644 --- a/packages/oneclient_app/src/view/onboarding/bundles/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/bundles/mod.rs @@ -16,8 +16,8 @@ use crate::routes::Route; use crate::theme::colors; use crate::ui::border_all_color; use crate::view::onboarding::{ - archive_selected, choice_row_sized, is_default_bundle, is_optional_file, onboarding_nav, - onboarding_slide, pkg_key, set_archive_selected, step_heading, version_chip, + archive_selected, choice_row_sized, is_default_bundle, onboarding_nav, onboarding_slide, + pkg_key, set_archive_selected, step_heading, version_chip, }; mod row; @@ -173,7 +173,7 @@ fn optional_mods(clusters: &[ClusterBundles]) -> Vec { .manifest .files .iter() - .filter(|f| is_optional_file(f)) + .filter(|f| f.is_optional_offer()) { let package_id = file.kind.package_id(); let entry = map.entry(package_id.clone()).or_insert_with(|| { diff --git a/packages/oneclient_app/src/view/onboarding/mod.rs b/packages/oneclient_app/src/view/onboarding/mod.rs index 4eefd147..460ed374 100644 --- a/packages/oneclient_app/src/view/onboarding/mod.rs +++ b/packages/oneclient_app/src/view/onboarding/mod.rs @@ -12,8 +12,7 @@ pub(crate) mod test_support; mod welcome; pub(crate) use selection::{ - archive_selected, default_selection, is_default_bundle, is_optional_file, pkg_key, - set_archive_selected, + archive_selected, default_selection, is_default_bundle, pkg_key, set_archive_selected, }; pub use account::OnboardingAccount; diff --git a/packages/oneclient_app/src/view/onboarding/selection.rs b/packages/oneclient_app/src/view/onboarding/selection.rs index c2b38160..a3332ece 100644 --- a/packages/oneclient_app/src/view/onboarding/selection.rs +++ b/packages/oneclient_app/src/view/onboarding/selection.rs @@ -16,10 +16,6 @@ pub fn is_default_file(file: &BundleFile) -> bool { file.enabled && !file.hidden } -pub fn is_optional_file(file: &BundleFile) -> bool { - !file.enabled && !file.hidden -} - pub fn default_selection( items: &[ClusterBundles], opted_in_categories: Option<&[String]>, diff --git a/packages/oneclient_app/src/view/onboarding/test_support.rs b/packages/oneclient_app/src/view/onboarding/test_support.rs index 118f16a7..ddb4ae3a 100644 --- a/packages/oneclient_app/src/view/onboarding/test_support.rs +++ b/packages/oneclient_app/src/view/onboarding/test_support.rs @@ -1,7 +1,9 @@ use oneclient_common::domain::{ContentType, GameLoader}; use oneclient_content::packages::types::ExternalFile; use oneclient_core::clusters::{Cluster, ClusterStage}; -use oneclient_core::{Bundle, BundleArchive, BundleFile, BundleFileKind, BundleManifest}; +use oneclient_core::{ + Bundle, BundleArchive, BundleFile, BundleFileKind, BundleFileType, BundleManifest, +}; use std::path::PathBuf; pub fn file(package_id: &str, enabled: bool, hidden: bool) -> BundleFile { @@ -10,6 +12,7 @@ pub fn file(package_id: &str, enabled: bool, hidden: bool) -> BundleFile { hidden, path: format!("mods/{package_id}.jar"), size: 1, + file_type: BundleFileType::Normal, kind: BundleFileKind::External { file: ExternalFile { name: format!("{package_id}.jar"), diff --git a/packages/oneclient_content/src/bundles/install.rs b/packages/oneclient_content/src/bundles/install.rs index 7706ac88..0273ef9f 100644 --- a/packages/oneclient_content/src/bundles/install.rs +++ b/packages/oneclient_content/src/bundles/install.rs @@ -896,6 +896,7 @@ pub async fn delete_artifact(cluster_id: i64, hash: &str, ctx: &ContentCtx) -> C #[cfg(test)] mod tests { use super::*; + use crate::bundles::types::BundleFileType; use oneclient_common::domain::ContentType; use oneclient_db::models::ClusterBundleOverrideRow; @@ -905,6 +906,7 @@ mod tests { hidden: false, path: "mods/example.jar".to_string(), size: 1, + file_type: BundleFileType::Normal, kind: BundleFileKind::External { file: ExternalFile { name: "example.jar".to_string(), diff --git a/packages/oneclient_content/src/bundles/mod.rs b/packages/oneclient_content/src/bundles/mod.rs index da48742a..ca5661ee 100644 --- a/packages/oneclient_content/src/bundles/mod.rs +++ b/packages/oneclient_content/src/bundles/mod.rs @@ -24,9 +24,9 @@ pub use optional::{ PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods, }; pub use types::{ - ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleManifest, - BundleOptionalPackage, BundlePackageAddition, BundlePackageRemoval, BundlePackageUpdate, - BundleUpdateCheckResult, BundleWithUpdateStatus, FileUpdateStatus, + ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleFileType, + BundleManifest, BundleOptionalPackage, BundlePackageAddition, BundlePackageRemoval, + BundlePackageUpdate, BundleUpdateCheckResult, BundleWithUpdateStatus, FileUpdateStatus, }; pub use updates::{ apply_bundle_updates, apply_bundle_updates_with, check_bundle_updates, diff --git a/packages/oneclient_content/src/bundles/optional.rs b/packages/oneclient_content/src/bundles/optional.rs index 9328b8c0..5e2b8e34 100644 --- a/packages/oneclient_content/src/bundles/optional.rs +++ b/packages/oneclient_content/src/bundles/optional.rs @@ -51,6 +51,10 @@ pub async fn pending_optional_mods( continue; }; + if !file.is_optional_offer() { + continue; + } + // The queue row can outlive the offer the user may have opted in from // the package manager since it was written let user_override = overrides diff --git a/packages/oneclient_content/src/bundles/polymrpack.rs b/packages/oneclient_content/src/bundles/polymrpack.rs index 930161d5..f6fb821b 100644 --- a/packages/oneclient_content/src/bundles/polymrpack.rs +++ b/packages/oneclient_content/src/bundles/polymrpack.rs @@ -6,7 +6,8 @@ use serde::Deserialize; use crate::bundles::error::BundleError; use crate::bundles::types::{ - BundleFile, BundleFileKind, BundleManifest, ExternalFileMeta, content_type_from_bundle_path, + BundleFile, BundleFileKind, BundleFileType, BundleManifest, ExternalFileMeta, + content_type_from_bundle_path, }; use crate::error::ContentResult; use crate::packages::types::ExternalFile; @@ -49,6 +50,8 @@ struct PolyMrpackFile { pub enabled: bool, #[serde(default)] pub hidden: bool, + #[serde(rename = "type", default)] + pub file_type: Option, #[serde(default)] pub id: Option, #[serde(default)] @@ -156,6 +159,7 @@ fn parse_bundle_file(file: &PolyMrpackFile) -> Option { hidden: file.hidden, path: file.path.clone(), size: file.file_size, + file_type: file.file_type.unwrap_or_default(), kind: BundleFileKind::Managed { provider: ProviderId::Modrinth, project_id: paths[0].to_string(), @@ -181,6 +185,7 @@ fn parse_bundle_file(file: &PolyMrpackFile) -> Option { hidden: file.hidden, path: file.path.clone(), size: file.file_size, + file_type: file.file_type.unwrap_or_default(), kind: BundleFileKind::External { file: ExternalFile { name: file_name, diff --git a/packages/oneclient_content/src/bundles/types.rs b/packages/oneclient_content/src/bundles/types.rs index 0c77ed82..e0158249 100644 --- a/packages/oneclient_content/src/bundles/types.rs +++ b/packages/oneclient_content/src/bundles/types.rs @@ -30,10 +30,25 @@ pub struct BundleFile { pub path: String, #[serde(default)] pub size: u64, + #[serde(default)] + pub file_type: BundleFileType, pub kind: BundleFileKind, } +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum BundleFileType { + Advanced, + #[default] + #[serde(other)] + Normal, +} + impl BundleFile { + pub fn is_optional_offer(&self) -> bool { + !self.enabled && !self.hidden && self.file_type == BundleFileType::Normal + } + pub fn content_type(&self) -> ContentType { if let BundleFileKind::External { file, .. } = &self.kind { return file.content_type; diff --git a/packages/oneclient_content/src/bundles/updates.rs b/packages/oneclient_content/src/bundles/updates.rs index 52030d4b..dab3a56c 100644 --- a/packages/oneclient_content/src/bundles/updates.rs +++ b/packages/oneclient_content/src/bundles/updates.rs @@ -356,7 +356,7 @@ async fn check_bundle_updates_inner( if !crate::bundles::effective_enabled(file, user_override) { if user_override.is_none() - && !file.hidden + && file.is_optional_offer() && !optional_available.iter().any(|(key, _)| *key == file_key) { optional_available.push(( diff --git a/packages/oneclient_core/src/lib.rs b/packages/oneclient_core/src/lib.rs index bb615f59..749d31c5 100644 --- a/packages/oneclient_core/src/lib.rs +++ b/packages/oneclient_core/src/lib.rs @@ -43,15 +43,15 @@ pub use oneclient_cluster::screenshots::{ }; pub use oneclient_content::bundles::{ ApplyBundleUpdatesResult, Bundle, BundleArchive, BundleError, BundleFile, BundleFileKind, - BundleManifest, BundleOptionalPackage, BundleUpdateCheckResult, BundleWithUpdateStatus, - BundlesManager, FileUpdateStatus, PendingOptionalMod, apply_bundle_updates, - apply_bundle_updates_with, check_bundle_updates, cluster_has_bundle_content, - clusters_sharing_artifact, delete_artifact, effective_enabled, get_bundles_with_update_status, - install_bundle, install_cluster_bundles, install_package_from_bundle, - list_cluster_bundle_overrides, pending_optional_mods, remove_artifact_from_cluster, - resolve_optional_mods, set_artifact_enabled_to, set_bundle_package_enabled, - set_bundle_package_opt_in, set_bundle_package_override, set_bundle_package_overrides, - skip_optional_mods, + BundleFileType, BundleManifest, BundleOptionalPackage, BundleUpdateCheckResult, + BundleWithUpdateStatus, BundlesManager, FileUpdateStatus, PendingOptionalMod, + apply_bundle_updates, apply_bundle_updates_with, check_bundle_updates, + cluster_has_bundle_content, clusters_sharing_artifact, delete_artifact, effective_enabled, + get_bundles_with_update_status, install_bundle, install_cluster_bundles, + install_package_from_bundle, list_cluster_bundle_overrides, pending_optional_mods, + remove_artifact_from_cluster, resolve_optional_mods, set_artifact_enabled_to, + set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override, + set_bundle_package_overrides, skip_optional_mods, }; pub use oneclient_content::packages::LinkedArtifactInfo; pub use oneclient_content::packages::updates::{ diff --git a/packages/oneclient_core/tests/bundle_updates.rs b/packages/oneclient_core/tests/bundle_updates.rs index efb88385..3497ce61 100644 --- a/packages/oneclient_core/tests/bundle_updates.rs +++ b/packages/oneclient_core/tests/bundle_updates.rs @@ -1,6 +1,6 @@ use oneclient_common::domain::{ContentType, GameLoader, ProviderId}; use oneclient_content::bundles::{ - BundleFile, BundleFileKind, BundleManifest, check_bundle_updates, + BundleFile, BundleFileKind, BundleFileType, BundleManifest, check_bundle_updates, }; use oneclient_core::LauncherState; use oneclient_core::clusters::CreateClusterOptions; @@ -19,6 +19,7 @@ fn managed_file(enabled: bool) -> BundleFile { hidden: false, path: "mods/sodium.jar".to_string(), size: 1, + file_type: BundleFileType::Normal, kind: BundleFileKind::Managed { provider: ProviderId::Modrinth, project_id: PROJECT_ID.to_string(), @@ -35,6 +36,7 @@ fn newly_shipped_file() -> BundleFile { hidden: false, path: "mods/newcomer.jar".to_string(), size: 1, + file_type: BundleFileType::Normal, kind: BundleFileKind::Managed { provider: ProviderId::Modrinth, project_id: "newcomer".to_string(),