Merge branch 'main' of https://github.com/Polyfrost/OneLauncher
This commit is contained in:
commit
80e7cf0e59
25 changed files with 1685 additions and 120 deletions
12
.sqlx/query-59a29c97c422e8721c29b9517ae834ea97776c57bb129a9d67007cdd979ae841.json
generated
Normal file
12
.sqlx/query-59a29c97c422e8721c29b9517ae834ea97776c57bb129a9d67007cdd979ae841.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT OR IGNORE INTO cluster_bundle_type_opt_outs (cluster_id, bundle_name, content_type)\n VALUES (?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "59a29c97c422e8721c29b9517ae834ea97776c57bb129a9d67007cdd979ae841"
|
||||
}
|
||||
38
.sqlx/query-5f45ecf4e5ac27ad3a7ef4e5f9c2e6466ef51af11c332a2e6089c90759b9871f.json
generated
Normal file
38
.sqlx/query-5f45ecf4e5ac27ad3a7ef4e5f9c2e6466ef51af11c332a2e6089c90759b9871f.json
generated
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT bundle_name, content_type\n FROM cluster_bundle_type_opt_outs\n WHERE cluster_id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "bundle_name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_bundle_type_opt_outs",
|
||||
"name": "bundle_name"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "content_type",
|
||||
"ordinal": 1,
|
||||
"type_info": "Integer",
|
||||
"origin": {
|
||||
"Table": {
|
||||
"table": "cluster_bundle_type_opt_outs",
|
||||
"name": "content_type"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5f45ecf4e5ac27ad3a7ef4e5f9c2e6466ef51af11c332a2e6089c90759b9871f"
|
||||
}
|
||||
12
.sqlx/query-dff3f2fe8e50eaa7ffebab7490306bc40e846ad708975991346aab83870e0968.json
generated
Normal file
12
.sqlx/query-dff3f2fe8e50eaa7ffebab7490306bc40e846ad708975991346aab83870e0968.json
generated
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n DELETE FROM cluster_bundle_type_opt_outs\n WHERE cluster_id = ? AND bundle_name = ? AND content_type = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "dff3f2fe8e50eaa7ffebab7490306bc40e846ad708975991346aab83870e0968"
|
||||
}
|
||||
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -4934,7 +4934,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_app"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
|
@ -4982,7 +4982,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_auth"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
|
|
@ -5006,7 +5006,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_cluster"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"bytes 1.12.1",
|
||||
"chrono",
|
||||
|
|
@ -5029,7 +5029,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_common"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"directories",
|
||||
"interfrost",
|
||||
|
|
@ -5042,7 +5042,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_content"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async_zip",
|
||||
|
|
@ -5070,7 +5070,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_core"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"async-compression",
|
||||
"async-stream",
|
||||
|
|
@ -5135,7 +5135,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_db"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"async-tempfile",
|
||||
"chrono",
|
||||
|
|
@ -5150,7 +5150,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_discord"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"discord-rich-presence",
|
||||
|
|
@ -5162,7 +5162,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_events"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
"tokio",
|
||||
|
|
@ -5172,7 +5172,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_java"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-tempfile",
|
||||
|
|
@ -5195,7 +5195,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_macro"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -5204,7 +5204,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_mc"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"chrono",
|
||||
|
|
@ -5226,7 +5226,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_net"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"async-trait",
|
||||
|
|
@ -5248,7 +5248,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "oneclient_polyplus"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"async-tungstenite 0.35.0",
|
||||
"bytes 1.12.1",
|
||||
|
|
@ -5591,7 +5591,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
|||
|
||||
[[package]]
|
||||
name = "polyio"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
dependencies = [
|
||||
"astral-tokio-tar",
|
||||
"async-compression",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ license = "GPL-3.0-only"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/Polyfrost/OneLauncher"
|
||||
rust-version = "1.97"
|
||||
version = "2.5.1"
|
||||
version = "2.6.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
# core
|
||||
|
|
|
|||
|
|
@ -153,10 +153,12 @@ SectionEnd
|
|||
|
||||
[package.metadata.packager.deb]
|
||||
desktop-template = "distribution/linux/org.polyfrost.oneclient.template"
|
||||
depends = ["libxdo3"]
|
||||
|
||||
[package.metadata.packager.appimage]
|
||||
# desktop entry generated from the deb template above via the linux config
|
||||
libs = ["libayatana-appindicator3.so.1"]
|
||||
excluded-libraries = ["libwayland-*"]
|
||||
|
||||
[package.metadata.generate-rpm]
|
||||
auto-req = "builtin"
|
||||
|
|
|
|||
|
|
@ -18,6 +18,17 @@ impl Component for ConfirmLinkOverlay {
|
|||
};
|
||||
|
||||
let open_url = url.clone();
|
||||
let (title, body) = if is_first_party(&url) {
|
||||
(
|
||||
"Leave the launcher?",
|
||||
"This will open the page in your web browser.",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Open external link?",
|
||||
"This link was provided by a third party. Only open it if you trust the source.",
|
||||
)
|
||||
};
|
||||
|
||||
OverlayPopup::new()
|
||||
.overlay_level(OVERLAY_BASE_LEVEL + 3)
|
||||
|
|
@ -49,7 +60,7 @@ impl Component for ConfirmLinkOverlay {
|
|||
)
|
||||
.child(
|
||||
label()
|
||||
.text("Open external link?")
|
||||
.text(title)
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
|
|
@ -57,7 +68,7 @@ impl Component for ConfirmLinkOverlay {
|
|||
)
|
||||
.child(
|
||||
label()
|
||||
.text("This link was provided by a third party. Only open it if you trust the source.")
|
||||
.text(body)
|
||||
.font_size(12.)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
|
|
@ -105,3 +116,13 @@ impl Component for ConfirmLinkOverlay {
|
|||
.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_first_party(url: &str) -> bool {
|
||||
let host = url
|
||||
.split_once("://")
|
||||
.map_or(url, |(_, rest)| rest)
|
||||
.split(['/', '?', '#', ':'])
|
||||
.next()
|
||||
.unwrap_or_default();
|
||||
host == "polyfrost.org" || host.ends_with(".polyfrost.org")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ pub use package_delete_dialog::use_shared_delete;
|
|||
pub(crate) use package_row::{
|
||||
CARD_BG, CARD_GRID_H, CARD_H, CARD_NAME, GRID_GAP, GRID_MIN_W, badge, disable_warning_body,
|
||||
grid_card, icon_box, kebab_button, meta_size, meta_text, on_secondary, package_icon, pill,
|
||||
remote_icon, toggle_action,
|
||||
remote_icon, set_enabled_action, toggle_action,
|
||||
};
|
||||
pub use package_row::{
|
||||
CardLayout, PackageEntry, PackageRow, github_badge, package_context_menu, provider_badge,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,8 @@ use crate::{
|
|||
components::{Avatar, Icon, IconType},
|
||||
hooks::{
|
||||
settled_or_loading, try_default_account, use_active_cluster_id, use_browser_type,
|
||||
use_clusters, use_current_account, use_dispatch, use_notifications_snapshot,
|
||||
use_clusters, use_current_account, use_dispatch, use_link_confirm,
|
||||
use_notifications_snapshot,
|
||||
},
|
||||
theme,
|
||||
utils::default_cluster,
|
||||
|
|
@ -185,6 +186,7 @@ impl Component for NavLink {
|
|||
let mut hovering = use_state(|| false);
|
||||
let a11y_id = use_a11y();
|
||||
let focused = use_focus(a11y_id);
|
||||
let mut confirm_link = use_link_confirm();
|
||||
|
||||
let active = self.active;
|
||||
let target = self.target.clone();
|
||||
|
|
@ -229,7 +231,7 @@ impl Component for NavLink {
|
|||
NavTarget::Route(route) => {
|
||||
let _ = RouterContext::get().push(route.clone());
|
||||
}
|
||||
NavTarget::External(url) => crate::platform::open_url(url),
|
||||
NavTarget::External(url) => confirm_link.set(Some((*url).to_string())),
|
||||
}
|
||||
})
|
||||
.on_pointer_over(move |_| hovering.set(true))
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ fn entry_from_item(
|
|||
hash: None,
|
||||
manifest_default: false,
|
||||
hidden: false,
|
||||
opted_in: true,
|
||||
advanced: false,
|
||||
update_available: false,
|
||||
// Offered, not installed, so there is no recency badge to show
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ pub struct PackageEntry {
|
|||
pub manifest_default: bool,
|
||||
/// Private bundle dependency only set for bundle rows
|
||||
pub hidden: bool,
|
||||
/// False for uninstalled rows of bundles the cluster never took keeps them out of the All tab
|
||||
pub opted_in: bool,
|
||||
pub advanced: bool,
|
||||
/// Only set for browser-installed content bundle packages use the bundle update flow
|
||||
pub update_available: bool,
|
||||
|
|
@ -245,22 +247,38 @@ pub(crate) fn toggle_action(
|
|||
cluster_id: i64,
|
||||
enabled: bool,
|
||||
) -> Option<ClusterAction> {
|
||||
if let Some(hash) = &item.hash {
|
||||
set_enabled_action(
|
||||
cluster_id,
|
||||
item.hash.as_deref(),
|
||||
item.bundle_name.as_deref(),
|
||||
&item.package_id,
|
||||
item.manifest_default,
|
||||
enabled,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn set_enabled_action(
|
||||
cluster_id: i64,
|
||||
hash: Option<&str>,
|
||||
bundle_name: Option<&str>,
|
||||
package_id: &str,
|
||||
manifest_default: bool,
|
||||
enabled: bool,
|
||||
) -> Option<ClusterAction> {
|
||||
if let Some(hash) = hash {
|
||||
Some(ClusterAction::SetArtifactEnabled {
|
||||
cluster_id,
|
||||
hash: hash.clone(),
|
||||
hash: hash.to_string(),
|
||||
enabled,
|
||||
})
|
||||
} else {
|
||||
item.bundle_name
|
||||
.clone()
|
||||
.map(|bundle_name| ClusterAction::SetBundlePackageEnabled {
|
||||
cluster_id,
|
||||
bundle_name,
|
||||
package_id: item.package_id.clone(),
|
||||
enabled,
|
||||
manifest_default: item.manifest_default,
|
||||
})
|
||||
bundle_name.map(|bundle_name| ClusterAction::SetBundlePackageEnabled {
|
||||
cluster_id,
|
||||
bundle_name: bundle_name.to_string(),
|
||||
package_id: package_id.to_string(),
|
||||
enabled,
|
||||
manifest_default,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ use oneclient_core::clusters::ModpackSource;
|
|||
|
||||
use crate::components::{Button, Icon, IconType};
|
||||
use crate::hooks::{
|
||||
ALL_VERSIONS, VERSIONS_PAGE_SIZE, content_type_for_slug, use_browser_compat, use_cluster,
|
||||
use_dispatch, use_installs_snapshot, use_package_versions_when, version_list,
|
||||
ALL_VERSIONS, ClusterAction, VERSIONS_PAGE_SIZE, content_type_for_slug, use_browser_compat,
|
||||
use_cluster, use_dispatch, use_installs_snapshot, use_package_versions_when, version_list,
|
||||
};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -21,10 +21,25 @@ type InstalledMap = HashMap<(ProviderId, String), Installed>;
|
|||
/// Height of the install control, matched by the installed pill that replaces it
|
||||
const INSTALL_BUTTON_H: f32 = 28.;
|
||||
|
||||
fn installed_for(installed: &InstalledMap, item: &ProjectSummary) -> Option<InstallSource> {
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct CardInstalled {
|
||||
source: InstallSource,
|
||||
enable: Option<ClusterAction>,
|
||||
}
|
||||
|
||||
fn installed_for(
|
||||
installed: &InstalledMap,
|
||||
item: &ProjectSummary,
|
||||
cluster_id: i64,
|
||||
) -> Option<CardInstalled> {
|
||||
installed
|
||||
.get(&(item.provider, item.id.clone()))
|
||||
.map(|installed| installed.source)
|
||||
.map(|installed| CardInstalled {
|
||||
source: installed.source,
|
||||
enable: installed
|
||||
.disabled_bundled()
|
||||
.and_then(|version| version.enable_action(cluster_id)),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn grid_row(
|
||||
|
|
@ -36,9 +51,9 @@ pub(super) fn grid_row(
|
|||
) -> impl IntoElement {
|
||||
let package_type = package_type.to_string();
|
||||
let fill = cols.saturating_sub(row.len());
|
||||
let installed: Vec<Option<InstallSource>> = row
|
||||
let installed: Vec<Option<CardInstalled>> = row
|
||||
.iter()
|
||||
.map(|item| installed_for(installed, item))
|
||||
.map(|item| installed_for(installed, item, cluster_id))
|
||||
.collect();
|
||||
|
||||
rect()
|
||||
|
|
@ -77,7 +92,7 @@ struct PackageCard {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<InstallSource>,
|
||||
installed: Option<CardInstalled>,
|
||||
}
|
||||
|
||||
impl PackageCard {
|
||||
|
|
@ -85,7 +100,7 @@ impl PackageCard {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<InstallSource>,
|
||||
installed: Option<CardInstalled>,
|
||||
) -> Self {
|
||||
Self {
|
||||
item,
|
||||
|
|
@ -202,7 +217,7 @@ impl Component for PackageCard {
|
|||
&self.item,
|
||||
self.cluster_id,
|
||||
&self.package_type,
|
||||
self.installed,
|
||||
self.installed.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
|
@ -215,7 +230,7 @@ pub(super) fn list_row(
|
|||
installed: &InstalledMap,
|
||||
) -> impl IntoElement {
|
||||
ListRow {
|
||||
installed: installed_for(installed, &item),
|
||||
installed: installed_for(installed, &item, cluster_id),
|
||||
item,
|
||||
cluster_id,
|
||||
package_type: package_type.to_string(),
|
||||
|
|
@ -227,7 +242,7 @@ struct ListRow {
|
|||
item: ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: String,
|
||||
installed: Option<InstallSource>,
|
||||
installed: Option<CardInstalled>,
|
||||
}
|
||||
|
||||
impl Component for ListRow {
|
||||
|
|
@ -308,7 +323,7 @@ impl Component for ListRow {
|
|||
&self.item,
|
||||
self.cluster_id,
|
||||
&self.package_type,
|
||||
self.installed,
|
||||
self.installed.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
@ -320,7 +335,7 @@ struct InstallButton {
|
|||
cluster_id: i64,
|
||||
content_type: ContentType,
|
||||
/// Cluster already has this one, so the control becomes a static pill
|
||||
installed: Option<InstallSource>,
|
||||
installed: Option<CardInstalled>,
|
||||
}
|
||||
|
||||
impl InstallButton {
|
||||
|
|
@ -328,7 +343,7 @@ impl InstallButton {
|
|||
item: &ProjectSummary,
|
||||
cluster_id: i64,
|
||||
package_type: &str,
|
||||
installed: Option<InstallSource>,
|
||||
installed: Option<CardInstalled>,
|
||||
) -> Self {
|
||||
Self {
|
||||
provider: item.provider,
|
||||
|
|
@ -383,7 +398,17 @@ impl Component for InstallButton {
|
|||
let (installing, waiting) =
|
||||
use_installs_snapshot().package_busy(is_modpack, cluster_id, provider, &project_id);
|
||||
|
||||
if let Some(installed) = self.installed {
|
||||
if let Some(installed) = &self.installed {
|
||||
if let Some(action) = installed.enable.clone() {
|
||||
return EnableButton {
|
||||
action,
|
||||
variant: EnableVariant::Card {
|
||||
height: INSTALL_BUTTON_H,
|
||||
},
|
||||
}
|
||||
.into_element();
|
||||
}
|
||||
let installed = installed.source;
|
||||
let color = installed.color();
|
||||
return rect()
|
||||
.horizontal()
|
||||
|
|
|
|||
|
|
@ -19,11 +19,12 @@ use crate::components::{
|
|||
};
|
||||
use crate::hooks::use_cluster;
|
||||
use crate::hooks::{
|
||||
BROWSE_PAGE_SIZE, BrowserUiState, bundles_with_status_items, category_list,
|
||||
cluster_content_items, content_type_for_slug, pick_version_metadata, search_items,
|
||||
search_pending, search_total, settled_or_loading, use_browser_compat, use_browser_state_store,
|
||||
use_browser_type, use_bundles_with_status, use_cluster_content, use_clusters, use_debounced,
|
||||
use_package_categories, use_package_search, use_versions, use_view_state, versions_metadata,
|
||||
BROWSE_PAGE_SIZE, BrowserUiState, bundle_overrides_map, bundles_with_status_items,
|
||||
category_list, cluster_content_items, content_type_for_slug, pick_version_metadata,
|
||||
search_items, search_pending, search_total, settled_or_loading, use_browser_compat,
|
||||
use_browser_state_store, use_browser_type, use_bundle_overrides, use_bundles_with_status,
|
||||
use_cluster_content, use_clusters, use_debounced, use_package_categories, use_package_search,
|
||||
use_versions, use_view_state, versions_metadata,
|
||||
};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
|
|
@ -31,8 +32,8 @@ use crate::ui::grid_columns_for_width;
|
|||
use crate::view::app::cluster::supports_datapacks;
|
||||
|
||||
use super::{
|
||||
InstallSource, Installed, ModpackVersionPrompt, PackageBanner, Thumbnail, WorldInstallPrompt,
|
||||
installed_map, minecraft_choices, preferred_version,
|
||||
EnableButton, EnableVariant, InstallSource, Installed, ModpackVersionPrompt, PackageBanner,
|
||||
Thumbnail, WorldInstallPrompt, installed_map, minecraft_choices, preferred_version,
|
||||
};
|
||||
use crate::utils::{abbreviate_number, sort_clusters_for_home};
|
||||
|
||||
|
|
@ -250,6 +251,7 @@ impl Component for BrowserBody {
|
|||
let installed = installed_map(
|
||||
cluster_content_items(&use_cluster_content(cluster_id, content_type)),
|
||||
&bundles_with_status_items(&use_bundles_with_status(cluster_id)),
|
||||
&bundle_overrides_map(&use_bundle_overrides(cluster_id)),
|
||||
);
|
||||
let installed = if matches!(content_type, ContentType::DataPack | ContentType::Modpack) {
|
||||
Default::default()
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ use std::collections::HashMap;
|
|||
use freya::prelude::*;
|
||||
use oneclient_content::packages::ProviderId;
|
||||
use oneclient_core::{BundleFileKind, BundleWithUpdateStatus, LinkedArtifactInfo};
|
||||
use oneclient_db::models::OverrideType;
|
||||
|
||||
use crate::components::{Icon, IconType};
|
||||
use crate::hooks::{loaded_image, use_cached_image};
|
||||
use crate::components::{Button, Icon, IconType, set_enabled_action};
|
||||
use crate::hooks::{
|
||||
ClusterAction, loaded_image, mutation_is_running, use_cached_image, use_cluster_mutation,
|
||||
};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{ImageFallbackExt, border_all_color};
|
||||
|
||||
|
|
@ -61,15 +64,36 @@ impl InstallSource {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct BundlePin {
|
||||
pub bundle_name: String,
|
||||
pub package_id: String,
|
||||
pub manifest_default: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct InstalledVersion {
|
||||
pub version_id: String,
|
||||
/// The artifact to remove
|
||||
/// `None` when a bundle names this version but nothing is linked
|
||||
pub hash: Option<String>,
|
||||
/// Only meaningful alongside a `hash` a bundle pin with nothing linked is neither on nor off
|
||||
pub enabled: bool,
|
||||
pub source: InstallSource,
|
||||
pub bundle: Option<BundlePin>,
|
||||
}
|
||||
|
||||
impl InstalledVersion {
|
||||
pub fn enable_action(&self, cluster_id: i64) -> Option<ClusterAction> {
|
||||
let pin = self.bundle.as_ref()?;
|
||||
set_enabled_action(
|
||||
cluster_id,
|
||||
self.hash.as_deref(),
|
||||
Some(&pin.bundle_name),
|
||||
&pin.package_id,
|
||||
pin.manifest_default,
|
||||
true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds every version found rather than an arbitrary winner a cluster can end up with more than one
|
||||
|
|
@ -93,12 +117,20 @@ impl Installed {
|
|||
pub fn is_duplicated(&self) -> bool {
|
||||
self.versions.iter().filter(|v| v.hash.is_some()).count() > 1
|
||||
}
|
||||
|
||||
pub fn disabled_bundled(&self) -> Option<&InstalledVersion> {
|
||||
if self.versions.iter().any(|v| v.enabled) {
|
||||
return None;
|
||||
}
|
||||
self.versions.iter().find(|v| v.bundle.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
/// Local files and a bundle's external files are left out they have no project id to match against
|
||||
pub(crate) fn installed_map(
|
||||
content: Vec<LinkedArtifactInfo>,
|
||||
bundles: &[BundleWithUpdateStatus],
|
||||
overrides: &HashMap<(String, String), String>,
|
||||
) -> HashMap<(ProviderId, String), Installed> {
|
||||
let mut map: HashMap<(ProviderId, String), Installed> = HashMap::new();
|
||||
|
||||
|
|
@ -119,6 +151,7 @@ pub(crate) fn installed_map(
|
|||
hash: Some(item.hash),
|
||||
enabled: item.enabled,
|
||||
source: InstallSource::Manual,
|
||||
bundle: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +159,7 @@ pub(crate) fn installed_map(
|
|||
// Bundle membership wins a bundle's files would otherwise read as hand-installed
|
||||
// The manifest pin is only a fallback for a missing linked version
|
||||
for bundle in bundles {
|
||||
let bundle_name = &bundle.archive.manifest.name;
|
||||
for (file, _status) in &bundle.files {
|
||||
if let BundleFileKind::Managed {
|
||||
provider,
|
||||
|
|
@ -134,18 +168,34 @@ pub(crate) fn installed_map(
|
|||
..
|
||||
} = &file.kind
|
||||
{
|
||||
let pin = BundlePin {
|
||||
bundle_name: bundle_name.clone(),
|
||||
package_id: file.kind.package_id(),
|
||||
manifest_default: file.enabled,
|
||||
};
|
||||
match map.get_mut(&(*provider, project_id.clone())) {
|
||||
Some(installed) => {
|
||||
installed.source = InstallSource::Bundled;
|
||||
if let Some(version) = installed
|
||||
let only_copy = installed.versions.len() == 1;
|
||||
match installed
|
||||
.versions
|
||||
.iter_mut()
|
||||
.find(|v| &v.version_id == version_id)
|
||||
{
|
||||
version.source = InstallSource::Bundled;
|
||||
Some(version) => {
|
||||
version.source = InstallSource::Bundled;
|
||||
version.bundle.get_or_insert(pin);
|
||||
}
|
||||
None if only_copy => {
|
||||
installed.versions[0].bundle.get_or_insert(pin);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let user_override = overrides
|
||||
.get(&(bundle_name.clone(), pin.package_id.clone()))
|
||||
.and_then(|o| OverrideType::parse(o));
|
||||
map.insert(
|
||||
(*provider, project_id.clone()),
|
||||
Installed {
|
||||
|
|
@ -153,8 +203,9 @@ pub(crate) fn installed_map(
|
|||
versions: vec![InstalledVersion {
|
||||
version_id: version_id.clone(),
|
||||
hash: None,
|
||||
enabled: false,
|
||||
enabled: oneclient_core::effective_enabled(file, user_override),
|
||||
source: InstallSource::Bundled,
|
||||
bundle: Some(pin),
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
|
@ -167,6 +218,68 @@ pub(crate) fn installed_map(
|
|||
map
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) enum EnableVariant {
|
||||
Sidebar,
|
||||
Card { height: f32 },
|
||||
VersionRow,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub(crate) struct EnableButton {
|
||||
pub action: ClusterAction,
|
||||
pub variant: EnableVariant,
|
||||
}
|
||||
|
||||
impl Component for EnableButton {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mutation = use_cluster_mutation();
|
||||
let running = mutation_is_running(&mutation);
|
||||
let action = self.action.clone();
|
||||
|
||||
let button = Button::new()
|
||||
.enabled(!running)
|
||||
.on_press(move |_| mutation.mutate(action.clone()));
|
||||
|
||||
match self.variant {
|
||||
EnableVariant::Sidebar => button
|
||||
.primary()
|
||||
.width(Size::fill())
|
||||
.child(Icon::new(IconType::CheckCircle).size(14.))
|
||||
.text(if running { "Enabling..." } else { "Enable" })
|
||||
.into_element(),
|
||||
EnableVariant::Card { height } => rect()
|
||||
.on_press(|e: Event<PressEventData>| e.stop_propagation())
|
||||
.child(
|
||||
button
|
||||
.primary()
|
||||
.small()
|
||||
.height(Size::px(height))
|
||||
.padding(Gaps::new_symmetric(0., 11.))
|
||||
.child(
|
||||
Icon::new(if running {
|
||||
IconType::Loading02
|
||||
} else {
|
||||
IconType::CheckCircle
|
||||
})
|
||||
.size(12.)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(if running { "Enabling" } else { "Enable" })
|
||||
.font_size(11.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.into_element(),
|
||||
EnableVariant::VersionRow => button.secondary().small().text("Enable").into_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn installed_badge(installed: InstallSource, font_size: f32) -> impl IntoElement {
|
||||
badge(installed, font_size, installed.color().with_a(38), None)
|
||||
}
|
||||
|
|
@ -449,9 +562,34 @@ mod tests {
|
|||
.collect(),
|
||||
archive: archive("performance", true, files),
|
||||
has_updates: false,
|
||||
opted_in_types: [ContentType::Mod].into(),
|
||||
}]
|
||||
}
|
||||
|
||||
fn installed_map(
|
||||
content: Vec<LinkedArtifactInfo>,
|
||||
bundles: &[BundleWithUpdateStatus],
|
||||
) -> HashMap<(ProviderId, String), Installed> {
|
||||
super::installed_map(content, bundles, &HashMap::new())
|
||||
}
|
||||
|
||||
fn optional(project_id: &str, version_id: &str) -> BundleFile {
|
||||
BundleFile {
|
||||
enabled: false,
|
||||
..managed(project_id, version_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn overridden(
|
||||
project_id: &str,
|
||||
override_type: OverrideType,
|
||||
) -> HashMap<(String, String), String> {
|
||||
HashMap::from([(
|
||||
("performance".to_string(), project_id.to_string()),
|
||||
override_type.as_str().to_string(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn entry(map: &HashMap<(ProviderId, String), Installed>, project: &str) -> Installed {
|
||||
map.get(&(ProviderId::Modrinth, project.to_string()))
|
||||
.expect("project missing from the map")
|
||||
|
|
@ -580,4 +718,96 @@ mod tests {
|
|||
);
|
||||
assert_eq!(sodium.source, InstallSource::Manual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_optional_mod_never_downloaded_offers_enable_through_the_bundle() {
|
||||
let map = installed_map(Vec::new(), &bundles(vec![optional("sodium", "v1")]));
|
||||
|
||||
let sodium = entry(&map, "sodium");
|
||||
let disabled = sodium.disabled_bundled().expect("offers enable");
|
||||
assert!(matches!(
|
||||
disabled.enable_action(7),
|
||||
Some(ClusterAction::SetBundlePackageEnabled {
|
||||
cluster_id: 7,
|
||||
enabled: true,
|
||||
manifest_default: false,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_enabled_pin_waiting_for_sync_does_not_offer_enable() {
|
||||
let map = installed_map(Vec::new(), &bundles(vec![managed("sodium", "v1")]));
|
||||
|
||||
assert!(entry(&map, "sodium").disabled_bundled().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disabled_override_on_an_unlinked_pin_offers_enable() {
|
||||
let map = super::installed_map(
|
||||
Vec::new(),
|
||||
&bundles(vec![managed("sodium", "v1")]),
|
||||
&overridden("sodium", OverrideType::Disabled),
|
||||
);
|
||||
|
||||
let sodium = entry(&map, "sodium");
|
||||
assert!(matches!(
|
||||
sodium.disabled_bundled().and_then(|v| v.enable_action(7)),
|
||||
Some(ClusterAction::SetBundlePackageEnabled {
|
||||
manifest_default: true,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disabled_linked_bundle_artifact_is_enabled_by_hash() {
|
||||
let map = installed_map(
|
||||
vec![disabled_linked("sodium", Some("v1"), "hash-1", false)],
|
||||
&bundles(vec![managed("sodium", "v1")]),
|
||||
);
|
||||
|
||||
let sodium = entry(&map, "sodium");
|
||||
assert!(matches!(
|
||||
sodium.disabled_bundled().and_then(|v| v.enable_action(7)),
|
||||
Some(ClusterAction::SetArtifactEnabled { ref hash, enabled: true, .. }) if hash == "hash-1"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disabled_copy_from_an_older_bundle_pin_still_offers_enable() {
|
||||
let map = installed_map(
|
||||
vec![disabled_linked("sodium", Some("v1"), "hash-1", false)],
|
||||
&bundles(vec![managed("sodium", "v2")]),
|
||||
);
|
||||
|
||||
assert!(entry(&map, "sodium").disabled_bundled().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn another_enabled_copy_hides_enable() {
|
||||
let map = installed_map(
|
||||
vec![
|
||||
disabled_linked("sodium", Some("v1"), "hash-1", false),
|
||||
linked("sodium", Some("v2"), "hash-2"),
|
||||
],
|
||||
&bundles(vec![managed("sodium", "v1")]),
|
||||
);
|
||||
|
||||
assert!(
|
||||
entry(&map, "sodium").disabled_bundled().is_none(),
|
||||
"enabling the bundle copy would load two"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hand_installed_disabled_mod_is_not_offered_enable() {
|
||||
let map = installed_map(
|
||||
vec![disabled_linked("sodium", Some("v1"), "hash-1", false)],
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(entry(&map, "sodium").disabled_bundled().is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,19 +8,20 @@ use oneclient_core::clusters::ModpackSource;
|
|||
use crate::components::{ScrollArea, use_shared_delete};
|
||||
use crate::hooks::use_cluster;
|
||||
use crate::hooks::{
|
||||
ALL_VERSIONS, VERSIONS_PAGE_SIZE, bundles_with_status_items, cluster_content_items,
|
||||
content_type_for_slug, package_meta_batch, project_detail, use_browser_compat,
|
||||
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,
|
||||
ALL_VERSIONS, VERSIONS_PAGE_SIZE, bundle_overrides_map, bundles_with_status_items,
|
||||
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 crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
use super::{
|
||||
Installed, InstalledVersion, ModpackVersionPrompt, PackageBanner, Thumbnail,
|
||||
WorldInstallPrompt, activity_badge, installed_badge, installed_map, minecraft_choices,
|
||||
preferred_version,
|
||||
EnableButton, EnableVariant, Installed, InstalledVersion, ModpackVersionPrompt, PackageBanner,
|
||||
Thumbnail, WorldInstallPrompt, activity_badge, installed_badge, installed_map,
|
||||
minecraft_choices, preferred_version,
|
||||
};
|
||||
use crate::utils::abbreviate_number;
|
||||
|
||||
|
|
@ -220,6 +221,7 @@ impl Component for BrowserPackage {
|
|||
let installed = installed_map(
|
||||
cluster_content_items(&use_cluster_content(cluster_id, content_type)),
|
||||
&bundles_with_status_items(&use_bundles_with_status(cluster_id)),
|
||||
&bundle_overrides_map(&use_bundle_overrides(cluster_id)),
|
||||
)
|
||||
.remove(&(provider, project_id.clone()))
|
||||
.filter(|_| !is_datapack);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use oneclient_content::packages::types::{
|
|||
use crate::components::{
|
||||
Button, Icon, IconType, Markdown, MarkdownStyle, Segment, SegmentedControl,
|
||||
};
|
||||
use crate::hooks::VERSIONS_PAGE_SIZE;
|
||||
use crate::hooks::{ClusterAction, VERSIONS_PAGE_SIZE};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
use crate::utils::format_size;
|
||||
|
|
@ -142,6 +142,11 @@ pub(super) fn versions_panel(
|
|||
let duplicated = installed
|
||||
.as_ref()
|
||||
.is_some_and(|installed| installed.is_duplicated());
|
||||
let enable = installed
|
||||
.as_ref()
|
||||
.and_then(Installed::disabled_bundled)
|
||||
.filter(|disabled| disabled.version_id == v.version_id)
|
||||
.and_then(|disabled| disabled.enable_action(installer.cluster_id));
|
||||
version_row(
|
||||
v,
|
||||
&dependency_names,
|
||||
|
|
@ -151,6 +156,7 @@ pub(super) fn versions_panel(
|
|||
tag,
|
||||
duplicated,
|
||||
installing,
|
||||
enable,
|
||||
)
|
||||
.into_element()
|
||||
}))
|
||||
|
|
@ -226,6 +232,7 @@ fn version_row(
|
|||
// Saying which version is live only tells the user anything when there are several
|
||||
duplicated: bool,
|
||||
installing: bool,
|
||||
enable: Option<ClusterAction>,
|
||||
) -> impl IntoElement {
|
||||
let version_id = v.version_id.clone();
|
||||
let mut chips: Vec<String> = v.loaders.iter().map(|l| l.to_string()).collect();
|
||||
|
|
@ -300,9 +307,17 @@ fn version_row(
|
|||
.filter(|_| duplicated)
|
||||
.map(|installed| activity_badge(installed.enabled).into_element()),
|
||||
)
|
||||
.child(version_button(
|
||||
installed, v.name, project_id, version_id, installer, on_remove, installing,
|
||||
))
|
||||
.child(match enable {
|
||||
Some(action) => EnableButton {
|
||||
action,
|
||||
variant: EnableVariant::VersionRow,
|
||||
}
|
||||
.into_element(),
|
||||
None => version_button(
|
||||
installed, v.name, project_id, version_id, installer, on_remove, installing,
|
||||
)
|
||||
.into_element(),
|
||||
})
|
||||
}
|
||||
|
||||
/// A bundle pin with nothing linked leaves nothing to press no artifact to remove and installing by hand would duplicate it
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@ pub(super) fn sidebar(
|
|||
_ => false,
|
||||
};
|
||||
let can_install = latest_version.is_some() && !have_latest && !installing && !waiting;
|
||||
let enable = installed
|
||||
.as_ref()
|
||||
.and_then(Installed::disabled_bundled)
|
||||
.and_then(|version| version.enable_action(installer.cluster_id));
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
|
|
@ -101,8 +105,13 @@ pub(super) fn sidebar(
|
|||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.child(match enable {
|
||||
Some(action) => EnableButton {
|
||||
action,
|
||||
variant: EnableVariant::Sidebar,
|
||||
}
|
||||
.into_element(),
|
||||
None => Button::new()
|
||||
.primary()
|
||||
.width(Size::fill())
|
||||
.enabled(can_install)
|
||||
|
|
@ -118,8 +127,9 @@ pub(super) fn sidebar(
|
|||
"Latest installed"
|
||||
} else {
|
||||
"Install latest"
|
||||
}),
|
||||
)
|
||||
})
|
||||
.into_element(),
|
||||
})
|
||||
.maybe(
|
||||
!project.members.is_empty() || !project.author.is_empty(),
|
||||
|el| el.child(authors_card(&project, confirm)),
|
||||
|
|
|
|||
|
|
@ -144,10 +144,13 @@ pub fn bundle_packages(
|
|||
}
|
||||
}
|
||||
|
||||
let mut ordered: Vec<&BundleWithUpdateStatus> = bundles.iter().collect();
|
||||
ordered.sort_by_key(|b| !b.opted_in_types.contains(&content_type));
|
||||
|
||||
let mut rows = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
|
||||
for bundle in bundles {
|
||||
for bundle in ordered {
|
||||
let bundle_name = &bundle.archive.manifest.name;
|
||||
let category = bundle.archive.manifest.category.clone();
|
||||
for (file, status) in &bundle.files {
|
||||
|
|
@ -199,6 +202,8 @@ pub fn bundle_packages(
|
|||
vec![category.clone()]
|
||||
};
|
||||
|
||||
let opted_in =
|
||||
installed_info.is_some() || bundle.opted_in_types.contains(&content_type);
|
||||
let mut row = make_row(
|
||||
pid,
|
||||
Some(bundle_name.clone()),
|
||||
|
|
@ -214,6 +219,7 @@ pub fn bundle_packages(
|
|||
false,
|
||||
// Flagged rather than dropped `HiddenFilter` filters on the row and the seen id stops the loose-content pass resurrecting it as a local file
|
||||
file.hidden,
|
||||
opted_in,
|
||||
);
|
||||
row.advanced = advanced;
|
||||
rows.push(row);
|
||||
|
|
@ -246,6 +252,7 @@ pub fn bundle_packages(
|
|||
.unwrap_or_else(|| info.file_name.clone()),
|
||||
outdated,
|
||||
false,
|
||||
true,
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +274,7 @@ fn make_row(
|
|||
fallback_name: String,
|
||||
update_available: bool,
|
||||
hidden: bool,
|
||||
opted_in: bool,
|
||||
) -> PackageEntry {
|
||||
let name = m
|
||||
.map(|p| p.name.clone())
|
||||
|
|
@ -305,6 +313,7 @@ fn make_row(
|
|||
hash: installed_info.map(|i| i.hash.clone()),
|
||||
update_available,
|
||||
hidden,
|
||||
opted_in,
|
||||
advanced: false,
|
||||
seen_status: installed_info.map(|i| i.seen_status).unwrap_or_default(),
|
||||
}
|
||||
|
|
@ -330,7 +339,7 @@ impl Tab {
|
|||
|
||||
pub(super) fn matches(&self, p: &PackageEntry) -> bool {
|
||||
match self {
|
||||
Tab::All => true,
|
||||
Tab::All => p.opted_in,
|
||||
Tab::Category(c) => p.categories.iter().any(|pc| pc == c),
|
||||
Tab::Browser => (p.is_remote() || p.github_hosted) && !p.in_bundle(),
|
||||
Tab::Local => !p.is_remote() && !p.github_hosted,
|
||||
|
|
@ -687,3 +696,67 @@ impl Component for PackageManager {
|
|||
.maybe_child(delete_dialog)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::view::onboarding::test_support::{archive, file};
|
||||
use oneclient_core::{BundleFile, FileUpdateStatus};
|
||||
|
||||
fn bundle(category: &str, opted_in: bool, files: Vec<BundleFile>) -> BundleWithUpdateStatus {
|
||||
BundleWithUpdateStatus {
|
||||
files: files
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|f| (f, FileUpdateStatus::NotInstalled))
|
||||
.collect(),
|
||||
archive: archive(category, true, files),
|
||||
has_updates: false,
|
||||
opted_in_types: if opted_in {
|
||||
[ContentType::Mod].into()
|
||||
} else {
|
||||
HashSet::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_tab_skips_bundles_not_opted_into() {
|
||||
let bundles = [
|
||||
bundle(
|
||||
"Declined",
|
||||
false,
|
||||
vec![
|
||||
file("only-declined", true, false),
|
||||
file("shared", true, false),
|
||||
],
|
||||
),
|
||||
bundle(
|
||||
"Taken",
|
||||
true,
|
||||
vec![file("shared", true, false), file("only-taken", true, false)],
|
||||
),
|
||||
];
|
||||
let rows = bundle_packages(
|
||||
Vec::new(),
|
||||
&bundles,
|
||||
&HashMap::new(),
|
||||
&PackageMetaMap::new(),
|
||||
&HashSet::new(),
|
||||
ContentType::Mod,
|
||||
);
|
||||
let all: Vec<&str> = rows
|
||||
.iter()
|
||||
.filter(|p| Tab::All.matches(p))
|
||||
.map(|p| p.package_id.as_str())
|
||||
.collect();
|
||||
assert_eq!(all, ["shared", "only-taken"]);
|
||||
let shared = rows.iter().find(|p| p.package_id == "shared").unwrap();
|
||||
assert_eq!(shared.categories, ["Taken"]);
|
||||
let declined = Tab::Category("Declined".into());
|
||||
assert!(
|
||||
rows.iter()
|
||||
.any(|p| p.package_id == "only-declined" && declined.matches(p))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -613,8 +613,6 @@ pub async fn set_bundle_package_override(
|
|||
|
||||
/// For bundle files the cluster has not installed
|
||||
/// installed ones go through [`set_artifact_enabled_to`]
|
||||
/// Matching the manifest default clears the override
|
||||
/// switching *on* also drops objections filed under other bundles
|
||||
#[tracing::instrument(level = "debug", skip(ctx))]
|
||||
pub async fn set_bundle_package_enabled(
|
||||
cluster_id: i64,
|
||||
|
|
@ -625,18 +623,12 @@ pub async fn set_bundle_package_enabled(
|
|||
ctx: &ContentCtx,
|
||||
) -> ContentResult<()> {
|
||||
let override_type = match (enabled, manifest_default) {
|
||||
(true, true) | (false, false) => None,
|
||||
(true, false) => Some(OverrideType::Enabled),
|
||||
(true, _) => Some(OverrideType::Enabled),
|
||||
(false, false) => None,
|
||||
(false, true) => Some(OverrideType::Disabled),
|
||||
};
|
||||
|
||||
set_bundle_package_override(cluster_id, bundle_name, package_id, override_type, ctx).await?;
|
||||
|
||||
if enabled {
|
||||
clear_suppressing_overrides(cluster_id, package_id, ctx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
set_bundle_package_override(cluster_id, bundle_name, package_id, override_type, ctx).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(overrides, ctx), fields(count = overrides.len()))]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use std::collections::HashSet;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::bundles::Bundle;
|
||||
|
|
@ -253,4 +255,5 @@ pub struct BundleWithUpdateStatus {
|
|||
pub archive: BundleArchive,
|
||||
pub files: Vec<(BundleFile, FileUpdateStatus)>,
|
||||
pub has_updates: bool,
|
||||
pub opted_in_types: HashSet<ContentType>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use futures_util::StreamExt;
|
|||
|
||||
use crate::bundles::install::{
|
||||
BUNDLE_INSTALL_CONCURRENCY, bundle_cluster, disable_was_deliberate, external_ids_by_sha1,
|
||||
find_user_suppression, heal_bundle_activity, install_package_from_bundle,
|
||||
find_override, find_user_suppression, heal_bundle_activity, install_package_from_bundle,
|
||||
remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
};
|
||||
use crate::bundles::manager::BundlesManager;
|
||||
|
|
@ -32,7 +32,7 @@ use crate::ctx::ContentCtx;
|
|||
use crate::error::{ContentError, ContentResult};
|
||||
use crate::packages::store::PackageStore;
|
||||
use crate::packages::types::LinkedArtifactInfo;
|
||||
use oneclient_common::domain::{GameLoader, ProviderId};
|
||||
use oneclient_common::domain::{ContentType, GameLoader, ProviderId};
|
||||
|
||||
static CLUSTER_UPDATE_LOCKS: OnceLock<Mutex<HashMap<i64, Arc<AsyncMutex<()>>>>> = OnceLock::new();
|
||||
|
||||
|
|
@ -322,33 +322,40 @@ async fn check_bundle_updates_inner(
|
|||
|
||||
let addition_eligible_bundles = addition_eligible_bundles(
|
||||
ctx,
|
||||
&archives,
|
||||
&bundle_packages,
|
||||
&all_linked,
|
||||
&candidate_keys_by_bundle,
|
||||
overrides,
|
||||
&overrides_map,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut additions_available = Vec::new();
|
||||
let mut optional_available: Vec<(String, BundleOptionalPackage)> = Vec::new();
|
||||
let mut planned_addition_keys = all_installed_managed_keys.clone();
|
||||
let tracked_package_by_hash: HashMap<&str, &str> = bundle_packages
|
||||
.iter()
|
||||
.filter_map(|bp| Some((bp.hash.as_str(), bp.package_id.as_deref()?)))
|
||||
.collect();
|
||||
for hash in &all_installed_external_hashes {
|
||||
planned_addition_keys.insert(external_bundle_key(hash));
|
||||
if let Some(package_id) = tracked_package_by_hash.get(hash.as_str()) {
|
||||
planned_addition_keys.insert(external_bundle_key(package_id));
|
||||
}
|
||||
}
|
||||
planned_addition_keys.extend(installed_external_keys(
|
||||
&all_installed_external_hashes,
|
||||
&bundle_packages,
|
||||
));
|
||||
let installed_keys = planned_addition_keys.clone();
|
||||
let opt_outs = TypeOptOuts::load(
|
||||
cluster_id,
|
||||
&archives,
|
||||
&addition_eligible_bundles,
|
||||
overrides,
|
||||
&installed_keys,
|
||||
ctx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for archive in &archives {
|
||||
if !addition_eligible_bundles.contains(&archive.manifest.name) {
|
||||
continue;
|
||||
}
|
||||
let suppressed_types = opt_outs.remember(archive, ctx).await?;
|
||||
|
||||
for file in &archive.manifest.files {
|
||||
let suppressed = suppressed_types.contains(&file.content_type());
|
||||
let file_id = file.kind.package_id();
|
||||
let user_override = overrides_map
|
||||
.get(&(archive.manifest.name.clone(), file_id.clone()))
|
||||
|
|
@ -364,6 +371,10 @@ async fn check_bundle_updates_inner(
|
|||
continue;
|
||||
}
|
||||
|
||||
if suppressed && user_override.is_none() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !crate::bundles::effective_enabled(file, user_override) {
|
||||
if user_override.is_none()
|
||||
&& file.is_optional_offer()
|
||||
|
|
@ -773,7 +784,11 @@ async fn sync_cluster_loader_version_from_bundles(
|
|||
let subscribed: HashSet<String> = bundle_packages
|
||||
.iter()
|
||||
.filter_map(|bp| bp.bundle_name.clone())
|
||||
.chain(infer_subscribed_from_archives(&archives, &managed_keys))
|
||||
.chain(infer_subscribed_from_archives(
|
||||
&archives,
|
||||
&HashMap::new(),
|
||||
&managed_keys,
|
||||
))
|
||||
.collect();
|
||||
|
||||
let target = select_highest_loader_version(
|
||||
|
|
@ -843,6 +858,33 @@ pub async fn get_bundles_with_update_status(
|
|||
})
|
||||
.collect();
|
||||
|
||||
// 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,
|
||||
&archives,
|
||||
&bundle_packages,
|
||||
&all_linked,
|
||||
&overrides,
|
||||
&overrides_map,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (installed_managed_keys, installed_external_hashes) =
|
||||
installed_bundle_keys(ctx, &all_linked).await?;
|
||||
let mut installed_keys = installed_managed_keys;
|
||||
installed_keys.extend(installed_external_keys(
|
||||
&installed_external_hashes,
|
||||
&bundle_packages,
|
||||
));
|
||||
let opt_outs = TypeOptOuts::load(
|
||||
cluster_id,
|
||||
&archives,
|
||||
&live_bundles,
|
||||
&overrides,
|
||||
&installed_keys,
|
||||
ctx,
|
||||
)
|
||||
.await?;
|
||||
let mut results = Vec::new();
|
||||
|
||||
for archive in archives {
|
||||
|
|
@ -873,7 +915,21 @@ pub async fn get_bundles_with_update_status(
|
|||
files.push((file.clone(), status));
|
||||
}
|
||||
|
||||
let opted_in_types = if live_bundles.contains(&archive.manifest.name) {
|
||||
let suppressed = opt_outs.suppressed(&archive);
|
||||
archive
|
||||
.manifest
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| f.content_type())
|
||||
.filter(|ct| !suppressed.contains(ct))
|
||||
.collect()
|
||||
} else {
|
||||
HashSet::new()
|
||||
};
|
||||
|
||||
results.push(BundleWithUpdateStatus {
|
||||
opted_in_types,
|
||||
archive,
|
||||
files,
|
||||
has_updates,
|
||||
|
|
@ -904,41 +960,216 @@ fn bundle_package_key(
|
|||
}
|
||||
}
|
||||
|
||||
/// Keys both the exact file and the id it was tracked under so a GitHub-hosted file still counts as installed once the bundle moves to a newer release
|
||||
fn installed_external_keys(
|
||||
hashes: &HashSet<String>,
|
||||
bundle_packages: &[BundleTrackedArtifactRow],
|
||||
) -> HashSet<String> {
|
||||
let tracked_package_by_hash: HashMap<&str, &str> = bundle_packages
|
||||
.iter()
|
||||
.filter_map(|bp| Some((bp.hash.as_str(), bp.package_id.as_deref()?)))
|
||||
.collect();
|
||||
hashes
|
||||
.iter()
|
||||
.flat_map(|hash| {
|
||||
std::iter::once(hash.as_str())
|
||||
.chain(tracked_package_by_hash.get(hash.as_str()).copied())
|
||||
})
|
||||
.map(external_bundle_key)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn keys_shipped_by_several_bundles<'a>(
|
||||
archives: impl IntoIterator<Item = &'a BundleArchive>,
|
||||
) -> HashSet<String> {
|
||||
let mut counts: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for archive in archives {
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
for file in &archive.manifest.files {
|
||||
let key = file.kind.bundle_key();
|
||||
if seen.insert(key.clone()) {
|
||||
*counts.entry(key).or_default() += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
counts
|
||||
.into_iter()
|
||||
.filter_map(|(key, count)| (count > 1).then_some(key))
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct TypeOptOuts<'a> {
|
||||
cluster_id: i64,
|
||||
overrides: &'a [ClusterBundleOverrideRow],
|
||||
stray_overrides: Vec<ClusterBundleOverrideRow>,
|
||||
installed_keys: &'a HashSet<String>,
|
||||
shared_keys: HashSet<String>,
|
||||
remembered: HashMap<String, HashSet<ContentType>>,
|
||||
}
|
||||
|
||||
impl<'a> TypeOptOuts<'a> {
|
||||
async fn load(
|
||||
cluster_id: i64,
|
||||
archives: &[BundleArchive],
|
||||
live: &HashSet<String>,
|
||||
overrides: &'a [ClusterBundleOverrideRow],
|
||||
installed_keys: &'a HashSet<String>,
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<Self> {
|
||||
let shipped: HashSet<(&str, String)> = archives
|
||||
.iter()
|
||||
.flat_map(|a| {
|
||||
a.manifest
|
||||
.files
|
||||
.iter()
|
||||
.map(|f| (a.manifest.name.as_str(), f.kind.package_id()))
|
||||
})
|
||||
.collect();
|
||||
let stray_overrides = overrides
|
||||
.iter()
|
||||
.filter(|o| !shipped.contains(&(o.bundle_name.as_str(), o.package_id.clone())))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let mut remembered: HashMap<String, HashSet<ContentType>> = HashMap::new();
|
||||
for (bundle_name, content_type) in
|
||||
bundle_dao::list_type_opt_outs(&ctx.db, cluster_id).await?
|
||||
{
|
||||
if let Some(ct) = ContentType::from_repr(content_type as u8) {
|
||||
remembered.entry(bundle_name).or_default().insert(ct);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
cluster_id,
|
||||
overrides,
|
||||
stray_overrides,
|
||||
installed_keys,
|
||||
shared_keys: keys_shipped_by_several_bundles(
|
||||
archives.iter().filter(|a| live.contains(&a.manifest.name)),
|
||||
),
|
||||
remembered,
|
||||
})
|
||||
}
|
||||
|
||||
fn signals(&self, archive: &BundleArchive) -> (HashSet<ContentType>, HashSet<ContentType>) {
|
||||
let bundle_name = &archive.manifest.name;
|
||||
let mut all_suppressed: HashMap<ContentType, bool> = HashMap::new();
|
||||
|
||||
for file in archive.manifest.files.iter().filter(|f| !f.hidden) {
|
||||
let package_id = file.kind.package_id();
|
||||
let override_type = find_override(self.overrides, bundle_name, &package_id)
|
||||
.or_else(|| find_user_suppression(&self.stray_overrides, &package_id));
|
||||
let key = file.kind.bundle_key();
|
||||
let installed = self.installed_keys.contains(&key)
|
||||
|| matches!(&file.kind, BundleFileKind::External { file: ext, .. }
|
||||
if self.installed_keys.contains(&external_bundle_key(&ext.sha1)));
|
||||
if override_type.is_none() && !installed {
|
||||
continue;
|
||||
}
|
||||
if self.shared_keys.contains(&key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let suppressed = matches!(
|
||||
override_type,
|
||||
Some(OverrideType::Disabled | OverrideType::Removed)
|
||||
);
|
||||
let entry = all_suppressed.entry(file.content_type()).or_insert(true);
|
||||
*entry = *entry && suppressed;
|
||||
}
|
||||
|
||||
let seen = all_suppressed.keys().copied().collect();
|
||||
let suppressed = all_suppressed
|
||||
.into_iter()
|
||||
.filter_map(|(ct, all)| all.then_some(ct))
|
||||
.collect();
|
||||
(seen, suppressed)
|
||||
}
|
||||
|
||||
fn suppressed(&self, archive: &BundleArchive) -> HashSet<ContentType> {
|
||||
let (seen, mut suppressed) = self.signals(archive);
|
||||
suppressed.extend(
|
||||
self.remembered
|
||||
.get(&archive.manifest.name)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|ct| !seen.contains(ct)),
|
||||
);
|
||||
suppressed
|
||||
}
|
||||
|
||||
async fn remember(
|
||||
&self,
|
||||
archive: &BundleArchive,
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<HashSet<ContentType>> {
|
||||
let bundle_name = &archive.manifest.name;
|
||||
let (seen, suppressed) = self.signals(archive);
|
||||
let remembered = self.remembered.get(bundle_name);
|
||||
|
||||
for ct in &seen {
|
||||
let opted_out = suppressed.contains(ct);
|
||||
if opted_out != remembered.is_some_and(|r| r.contains(ct)) {
|
||||
bundle_dao::set_type_opt_out(
|
||||
&ctx.db,
|
||||
self.cluster_id,
|
||||
bundle_name,
|
||||
*ct as i64,
|
||||
opted_out,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.suppressed(archive))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stricter than subscription
|
||||
/// emptying a bundle is how a user opts out and an absent override is not
|
||||
/// consent for a file that did not exist back then
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn addition_eligible_bundles(
|
||||
ctx: &ContentCtx,
|
||||
archives: &[BundleArchive],
|
||||
bundle_packages: &[BundleTrackedArtifactRow],
|
||||
all_linked: &[LinkedArtifactInfo],
|
||||
candidate_keys_by_bundle: &HashMap<String, HashSet<String>>,
|
||||
overrides: &[ClusterBundleOverrideRow],
|
||||
overrides_map: &HashMap<(String, String), OverrideType>,
|
||||
) -> ContentResult<HashSet<String>> {
|
||||
let live: Vec<_> = all_linked.iter().filter(|item| item.enabled).collect();
|
||||
let (live_managed_keys, _) = installed_bundle_keys(ctx, live).await?;
|
||||
|
||||
let mut eligible: HashSet<String> = bundle_packages
|
||||
.iter()
|
||||
.filter(|bp| bp.enabled != 0)
|
||||
.filter_map(|bp| bp.bundle_name.clone())
|
||||
.collect();
|
||||
|
||||
eligible.extend(infer_bundle_names_from_unique_installed_keys(
|
||||
candidate_keys_by_bundle,
|
||||
let mut eligible = live_bundle_names(bundle_packages, overrides);
|
||||
eligible.extend(infer_subscribed_from_archives(
|
||||
archives,
|
||||
overrides_map,
|
||||
&live_managed_keys,
|
||||
));
|
||||
|
||||
eligible.extend(
|
||||
overrides
|
||||
.iter()
|
||||
.filter(|o| OverrideType::parse(&o.override_type) == Some(OverrideType::Enabled))
|
||||
.map(|o| o.bundle_name.clone()),
|
||||
);
|
||||
|
||||
Ok(eligible)
|
||||
}
|
||||
|
||||
fn live_bundle_names(
|
||||
bundle_packages: &[BundleTrackedArtifactRow],
|
||||
overrides: &[ClusterBundleOverrideRow],
|
||||
) -> HashSet<String> {
|
||||
bundle_packages
|
||||
.iter()
|
||||
.filter(|bp| bp.enabled != 0)
|
||||
.filter_map(|bp| bp.bundle_name.clone())
|
||||
.chain(
|
||||
overrides
|
||||
.iter()
|
||||
.filter(|o| OverrideType::parse(&o.override_type) == Some(OverrideType::Enabled))
|
||||
.map(|o| o.bundle_name.clone()),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn installed_bundle_keys<'a>(
|
||||
ctx: &ContentCtx,
|
||||
|
|
@ -1005,13 +1236,17 @@ fn infer_bundle_names_from_unique_installed_keys(
|
|||
|
||||
fn infer_subscribed_from_archives(
|
||||
archives: &[BundleArchive],
|
||||
overrides_map: &HashMap<(String, String), OverrideType>,
|
||||
managed_keys: &HashSet<String>,
|
||||
) -> HashSet<String> {
|
||||
let mut candidate_keys_by_bundle = HashMap::new();
|
||||
for archive in archives {
|
||||
let mut keys = HashSet::new();
|
||||
for file in &archive.manifest.files {
|
||||
if !file.enabled {
|
||||
let user_override = overrides_map
|
||||
.get(&(archive.manifest.name.clone(), file.kind.package_id()))
|
||||
.copied();
|
||||
if !crate::bundles::effective_enabled(file, user_override) {
|
||||
continue;
|
||||
}
|
||||
if let BundleFileKind::Managed {
|
||||
|
|
|
|||
|
|
@ -1,6 +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,
|
||||
};
|
||||
use oneclient_core::LauncherState;
|
||||
use oneclient_core::clusters::CreateClusterOptions;
|
||||
|
|
@ -12,6 +13,9 @@ const MC_VERSION: &str = "1.21.1";
|
|||
const PROJECT_ID: &str = "sodium";
|
||||
const VERSION_ID: &str = "v1";
|
||||
const HASH: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||||
const SHARED_BUNDLE: &str = "Shared Bundle";
|
||||
const FABRIC_API_HASH: &str = "dddddddddddddddddddddddddddddddddddddddd";
|
||||
const HIDDEN_DEP_HASH: &str = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
|
||||
|
||||
fn managed_file(enabled: bool) -> BundleFile {
|
||||
BundleFile {
|
||||
|
|
@ -487,3 +491,805 @@ async fn tracked_bundle_that_never_synced_is_not_removed() {
|
|||
check.removals_available
|
||||
);
|
||||
}
|
||||
|
||||
fn resource_pack_file() -> BundleFile {
|
||||
BundleFile {
|
||||
enabled: true,
|
||||
hidden: false,
|
||||
file_type: BundleFileType::Normal,
|
||||
path: "resourcepacks/looks.zip".to_string(),
|
||||
size: 1,
|
||||
kind: BundleFileKind::Managed {
|
||||
provider: ProviderId::Modrinth,
|
||||
project_id: "looks".to_string(),
|
||||
version_id: "v1".to_string(),
|
||||
sha1: "cccccccccccccccccccccccccccccccccccccccc".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabling_every_mod_stops_new_mods_while_a_resource_pack_stays_on() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![
|
||||
managed_file(true),
|
||||
newly_shipped_file(),
|
||||
resource_pack_file(),
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
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();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"looks",
|
||||
OverrideType::Enabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let added: Vec<String> = check
|
||||
.additions_available
|
||||
.iter()
|
||||
.map(|a| a.new_file.kind.package_id())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"mods were switched off for this bundle so a new mod must not arrive: {added:?}"
|
||||
);
|
||||
assert!(
|
||||
added.iter().any(|id| id == "looks"),
|
||||
"the resource pack side of the bundle is still live: {added:?}"
|
||||
);
|
||||
assert!(
|
||||
!check
|
||||
.optional_available
|
||||
.iter()
|
||||
.any(|o| o.package_id == "newcomer"),
|
||||
"mods were switched off for this bundle so a new mod must not be offered either"
|
||||
);
|
||||
|
||||
let status = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let types = &status
|
||||
.iter()
|
||||
.find(|b| b.archive.manifest.name == BUNDLE)
|
||||
.unwrap()
|
||||
.opted_in_types;
|
||||
assert!(
|
||||
!types.contains(&ContentType::Mod) && types.contains(&ContentType::ResourcePack),
|
||||
"the package list must agree with the update path on which side is opted out: {types:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn fabric_api_file() -> BundleFile {
|
||||
BundleFile {
|
||||
enabled: true,
|
||||
hidden: false,
|
||||
file_type: BundleFileType::Normal,
|
||||
path: "mods/fabric-api.jar".to_string(),
|
||||
size: 1,
|
||||
kind: BundleFileKind::Managed {
|
||||
provider: ProviderId::Modrinth,
|
||||
project_id: "fabric-api".to_string(),
|
||||
version_id: "v1".to_string(),
|
||||
sha1: FABRIC_API_HASH.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn named_manifest(name: &str, files: Vec<BundleFile>) -> BundleManifest {
|
||||
let mut m = manifest(files);
|
||||
m.name = name.to_string();
|
||||
m
|
||||
}
|
||||
|
||||
async fn install_fabric_api(state: &LauncherState, cluster_id: i64) {
|
||||
artifact_dao::insert_artifact(
|
||||
&state.services.db,
|
||||
FABRIC_API_HASH,
|
||||
ContentType::Mod as i64,
|
||||
"artifacts/fabric-api.jar",
|
||||
"fabric-api.jar",
|
||||
Some(1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::link_cluster_artifact(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
FABRIC_API_HASH,
|
||||
"fabric-api.jar",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::upsert_provider_release(
|
||||
&state.services.db,
|
||||
ProviderId::Modrinth as i64,
|
||||
"fabric-api",
|
||||
"v1",
|
||||
FABRIC_API_HASH,
|
||||
"Fabric API",
|
||||
"1.0.0",
|
||||
None,
|
||||
MC_VERSION,
|
||||
"fabric",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::track_bundle_artifact(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
FABRIC_API_HASH,
|
||||
SHARED_BUNDLE,
|
||||
"v1",
|
||||
"fabric-api",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_mod_every_bundle_ships_does_not_keep_a_disabled_bundle_taking_new_mods() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest(
|
||||
BUNDLE,
|
||||
vec![managed_file(true), newly_shipped_file(), fabric_api_file()],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest(SHARED_BUNDLE, vec![fabric_api_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
install_fabric_api(&state, cluster_id).await;
|
||||
|
||||
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let added: Vec<String> = check
|
||||
.additions_available
|
||||
.iter()
|
||||
.map(|a| a.new_file.kind.package_id())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"a mod shared with another bundle must not hold this bundle open: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn hidden_dependency_file() -> BundleFile {
|
||||
BundleFile {
|
||||
enabled: true,
|
||||
hidden: true,
|
||||
file_type: BundleFileType::Normal,
|
||||
path: "mods/sodium-extra-lib.jar".to_string(),
|
||||
size: 1,
|
||||
kind: BundleFileKind::Managed {
|
||||
provider: ProviderId::Modrinth,
|
||||
project_id: "sodium-extra-lib".to_string(),
|
||||
version_id: "v1".to_string(),
|
||||
sha1: HIDDEN_DEP_HASH.to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_hidden_dependency_only_this_bundle_ships_does_not_hold_it_open() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![
|
||||
managed_file(true),
|
||||
newly_shipped_file(),
|
||||
hidden_dependency_file(),
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
|
||||
artifact_dao::insert_artifact(
|
||||
&state.services.db,
|
||||
HIDDEN_DEP_HASH,
|
||||
ContentType::Mod as i64,
|
||||
"artifacts/sodium-extra-lib.jar",
|
||||
"sodium-extra-lib.jar",
|
||||
Some(1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::link_cluster_artifact(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
HIDDEN_DEP_HASH,
|
||||
"sodium-extra-lib.jar",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::upsert_provider_release(
|
||||
&state.services.db,
|
||||
ProviderId::Modrinth as i64,
|
||||
"sodium-extra-lib",
|
||||
"v1",
|
||||
HIDDEN_DEP_HASH,
|
||||
"Sodium Extra Lib",
|
||||
"1.0.0",
|
||||
None,
|
||||
MC_VERSION,
|
||||
"fabric",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::track_bundle_artifact(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
HIDDEN_DEP_HASH,
|
||||
BUNDLE,
|
||||
"v1",
|
||||
"sodium-extra-lib",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
artifact_dao::update_cluster_artifact(&state.services.db, cluster_id, HASH, "sodium.jar", 0)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let added: Vec<String> = check
|
||||
.additions_available
|
||||
.iter()
|
||||
.map(|a| a.new_file.kind.package_id())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"a dependency the user was never shown must not read as wanting the bundle: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_unrelated_catalog_bundle_sharing_a_mod_does_not_undo_the_opt_out() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest(
|
||||
BUNDLE,
|
||||
vec![
|
||||
managed_file(true),
|
||||
newly_shipped_file(),
|
||||
resource_pack_file(),
|
||||
],
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest("Unrelated Bundle", vec![managed_file(true)]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"looks",
|
||||
OverrideType::Enabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let added: Vec<String> = check
|
||||
.additions_available
|
||||
.iter()
|
||||
.map(|a| a.new_file.kind.package_id())
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"a bundle the user never installed must not speak for this one: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn untracked_older_install_counts_as_opted_in() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![managed_file(true), newly_shipped_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
bundle_dao::clear_bundle_tracking(&state.services.db, cluster_id, HASH)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let bundles = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
bundles
|
||||
.iter()
|
||||
.all(|b| b.opted_in_types.contains(&ContentType::Mod)),
|
||||
"a bundle the updater infers from its installed mods must not have its files hidden from the All tab"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn package_list_and_updater_infer_the_same_bundle_through_overrides() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest(BUNDLE, vec![managed_file(true), newly_shipped_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
named_manifest(SHARED_BUNDLE, vec![managed_file(true)]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
bundle_dao::clear_bundle_tracking(&state.services.db, cluster_id, HASH)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
SHARED_BUNDLE,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
check
|
||||
.additions_available
|
||||
.iter()
|
||||
.any(|a| a.new_file.kind.package_id() == "newcomer"),
|
||||
"the updater infers this bundle from its uniquely installed mod"
|
||||
);
|
||||
|
||||
let status = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let types = &status
|
||||
.iter()
|
||||
.find(|b| b.archive.manifest.name == BUNDLE)
|
||||
.unwrap()
|
||||
.opted_in_types;
|
||||
assert!(
|
||||
types.contains(&ContentType::Mod),
|
||||
"the package list must infer the same bundle the updater does: {types:?}"
|
||||
);
|
||||
}
|
||||
|
||||
const GITHUB_MOD_ID: &str = "github-mod";
|
||||
const GITHUB_OLD_HASH: &str = "1111111111111111111111111111111111111111";
|
||||
|
||||
/// The catalog has moved on to a newer release than the one installed
|
||||
fn github_mod_file() -> BundleFile {
|
||||
BundleFile {
|
||||
enabled: true,
|
||||
hidden: false,
|
||||
file_type: BundleFileType::Normal,
|
||||
path: "mods/github-mod.jar".to_string(),
|
||||
size: 1,
|
||||
kind: BundleFileKind::External {
|
||||
file: oneclient_content::packages::types::ExternalFile {
|
||||
name: "github-mod.jar".to_string(),
|
||||
url: "https://github.com/example/github-mod/releases/download/v2/github-mod.jar"
|
||||
.to_string(),
|
||||
sha1: "2222222222222222222222222222222222222222".to_string(),
|
||||
size: 1,
|
||||
content_type: ContentType::Mod,
|
||||
},
|
||||
id: Some(GITHUB_MOD_ID.to_string()),
|
||||
meta: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_outdated_github_mod_left_on_keeps_the_bundle_taking_mods() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![
|
||||
managed_file(true),
|
||||
github_mod_file(),
|
||||
newly_shipped_file(),
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_tracked_mod(&state).await;
|
||||
|
||||
let db = &state.services.db;
|
||||
artifact_dao::update_cluster_artifact(db, cluster_id, HASH, "sodium.jar", 0)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(db, cluster_id, BUNDLE, PROJECT_ID, OverrideType::Disabled)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::insert_artifact(
|
||||
db,
|
||||
GITHUB_OLD_HASH,
|
||||
ContentType::Mod as i64,
|
||||
"artifacts/github-mod.jar",
|
||||
"github-mod.jar",
|
||||
Some(1),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
artifact_dao::link_cluster_artifact(db, cluster_id, GITHUB_OLD_HASH, "github-mod.jar")
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::track_bundle_artifact(
|
||||
db,
|
||||
cluster_id,
|
||||
GITHUB_OLD_HASH,
|
||||
BUNDLE,
|
||||
GITHUB_OLD_HASH,
|
||||
GITHUB_MOD_ID,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let check = check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(
|
||||
check
|
||||
.additions_available
|
||||
.iter()
|
||||
.any(|a| a.new_file.kind.package_id() == "newcomer"),
|
||||
"the GitHub mod is still on even though its installed release is older than the catalog's"
|
||||
);
|
||||
|
||||
let status = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let types = &status
|
||||
.iter()
|
||||
.find(|b| b.archive.manifest.name == BUNDLE)
|
||||
.unwrap()
|
||||
.opted_in_types;
|
||||
assert!(types.contains(&ContentType::Mod), "{types:?}");
|
||||
}
|
||||
|
||||
async fn cluster_with_mods_opted_out(state: &LauncherState, disabled_under: &str) -> i64 {
|
||||
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();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
disabled_under,
|
||||
PROJECT_ID,
|
||||
OverrideType::Disabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
bundle_dao::save_override(
|
||||
&state.services.db,
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"looks",
|
||||
OverrideType::Enabled,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
cluster_id
|
||||
}
|
||||
|
||||
async fn added_ids(state: &LauncherState, cluster_id: i64) -> Vec<String> {
|
||||
check_bundle_updates(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.additions_available
|
||||
.iter()
|
||||
.map(|a| a.new_file.kind.package_id())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_disable_filed_under_another_bundle_still_opts_the_mods_out() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![
|
||||
managed_file(true),
|
||||
newly_shipped_file(),
|
||||
resource_pack_file(),
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_mods_opted_out(&state, "Old Bundle").await;
|
||||
|
||||
let added = added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"the user switched sodium off while it was filed under another bundle: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_catalog_dropping_every_switched_off_mod_keeps_the_opt_out() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![managed_file(true), resource_pack_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_mods_opted_out(&state, BUNDLE).await;
|
||||
added_ids(&state, cluster_id).await;
|
||||
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![newly_shipped_file(), resource_pack_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let added = added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"mods stay opted out after the catalog replaces them: {added:?}"
|
||||
);
|
||||
let status = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let types = &status
|
||||
.iter()
|
||||
.find(|b| b.archive.manifest.name == BUNDLE)
|
||||
.unwrap()
|
||||
.opted_in_types;
|
||||
assert!(
|
||||
!types.contains(&ContentType::Mod),
|
||||
"the package list must remember the opt-out too: {types:?}"
|
||||
);
|
||||
|
||||
set_bundle_package_enabled(
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"looks",
|
||||
true,
|
||||
true,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let added = added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
!added.iter().any(|id| id == "newcomer"),
|
||||
"switching a resource pack on must not opt the mods back in: {added:?}"
|
||||
);
|
||||
|
||||
set_bundle_package_enabled(
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"newcomer",
|
||||
true,
|
||||
true,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let added = added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
added.iter().any(|id| id == "newcomer"),
|
||||
"switching a mod back on opts the mods back in: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn switching_a_new_mod_on_beside_switched_off_ones_installs_it() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![
|
||||
managed_file(true),
|
||||
newly_shipped_file(),
|
||||
resource_pack_file(),
|
||||
]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_mods_opted_out(&state, BUNDLE).await;
|
||||
added_ids(&state, cluster_id).await;
|
||||
|
||||
set_bundle_package_enabled(
|
||||
cluster_id,
|
||||
BUNDLE,
|
||||
"newcomer",
|
||||
true,
|
||||
true,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let added = added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
added.iter().any(|id| id == "newcomer"),
|
||||
"sodium staying off must not swallow the user switching newcomer on: {added:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn the_package_list_does_not_save_the_opt_out() {
|
||||
let state = oneclient_core::dev::ephemeral_state().await.unwrap();
|
||||
oneclient_core::dev::seed_bundle_archive(
|
||||
&state,
|
||||
manifest(vec![managed_file(true), resource_pack_file()]),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let cluster_id = cluster_with_mods_opted_out(&state, BUNDLE).await;
|
||||
|
||||
let status = get_bundles_with_update_status(
|
||||
cluster_id,
|
||||
state.bundles.as_ref(),
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let types = &status
|
||||
.iter()
|
||||
.find(|b| b.archive.manifest.name == BUNDLE)
|
||||
.unwrap()
|
||||
.opted_in_types;
|
||||
assert!(!types.contains(&ContentType::Mod), "{types:?}");
|
||||
assert!(
|
||||
bundle_dao::list_type_opt_outs(&state.services.db, cluster_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty(),
|
||||
"only the updater saves the opt-out"
|
||||
);
|
||||
|
||||
added_ids(&state, cluster_id).await;
|
||||
assert!(
|
||||
!bundle_dao::list_type_opt_outs(&state.services.db, cluster_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE cluster_bundle_type_opt_outs (
|
||||
cluster_id INTEGER NOT NULL,
|
||||
bundle_name TEXT NOT NULL,
|
||||
content_type INTEGER NOT NULL,
|
||||
PRIMARY KEY (cluster_id, bundle_name, content_type),
|
||||
FOREIGN KEY (cluster_id) REFERENCES clusters (id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
@ -243,3 +243,58 @@ pub async fn list_overrides(
|
|||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_type_opt_outs(
|
||||
pool: &SqlitePool,
|
||||
cluster_id: i64,
|
||||
) -> Result<Vec<(String, i64)>, sqlx::Error> {
|
||||
let rows = sqlx::query!(
|
||||
r#"
|
||||
SELECT bundle_name, content_type
|
||||
FROM cluster_bundle_type_opt_outs
|
||||
WHERE cluster_id = ?
|
||||
"#,
|
||||
cluster_id
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| (r.bundle_name, r.content_type))
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn set_type_opt_out(
|
||||
pool: &SqlitePool,
|
||||
cluster_id: i64,
|
||||
bundle_name: &str,
|
||||
content_type: i64,
|
||||
opted_out: bool,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
if opted_out {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT OR IGNORE INTO cluster_bundle_type_opt_outs (cluster_id, bundle_name, content_type)
|
||||
VALUES (?, ?, ?)
|
||||
"#,
|
||||
cluster_id,
|
||||
bundle_name,
|
||||
content_type
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM cluster_bundle_type_opt_outs
|
||||
WHERE cluster_id = ? AND bundle_name = ? AND content_type = ?
|
||||
"#,
|
||||
cluster_id,
|
||||
bundle_name,
|
||||
content_type
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,10 @@ pub fn java_arguments(
|
|||
|
||||
parsed.extend(performance_flags(java_major, java_arch, mem_max, &custom));
|
||||
|
||||
if java_major >= 21 {
|
||||
parsed.push("--add-modules=jdk.incubator.vector".to_string());
|
||||
}
|
||||
|
||||
parsed.push(format!("-Xmx{mem_max}M"));
|
||||
parsed.extend(custom);
|
||||
|
||||
|
|
@ -893,7 +897,7 @@ mod tests {
|
|||
let cp = classpaths(&dir, &libraries, &client, "x86", false).unwrap();
|
||||
|
||||
assert!(
|
||||
!cp.contains("asm-all"),
|
||||
!cp.contains("asm-all-4.1.jar"),
|
||||
"fabric aborts on two copies of org/objectweb/asm/ClassReader.class: {cp}"
|
||||
);
|
||||
assert!(cp.contains("asm-9.10.1.jar"), "{cp}");
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue