feat: added sections for advanced mods (#876)
* feat: added sections for advanced mods * fix: treat a null bundle file type as normal * chore: drop ScrollArea::lazy_grid * chore: pass one header height to lazy_sections * chore: name BundleFileType::Normal in test fixtures * feat: don't offer advanced bundle mods as optional mods --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
parent
f94ca969b0
commit
7f1a3176cf
19 changed files with 303 additions and 69 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<LazySection>,
|
||||
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 =
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<String> = HashSet::new();
|
||||
let mut normal_elsewhere: HashSet<String> = 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,
|
||||
|
|
|
|||
|
|
@ -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<bool>,
|
||||
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<PackageEntry>,
|
||||
advanced: Vec<PackageEntry>,
|
||||
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<PackageEntry>,
|
||||
advanced: Vec<PackageEntry>,
|
||||
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)| {
|
||||
|
|
|
|||
|
|
@ -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<OptionalMod> {
|
|||
.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(|| {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]>,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<BundleFileType>,
|
||||
#[serde(default)]
|
||||
pub id: Option<String>,
|
||||
#[serde(default)]
|
||||
|
|
@ -156,6 +159,7 @@ fn parse_bundle_file(file: &PolyMrpackFile) -> Option<BundleFile> {
|
|||
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<BundleFile> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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((
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue