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.))
|
||||
.background(colors::component_bg())
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
Markdown::new(self.body.clone())
|
||||
.width(Size::fill())
|
||||
.style(MarkdownStyle {
|
||||
color: Color::WHITE,
|
||||
color_link: colors::code_info(),
|
||||
color_code: Color::WHITE,
|
||||
background_code: CARD_BG,
|
||||
background_blockquote: CARD_BG,
|
||||
border_blockquote: colors::component_border(),
|
||||
background_divider: colors::component_border(),
|
||||
headings: [16., 14., 13., 12., 12., 12.],
|
||||
paragraph_size: 12.,
|
||||
code_font_size: 11.,
|
||||
..MarkdownStyle::default()
|
||||
}),
|
||||
),
|
||||
.child(Markdown::new(self.body.clone()).width(Size::fill()).style(
|
||||
MarkdownStyle {
|
||||
color: Color::WHITE,
|
||||
color_link: colors::code_info(),
|
||||
color_code: Color::WHITE,
|
||||
background_code: CARD_BG,
|
||||
background_blockquote: CARD_BG,
|
||||
border_blockquote: colors::component_border(),
|
||||
background_divider: colors::component_border(),
|
||||
headings: [16., 14., 13., 12., 12., 12.],
|
||||
paragraph_size: 12.,
|
||||
code_font_size: 11.,
|
||||
..MarkdownStyle::default()
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod active_cluster_panel;
|
|||
mod asset_image;
|
||||
mod avatar;
|
||||
mod blocked_downloads_popup;
|
||||
mod bundle_choices_popup;
|
||||
mod button;
|
||||
mod charts;
|
||||
mod checkbox;
|
||||
|
|
@ -63,6 +64,7 @@ pub use active_cluster_panel::ActiveClusterPanel;
|
|||
pub use asset_image::AssetImage;
|
||||
pub use avatar::Avatar;
|
||||
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 charts::{BarChart, PieChart, ValueUnit, slice_color};
|
||||
pub use checkbox::checkbox_labeled;
|
||||
|
|
|
|||
|
|
@ -85,14 +85,16 @@ fn navbar_left(show_logo: bool) -> impl IntoElement {
|
|||
Size::auto()
|
||||
})
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(NAV_LINK_SPACING_PX / 2.)
|
||||
.spacing(NAV_LINK_SPACING_PX / 2.)
|
||||
.maybe(!show_logo, |el| {
|
||||
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)
|
||||
.into_element())
|
||||
.maybe(show_logo, |rect| rect.child(NavbarLogo.into_element()))
|
||||
.into_element(),
|
||||
)
|
||||
.maybe(show_logo, |rect| rect.child(NavbarLogo.into_element()))
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -134,7 +136,7 @@ fn navbar_center(is_small: bool) -> impl IntoElement {
|
|||
target: NavTarget::Route(Route::Clusters {}),
|
||||
nav_label: "Versions",
|
||||
})
|
||||
.child(NavLink {
|
||||
.child(NavLink {
|
||||
active: matches!(
|
||||
route,
|
||||
Route::Browser {
|
||||
|
|
@ -203,7 +205,7 @@ impl Component for NavLink {
|
|||
} else if hovering() || focused().is_focused() {
|
||||
theme::colors::ghost_overlay_hover()
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
|
||||
rect()
|
||||
|
|
|
|||
|
|
@ -6,9 +6,8 @@ use oneclient_core::SeenStatus;
|
|||
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
||||
use crate::essential::EssentialPackage;
|
||||
use crate::hooks::{
|
||||
ClusterAction, EssentialGuardKind, PendingEssential, disable_warnings,
|
||||
loaded_image, use_cached_image, use_cluster_mutation, use_disable_warnings,
|
||||
use_essential_guard,
|
||||
ClusterAction, EssentialGuardKind, PendingEssential, disable_warnings, loaded_image,
|
||||
use_cached_image, use_cluster_mutation, use_disable_warnings, use_essential_guard,
|
||||
};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -198,7 +197,14 @@ impl Component for PackageRow {
|
|||
|
||||
let (card, radius) = match layout {
|
||||
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.,
|
||||
),
|
||||
CardLayout::Grid => (
|
||||
|
|
@ -291,7 +297,9 @@ pub(crate) fn disable_warning_body(
|
|||
|
||||
match warnings {
|
||||
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 bg = match (enabled, hovering) {
|
||||
(_, true) => colors::component_bg_hover(),
|
||||
(_, true) => colors::component_bg_hover(),
|
||||
(true, false) => colors::component_bg(),
|
||||
(false, false) => colors::component_bg_disabled(),
|
||||
(false, false) => colors::component_bg_disabled(),
|
||||
};
|
||||
|
||||
let alpha = if enabled {
|
||||
255u8
|
||||
} else {
|
||||
115u8
|
||||
};
|
||||
let alpha = if enabled { 255u8 } else { 115u8 };
|
||||
|
||||
let border = if !hovering {
|
||||
colors::component_border()
|
||||
|
|
@ -428,11 +432,7 @@ pub(crate) fn grid_card(
|
|||
// .cross_align(Alignment::Center)
|
||||
.spacing(11.)
|
||||
.content(Content::Flex)
|
||||
.child(
|
||||
rect()
|
||||
.opacity(alpha as f32 / 255.)
|
||||
.child(icon)
|
||||
)
|
||||
.child(rect().opacity(alpha as f32 / 255.).child(icon))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
|
|
|
|||
|
|
@ -167,6 +167,7 @@ impl Component for ReleaseMigrationPopup {
|
|||
|
||||
let other_popup = snapshot.cluster_update.is_some()
|
||||
|| snapshot.optional_mods.is_some()
|
||||
|| snapshot.bundle_choices.is_some()
|
||||
|| snapshot.package_updates.is_some()
|
||||
|| snapshot.pending_prompt.is_some();
|
||||
if blocked_by_route(&route) || other_popup {
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ use tokio::sync::mpsc;
|
|||
use crate::components::IconType;
|
||||
use crate::launcher::off_ui;
|
||||
use crate::notifications::{
|
||||
ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef, OptionalModsGroup,
|
||||
OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
|
||||
BundleChoices, ClusterUpdateSummary, NotificationAction, NotificationSpec, OptionalModRef,
|
||||
OptionalModsGroup, OptionalModsOutcome, PackageUpdateGroup, PendingPrompt,
|
||||
};
|
||||
use crate::state::{AppChannel, AppState, AsyncStatus, FlaggedInstallPrompt, RelocationState};
|
||||
use crate::{invalidate_java_queries, launcher};
|
||||
|
|
@ -756,6 +756,10 @@ impl Actions {
|
|||
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) {
|
||||
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(
|
||||
&self,
|
||||
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
|
||||
// startup
|
||||
actions
|
||||
.resolve_bundle_choices_before_launch(&state, cluster_id)
|
||||
.await;
|
||||
actions
|
||||
.resolve_bundle_updates_before_launch(&state, cluster_id)
|
||||
.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() {
|
||||
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");
|
||||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -112,10 +112,7 @@ impl Component for ClusterShell {
|
|||
let game = use_game_snapshot();
|
||||
let launcher = use_launcher();
|
||||
let cluster = use_cluster(cluster_id);
|
||||
let syncing = launch_syncing(
|
||||
&launcher,
|
||||
cluster.as_ref().is_none_or(|c| c.uses_bundles()),
|
||||
);
|
||||
let syncing = launch_syncing(&launcher, cluster.as_ref().is_none_or(|c| c.uses_bundles()));
|
||||
|
||||
let show_game_log = game.is_active(cluster_id);
|
||||
let show_datapacks = cluster
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ use freya::prelude::*;
|
|||
use freya::router::*;
|
||||
|
||||
use crate::components::{
|
||||
AccountSwitcher, BlockedDownloadsPopup, ClusterUpdatePopup, ControlCenter, FlaggedInstallPopup,
|
||||
GenericPromptOverlay, JavaPromptOverlay, MicrosoftJavaPromptOverlay, ModpackConfirmPopup,
|
||||
NotificationCenter, OptionalModsPopup, PackageUpdatePopup, ReleaseMigrationPopup,
|
||||
SplashCurtain, StatusBar, Toasts, TooltipHost, UpdatePromptOverlay, use_provide_tooltips,
|
||||
AccountSwitcher, BlockedDownloadsPopup, BundleChoicesPopup, ClusterUpdatePopup, ControlCenter,
|
||||
FlaggedInstallPopup, GenericPromptOverlay, JavaPromptOverlay, MicrosoftJavaPromptOverlay,
|
||||
ModpackConfirmPopup, NotificationCenter, OptionalModsPopup, PackageUpdatePopup,
|
||||
ReleaseMigrationPopup, SplashCurtain, StatusBar, Toasts, TooltipHost, UpdatePromptOverlay,
|
||||
use_provide_tooltips,
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use crate::hooks::use_start_maximized;
|
||||
|
|
@ -52,6 +53,7 @@ impl Component for RootLayout {
|
|||
.child(GenericPromptOverlay)
|
||||
.child(ClusterUpdatePopup)
|
||||
.child(OptionalModsPopup)
|
||||
.child(BundleChoicesPopup)
|
||||
.child(BlockedDownloadsPopup)
|
||||
.child(ModpackConfirmPopup)
|
||||
.child(PackageUpdatePopup)
|
||||
|
|
|
|||
|
|
@ -243,8 +243,8 @@ fn main() {
|
|||
.and_then(|v| v.parse::<usize>().ok())
|
||||
.unwrap_or(96 * 1024 * 1024),
|
||||
)
|
||||
.with_plugin(freya::borderless::BorderlessPlugin::new())
|
||||
.with_plugin(freya::metrics::MetricsPlugin::default())
|
||||
.with_plugin(freya::borderless::BorderlessPlugin::new())
|
||||
.with_plugin(freya::metrics::MetricsPlugin::default())
|
||||
.with_default_font(theme::DEFAULT_FONT);
|
||||
|
||||
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)]
|
||||
pub struct OptionalModsGroup {
|
||||
pub cluster_id: ClusterId,
|
||||
|
|
@ -251,6 +257,7 @@ pub struct NotificationSnapshot {
|
|||
pub pending_prompt: Option<PendingPromptView>,
|
||||
pub cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
||||
pub optional_mods: Option<Vec<OptionalModsGroup>>,
|
||||
pub bundle_choices: Option<BundleChoices>,
|
||||
pub package_updates: Option<Vec<PackageUpdateGroup>>,
|
||||
pub blocked_downloads: Option<BlockedDownloads>,
|
||||
pub open_cluster: Option<ClusterId>,
|
||||
|
|
@ -274,6 +281,7 @@ pub struct NotificationState {
|
|||
pending_timers: Vec<ToastDismissTimer>,
|
||||
cluster_update: Option<Vec<ClusterUpdateSummary>>,
|
||||
optional_mods: Option<Vec<OptionalModsGroup>>,
|
||||
bundle_choices: Option<BundleChoices>,
|
||||
package_updates: Option<Vec<PackageUpdateGroup>>,
|
||||
blocked_downloads: Option<BlockedDownloads>,
|
||||
open_cluster: Option<ClusterId>,
|
||||
|
|
@ -283,6 +291,7 @@ pub struct NotificationState {
|
|||
/// because every way the modal can end goes through this state
|
||||
package_updates_done: Option<oneshot::Sender<Vec<BrowserPackageUpdate>>>,
|
||||
optional_mods_done: Option<oneshot::Sender<OptionalModsOutcome>>,
|
||||
bundle_choices_done: Option<oneshot::Sender<Option<HashSet<String>>>>,
|
||||
}
|
||||
|
||||
const CATEGORY_ORDER: [TaskCategory; 7] = [
|
||||
|
|
@ -390,6 +399,7 @@ impl NotificationState {
|
|||
pending_prompt,
|
||||
cluster_update: self.cluster_update.clone(),
|
||||
optional_mods: self.optional_mods.clone(),
|
||||
bundle_choices: self.bundle_choices.clone(),
|
||||
package_updates: self.package_updates.clone(),
|
||||
blocked_downloads: self.blocked_downloads.clone(),
|
||||
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) {
|
||||
self.optional_mods = None;
|
||||
}
|
||||
|
|
@ -1272,7 +1299,10 @@ mod package_update_tests {
|
|||
let id = state.push_custom(&mut inbox, spec("kept"));
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -84,7 +84,12 @@ pub(crate) fn browsable_type(package_type: &str, cluster: &Cluster) -> String {
|
|||
if cluster.lacks_mod_loader() {
|
||||
let unsupported = LOADER_SLUGS.contains(&package_type)
|
||||
|| (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) {
|
||||
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,
|
||||
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_package_project, use_package_versions_when,
|
||||
version_list, versions_total,
|
||||
use_package_project, use_package_versions_when, version_list, versions_total,
|
||||
};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
|
|
|||
|
|
@ -479,7 +479,9 @@ impl Component for PackageManager {
|
|||
let shares_content = cluster
|
||||
.as_ref()
|
||||
.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 search = use_state(String::new);
|
||||
|
|
|
|||
|
|
@ -101,7 +101,12 @@ pub fn shell(parts: Shell) -> 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()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ impl Component for AddRow {
|
|||
.notify("Java runtimes refreshed")
|
||||
.body("The installed runtime list is up to date")
|
||||
.info()
|
||||
.toast_only()
|
||||
.toast_only()
|
||||
.send();
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ struct ClusterPlan {
|
|||
cluster_id: i64,
|
||||
/// `(bundle_name, package_id, override)` for every file whose fate differs from the manifest default
|
||||
overrides: Vec<(String, String, OverrideType)>,
|
||||
choices: Vec<(String, bool)>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -173,9 +174,19 @@ fn build_plans(
|
|||
for archive in &cb.archives {
|
||||
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 {
|
||||
cluster_id: cb.cluster.id,
|
||||
overrides,
|
||||
choices,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
|
|
@ -284,7 +295,7 @@ fn finish_setup(
|
|||
mut failure: State<Option<String>>,
|
||||
) {
|
||||
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 {
|
||||
// Expected failures (a busy or unwritable database) are not crashes warn! is a breadcrumb error! reports to Sentry
|
||||
if err.is_sentry_excluded() {
|
||||
|
|
@ -378,6 +389,12 @@ async fn apply_overrides(plan: &ClusterPlan) -> oneclient_core::LauncherResult<(
|
|||
&state.services.content(),
|
||||
)
|
||||
.await?;
|
||||
oneclient_content::bundles::set_bundle_choices(
|
||||
plan.cluster_id,
|
||||
&plan.choices,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,93 @@ fn is_base62(s: &str) -> bool {
|
|||
!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 {
|
||||
match user_override {
|
||||
Some(OverrideType::Removed | OverrideType::Disabled) => false,
|
||||
|
|
@ -190,7 +277,7 @@ pub async fn install_bundle(
|
|||
.find(|a| a.manifest.name == bundle_name)
|
||||
.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
|
||||
|
|
@ -393,11 +480,14 @@ pub async fn heal_bundle_activity(
|
|||
pub async fn install_enabled_bundle_files(
|
||||
archive: &BundleArchive,
|
||||
cluster_id: i64,
|
||||
taken: bool,
|
||||
skip_compatibility: bool,
|
||||
progress: Option<&GroupedProgressSession>,
|
||||
ctx: &ContentCtx,
|
||||
) -> 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?;
|
||||
|
||||
let overrides = bundle_dao::list_overrides(&ctx.db, cluster_id).await?;
|
||||
|
|
@ -415,8 +505,11 @@ pub async fn install_enabled_bundle_files(
|
|||
.iter()
|
||||
.filter(|file| {
|
||||
let package_id = file.kind.package_id();
|
||||
effective_enabled(file, find_override(&overrides, &bundle_name, &package_id))
|
||||
&& !present.contains(file)
|
||||
wants_file(
|
||||
file,
|
||||
find_override(&overrides, &bundle_name, &package_id),
|
||||
taken,
|
||||
) && !present.contains(file)
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
|
|
@ -562,15 +655,20 @@ pub async fn enabled_bundle_bytes(
|
|||
.archives_for(ctx, &cluster.mc_version, loader)
|
||||
.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 mut total = 0u64;
|
||||
for archive in &archives {
|
||||
let bundle_name = &archive.manifest.name;
|
||||
let taken = takes_bundle(archive, &accepted);
|
||||
for file in &archive.manifest.files {
|
||||
let package_id = file.kind.package_id();
|
||||
if !effective_enabled(file, find_override(&overrides, bundle_name, &package_id))
|
||||
|| present.contains(file)
|
||||
if !wants_file(
|
||||
file,
|
||||
find_override(&overrides, bundle_name, &package_id),
|
||||
taken,
|
||||
) || present.contains(file)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -693,15 +791,21 @@ pub async fn enabled_bundle_projects(
|
|||
.archives_for(ctx, &cluster.mc_version, loader)
|
||||
.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();
|
||||
for archive in &archives {
|
||||
let bundle_name = &archive.manifest.name;
|
||||
let taken = takes_bundle(archive, &accepted);
|
||||
for file in &archive.manifest.files {
|
||||
let BundleFileKind::Managed { project_id, .. } = &file.kind else {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
@ -746,9 +850,11 @@ pub async fn install_cluster_bundles(
|
|||
bundles = archives.len(),
|
||||
"installing enabled bundle content"
|
||||
);
|
||||
let accepted = accepted_bundles(cluster_id, ctx).await?;
|
||||
for archive in &archives {
|
||||
let taken = takes_bundle(archive, &accepted);
|
||||
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!(
|
||||
cluster_id,
|
||||
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
|
||||
let deferred = match (target, link) {
|
||||
(Some(content_type), Some(link)) => {
|
||||
try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db)
|
||||
.await
|
||||
try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db).await
|
||||
== LiveSync::Deferred
|
||||
}
|
||||
_ => false,
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ pub use error::BundleError;
|
|||
pub use install::{
|
||||
clusters_sharing_artifact, delete_artifact, effective_enabled, enabled_bundle_bytes,
|
||||
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,
|
||||
on_user_enable_artifact, on_user_remove_artifact, remove_artifact_from_cluster,
|
||||
set_artifact_enabled_to, set_bundle_package_enabled, set_bundle_package_opt_in,
|
||||
set_bundle_package_override, set_bundle_package_overrides,
|
||||
on_user_enable_artifact, on_user_remove_artifact, pending_bundle_choices,
|
||||
remove_artifact_from_cluster, set_artifact_enabled_to, set_bundle_choices,
|
||||
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 manifest::BundleManifest as RemoteBundleManifest;
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@ use tokio::sync::Mutex as AsyncMutex;
|
|||
use futures_util::StreamExt;
|
||||
|
||||
use crate::bundles::install::{
|
||||
BUNDLE_INSTALL_CONCURRENCY, bundle_cluster, disable_was_deliberate, external_ids_by_sha1,
|
||||
find_override, find_user_suppression, heal_bundle_activity, install_package_from_bundle,
|
||||
remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
BUNDLE_INSTALL_CONCURRENCY, accepted_bundles, bundle_cluster, disable_was_deliberate,
|
||||
external_ids_by_sha1, find_override, find_user_suppression, heal_bundle_activity,
|
||||
install_package_from_bundle, remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
takes_bundle,
|
||||
};
|
||||
use crate::bundles::manager::BundlesManager;
|
||||
use crate::bundles::overrides;
|
||||
|
|
@ -322,6 +323,7 @@ async fn check_bundle_updates_inner(
|
|||
|
||||
let addition_eligible_bundles = addition_eligible_bundles(
|
||||
ctx,
|
||||
cluster_id,
|
||||
&archives,
|
||||
&bundle_packages,
|
||||
&all_linked,
|
||||
|
|
@ -621,7 +623,8 @@ pub async fn apply_bundle_updates_with(
|
|||
let cluster = PackageStore::get_cluster(cluster_id, ctx).await?;
|
||||
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 {
|
||||
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(
|
||||
&archive.bundle.path,
|
||||
&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
|
||||
let live_bundles = addition_eligible_bundles(
|
||||
ctx,
|
||||
cluster_id,
|
||||
&archives,
|
||||
&bundle_packages,
|
||||
&all_linked,
|
||||
|
|
@ -1134,6 +1138,7 @@ impl<'a> TypeOptOuts<'a> {
|
|||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn addition_eligible_bundles(
|
||||
ctx: &ContentCtx,
|
||||
cluster_id: i64,
|
||||
archives: &[BundleArchive],
|
||||
bundle_packages: &[BundleTrackedArtifactRow],
|
||||
all_linked: &[LinkedArtifactInfo],
|
||||
|
|
@ -1149,6 +1154,12 @@ async fn addition_eligible_bundles(
|
|||
overrides_map,
|
||||
&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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -366,13 +366,8 @@ impl PackageStore {
|
|||
let live = if enabled {
|
||||
link::try_link_materialized(&cluster, &artifact, &file_name).await
|
||||
} else {
|
||||
link::try_unlink_materialized(
|
||||
&cluster,
|
||||
content_type,
|
||||
&link.cluster_file_name,
|
||||
&ctx.db,
|
||||
)
|
||||
.await;
|
||||
link::try_unlink_materialized(&cluster, content_type, &link.cluster_file_name, &ctx.db)
|
||||
.await;
|
||||
if link.cluster_file_name != file_name {
|
||||
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(());
|
||||
}
|
||||
|
||||
let mut overrides = archives.iter().filter_map(|archive| {
|
||||
archive
|
||||
.manifest
|
||||
.java_version_override
|
||||
.map(|major| (archive.manifest.name.as_str(), major))
|
||||
});
|
||||
let taken = oneclient_content::bundles::taken_bundle_names(
|
||||
cluster_id,
|
||||
&archives,
|
||||
&state.services.content(),
|
||||
)
|
||||
.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 {
|
||||
return Ok(());
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const UI_FLOOD_TARGETS: &[&str] = &[
|
|||
"freya_core::runner",
|
||||
"freya_core::tree",
|
||||
"freya_winit::renderer",
|
||||
"freya::metrics",
|
||||
"freya::metrics",
|
||||
"ragnarok::executor",
|
||||
"ragnarok::nodes_state",
|
||||
"torin::torin",
|
||||
|
|
|
|||
|
|
@ -236,10 +236,9 @@ async fn adopt_cluster(
|
|||
let profile =
|
||||
create_profile_from_global(&state.services.db, &global, folder_name, None, None).await?;
|
||||
|
||||
let tags = recorded.as_ref().map_or_else(
|
||||
|| "[]".to_string(),
|
||||
|identity| encode_tags(&identity.tags),
|
||||
);
|
||||
let tags = recorded
|
||||
.as_ref()
|
||||
.map_or_else(|| "[]".to_string(), |identity| encode_tags(&identity.tags));
|
||||
|
||||
let row = cluster_dao::insert(
|
||||
&state.services.db,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use oneclient_common::domain::{ContentType, GameLoader, ProviderId};
|
||||
use oneclient_content::bundles::{
|
||||
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::clusters::CreateClusterOptions;
|
||||
|
|
@ -1293,3 +1293,127 @@ async fn the_package_list_does_not_save_the_opt_out() {
|
|||
.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(())
|
||||
}
|
||||
|
||||
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