fix opt-in bundles not being asked for new mc versions (#939)
This commit is contained in:
parent
7683651ad4
commit
1ab294f293
33 changed files with 795 additions and 98 deletions
5
.changeset/opt_in_bundles_are_asked_on_new_versions.md
Normal file
5
.changeset/opt_in_bundles_are_asked_on_new_versions.md
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
default: patch
|
||||||
|
---
|
||||||
|
|
||||||
|
# Optional bundles are now offered before the first launch of every new version instead of being installed or skipped silently
|
||||||
12
.sqlx/query-001a994c740f7a015152319eb16fc113954af25ebd2a44d55e95833adda53268.json
generated
Normal file
12
.sqlx/query-001a994c740f7a015152319eb16fc113954af25ebd2a44d55e95833adda53268.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT OR IGNORE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)\n SELECT ?, bundle_name, accepted\n FROM cluster_bundle_choices\n WHERE cluster_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 2
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "001a994c740f7a015152319eb16fc113954af25ebd2a44d55e95833adda53268"
|
||||||
|
}
|
||||||
38
.sqlx/query-171c6771d06656c2fe375f6a0da889f828f400e950f103f0ed44f5f98a48dc50.json
generated
Normal file
38
.sqlx/query-171c6771d06656c2fe375f6a0da889f828f400e950f103f0ed44f5f98a48dc50.json
generated
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n SELECT bundle_name, accepted\n FROM cluster_bundle_choices\n WHERE cluster_id = ?\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "bundle_name",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_bundle_choices",
|
||||||
|
"name": "bundle_name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "accepted",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Integer",
|
||||||
|
"origin": {
|
||||||
|
"Table": {
|
||||||
|
"table": "cluster_bundle_choices",
|
||||||
|
"name": "accepted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "171c6771d06656c2fe375f6a0da889f828f400e950f103f0ed44f5f98a48dc50"
|
||||||
|
}
|
||||||
12
.sqlx/query-c06af5530a824d31e6e18a3a52cd57639b74e7e4d2ae30e29a6c49fda9521b4d.json
generated
Normal file
12
.sqlx/query-c06af5530a824d31e6e18a3a52cd57639b74e7e4d2ae30e29a6c49fda9521b4d.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT OR REPLACE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)\n VALUES (?, ?, ?)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "c06af5530a824d31e6e18a3a52cd57639b74e7e4d2ae30e29a6c49fda9521b4d"
|
||||||
|
}
|
||||||
140
packages/oneclient_app/src/components/bundle_choices_popup.rs
Normal file
140
packages/oneclient_app/src/components/bundle_choices_popup.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use freya::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Button, Icon, IconType, OverlayPopup, toggle_controlled};
|
||||||
|
use crate::hooks::{use_dispatch, use_notifications_snapshot};
|
||||||
|
use crate::theme::colors;
|
||||||
|
use crate::ui::border_all_color;
|
||||||
|
|
||||||
|
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||||
|
const DIALOG_W: f32 = 460.;
|
||||||
|
const LIST_MAX_H: f32 = 306.;
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub struct BundleChoicesPopup;
|
||||||
|
|
||||||
|
impl Component for BundleChoicesPopup {
|
||||||
|
fn render(&self) -> impl IntoElement {
|
||||||
|
let snapshot = use_notifications_snapshot();
|
||||||
|
let dispatch = use_dispatch();
|
||||||
|
let mut chosen = use_state(HashSet::<String>::new);
|
||||||
|
|
||||||
|
let Some(choices) = snapshot.bundle_choices.clone() else {
|
||||||
|
return rect().into_element();
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut list = rect().vertical().width(Size::fill()).spacing(6.);
|
||||||
|
for (name, title) in &choices.bundles {
|
||||||
|
let key = name.clone();
|
||||||
|
let on_toggle: EventHandler<()> = (move |()| {
|
||||||
|
let mut next = chosen.read().clone();
|
||||||
|
if !next.remove(&key) {
|
||||||
|
next.insert(key.clone());
|
||||||
|
}
|
||||||
|
chosen.set(next);
|
||||||
|
})
|
||||||
|
.into();
|
||||||
|
list = list.child(
|
||||||
|
rect()
|
||||||
|
.key(name.clone())
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.content(Content::Flex)
|
||||||
|
.cross_align(Alignment::Center)
|
||||||
|
.padding(Gaps::new_symmetric(8., 10.))
|
||||||
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
|
.background(colors::component_bg())
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(title.clone())
|
||||||
|
.font_size(13.)
|
||||||
|
.max_lines(1)
|
||||||
|
.width(Size::flex(1.0))
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(toggle_controlled(chosen.read().contains(name), on_toggle)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let later = dispatch.clone();
|
||||||
|
let close = dispatch.clone();
|
||||||
|
|
||||||
|
OverlayPopup::new()
|
||||||
|
.on_close(move |_| {
|
||||||
|
chosen.set(HashSet::new());
|
||||||
|
close.close_bundle_choices(None);
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.width(Size::window_percent(100.))
|
||||||
|
.height(Size::window_percent(100.))
|
||||||
|
.center()
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.vertical()
|
||||||
|
.width(Size::px(DIALOG_W))
|
||||||
|
.height(Size::Inner)
|
||||||
|
.max_width(Size::window_percent(95.))
|
||||||
|
.max_height(Size::window_percent(85.))
|
||||||
|
.padding(Gaps::new_all(22.))
|
||||||
|
.spacing(14.)
|
||||||
|
.corner_radius(CornerRadius::new_all(16.))
|
||||||
|
.background(CARD_BG)
|
||||||
|
.border(border_all_color(1., colors::component_border()))
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text("Choose your bundles")
|
||||||
|
.font_size(17.)
|
||||||
|
.font_weight(FontWeight::SEMI_BOLD)
|
||||||
|
.color(colors::fg_primary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
label()
|
||||||
|
.text(format!(
|
||||||
|
"{} has optional bundles you have not chosen yet. Turn on the ones you want.",
|
||||||
|
choices.cluster_name
|
||||||
|
))
|
||||||
|
.font_size(12.5)
|
||||||
|
.max_lines(4)
|
||||||
|
.color(colors::fg_secondary()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ScrollView::new()
|
||||||
|
.width(Size::fill())
|
||||||
|
.height(Size::Inner)
|
||||||
|
.max_height(Size::px(LIST_MAX_H))
|
||||||
|
.child(list),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
rect()
|
||||||
|
.horizontal()
|
||||||
|
.width(Size::fill())
|
||||||
|
.main_align(Alignment::End)
|
||||||
|
.spacing(8.)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.ghost()
|
||||||
|
.on_press(move |_| {
|
||||||
|
chosen.set(HashSet::new());
|
||||||
|
later.close_bundle_choices(None);
|
||||||
|
})
|
||||||
|
.text("Ask me later"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new()
|
||||||
|
.primary()
|
||||||
|
.on_press(move |_| {
|
||||||
|
let picked = chosen.read().clone();
|
||||||
|
chosen.set(HashSet::new());
|
||||||
|
dispatch.close_bundle_choices(Some(picked));
|
||||||
|
})
|
||||||
|
.child(Icon::new(IconType::Check).size(15.))
|
||||||
|
.text("Confirm & launch"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -125,23 +125,21 @@ impl Component for WarningBody {
|
||||||
.corner_radius(CornerRadius::new_all(8.))
|
.corner_radius(CornerRadius::new_all(8.))
|
||||||
.background(colors::component_bg())
|
.background(colors::component_bg())
|
||||||
.border(border_all_color(1., colors::component_border()))
|
.border(border_all_color(1., colors::component_border()))
|
||||||
.child(
|
.child(Markdown::new(self.body.clone()).width(Size::fill()).style(
|
||||||
Markdown::new(self.body.clone())
|
MarkdownStyle {
|
||||||
.width(Size::fill())
|
color: Color::WHITE,
|
||||||
.style(MarkdownStyle {
|
color_link: colors::code_info(),
|
||||||
color: Color::WHITE,
|
color_code: Color::WHITE,
|
||||||
color_link: colors::code_info(),
|
background_code: CARD_BG,
|
||||||
color_code: Color::WHITE,
|
background_blockquote: CARD_BG,
|
||||||
background_code: CARD_BG,
|
border_blockquote: colors::component_border(),
|
||||||
background_blockquote: CARD_BG,
|
background_divider: colors::component_border(),
|
||||||
border_blockquote: colors::component_border(),
|
headings: [16., 14., 13., 12., 12., 12.],
|
||||||
background_divider: colors::component_border(),
|
paragraph_size: 12.,
|
||||||
headings: [16., 14., 13., 12., 12., 12.],
|
code_font_size: 11.,
|
||||||
paragraph_size: 12.,
|
..MarkdownStyle::default()
|
||||||
code_font_size: 11.,
|
},
|
||||||
..MarkdownStyle::default()
|
)),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ mod active_cluster_panel;
|
||||||
mod asset_image;
|
mod asset_image;
|
||||||
mod avatar;
|
mod avatar;
|
||||||
mod blocked_downloads_popup;
|
mod blocked_downloads_popup;
|
||||||
|
mod bundle_choices_popup;
|
||||||
mod button;
|
mod button;
|
||||||
mod charts;
|
mod charts;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
|
|
@ -63,6 +64,7 @@ pub use active_cluster_panel::ActiveClusterPanel;
|
||||||
pub use asset_image::AssetImage;
|
pub use asset_image::AssetImage;
|
||||||
pub use avatar::Avatar;
|
pub use avatar::Avatar;
|
||||||
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
pub use blocked_downloads_popup::BlockedDownloadsPopup;
|
||||||
|
pub use bundle_choices_popup::BundleChoicesPopup;
|
||||||
pub use button::{Button, ButtonSize, ButtonVariant, copy_button, link_button, open_folder_button};
|
pub use button::{Button, ButtonSize, ButtonVariant, copy_button, link_button, open_folder_button};
|
||||||
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
|
pub use charts::{BarChart, PieChart, ValueUnit, slice_color};
|
||||||
pub use checkbox::checkbox_labeled;
|
pub use checkbox::checkbox_labeled;
|
||||||
|
|
|
||||||
|
|
@ -85,14 +85,16 @@ fn navbar_left(show_logo: bool) -> impl IntoElement {
|
||||||
Size::auto()
|
Size::auto()
|
||||||
})
|
})
|
||||||
.cross_align(Alignment::Center)
|
.cross_align(Alignment::Center)
|
||||||
.spacing(NAV_LINK_SPACING_PX / 2.)
|
.spacing(NAV_LINK_SPACING_PX / 2.)
|
||||||
.maybe(!show_logo, |el| {
|
.maybe(!show_logo, |el| {
|
||||||
el.padding(Gaps::new(0., NAV_LINK_SPACING_PX, 0., 0.))
|
el.padding(Gaps::new(0., NAV_LINK_SPACING_PX, 0., 0.))
|
||||||
})
|
})
|
||||||
.child(Icon::new(IconType::IconLogo)
|
.child(
|
||||||
|
Icon::new(IconType::IconLogo)
|
||||||
.size(COMPACT_LOGO_PX)
|
.size(COMPACT_LOGO_PX)
|
||||||
.into_element())
|
.into_element(),
|
||||||
.maybe(show_logo, |rect| rect.child(NavbarLogo.into_element()))
|
)
|
||||||
|
.maybe(show_logo, |rect| rect.child(NavbarLogo.into_element()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -134,7 +136,7 @@ fn navbar_center(is_small: bool) -> impl IntoElement {
|
||||||
target: NavTarget::Route(Route::Clusters {}),
|
target: NavTarget::Route(Route::Clusters {}),
|
||||||
nav_label: "Versions",
|
nav_label: "Versions",
|
||||||
})
|
})
|
||||||
.child(NavLink {
|
.child(NavLink {
|
||||||
active: matches!(
|
active: matches!(
|
||||||
route,
|
route,
|
||||||
Route::Browser {
|
Route::Browser {
|
||||||
|
|
@ -203,7 +205,7 @@ impl Component for NavLink {
|
||||||
} else if hovering() || focused().is_focused() {
|
} else if hovering() || focused().is_focused() {
|
||||||
theme::colors::ghost_overlay_hover()
|
theme::colors::ghost_overlay_hover()
|
||||||
} else {
|
} else {
|
||||||
Color::TRANSPARENT
|
Color::TRANSPARENT
|
||||||
};
|
};
|
||||||
|
|
||||||
rect()
|
rect()
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,8 @@ use oneclient_core::SeenStatus;
|
||||||
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
||||||
use crate::essential::EssentialPackage;
|
use crate::essential::EssentialPackage;
|
||||||
use crate::hooks::{
|
use crate::hooks::{
|
||||||
ClusterAction, EssentialGuardKind, PendingEssential, disable_warnings,
|
ClusterAction, EssentialGuardKind, PendingEssential, disable_warnings, loaded_image,
|
||||||
loaded_image, use_cached_image, use_cluster_mutation, use_disable_warnings,
|
use_cached_image, use_cluster_mutation, use_disable_warnings, use_essential_guard,
|
||||||
use_essential_guard,
|
|
||||||
};
|
};
|
||||||
use crate::routes::Route;
|
use crate::routes::Route;
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
|
|
@ -198,7 +197,14 @@ impl Component for PackageRow {
|
||||||
|
|
||||||
let (card, radius) = match layout {
|
let (card, radius) = match layout {
|
||||||
CardLayout::List => (
|
CardLayout::List => (
|
||||||
list_card(&item, package_type, cluster_id, icon, on_toggle, on_context.clone()),
|
list_card(
|
||||||
|
&item,
|
||||||
|
package_type,
|
||||||
|
cluster_id,
|
||||||
|
icon,
|
||||||
|
on_toggle,
|
||||||
|
on_context.clone(),
|
||||||
|
),
|
||||||
8.,
|
8.,
|
||||||
),
|
),
|
||||||
CardLayout::Grid => (
|
CardLayout::Grid => (
|
||||||
|
|
@ -291,7 +297,9 @@ pub(crate) fn disable_warning_body(
|
||||||
|
|
||||||
match warnings {
|
match warnings {
|
||||||
Some(warnings) if bundled => warnings.body_for(&item.package_id).map(str::to_string),
|
Some(warnings) if bundled => warnings.body_for(&item.package_id).map(str::to_string),
|
||||||
_ => item.essential.map(|package| package.disable_body.to_string()),
|
_ => item
|
||||||
|
.essential
|
||||||
|
.map(|package| package.disable_body.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,16 +413,12 @@ pub(crate) fn grid_card(
|
||||||
let hovering = *hovered.read();
|
let hovering = *hovered.read();
|
||||||
|
|
||||||
let bg = match (enabled, hovering) {
|
let bg = match (enabled, hovering) {
|
||||||
(_, true) => colors::component_bg_hover(),
|
(_, true) => colors::component_bg_hover(),
|
||||||
(true, false) => colors::component_bg(),
|
(true, false) => colors::component_bg(),
|
||||||
(false, false) => colors::component_bg_disabled(),
|
(false, false) => colors::component_bg_disabled(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let alpha = if enabled {
|
let alpha = if enabled { 255u8 } else { 115u8 };
|
||||||
255u8
|
|
||||||
} else {
|
|
||||||
115u8
|
|
||||||
};
|
|
||||||
|
|
||||||
let border = if !hovering {
|
let border = if !hovering {
|
||||||
colors::component_border()
|
colors::component_border()
|
||||||
|
|
@ -428,11 +432,7 @@ pub(crate) fn grid_card(
|
||||||
// .cross_align(Alignment::Center)
|
// .cross_align(Alignment::Center)
|
||||||
.spacing(11.)
|
.spacing(11.)
|
||||||
.content(Content::Flex)
|
.content(Content::Flex)
|
||||||
.child(
|
.child(rect().opacity(alpha as f32 / 255.).child(icon))
|
||||||
rect()
|
|
||||||
.opacity(alpha as f32 / 255.)
|
|
||||||
.child(icon)
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
rect()
|
rect()
|
||||||
.vertical()
|
.vertical()
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,7 @@ impl Component for ReleaseMigrationPopup {
|
||||||
|
|
||||||
let other_popup = snapshot.cluster_update.is_some()
|
let other_popup = snapshot.cluster_update.is_some()
|
||||||
|| snapshot.optional_mods.is_some()
|
|| snapshot.optional_mods.is_some()
|
||||||
|
|| snapshot.bundle_choices.is_some()
|
||||||
|| snapshot.package_updates.is_some()
|
|| snapshot.package_updates.is_some()
|
||||||
|| snapshot.pending_prompt.is_some();
|
|| snapshot.pending_prompt.is_some();
|
||||||
if blocked_by_route(&route) || other_popup {
|
if blocked_by_route(&route) || other_popup {
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ use tokio::sync::mpsc;
|
||||||
use crate::components::IconType;
|
use crate::components::IconType;
|
||||||
use crate::launcher::off_ui;
|
use crate::launcher::off_ui;
|
||||||
use crate::notifications::{
|
use crate::notifications::{
|
||||||
ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup,
|
BundleChoices, ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef,
|
||||||
OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
|
OptionalModsGroup, OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
|
||||||
};
|
};
|
||||||
use crate::state::{AppChannel, AppState, AsyncStatus, FlaggedInstallPrompt, RelocationState};
|
use crate::state::{AppChannel, AppState, AsyncStatus, FlaggedInstallPrompt, RelocationState};
|
||||||
use crate::{invalidate_java_queries, launcher};
|
use crate::{invalidate_java_queries, launcher};
|
||||||
|
|
@ -756,6 +756,10 @@ impl Actions {
|
||||||
self.with_engine(move |state| state.notifications.open_optional_mods(groups, done));
|
self.with_engine(move |state| state.notifications.open_optional_mods(groups, done));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn close_bundle_choices(&self, chosen: Option<std::collections::HashSet<String>>) {
|
||||||
|
self.with_engine(move |state| state.notifications.finish_bundle_choices(chosen));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn close_optional_mods(&self, outcome: OptionalModsOutcome) {
|
pub fn close_optional_mods(&self, outcome: OptionalModsOutcome) {
|
||||||
self.with_engine(move |state| state.notifications.finish_optional_mods(outcome));
|
self.with_engine(move |state| state.notifications.finish_optional_mods(outcome));
|
||||||
}
|
}
|
||||||
|
|
@ -1796,6 +1800,73 @@ impl Actions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn resolve_bundle_choices_before_launch(
|
||||||
|
&self,
|
||||||
|
state: &Arc<oneclient_core::LauncherState>,
|
||||||
|
cluster_id: ClusterId,
|
||||||
|
) {
|
||||||
|
let bundles = match off_ui({
|
||||||
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
oneclient_content::bundles::pending_bundle_choices(
|
||||||
|
cluster_id,
|
||||||
|
state.bundles.as_ref(),
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(bundles) if !bundles.is_empty() => bundles,
|
||||||
|
Ok(_) => return,
|
||||||
|
Err(err) => {
|
||||||
|
tracing::warn!(cluster_id, error = %err, "could not read the bundles to ask about, launching anyway");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let choices_for: Vec<String> = bundles.iter().map(|a| a.manifest.name.clone()).collect();
|
||||||
|
let choices = BundleChoices {
|
||||||
|
cluster_name: crate::install::cluster_display_name(cluster_id, &state.services).await,
|
||||||
|
bundles: bundles
|
||||||
|
.iter()
|
||||||
|
.map(|a| {
|
||||||
|
(
|
||||||
|
a.manifest.name.clone(),
|
||||||
|
crate::utils::bundle_display_name(a),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let (done, wait) = tokio::sync::oneshot::channel();
|
||||||
|
self.with_engine(move |state| {
|
||||||
|
state.notifications.open_bundle_choices(choices, done);
|
||||||
|
state.center_open = false;
|
||||||
|
});
|
||||||
|
let Ok(Some(chosen)) = wait.await else { return };
|
||||||
|
|
||||||
|
let choices: Vec<_> = choices_for
|
||||||
|
.iter()
|
||||||
|
.map(|name| (name.clone(), chosen.contains(name)))
|
||||||
|
.collect();
|
||||||
|
if let Err(err) = off_ui({
|
||||||
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
oneclient_content::bundles::set_bundle_choices(
|
||||||
|
cluster_id,
|
||||||
|
&choices,
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(cluster_id, error = %err, "could not record the bundle choices");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_optional_mods_before_launch(
|
async fn resolve_optional_mods_before_launch(
|
||||||
&self,
|
&self,
|
||||||
state: &Arc<oneclient_core::LauncherState>,
|
state: &Arc<oneclient_core::LauncherState>,
|
||||||
|
|
@ -2002,6 +2073,9 @@ async fn launch(actions: &Actions, cluster_id: ClusterId) {
|
||||||
|
|
||||||
// Before the game process never after Minecraft reads its mods once at
|
// Before the game process never after Minecraft reads its mods once at
|
||||||
// startup
|
// startup
|
||||||
|
actions
|
||||||
|
.resolve_bundle_choices_before_launch(&state, cluster_id)
|
||||||
|
.await;
|
||||||
actions
|
actions
|
||||||
.resolve_bundle_updates_before_launch(&state, cluster_id)
|
.resolve_bundle_updates_before_launch(&state, cluster_id)
|
||||||
.await;
|
.await;
|
||||||
|
|
|
||||||
|
|
@ -684,6 +684,22 @@ impl Actions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(source) = prompt.source() {
|
||||||
|
let (source_id, target_id) = (source.id, prompt.target.id);
|
||||||
|
spawn_forever(async move {
|
||||||
|
let Ok(state) = launcher::state() else { return };
|
||||||
|
if let Err(err) = oneclient_content::bundles::inherit_bundle_choices(
|
||||||
|
source_id,
|
||||||
|
target_id,
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(error = %err, "could not carry the bundle choices over to the target");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if packages.is_empty() {
|
if packages.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -353,6 +353,21 @@ impl MutationCapability for ClusterMutation {
|
||||||
{
|
{
|
||||||
tracing::warn!(cluster_id = cluster.id, error = %err, "failed to record the bundle choices for the new instance");
|
tracing::warn!(cluster_id = cluster.id, error = %err, "failed to record the bundle choices for the new instance");
|
||||||
}
|
}
|
||||||
|
let choices: Vec<_> = archives
|
||||||
|
.iter()
|
||||||
|
.filter(|archive| !archive.manifest.enabled)
|
||||||
|
.map(|archive| {
|
||||||
|
let name = &archive.manifest.name;
|
||||||
|
(name.clone(), selected.contains(name))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if let Err(err) = oneclient_content::bundles::set_bundle_choices(
|
||||||
|
cluster.id, &choices, content,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!(cluster_id = cluster.id, error = %err, "failed to record the bundle choices for the new instance");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = oneclient_events::GroupedProgressSession::start(
|
let session = oneclient_events::GroupedProgressSession::start(
|
||||||
|
|
|
||||||
|
|
@ -112,10 +112,7 @@ impl Component for ClusterShell {
|
||||||
let game = use_game_snapshot();
|
let game = use_game_snapshot();
|
||||||
let launcher = use_launcher();
|
let launcher = use_launcher();
|
||||||
let cluster = use_cluster(cluster_id);
|
let cluster = use_cluster(cluster_id);
|
||||||
let syncing = launch_syncing(
|
let syncing = launch_syncing(&launcher, cluster.as_ref().is_none_or(|c| c.uses_bundles()));
|
||||||
&launcher,
|
|
||||||
cluster.as_ref().is_none_or(|c| c.uses_bundles()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let show_game_log = game.is_active(cluster_id);
|
let show_game_log = game.is_active(cluster_id);
|
||||||
let show_datapacks = cluster
|
let show_datapacks = cluster
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ use freya::prelude::*;
|
||||||
use freya::router::*;
|
use freya::router::*;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
AccountSwitcher, BlockedDownloadsPopup, ClusterUpdatePopup, ControlCenter, FlaggedInstallPopup,
|
AccountSwitcher, BlockedDownloadsPopup, BundleChoicesPopup, ClusterUpdatePopup, ControlCenter,
|
||||||
GenericPromptOverlay, JavaPromptOverlay, MicrosoftJavaPromptOverlay, ModpackConfirmPopup,
|
FlaggedInstallPopup, GenericPromptOverlay, JavaPromptOverlay, MicrosoftJavaPromptOverlay,
|
||||||
NotificationCenter, OptionalModsPopup, PackageUpdatePopup, ReleaseMigrationPopup,
|
ModpackConfirmPopup, NotificationCenter, OptionalModsPopup, PackageUpdatePopup,
|
||||||
SplashCurtain, StatusBar, Toasts, TooltipHost, UpdatePromptOverlay, use_provide_tooltips,
|
ReleaseMigrationPopup, SplashCurtain, StatusBar, Toasts, TooltipHost, UpdatePromptOverlay,
|
||||||
|
use_provide_tooltips,
|
||||||
};
|
};
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
use crate::hooks::use_start_maximized;
|
use crate::hooks::use_start_maximized;
|
||||||
|
|
@ -52,6 +53,7 @@ impl Component for RootLayout {
|
||||||
.child(GenericPromptOverlay)
|
.child(GenericPromptOverlay)
|
||||||
.child(ClusterUpdatePopup)
|
.child(ClusterUpdatePopup)
|
||||||
.child(OptionalModsPopup)
|
.child(OptionalModsPopup)
|
||||||
|
.child(BundleChoicesPopup)
|
||||||
.child(BlockedDownloadsPopup)
|
.child(BlockedDownloadsPopup)
|
||||||
.child(ModpackConfirmPopup)
|
.child(ModpackConfirmPopup)
|
||||||
.child(PackageUpdatePopup)
|
.child(PackageUpdatePopup)
|
||||||
|
|
|
||||||
|
|
@ -243,8 +243,8 @@ fn main() {
|
||||||
.and_then(|v| v.parse::<usize>().ok())
|
.and_then(|v| v.parse::<usize>().ok())
|
||||||
.unwrap_or(96 * 1024 * 1024),
|
.unwrap_or(96 * 1024 * 1024),
|
||||||
)
|
)
|
||||||
.with_plugin(freya::borderless::BorderlessPlugin::new())
|
.with_plugin(freya::borderless::BorderlessPlugin::new())
|
||||||
.with_plugin(freya::metrics::MetricsPlugin::default())
|
.with_plugin(freya::metrics::MetricsPlugin::default())
|
||||||
.with_default_font(theme::DEFAULT_FONT);
|
.with_default_font(theme::DEFAULT_FONT);
|
||||||
|
|
||||||
if show_tray_icon && platform::tray::available() {
|
if show_tray_icon && platform::tray::available() {
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,12 @@ impl BlockedDownloads {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BundleChoices {
|
||||||
|
pub cluster_name: String,
|
||||||
|
pub bundles: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct OptionalModsGroup {
|
pub struct OptionalModsGroup {
|
||||||
pub cluster_id: ClusterId,
|
pub cluster_id: ClusterId,
|
||||||
|
|
@ -251,6 +257,7 @@ pub struct NotificationSnapshot {
|
||||||
pub pending_prompt: Option<PendingPromptView>,
|
pub pending_prompt: Option<PendingPromptView>,
|
||||||
pub cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
pub cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
||||||
pub optional_mods: Option<Vec<OptionalModsGroup>>,
|
pub optional_mods: Option<Vec<OptionalModsGroup>>,
|
||||||
|
pub bundle_choices: Option<BundleChoices>,
|
||||||
pub package_updates: Option<Vec<PackageUpdateGroup>>,
|
pub package_updates: Option<Vec<PackageUpdateGroup>>,
|
||||||
pub blocked_downloads: Option<BlockedDownloads>,
|
pub blocked_downloads: Option<BlockedDownloads>,
|
||||||
pub open_cluster: Option<ClusterId>,
|
pub open_cluster: Option<ClusterId>,
|
||||||
|
|
@ -274,6 +281,7 @@ pub struct NotificationState {
|
||||||
pending_timers: Vec<ToastDismissTimer>,
|
pending_timers: Vec<ToastDismissTimer>,
|
||||||
cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
||||||
optional_mods: Option<Vec<OptionalModsGroup>>,
|
optional_mods: Option<Vec<OptionalModsGroup>>,
|
||||||
|
bundle_choices: Option<BundleChoices>,
|
||||||
package_updates: Option<Vec<PackageUpdateGroup>>,
|
package_updates: Option<Vec<PackageUpdateGroup>>,
|
||||||
blocked_downloads: Option<BlockedDownloads>,
|
blocked_downloads: Option<BlockedDownloads>,
|
||||||
open_cluster: Option<ClusterId>,
|
open_cluster: Option<ClusterId>,
|
||||||
|
|
@ -283,6 +291,7 @@ pub struct NotificationState {
|
||||||
/// because every way the modal can end goes through this state
|
/// because every way the modal can end goes through this state
|
||||||
package_updates_done: Option<oneshot::Sender<Vec<BrowserPackageUpdate>>>,
|
package_updates_done: Option<oneshot::Sender<Vec<BrowserPackageUpdate>>>,
|
||||||
optional_mods_done: Option<oneshot::Sender<OptionalModsOutcome>>,
|
optional_mods_done: Option<oneshot::Sender<OptionalModsOutcome>>,
|
||||||
|
bundle_choices_done: Option<oneshot::Sender<Option<HashSet<String>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CATEGORY_ORDER: [TaskCategory; 7] = [
|
const CATEGORY_ORDER: [TaskCategory; 7] = [
|
||||||
|
|
@ -390,6 +399,7 @@ impl NotificationState {
|
||||||
pending_prompt,
|
pending_prompt,
|
||||||
cluster_update: self.cluster_update.clone(),
|
cluster_update: self.cluster_update.clone(),
|
||||||
optional_mods: self.optional_mods.clone(),
|
optional_mods: self.optional_mods.clone(),
|
||||||
|
bundle_choices: self.bundle_choices.clone(),
|
||||||
package_updates: self.package_updates.clone(),
|
package_updates: self.package_updates.clone(),
|
||||||
blocked_downloads: self.blocked_downloads.clone(),
|
blocked_downloads: self.blocked_downloads.clone(),
|
||||||
open_cluster: self.open_cluster,
|
open_cluster: self.open_cluster,
|
||||||
|
|
@ -470,6 +480,23 @@ impl NotificationState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_bundle_choices(
|
||||||
|
&mut self,
|
||||||
|
choices: BundleChoices,
|
||||||
|
done: oneshot::Sender<Option<HashSet<String>>>,
|
||||||
|
) {
|
||||||
|
self.finish_bundle_choices(None);
|
||||||
|
self.bundle_choices = Some(choices);
|
||||||
|
self.bundle_choices_done = Some(done);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish_bundle_choices(&mut self, chosen: Option<HashSet<String>>) {
|
||||||
|
self.bundle_choices = None;
|
||||||
|
if let Some(done) = self.bundle_choices_done.take() {
|
||||||
|
let _ = done.send(chosen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hide_optional_mods(&mut self) {
|
pub fn hide_optional_mods(&mut self) {
|
||||||
self.optional_mods = None;
|
self.optional_mods = None;
|
||||||
}
|
}
|
||||||
|
|
@ -1272,7 +1299,10 @@ mod package_update_tests {
|
||||||
let id = state.push_custom(&mut inbox, spec("kept"));
|
let id = state.push_custom(&mut inbox, spec("kept"));
|
||||||
state.expire_toast(&mut inbox, id);
|
state.expire_toast(&mut inbox, id);
|
||||||
|
|
||||||
assert!(inbox.iter().any(|e| e.id == id), "the inbox entry must stay");
|
assert!(
|
||||||
|
inbox.iter().any(|e| e.id == id),
|
||||||
|
"the inbox entry must stay"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,12 @@ pub(crate) fn browsable_type(package_type: &str, cluster: &Cluster) -> String {
|
||||||
if cluster.lacks_mod_loader() {
|
if cluster.lacks_mod_loader() {
|
||||||
let unsupported = LOADER_SLUGS.contains(&package_type)
|
let unsupported = LOADER_SLUGS.contains(&package_type)
|
||||||
|| (package_type == DATAPACK_SLUG && !supports_datapacks(&cluster.mc_version));
|
|| (package_type == DATAPACK_SLUG && !supports_datapacks(&cluster.mc_version));
|
||||||
return if unsupported { TEXTURE_SLUG } else { package_type }.to_string();
|
return if unsupported {
|
||||||
|
TEXTURE_SLUG
|
||||||
|
} else {
|
||||||
|
package_type
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
}
|
}
|
||||||
if package_type == DATAPACK_SLUG && !supports_datapacks(&cluster.mc_version) {
|
if package_type == DATAPACK_SLUG && !supports_datapacks(&cluster.mc_version) {
|
||||||
BROWSE_TYPES[0].0.to_string()
|
BROWSE_TYPES[0].0.to_string()
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ use crate::hooks::{
|
||||||
cluster_content_items, content_type_for_slug, package_meta_batch, project_detail,
|
cluster_content_items, content_type_for_slug, package_meta_batch, project_detail,
|
||||||
use_browser_compat, use_bundle_overrides, use_bundles_with_status, use_cluster_content,
|
use_browser_compat, use_bundle_overrides, use_bundles_with_status, use_cluster_content,
|
||||||
use_dispatch, use_installs_snapshot, use_link_confirm, use_package_meta_batch,
|
use_dispatch, use_installs_snapshot, use_link_confirm, use_package_meta_batch,
|
||||||
use_package_project, use_package_versions_when,
|
use_package_project, use_package_versions_when, version_list, versions_total,
|
||||||
version_list, versions_total,
|
|
||||||
};
|
};
|
||||||
use crate::theme::colors;
|
use crate::theme::colors;
|
||||||
use crate::ui::border_all_color;
|
use crate::ui::border_all_color;
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,9 @@ impl Component for PackageManager {
|
||||||
let shares_content = cluster
|
let shares_content = cluster
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|cluster| cluster.shares_content(content_type));
|
.map(|cluster| cluster.shares_content(content_type));
|
||||||
let uses_bundles = cluster.as_ref().is_none_or(|cluster| cluster.uses_bundles());
|
let uses_bundles = cluster
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|cluster| cluster.uses_bundles());
|
||||||
let active = use_state(|| 0usize);
|
let active = use_state(|| 0usize);
|
||||||
|
|
||||||
let search = use_state(String::new);
|
let search = use_state(String::new);
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,12 @@ pub fn shell(parts: Shell) -> Element {
|
||||||
.into_element()
|
.into_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header(eyebrow: Option<String>, title: String, subtitle: String, on_close: EventHandler<()>) -> Element {
|
fn header(
|
||||||
|
eyebrow: Option<String>,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
on_close: EventHandler<()>,
|
||||||
|
) -> Element {
|
||||||
rect()
|
rect()
|
||||||
.horizontal()
|
.horizontal()
|
||||||
.width(Size::fill())
|
.width(Size::fill())
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ impl Component for AddRow {
|
||||||
.notify("Java runtimes refreshed")
|
.notify("Java runtimes refreshed")
|
||||||
.body("The installed runtime list is up to date")
|
.body("The installed runtime list is up to date")
|
||||||
.info()
|
.info()
|
||||||
.toast_only()
|
.toast_only()
|
||||||
.send();
|
.send();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ struct ClusterPlan {
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
/// `(bundle_name, package_id, override)` for every file whose fate differs from the manifest default
|
/// `(bundle_name, package_id, override)` for every file whose fate differs from the manifest default
|
||||||
overrides: Vec<(String, String, OverrideType)>,
|
overrides: Vec<(String, String, OverrideType)>,
|
||||||
|
choices: Vec<(String, bool)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -173,9 +174,19 @@ fn build_plans(
|
||||||
for archive in &cb.archives {
|
for archive in &cb.archives {
|
||||||
overrides.extend(archive_overrides(cb.cluster.id, archive, selected, &kept));
|
overrides.extend(archive_overrides(cb.cluster.id, archive, selected, &kept));
|
||||||
}
|
}
|
||||||
|
let choices = cb
|
||||||
|
.archives
|
||||||
|
.iter()
|
||||||
|
.filter(|archive| !archive.manifest.enabled)
|
||||||
|
.map(|archive| {
|
||||||
|
let taken = bundle_taken(cb.cluster.id, archive, selected);
|
||||||
|
(archive.manifest.name.clone(), taken)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
ClusterPlan {
|
ClusterPlan {
|
||||||
cluster_id: cb.cluster.id,
|
cluster_id: cb.cluster.id,
|
||||||
overrides,
|
overrides,
|
||||||
|
choices,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -284,7 +295,7 @@ fn finish_setup(
|
||||||
mut failure: State<Option<String>>,
|
mut failure: State<Option<String>>,
|
||||||
) {
|
) {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
for plan in plans.iter().filter(|p| !p.overrides.is_empty()) {
|
for plan in &plans {
|
||||||
if let Err(err) = apply_overrides(plan).await {
|
if let Err(err) = apply_overrides(plan).await {
|
||||||
// Expected failures (a busy or unwritable database) are not crashes warn! is a breadcrumb error! reports to Sentry
|
// Expected failures (a busy or unwritable database) are not crashes warn! is a breadcrumb error! reports to Sentry
|
||||||
if err.is_sentry_excluded() {
|
if err.is_sentry_excluded() {
|
||||||
|
|
@ -378,6 +389,12 @@ async fn apply_overrides(plan: &ClusterPlan) -> oneclient_core::LauncherResult<(
|
||||||
&state.services.content(),
|
&state.services.content(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
oneclient_content::bundles::set_bundle_choices(
|
||||||
|
plan.cluster_id,
|
||||||
|
&plan.choices,
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,93 @@ fn is_base62(s: &str) -> bool {
|
||||||
!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric())
|
!s.is_empty() && s.chars().all(|c| c.is_ascii_alphanumeric())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn takes_bundle(
|
||||||
|
archive: &BundleArchive,
|
||||||
|
accepted: &std::collections::HashSet<String>,
|
||||||
|
) -> bool {
|
||||||
|
archive.manifest.enabled || accepted.contains(&archive.manifest.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_file(file: &BundleFile, user_override: Option<OverrideType>, taken: bool) -> bool {
|
||||||
|
if taken {
|
||||||
|
effective_enabled(file, user_override)
|
||||||
|
} else {
|
||||||
|
user_override == Some(OverrideType::Enabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn accepted_bundles(
|
||||||
|
cluster_id: i64,
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) -> ContentResult<std::collections::HashSet<String>> {
|
||||||
|
Ok(bundle_dao::list_bundle_choices(&ctx.db, cluster_id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(name, accepted)| accepted.then_some(name))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn taken_bundle_names(
|
||||||
|
cluster_id: i64,
|
||||||
|
archives: &[BundleArchive],
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) -> ContentResult<std::collections::HashSet<String>> {
|
||||||
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
|
Ok(archives
|
||||||
|
.iter()
|
||||||
|
.filter(|archive| takes_bundle(archive, &accepted))
|
||||||
|
.map(|archive| archive.manifest.name.clone())
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(bundles, ctx))]
|
||||||
|
pub async fn pending_bundle_choices(
|
||||||
|
cluster_id: i64,
|
||||||
|
bundles: &BundlesManager,
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) -> ContentResult<Vec<BundleArchive>> {
|
||||||
|
let Some(cluster) = bundle_cluster(cluster_id, ctx).await? else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
let loader = GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Fabric);
|
||||||
|
let decided: std::collections::HashSet<String> =
|
||||||
|
bundle_dao::list_bundle_choices(&ctx.db, cluster_id)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, _)| name)
|
||||||
|
.collect();
|
||||||
|
Ok(bundles
|
||||||
|
.archives_for(ctx, &cluster.mc_version, loader)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|archive| {
|
||||||
|
!archive.manifest.enabled
|
||||||
|
&& !archive.bundle.hidden
|
||||||
|
&& !decided.contains(&archive.manifest.name)
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(ctx))]
|
||||||
|
pub async fn set_bundle_choices(
|
||||||
|
cluster_id: i64,
|
||||||
|
choices: &[(String, bool)],
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) -> ContentResult<()> {
|
||||||
|
bundle_dao::save_bundle_choices(&ctx.db, cluster_id, choices).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip(ctx))]
|
||||||
|
pub async fn inherit_bundle_choices(
|
||||||
|
source_cluster_id: i64,
|
||||||
|
target_cluster_id: i64,
|
||||||
|
ctx: &ContentCtx,
|
||||||
|
) -> ContentResult<()> {
|
||||||
|
bundle_dao::copy_bundle_choices(&ctx.db, source_cluster_id, target_cluster_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn effective_enabled(file: &BundleFile, user_override: Option<OverrideType>) -> bool {
|
pub fn effective_enabled(file: &BundleFile, user_override: Option<OverrideType>) -> bool {
|
||||||
match user_override {
|
match user_override {
|
||||||
Some(OverrideType::Removed | OverrideType::Disabled) => false,
|
Some(OverrideType::Removed | OverrideType::Disabled) => false,
|
||||||
|
|
@ -190,7 +277,7 @@ pub async fn install_bundle(
|
||||||
.find(|a| a.manifest.name == bundle_name)
|
.find(|a| a.manifest.name == bundle_name)
|
||||||
.ok_or(BundleError::NotFound(bundle_name.to_string()))?;
|
.ok_or(BundleError::NotFound(bundle_name.to_string()))?;
|
||||||
|
|
||||||
install_enabled_bundle_files(&archive, cluster_id, skip_compatibility, None, ctx).await
|
install_enabled_bundle_files(&archive, cluster_id, true, skip_compatibility, None, ctx).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `suppression` comes from [`find_user_suppression`] so a choice filed under a
|
/// `suppression` comes from [`find_user_suppression`] so a choice filed under a
|
||||||
|
|
@ -393,11 +480,14 @@ pub async fn heal_bundle_activity(
|
||||||
pub async fn install_enabled_bundle_files(
|
pub async fn install_enabled_bundle_files(
|
||||||
archive: &BundleArchive,
|
archive: &BundleArchive,
|
||||||
cluster_id: i64,
|
cluster_id: i64,
|
||||||
|
taken: bool,
|
||||||
skip_compatibility: bool,
|
skip_compatibility: bool,
|
||||||
progress: Option<&GroupedProgressSession>,
|
progress: Option<&GroupedProgressSession>,
|
||||||
ctx: &ContentCtx,
|
ctx: &ContentCtx,
|
||||||
) -> ContentResult<Vec<String>> {
|
) -> ContentResult<Vec<String>> {
|
||||||
extract_bundle_overrides_for_cluster(archive, cluster_id, ctx).await?;
|
if taken {
|
||||||
|
extract_bundle_overrides_for_cluster(archive, cluster_id, ctx).await?;
|
||||||
|
}
|
||||||
heal_bundle_activity(cluster_id, std::slice::from_ref(archive), ctx).await?;
|
heal_bundle_activity(cluster_id, std::slice::from_ref(archive), ctx).await?;
|
||||||
|
|
||||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||||
|
|
@ -415,8 +505,11 @@ pub async fn install_enabled_bundle_files(
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|file| {
|
.filter(|file| {
|
||||||
let package_id = file.kind.package_id();
|
let package_id = file.kind.package_id();
|
||||||
effective_enabled(file, find_override(&overrides, &bundle_name, &package_id))
|
wants_file(
|
||||||
&& !present.contains(file)
|
file,
|
||||||
|
find_override(&overrides, &bundle_name, &package_id),
|
||||||
|
taken,
|
||||||
|
) && !present.contains(file)
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -562,15 +655,20 @@ pub async fn enabled_bundle_bytes(
|
||||||
.archives_for(ctx, &cluster.mc_version, loader)
|
.archives_for(ctx, &cluster.mc_version, loader)
|
||||||
.await?;
|
.await?;
|
||||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||||
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
let present = PresentContent::load(cluster_id, ctx).await?;
|
let present = PresentContent::load(cluster_id, ctx).await?;
|
||||||
|
|
||||||
let mut total = 0u64;
|
let mut total = 0u64;
|
||||||
for archive in &archives {
|
for archive in &archives {
|
||||||
let bundle_name = &archive.manifest.name;
|
let bundle_name = &archive.manifest.name;
|
||||||
|
let taken = takes_bundle(archive, &accepted);
|
||||||
for file in &archive.manifest.files {
|
for file in &archive.manifest.files {
|
||||||
let package_id = file.kind.package_id();
|
let package_id = file.kind.package_id();
|
||||||
if !effective_enabled(file, find_override(&overrides, bundle_name, &package_id))
|
if !wants_file(
|
||||||
|| present.contains(file)
|
file,
|
||||||
|
find_override(&overrides, bundle_name, &package_id),
|
||||||
|
taken,
|
||||||
|
) || present.contains(file)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -693,15 +791,21 @@ pub async fn enabled_bundle_projects(
|
||||||
.archives_for(ctx, &cluster.mc_version, loader)
|
.archives_for(ctx, &cluster.mc_version, loader)
|
||||||
.await?;
|
.await?;
|
||||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||||
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
|
|
||||||
let mut projects = std::collections::HashSet::new();
|
let mut projects = std::collections::HashSet::new();
|
||||||
for archive in &archives {
|
for archive in &archives {
|
||||||
let bundle_name = &archive.manifest.name;
|
let bundle_name = &archive.manifest.name;
|
||||||
|
let taken = takes_bundle(archive, &accepted);
|
||||||
for file in &archive.manifest.files {
|
for file in &archive.manifest.files {
|
||||||
let BundleFileKind::Managed { project_id, .. } = &file.kind else {
|
let BundleFileKind::Managed { project_id, .. } = &file.kind else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if effective_enabled(file, find_override(&overrides, bundle_name, project_id)) {
|
if wants_file(
|
||||||
|
file,
|
||||||
|
find_override(&overrides, bundle_name, project_id),
|
||||||
|
taken,
|
||||||
|
) {
|
||||||
projects.insert(project_id.clone());
|
projects.insert(project_id.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -746,9 +850,11 @@ pub async fn install_cluster_bundles(
|
||||||
bundles = archives.len(),
|
bundles = archives.len(),
|
||||||
"installing enabled bundle content"
|
"installing enabled bundle content"
|
||||||
);
|
);
|
||||||
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
for archive in &archives {
|
for archive in &archives {
|
||||||
|
let taken = takes_bundle(archive, &accepted);
|
||||||
let installed =
|
let installed =
|
||||||
install_enabled_bundle_files(archive, cluster_id, true, progress, ctx).await?;
|
install_enabled_bundle_files(archive, cluster_id, taken, true, progress, ctx).await?;
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
cluster_id,
|
cluster_id,
|
||||||
bundle = %archive.manifest.name,
|
bundle = %archive.manifest.name,
|
||||||
|
|
@ -881,8 +987,7 @@ pub async fn remove_artifact_from_cluster(
|
||||||
// Best-effort folder cleanup failure here is not an error
|
// Best-effort folder cleanup failure here is not an error
|
||||||
let deferred = match (target, link) {
|
let deferred = match (target, link) {
|
||||||
(Some(content_type), Some(link)) => {
|
(Some(content_type), Some(link)) => {
|
||||||
try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db)
|
try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db).await
|
||||||
.await
|
|
||||||
== LiveSync::Deferred
|
== LiveSync::Deferred
|
||||||
}
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@ pub use error::BundleError;
|
||||||
pub use install::{
|
pub use install::{
|
||||||
clusters_sharing_artifact, delete_artifact, effective_enabled, enabled_bundle_bytes,
|
clusters_sharing_artifact, delete_artifact, effective_enabled, enabled_bundle_bytes,
|
||||||
enabled_bundle_projects, extract_bundle_overrides_for_cluster, heal_bundle_activity,
|
enabled_bundle_projects, extract_bundle_overrides_for_cluster, heal_bundle_activity,
|
||||||
install_bundle, install_cluster_bundles, install_enabled_bundle_files,
|
inherit_bundle_choices, install_bundle, install_cluster_bundles, install_enabled_bundle_files,
|
||||||
install_package_from_bundle, list_cluster_bundle_overrides, on_user_disable_artifact,
|
install_package_from_bundle, list_cluster_bundle_overrides, on_user_disable_artifact,
|
||||||
on_user_enable_artifact, on_user_remove_artifact, remove_artifact_from_cluster,
|
on_user_enable_artifact, on_user_remove_artifact, pending_bundle_choices,
|
||||||
set_artifact_enabled_to, set_bundle_package_enabled, set_bundle_package_opt_in,
|
remove_artifact_from_cluster, set_artifact_enabled_to, set_bundle_choices,
|
||||||
set_bundle_package_override, set_bundle_package_overrides,
|
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
||||||
|
set_bundle_package_overrides, taken_bundle_names,
|
||||||
};
|
};
|
||||||
pub use manager::{Bundle, BundlesManager};
|
pub use manager::{Bundle, BundlesManager};
|
||||||
pub use manifest::BundleManifest as RemoteBundleManifest;
|
pub use manifest::BundleManifest as RemoteBundleManifest;
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,10 @@ use tokio::sync::Mutex as AsyncMutex;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
|
||||||
use crate::bundles::install::{
|
use crate::bundles::install::{
|
||||||
BUNDLE_INSTALL_CONCURRENCY, bundle_cluster, disable_was_deliberate, external_ids_by_sha1,
|
BUNDLE_INSTALL_CONCURRENCY, accepted_bundles, bundle_cluster, disable_was_deliberate,
|
||||||
find_override, find_user_suppression, heal_bundle_activity, install_package_from_bundle,
|
external_ids_by_sha1, find_override, find_user_suppression, heal_bundle_activity,
|
||||||
remove_artifact_from_cluster, set_artifact_enabled_to,
|
install_package_from_bundle, remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||||
|
takes_bundle,
|
||||||
};
|
};
|
||||||
use crate::bundles::manager::BundlesManager;
|
use crate::bundles::manager::BundlesManager;
|
||||||
use crate::bundles::overrides;
|
use crate::bundles::overrides;
|
||||||
|
|
@ -322,6 +323,7 @@ async fn check_bundle_updates_inner(
|
||||||
|
|
||||||
let addition_eligible_bundles = addition_eligible_bundles(
|
let addition_eligible_bundles = addition_eligible_bundles(
|
||||||
ctx,
|
ctx,
|
||||||
|
cluster_id,
|
||||||
&archives,
|
&archives,
|
||||||
&bundle_packages,
|
&bundle_packages,
|
||||||
&all_linked,
|
&all_linked,
|
||||||
|
|
@ -621,7 +623,8 @@ pub async fn apply_bundle_updates_with(
|
||||||
let cluster = PackageStore::get_cluster(cluster_id, ctx).await?;
|
let cluster = PackageStore::get_cluster(cluster_id, ctx).await?;
|
||||||
let loader = GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Fabric);
|
let loader = GameLoader::from_repr(cluster.mc_loader as u8).unwrap_or(GameLoader::Fabric);
|
||||||
if let Ok(archives) = bundles.archives_for(ctx, &cluster.mc_version, loader).await {
|
if let Ok(archives) = bundles.archives_for(ctx, &cluster.mc_version, loader).await {
|
||||||
for archive in archives {
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
|
for archive in archives.iter().filter(|a| takes_bundle(a, &accepted)) {
|
||||||
if let Err(err) = overrides::sync_bundle_overrides(
|
if let Err(err) = overrides::sync_bundle_overrides(
|
||||||
&archive.bundle.path,
|
&archive.bundle.path,
|
||||||
&archive.manifest.name,
|
&archive.manifest.name,
|
||||||
|
|
@ -861,6 +864,7 @@ pub async fn get_bundles_with_update_status(
|
||||||
// Same liveness the updater uses so an untracked older install is not hidden from the list while it still takes on new files
|
// Same liveness the updater uses so an untracked older install is not hidden from the list while it still takes on new files
|
||||||
let live_bundles = addition_eligible_bundles(
|
let live_bundles = addition_eligible_bundles(
|
||||||
ctx,
|
ctx,
|
||||||
|
cluster_id,
|
||||||
&archives,
|
&archives,
|
||||||
&bundle_packages,
|
&bundle_packages,
|
||||||
&all_linked,
|
&all_linked,
|
||||||
|
|
@ -1134,6 +1138,7 @@ impl<'a> TypeOptOuts<'a> {
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn addition_eligible_bundles(
|
async fn addition_eligible_bundles(
|
||||||
ctx: &ContentCtx,
|
ctx: &ContentCtx,
|
||||||
|
cluster_id: i64,
|
||||||
archives: &[BundleArchive],
|
archives: &[BundleArchive],
|
||||||
bundle_packages: &[BundleTrackedArtifactRow],
|
bundle_packages: &[BundleTrackedArtifactRow],
|
||||||
all_linked: &[LinkedArtifactInfo],
|
all_linked: &[LinkedArtifactInfo],
|
||||||
|
|
@ -1149,6 +1154,12 @@ async fn addition_eligible_bundles(
|
||||||
overrides_map,
|
overrides_map,
|
||||||
&live_managed_keys,
|
&live_managed_keys,
|
||||||
));
|
));
|
||||||
|
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||||
|
eligible.retain(|name| {
|
||||||
|
!archives
|
||||||
|
.iter()
|
||||||
|
.any(|archive| archive.manifest.name == *name && !takes_bundle(archive, &accepted))
|
||||||
|
});
|
||||||
|
|
||||||
Ok(eligible)
|
Ok(eligible)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -366,13 +366,8 @@ impl PackageStore {
|
||||||
let live = if enabled {
|
let live = if enabled {
|
||||||
link::try_link_materialized(&cluster, &artifact, &file_name).await
|
link::try_link_materialized(&cluster, &artifact, &file_name).await
|
||||||
} else {
|
} else {
|
||||||
link::try_unlink_materialized(
|
link::try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db)
|
||||||
&cluster,
|
.await;
|
||||||
content_type,
|
|
||||||
&link.cluster_file_name,
|
|
||||||
&ctx.db,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
if link.cluster_file_name != file_name {
|
if link.cluster_file_name != file_name {
|
||||||
link::try_unlink_materialized(&cluster, content_type, &file_name, &ctx.db).await;
|
link::try_unlink_materialized(&cluster, content_type, &file_name, &ctx.db).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,12 +40,21 @@ pub async fn apply_bundle_java_override(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut overrides = archives.iter().filter_map(|archive| {
|
let taken = oneclient_content::bundles::taken_bundle_names(
|
||||||
archive
|
cluster_id,
|
||||||
.manifest
|
&archives,
|
||||||
.java_version_override
|
&state.services.content(),
|
||||||
.map(|major| (archive.manifest.name.as_str(), major))
|
)
|
||||||
});
|
.await?;
|
||||||
|
let mut overrides = archives
|
||||||
|
.iter()
|
||||||
|
.filter(|archive| taken.contains(&archive.manifest.name))
|
||||||
|
.filter_map(|archive| {
|
||||||
|
archive
|
||||||
|
.manifest
|
||||||
|
.java_version_override
|
||||||
|
.map(|major| (archive.manifest.name.as_str(), major))
|
||||||
|
});
|
||||||
|
|
||||||
let Some((bundle, major)) = overrides.next() else {
|
let Some((bundle, major)) = overrides.next() else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const UI_FLOOD_TARGETS: &[&str] = &[
|
||||||
"freya_core::runner",
|
"freya_core::runner",
|
||||||
"freya_core::tree",
|
"freya_core::tree",
|
||||||
"freya_winit::renderer",
|
"freya_winit::renderer",
|
||||||
"freya::metrics",
|
"freya::metrics",
|
||||||
"ragnarok::executor",
|
"ragnarok::executor",
|
||||||
"ragnarok::nodes_state",
|
"ragnarok::nodes_state",
|
||||||
"torin::torin",
|
"torin::torin",
|
||||||
|
|
|
||||||
|
|
@ -236,10 +236,9 @@ async fn adopt_cluster(
|
||||||
let profile =
|
let profile =
|
||||||
create_profile_from_global(&state.services.db, &global, folder_name, None, None).await?;
|
create_profile_from_global(&state.services.db, &global, folder_name, None, None).await?;
|
||||||
|
|
||||||
let tags = recorded.as_ref().map_or_else(
|
let tags = recorded
|
||||||
|| "[]".to_string(),
|
.as_ref()
|
||||||
|identity| encode_tags(&identity.tags),
|
.map_or_else(|| "[]".to_string(), |identity| encode_tags(&identity.tags));
|
||||||
);
|
|
||||||
|
|
||||||
let row = cluster_dao::insert(
|
let row = cluster_dao::insert(
|
||||||
&state.services.db,
|
&state.services.db,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use oneclient_common::domain::{ContentType, GameLoader, ProviderId};
|
use oneclient_common::domain::{ContentType, GameLoader, ProviderId};
|
||||||
use oneclient_content::bundles::{
|
use oneclient_content::bundles::{
|
||||||
BundleFile, BundleFileKind, BundleFileType, BundleManifest, check_bundle_updates,
|
BundleFile, BundleFileKind, BundleFileType, BundleManifest, check_bundle_updates,
|
||||||
get_bundles_with_update_status, set_bundle_package_enabled,
|
get_bundles_with_update_status, install_cluster_bundles, set_bundle_package_enabled,
|
||||||
};
|
};
|
||||||
use oneclient_core::LauncherState;
|
use oneclient_core::LauncherState;
|
||||||
use oneclient_core::clusters::CreateClusterOptions;
|
use oneclient_core::clusters::CreateClusterOptions;
|
||||||
|
|
@ -1293,3 +1293,127 @@ async fn the_package_list_does_not_save_the_opt_out() {
|
||||||
.is_empty()
|
.is_empty()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn seed_opt_in_bundle(state: &LauncherState, name: &str) {
|
||||||
|
let mut m = named_manifest(name, vec![managed_file(true)]);
|
||||||
|
m.enabled = false;
|
||||||
|
seed_bundle_with_pack(state, m).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn seed_bundle_with_pack(state: &LauncherState, m: BundleManifest) {
|
||||||
|
let name = &m.name.clone();
|
||||||
|
oneclient_core::dev::seed_bundle_archive(state, m)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let dir = oneclient_common::paths::data_dir().unwrap().join("bundles");
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let mut empty_zip = b"PK\x05\x06".to_vec();
|
||||||
|
empty_zip.extend([0u8; 18]);
|
||||||
|
std::fs::write(dir.join(format!("{name}.mrpack")), empty_zip).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn taking_a_bundle_at_the_prompt_records_the_choice() {
|
||||||
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||||
|
seed_opt_in_bundle(&state, "Opt In").await;
|
||||||
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||||
|
let ctx = state.services.content();
|
||||||
|
let pending = || async {
|
||||||
|
oneclient_content::bundles::pending_bundle_choices(cluster_id, state.bundles.as_ref(), &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|archive| archive.manifest.name)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(pending().await, ["Opt In".to_string()]);
|
||||||
|
|
||||||
|
oneclient_content::bundles::set_bundle_choices(
|
||||||
|
cluster_id,
|
||||||
|
&[("Opt In".to_string(), false)],
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(pending().await.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_opt_in_bundle_switched_off_by_accident_is_repaired_at_install() {
|
||||||
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||||
|
seed_opt_in_bundle(&state, BUNDLE).await;
|
||||||
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||||
|
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
install_cluster_bundles(
|
||||||
|
cluster_id,
|
||||||
|
state.bundles.as_ref(),
|
||||||
|
None,
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let tracked = bundle_dao::list_bundle_tracked(&state.services.db, cluster_id)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
tracked.iter().all(|row| row.enabled != 0),
|
||||||
|
"nothing recorded the disable so the install should undo it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn an_opt_in_bundle_counts_as_taken_only_on_a_recorded_choice() {
|
||||||
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||||
|
seed_opt_in_bundle(&state, "Opt In").await;
|
||||||
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||||
|
let ctx = state.services.content();
|
||||||
|
let taken = || async {
|
||||||
|
let archives = state
|
||||||
|
.bundles
|
||||||
|
.archives_for(&ctx, MC_VERSION, GameLoader::Fabric)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
oneclient_content::bundles::taken_bundle_names(cluster_id, &archives, &ctx)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!taken().await.contains("Opt In"));
|
||||||
|
|
||||||
|
oneclient_content::bundles::set_bundle_choices(
|
||||||
|
cluster_id,
|
||||||
|
&[("Opt In".to_string(), true)],
|
||||||
|
&ctx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(taken().await.contains("Opt In"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn holding_one_mod_of_an_opt_in_bundle_does_not_bring_the_rest() {
|
||||||
|
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||||
|
let mut m = named_manifest("Opt In", vec![managed_file(true), fabric_api_file()]);
|
||||||
|
m.enabled = false;
|
||||||
|
seed_bundle_with_pack(&state, m).await;
|
||||||
|
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||||
|
|
||||||
|
let check = check_bundle_updates(
|
||||||
|
cluster_id,
|
||||||
|
state.bundles.as_ref(),
|
||||||
|
&state.services.content(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
check.additions_available.is_empty(),
|
||||||
|
"an opt-in bundle nobody took should add nothing"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
CREATE TABLE cluster_bundle_choices (
|
||||||
|
cluster_id INTEGER NOT NULL,
|
||||||
|
bundle_name TEXT NOT NULL,
|
||||||
|
accepted INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY (cluster_id, bundle_name),
|
||||||
|
FOREIGN KEY (cluster_id) REFERENCES clusters (id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Opt-in bundles used to install without asking, so whatever a cluster already holds counts as taken
|
||||||
|
INSERT OR IGNORE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)
|
||||||
|
SELECT DISTINCT cluster_id, bundle_name, 1
|
||||||
|
FROM cluster_artifacts
|
||||||
|
WHERE bundle_name IS NOT NULL;
|
||||||
|
|
||||||
|
-- A bundle setup recorded removals for and that installed nothing was turned down at setup
|
||||||
|
INSERT OR IGNORE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)
|
||||||
|
SELECT DISTINCT cluster_id, bundle_name, 0
|
||||||
|
FROM cluster_bundle_overrides
|
||||||
|
WHERE override_type = 'removed';
|
||||||
|
|
@ -298,3 +298,65 @@ pub async fn set_type_opt_out(
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_bundle_choices(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
cluster_id: i64,
|
||||||
|
) -> Result<Vec<(String, bool)>, sqlx::Error> {
|
||||||
|
let rows = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
SELECT bundle_name, accepted
|
||||||
|
FROM cluster_bundle_choices
|
||||||
|
WHERE cluster_id = ?
|
||||||
|
"#,
|
||||||
|
cluster_id
|
||||||
|
)
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| (r.bundle_name, r.accepted != 0))
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_bundle_choices(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
cluster_id: i64,
|
||||||
|
choices: &[(String, bool)],
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
for (bundle_name, accepted) in choices {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT OR REPLACE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
"#,
|
||||||
|
cluster_id,
|
||||||
|
bundle_name,
|
||||||
|
accepted
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
tx.commit().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn copy_bundle_choices(
|
||||||
|
pool: &SqlitePool,
|
||||||
|
source_cluster_id: i64,
|
||||||
|
target_cluster_id: i64,
|
||||||
|
) -> Result<(), sqlx::Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT OR IGNORE INTO cluster_bundle_choices (cluster_id, bundle_name, accepted)
|
||||||
|
SELECT ?, bundle_name, accepted
|
||||||
|
FROM cluster_bundle_choices
|
||||||
|
WHERE cluster_id = ?
|
||||||
|
"#,
|
||||||
|
target_cluster_id,
|
||||||
|
source_cluster_id
|
||||||
|
)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue