feat: polyplus mark and removal warning (#877)
* feat: polyplus mark and removal warning (some issues with merging) * chore: deleted the blue background from the polyplus mod * feat: added warning for removing bundle mods; fixed the new/updated badges on mods cards
This commit is contained in:
parent
45da20fac2
commit
495dbf05dc
15 changed files with 563 additions and 49 deletions
148
packages/oneclient_app/src/components/essential_confirm.rs
Normal file
148
packages/oneclient_app/src/components/essential_confirm.rs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
Button, Icon, IconType, Markdown, MarkdownStyle, OverlayPopup, ScrollArea,
|
||||
};
|
||||
use crate::hooks::{EssentialGuardKind, use_cluster_mutation, use_essential_guard};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||
const BODY_MAX_H: f32 = 400.;
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub struct EssentialConfirmOverlay;
|
||||
|
||||
impl Component for EssentialConfirmOverlay {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut pending = use_essential_guard();
|
||||
let cluster = use_cluster_mutation();
|
||||
|
||||
let Some(request) = pending.read().clone() else {
|
||||
return rect().into_element();
|
||||
};
|
||||
|
||||
let (title, confirm_label) = match request.kind {
|
||||
EssentialGuardKind::Disable => (format!("Turn off {}?", request.name), "Turn it off"),
|
||||
EssentialGuardKind::Remove => (format!("Remove {}?", request.name), "Remove anyway"),
|
||||
};
|
||||
|
||||
let action = request.action.clone();
|
||||
|
||||
OverlayPopup::new()
|
||||
.on_close(move |_| pending.set(None))
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.center()
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::px(440.))
|
||||
.max_width(Size::window_percent(90.))
|
||||
.spacing(14.)
|
||||
.padding(Gaps::new_all(20.))
|
||||
.corner_radius(CornerRadius::new_all(14.))
|
||||
.background(CARD_BG)
|
||||
.border(border_all_color(1., colors::component_border()))
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(10.)
|
||||
.child(
|
||||
Icon::new(IconType::AlertTriangle)
|
||||
.size(20.)
|
||||
.color(colors::code_warn()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(title)
|
||||
.font_size(16.)
|
||||
.font_weight(FontWeight::SEMI_BOLD)
|
||||
.color(colors::fg_primary()),
|
||||
),
|
||||
)
|
||||
.child(WarningBody {
|
||||
body: request.body.clone(),
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| {
|
||||
cluster.mutate(action.clone());
|
||||
pending.set(None);
|
||||
})
|
||||
.text(confirm_label),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.primary()
|
||||
.on_press(move |_| pending.set(None))
|
||||
.text("Keep it"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct WarningBody {
|
||||
body: String,
|
||||
}
|
||||
|
||||
impl Component for WarningBody {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut content_h = use_state(|| 0f32);
|
||||
let height = content_h.read().min(BODY_MAX_H);
|
||||
|
||||
ScrollArea::new()
|
||||
.width(Size::fill())
|
||||
.height(Size::px(height))
|
||||
.scrollbar_gutter(true)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.on_sized(move |e: Event<SizedEventData>| {
|
||||
let h = e.area.height();
|
||||
if (*content_h.read() - h).abs() > 0.5 {
|
||||
content_h.set(h);
|
||||
}
|
||||
})
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.padding(Gaps::new_all(12.))
|
||||
.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()
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ mod control_center;
|
|||
mod date_range_picker;
|
||||
mod dropdown;
|
||||
mod dynamic_art;
|
||||
mod essential_confirm;
|
||||
mod file_drop;
|
||||
mod flagged_install_popup;
|
||||
mod generic_prompt;
|
||||
|
|
@ -63,6 +64,7 @@ pub use control_center::ControlCenter;
|
|||
pub use date_range_picker::{DateRange, DateRangePicker};
|
||||
pub use dropdown::Dropdown;
|
||||
pub use dynamic_art::{ART_PREVIEW_EDGE, DynamicArt};
|
||||
pub use essential_confirm::EssentialConfirmOverlay;
|
||||
pub use file_drop::FileDropOverlay;
|
||||
pub use flagged_install_popup::FlaggedInstallPopup;
|
||||
pub use generic_prompt::GenericPromptOverlay;
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ fn entry_from_item(
|
|||
.and_then(|project_id| meta.get(&(item.provider, project_id.clone())));
|
||||
|
||||
PackageEntry {
|
||||
essential: crate::essential::lookup(item.provider, &package_id),
|
||||
package_id,
|
||||
bundle_name: item.offer.as_ref().map(|(bundle, _)| bundle.clone()),
|
||||
provider: item.provider,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@ use oneclient_content::packages::ProviderId;
|
|||
use oneclient_core::SeenStatus;
|
||||
|
||||
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
||||
use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation};
|
||||
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,
|
||||
};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{ImageFallbackExt, border_all_color};
|
||||
|
|
@ -16,6 +21,10 @@ pub(crate) const CARD_H: f32 = 84.;
|
|||
pub(crate) const CARD_GRID_H: f32 = 112.;
|
||||
pub(crate) const GRID_GAP: f32 = 12.;
|
||||
pub(crate) const GRID_MIN_W: f32 = 290.;
|
||||
const GRID_CARD_PADDING: f32 = 14.;
|
||||
const BADGE_STRIP_H: f32 = 20.;
|
||||
const BADGE_STRIP_LIFT: f32 = 2.;
|
||||
const TAG_LETTER_SPACING: f32 = 0.6;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum CardLayout {
|
||||
|
|
@ -57,6 +66,7 @@ pub struct PackageEntry {
|
|||
pub update_available: bool,
|
||||
/// Recency badge state cleared once the user views the list
|
||||
pub seen_status: SeenStatus,
|
||||
pub essential: Option<&'static EssentialPackage>,
|
||||
}
|
||||
|
||||
impl PackageEntry {
|
||||
|
|
@ -128,8 +138,12 @@ impl Component for PackageRow {
|
|||
let package_type = self.package_type;
|
||||
let layout = self.layout;
|
||||
let cluster = use_cluster_mutation();
|
||||
let guard = use_essential_guard();
|
||||
let warnings_query = use_disable_warnings();
|
||||
let hovered = use_state(|| false);
|
||||
|
||||
let warning_body = disable_warning_body(&item, disable_warnings(&warnings_query));
|
||||
|
||||
let icon_size = match layout {
|
||||
CardLayout::List => 44.,
|
||||
CardLayout::Grid => 40.,
|
||||
|
|
@ -143,21 +157,35 @@ impl Component for PackageRow {
|
|||
let package_id = item.package_id.clone();
|
||||
let enabled_now = item.enabled;
|
||||
let manifest_default = item.manifest_default;
|
||||
let name = item.name.clone();
|
||||
let mut guard = guard;
|
||||
(move |()| {
|
||||
if let Some(h) = &hash {
|
||||
cluster.mutate(ClusterAction::SetArtifactEnabled {
|
||||
let action = if let Some(h) = &hash {
|
||||
ClusterAction::SetArtifactEnabled {
|
||||
cluster_id,
|
||||
hash: h.clone(),
|
||||
enabled: !enabled_now,
|
||||
});
|
||||
}
|
||||
} else if let Some(bundle) = &bundle_name {
|
||||
cluster.mutate(ClusterAction::SetBundlePackageEnabled {
|
||||
ClusterAction::SetBundlePackageEnabled {
|
||||
cluster_id,
|
||||
bundle_name: bundle.clone(),
|
||||
package_id: package_id.clone(),
|
||||
enabled: !enabled_now,
|
||||
manifest_default,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
match warning_body.clone().filter(|_| enabled_now) {
|
||||
Some(body) => guard.set(Some(PendingEssential {
|
||||
name: name.clone(),
|
||||
body,
|
||||
kind: EssentialGuardKind::Disable,
|
||||
action,
|
||||
})),
|
||||
None => cluster.mutate(action),
|
||||
}
|
||||
})
|
||||
.into()
|
||||
|
|
@ -166,9 +194,7 @@ impl Component for PackageRow {
|
|||
let on_context = has_menu(&item).then(|| self.on_context.clone());
|
||||
|
||||
match layout {
|
||||
CardLayout::List => {
|
||||
list_card(&item, package_type, cluster_id, icon, on_toggle, on_context)
|
||||
}
|
||||
CardLayout::List => list_card(&item, package_type, cluster_id, icon, on_toggle, on_context),
|
||||
CardLayout::Grid => grid_card(
|
||||
&item,
|
||||
package_type,
|
||||
|
|
@ -183,6 +209,18 @@ impl Component for PackageRow {
|
|||
}
|
||||
}
|
||||
|
||||
fn disable_warning_body(
|
||||
item: &PackageEntry,
|
||||
warnings: Option<oneclient_core::DisableWarnings>,
|
||||
) -> Option<String> {
|
||||
let bundled = item.in_bundle() && item.provider == ProviderId::Modrinth;
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_menu(item: &PackageEntry) -> bool {
|
||||
item.is_remote() || item.hash.is_some()
|
||||
}
|
||||
|
|
@ -353,7 +391,13 @@ pub(crate) fn grid_card(
|
|||
let floating = (item.is_outdated() || item.recency_badge().is_some()).then(|| {
|
||||
rect()
|
||||
.horizontal()
|
||||
.position(Position::new_absolute().bottom(10.).right(10.))
|
||||
.width(Size::fill())
|
||||
.height(Size::px(BADGE_STRIP_H))
|
||||
.position(
|
||||
Position::new_absolute()
|
||||
.bottom(-(GRID_CARD_PADDING + BADGE_STRIP_H / 2.) + BADGE_STRIP_LIFT),
|
||||
)
|
||||
.main_align(Alignment::End)
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(4.)
|
||||
.maybe_child(item.is_outdated().then(outdated_badge))
|
||||
|
|
@ -366,11 +410,10 @@ pub(crate) fn grid_card(
|
|||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.spacing(9.)
|
||||
.padding(Gaps::new_all(14.))
|
||||
.padding(Gaps::new_all(GRID_CARD_PADDING))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(bg.with_a(alpha))
|
||||
.border(border_all_color(1., border))
|
||||
.overflow(Overflow::Clip)
|
||||
.content(Content::Flex)
|
||||
.cursor(CursorIcon::Pointer)
|
||||
.on_pointer_enter(move |_| hovered.set(true))
|
||||
|
|
@ -383,6 +426,7 @@ pub(crate) fn grid_card(
|
|||
.vertical()
|
||||
.width(Size::fill())
|
||||
.height(Size::flex(1.0))
|
||||
.overflow(Overflow::Clip)
|
||||
.maybe_child(description),
|
||||
)
|
||||
.maybe_child(floating)
|
||||
|
|
@ -727,36 +771,37 @@ pub fn github_badge() -> Element {
|
|||
}
|
||||
|
||||
fn outdated_badge() -> Element {
|
||||
accent_badge(
|
||||
Icon::new(IconType::RefreshCw01)
|
||||
.size(12.)
|
||||
.color(colors::brand())
|
||||
.into_element(),
|
||||
"Update available".to_string(),
|
||||
colors::brand(),
|
||||
)
|
||||
status_tag("Update available", colors::brand())
|
||||
}
|
||||
|
||||
fn new_badge() -> Element {
|
||||
accent_badge(
|
||||
Icon::new(IconType::Plus)
|
||||
.size(12.)
|
||||
.color(colors::success())
|
||||
.into_element(),
|
||||
"New".to_string(),
|
||||
colors::success(),
|
||||
)
|
||||
status_tag("New", colors::success())
|
||||
}
|
||||
|
||||
fn updated_badge() -> Element {
|
||||
accent_badge(
|
||||
Icon::new(IconType::RefreshCcw02)
|
||||
.size(12.)
|
||||
.color(colors::success())
|
||||
.into_element(),
|
||||
"Updated".to_string(),
|
||||
colors::success(),
|
||||
)
|
||||
status_tag("Updated", colors::success())
|
||||
}
|
||||
|
||||
fn status_tag(text: &str, accent: Color) -> Element {
|
||||
rect()
|
||||
.corner_radius(CornerRadius::new_all(4.))
|
||||
.background(colors::component_bg())
|
||||
.child(
|
||||
rect()
|
||||
.padding(Gaps::new(1., 6., 3., 6. + TAG_LETTER_SPACING))
|
||||
.corner_radius(CornerRadius::new_all(4.))
|
||||
.background(accent.with_a(38))
|
||||
.border(border_all_color(1., accent.with_a(96)))
|
||||
.child(
|
||||
label()
|
||||
.text(text.to_uppercase())
|
||||
.font_size(9.)
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.letter_spacing(TAG_LETTER_SPACING)
|
||||
.color(accent),
|
||||
),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn local_badge() -> Element {
|
||||
|
|
|
|||
44
packages/oneclient_app/src/essential.rs
Normal file
44
packages/oneclient_app/src/essential.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use oneclient_content::packages::ProviderId;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct EssentialPackage {
|
||||
pub provider: ProviderId,
|
||||
pub project_id: &'static str,
|
||||
pub name: &'static str,
|
||||
pub disable_body: &'static str,
|
||||
pub remove_body: &'static str,
|
||||
}
|
||||
|
||||
pub const ESSENTIAL_PACKAGES: &[EssentialPackage] = &[EssentialPackage {
|
||||
provider: ProviderId::Modrinth,
|
||||
project_id: "Iw9mZi4a",
|
||||
name: "PolyPlus+",
|
||||
disable_body: "PolyPlus+ powers your Poly+ cosmetics and playtime tracking. Turning it off disables them in game, and it stays off through future bundle updates until you turn it back on.",
|
||||
remove_body: "PolyPlus+ powers your Poly+ cosmetics and playtime tracking. Removing it disables them in game and deletes the file from this cluster, so you would have to install it again from the Browser.",
|
||||
}];
|
||||
|
||||
pub fn lookup(provider: ProviderId, package_id: &str) -> Option<&'static EssentialPackage> {
|
||||
ESSENTIAL_PACKAGES
|
||||
.iter()
|
||||
.find(|package| package.provider == provider && package.project_id == package_id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn polyplus_is_essential_on_modrinth_only() {
|
||||
assert_eq!(
|
||||
lookup(ProviderId::Modrinth, "Iw9mZi4a").map(|p| p.name),
|
||||
Some("PolyPlus+")
|
||||
);
|
||||
assert!(lookup(ProviderId::CurseForge, "Iw9mZi4a").is_none());
|
||||
assert!(lookup(ProviderId::Local, "Iw9mZi4a").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_packages_are_not_guarded() {
|
||||
assert!(lookup(ProviderId::Modrinth, "AibBIVmj").is_none());
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ use oneclient_content::packages::ProviderId;
|
|||
use oneclient_content::packages::types::SearchSort;
|
||||
use oneclient_db::models::ClusterId;
|
||||
|
||||
use crate::hooks::ClusterAction;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ActiveClusterState(pub State<Option<ClusterId>>);
|
||||
|
||||
|
|
@ -91,6 +93,31 @@ pub fn use_link_confirm() -> State<Option<String>> {
|
|||
consume_root_context::<LinkConfirmState>().0
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum EssentialGuardKind {
|
||||
Disable,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct PendingEssential {
|
||||
pub name: String,
|
||||
pub body: String,
|
||||
pub kind: EssentialGuardKind,
|
||||
pub action: ClusterAction,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EssentialGuardState(pub State<Option<PendingEssential>>);
|
||||
|
||||
pub fn use_provide_essential_guard(state: EssentialGuardState) {
|
||||
use_provide_root_context(move || state.clone());
|
||||
}
|
||||
|
||||
pub fn use_essential_guard() -> State<Option<PendingEssential>> {
|
||||
consume_root_context::<EssentialGuardState>().0
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BrowserUiState {
|
||||
pub query: String,
|
||||
|
|
|
|||
|
|
@ -15,20 +15,21 @@ pub use view_state::{PersistedView, use_view_state};
|
|||
|
||||
pub use active_cluster::{
|
||||
ActiveClusterState, BROWSER_COMPAT_DEFAULT, BrowserCompatState, BrowserStateStore,
|
||||
BrowserTypeState, BrowserUiState, DataPackWorldState, LinkConfirmState,
|
||||
OnboardingSelectionState, SplashState, StartMaximizedState, use_active_cluster_id,
|
||||
use_browser_compat, use_browser_state_store, use_browser_type, use_datapack_world,
|
||||
use_link_confirm, use_onboarding_selection, use_provide_active_cluster,
|
||||
use_provide_browser_compat, use_provide_browser_state, use_provide_browser_type,
|
||||
use_provide_datapack_world, use_provide_link_confirm, use_provide_onboarding_selection,
|
||||
use_provide_splash, use_provide_start_maximized, use_splash, use_start_maximized,
|
||||
BrowserTypeState, BrowserUiState, DataPackWorldState, EssentialGuardKind, EssentialGuardState,
|
||||
LinkConfirmState, OnboardingSelectionState, PendingEssential, SplashState, StartMaximizedState,
|
||||
use_active_cluster_id, use_browser_compat, use_browser_state_store, use_browser_type,
|
||||
use_datapack_world, use_essential_guard, use_link_confirm, use_onboarding_selection,
|
||||
use_provide_active_cluster, use_provide_browser_compat, use_provide_browser_state,
|
||||
use_provide_browser_type, use_provide_datapack_world, use_provide_essential_guard,
|
||||
use_provide_link_confirm, use_provide_onboarding_selection, use_provide_splash,
|
||||
use_provide_start_maximized, use_splash, use_start_maximized,
|
||||
};
|
||||
|
||||
pub use actions::{Actions, NotificationBuilder, PumpSignal};
|
||||
pub use queries::{
|
||||
AddOfflineAccountKeys, BROWSE_PAGE_SIZE, BeginMicrosoftLoginMutation, CachedImageQuery,
|
||||
CancelMicrosoftLoginKeys, CancelMicrosoftLoginMutation, ClusterAction, ClusterBundles,
|
||||
ClusterLogsQuery, ClusterMutation, DiscardLeftoversKeys, DiscardLeftoversMutation,
|
||||
ClusterLogsQuery, ClusterMutation, DisableWarningsQuery, DiscardLeftoversKeys, DiscardLeftoversMutation,
|
||||
FinishMicrosoftLoginMutation, LeftoversQuery, LogAction, LogContentQuery, MigrationQuery,
|
||||
OnboardingBundlesQuery, RefreshAccountKeys, RemoveAccountKeys, ScreenshotAction,
|
||||
SetDefaultAccountKeys, StorageAction, StorageActionMutation, StorageReportQuery, TermsQuery,
|
||||
|
|
@ -36,7 +37,7 @@ pub use queries::{
|
|||
UseRemoveAccount, UseScreenshotAction, UseSetDefaultAccount, UseStorageAction, UseUploadLog,
|
||||
VERSIONS_PAGE_SIZE, accounts_have_microsoft, bundle_overrides_map, bundles_with_status_items,
|
||||
category_list, changelog_error, changelog_groups, changelog_is_loading, cluster_content_items,
|
||||
content_type_for_slug, has_migration_data, invalidate_cluster_content_queries,
|
||||
content_type_for_slug, disable_warnings, has_migration_data, invalidate_cluster_content_queries,
|
||||
invalidate_cluster_queries, invalidate_java_queries, invalidate_leftovers_queries,
|
||||
invalidate_logs_queries, invalidate_profile_queries, invalidate_screenshots_queries,
|
||||
invalidate_storage_queries, java_runtimes, latest_changelog_version, loaded_image,
|
||||
|
|
@ -53,7 +54,7 @@ pub use queries::{
|
|||
use_cancel_microsoft_login, use_changelog, use_cluster, use_cluster_analytics,
|
||||
use_cluster_content, use_cluster_logs, use_cluster_mutation, use_cluster_profile,
|
||||
use_cluster_screenshots, use_cluster_settings, use_clusters, use_current_account,
|
||||
use_default_account, use_discard_leftovers, use_finish_microsoft_login, use_game_profile,
|
||||
use_default_account, use_disable_warnings, use_discard_leftovers, use_finish_microsoft_login, use_game_profile,
|
||||
use_global_analytics, use_java_runtimes, use_leftovers, use_loader_versions, use_local_image,
|
||||
use_log_action, use_log_content, use_migration, use_named_profiles, use_onboarding_bundles,
|
||||
use_package_categories, use_package_meta_batch, use_package_project, use_package_search,
|
||||
|
|
|
|||
34
packages/oneclient_app/src/hooks/queries/disable_warnings.rs
Normal file
34
packages/oneclient_app/src/hooks/queries/disable_warnings.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use freya::query::{Query, QueryCapability, UseQuery, use_query};
|
||||
use oneclient_core::{DisableWarnings, LauncherError, fetch_disable_warnings};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DisableWarningsKeys {
|
||||
pub meta_url_base: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DisableWarningsQuery;
|
||||
|
||||
impl QueryCapability for DisableWarningsQuery {
|
||||
type Ok = DisableWarnings;
|
||||
type Err = LauncherError;
|
||||
type Keys = DisableWarningsKeys;
|
||||
|
||||
async fn run(&self, _keys: &Self::Keys) -> Result<Self::Ok, Self::Err> {
|
||||
let state = crate::launcher::state()?;
|
||||
fetch_disable_warnings(&state.services.requester).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn use_disable_warnings() -> UseQuery<DisableWarningsQuery> {
|
||||
let meta_url_base = super::use_meta_url_key();
|
||||
|
||||
use_query(Query::new(
|
||||
DisableWarningsKeys { meta_url_base },
|
||||
DisableWarningsQuery,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn disable_warnings(query: &UseQuery<DisableWarningsQuery>) -> Option<DisableWarnings> {
|
||||
super::state::settled_or_loading(query)
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ mod bundles;
|
|||
mod changelog;
|
||||
mod cluster_content;
|
||||
mod clusters;
|
||||
mod disable_warnings;
|
||||
mod folder_watch;
|
||||
mod image;
|
||||
mod java;
|
||||
|
|
@ -59,6 +60,7 @@ pub use changelog::{
|
|||
};
|
||||
pub use cluster_content::{cluster_content_items, use_cluster_content};
|
||||
pub use clusters::{use_cluster, use_clusters};
|
||||
pub use disable_warnings::{DisableWarningsQuery, disable_warnings, use_disable_warnings};
|
||||
pub use image::{CachedImageQuery, loaded_image, use_cached_image};
|
||||
pub use java::{
|
||||
invalidate_java_queries, java_runtimes, provider_versions, use_java_runtimes,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
mod assets;
|
||||
pub mod cli;
|
||||
mod components;
|
||||
pub mod essential;
|
||||
pub mod events;
|
||||
pub(crate) mod file_content;
|
||||
pub mod hooks;
|
||||
|
|
@ -32,7 +33,7 @@ mod view;
|
|||
pub mod constants;
|
||||
|
||||
pub use assets::AppAssets;
|
||||
pub use components::ConfirmLinkOverlay;
|
||||
pub use components::{ConfirmLinkOverlay, EssentialConfirmOverlay};
|
||||
pub use events::EventPump;
|
||||
pub use hooks::*;
|
||||
pub use routes::{Route, router};
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ use freya::radio::use_init_radio_station;
|
|||
use oneclient_app::ipc::{self, Claim};
|
||||
use oneclient_app::state::{AppChannel, AppState, LauncherInit};
|
||||
use oneclient_app::{
|
||||
Actions, ConfirmLinkOverlay, EventPump, LinkConfirmState, StartMaximizedState, cli, constants,
|
||||
events, microsoft_java, platform, router, theme, use_provide_actions, use_provide_link_confirm,
|
||||
Actions, ConfirmLinkOverlay, EssentialConfirmOverlay, EssentialGuardState, EventPump,
|
||||
LinkConfirmState, StartMaximizedState, cli, constants, events, microsoft_java, platform,
|
||||
router, theme, use_provide_actions, use_provide_essential_guard, use_provide_link_confirm,
|
||||
use_provide_start_maximized,
|
||||
};
|
||||
use std::cell::Cell;
|
||||
|
|
@ -82,12 +83,16 @@ impl App for OneClientApp {
|
|||
let link_confirm = use_state(|| None::<String>);
|
||||
use_provide_link_confirm(LinkConfirmState(link_confirm));
|
||||
|
||||
let essential_guard = use_state(|| None);
|
||||
use_provide_essential_guard(EssentialGuardState(essential_guard));
|
||||
|
||||
use_provide_start_maximized(StartMaximizedState(self.start_maximized));
|
||||
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.height(Size::fill())
|
||||
.child(ConfirmLinkOverlay)
|
||||
.child(EssentialConfirmOverlay)
|
||||
.child(router())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,6 +276,7 @@ fn make_row(
|
|||
.unwrap_or_default();
|
||||
|
||||
PackageEntry {
|
||||
essential: crate::essential::lookup(provider, &package_id),
|
||||
package_id,
|
||||
bundle_name,
|
||||
provider,
|
||||
|
|
@ -474,6 +475,12 @@ impl Component for PackageManager {
|
|||
let hidden = *hidden_filter.read();
|
||||
let card_layout = CardLayout::from(*layout.read());
|
||||
|
||||
let disabled_essentials: Vec<&'static str> = items
|
||||
.iter()
|
||||
.filter(|package| !package.enabled)
|
||||
.filter_map(|package| package.essential.map(|essential| essential.name))
|
||||
.collect();
|
||||
|
||||
let tabs = build_tabs(&self.categories, &items, hidden);
|
||||
let active_idx = (*active.read()).min(tabs.len().saturating_sub(1));
|
||||
let tab_filter = tabs.get(active_idx);
|
||||
|
|
@ -508,6 +515,9 @@ impl Component for PackageManager {
|
|||
if session_live {
|
||||
notices.push(views::running_notice(noun_plural, content_type));
|
||||
}
|
||||
if !disabled_essentials.is_empty() {
|
||||
notices.push(views::essential_notice(&disabled_essentials));
|
||||
}
|
||||
|
||||
let (advanced, filtered): (Vec<_>, Vec<_>) = filtered.into_iter().partition(|p| p.advanced);
|
||||
|
||||
|
|
|
|||
|
|
@ -360,6 +360,16 @@ pub(super) fn global_notice(noun_plural: &'static str) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
pub(super) fn essential_notice(names: &[&'static str]) -> String {
|
||||
match names {
|
||||
[only] => format!("{only} is turned off, so its features will not work in game."),
|
||||
_ => format!(
|
||||
"{} are turned off, so their features will not work in game.",
|
||||
names.join(", ")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notice_bar(text: String) -> Element {
|
||||
rect()
|
||||
.horizontal()
|
||||
|
|
|
|||
182
packages/oneclient_core/src/disable_warnings.rs
Normal file
182
packages/oneclient_core/src/disable_warnings.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{LauncherError, LauncherResult};
|
||||
use oneclient_common::paths;
|
||||
use oneclient_net::RequestClient;
|
||||
use oneclient_net::{EtagPolicy, fetch_cached};
|
||||
|
||||
const MESSAGES_DIR: &str = "/oneclient/bundles/disable-warnings/";
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct Manifest {
|
||||
#[serde(default)]
|
||||
mods: HashMap<String, Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Entry {
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct DisableWarnings {
|
||||
mods: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl DisableWarnings {
|
||||
pub fn body_for(&self, project_id: &str) -> Option<&str> {
|
||||
self.mods.get(project_id).map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
fn message_file_name(path: &str) -> Option<&str> {
|
||||
let name = path.strip_prefix(MESSAGES_DIR)?;
|
||||
let valid = name.ends_with(".md")
|
||||
&& name.len() > ".md".len()
|
||||
&& !name.contains("..")
|
||||
&& !name.contains(['/', '\\', '?', '#']);
|
||||
valid.then_some(name)
|
||||
}
|
||||
|
||||
async fn fetch_message(net: &RequestClient, path: &str) -> Option<String> {
|
||||
let Some(file_name) = message_file_name(path) else {
|
||||
tracing::warn!(path, "ignoring disable warning outside {MESSAGES_DIR}");
|
||||
return None;
|
||||
};
|
||||
|
||||
let url = format!("{}{path}", net.config().meta_url_base);
|
||||
let cache_path = match paths::caches_dir() {
|
||||
Ok(dir) => dir.join("disable-warnings").join(file_name),
|
||||
Err(err) => {
|
||||
tracing::warn!(%err, "cannot resolve the cache dir for disable warnings");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match fetch_cached(net, &url, &cache_path, EtagPolicy::CommitNow).await {
|
||||
Ok(Some(fetched)) => Some(fetched.text()).filter(|body| !body.trim().is_empty()),
|
||||
Ok(None) => {
|
||||
tracing::warn!(path, "disable warning is unavailable and not cached");
|
||||
None
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(path, %err, "failed to fetch disable warning");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(manifest: Manifest, bodies: &HashMap<String, String>) -> DisableWarnings {
|
||||
let mods = manifest
|
||||
.mods
|
||||
.into_iter()
|
||||
.filter_map(|(project_id, entry)| {
|
||||
bodies
|
||||
.get(&entry.message)
|
||||
.map(|body| (project_id, body.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
DisableWarnings { mods }
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(net))]
|
||||
pub async fn fetch_disable_warnings(net: &RequestClient) -> LauncherResult<DisableWarnings> {
|
||||
let url = format!(
|
||||
"{}/oneclient/bundles/disable-warnings.json",
|
||||
net.config().meta_url_base
|
||||
);
|
||||
let cache_path = paths::caches_dir()?.join("disable-warnings.json");
|
||||
|
||||
let fetched = fetch_cached(net, &url, &cache_path, EtagPolicy::CommitNow)
|
||||
.await?
|
||||
.ok_or_else(|| LauncherError::InvalidSettingsProfile {
|
||||
reason: "disable warnings are unavailable and not cached".to_string(),
|
||||
})?;
|
||||
let manifest: Manifest = fetched.json()?;
|
||||
|
||||
let paths: HashSet<&str> = manifest
|
||||
.mods
|
||||
.values()
|
||||
.map(|entry| entry.message.as_str())
|
||||
.collect();
|
||||
|
||||
let mut bodies = HashMap::new();
|
||||
for path in paths {
|
||||
if let Some(body) = fetch_message(net, path).await {
|
||||
bodies.insert(path.to_string(), body);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolve(manifest, &bodies))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse(raw: &str) -> Manifest {
|
||||
serde_json::from_str(raw).unwrap()
|
||||
}
|
||||
|
||||
fn bodies(pairs: &[(&str, &str)]) -> HashMap<String, String> {
|
||||
pairs
|
||||
.iter()
|
||||
.map(|(path, body)| (path.to_string(), body.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_listed_mods_get_a_warning() {
|
||||
let manifest = parse(
|
||||
r#"{"mods":{"Iw9mZi4a":{"message":"/oneclient/bundles/disable-warnings/polyplus.md"}}}"#,
|
||||
);
|
||||
let warnings = resolve(
|
||||
manifest,
|
||||
&bodies(&[("/oneclient/bundles/disable-warnings/polyplus.md", "Poly+")]),
|
||||
);
|
||||
|
||||
assert_eq!(warnings.body_for("Iw9mZi4a"), Some("Poly+"));
|
||||
assert_eq!(warnings.body_for("AANobbMI"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unfetched_message_means_no_warning() {
|
||||
let manifest = parse(
|
||||
r#"{"mods":{"Iw9mZi4a":{"message":"/oneclient/bundles/disable-warnings/polyplus.md"}}}"#,
|
||||
);
|
||||
let warnings = resolve(manifest, &HashMap::new());
|
||||
|
||||
assert_eq!(warnings.body_for("Iw9mZi4a"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_manifest_means_no_warnings() {
|
||||
let warnings = resolve(parse("{}"), &HashMap::new());
|
||||
|
||||
assert_eq!(warnings.body_for("Iw9mZi4a"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_paths_are_confined_to_the_warnings_dir() {
|
||||
assert_eq!(
|
||||
message_file_name("/oneclient/bundles/disable-warnings/polyplus.md"),
|
||||
Some("polyplus.md")
|
||||
);
|
||||
assert_eq!(
|
||||
message_file_name("/oneclient/bundles/disable-warnings/../x.md"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
message_file_name("/oneclient/bundles/disable-warnings/a/b.md"),
|
||||
None
|
||||
);
|
||||
assert_eq!(message_file_name("/oneclient/tos.json"), None);
|
||||
assert_eq!(
|
||||
message_file_name("/oneclient/bundles/disable-warnings/.md"),
|
||||
None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ pub mod dev;
|
|||
|
||||
pub mod changelog;
|
||||
pub mod clusters;
|
||||
pub mod disable_warnings;
|
||||
mod error;
|
||||
pub mod game;
|
||||
pub mod images;
|
||||
|
|
@ -27,6 +28,7 @@ pub use clusters::{
|
|||
Cluster, ClusterError, ClusterManager, ClusterStage, ClusterUpdate, CreateClusterOptions,
|
||||
ensure_from_bundles, ensure_from_versions, estimate_cluster_download, required_java_major,
|
||||
};
|
||||
pub use disable_warnings::{DisableWarnings, fetch_disable_warnings};
|
||||
pub use error::{LauncherError, LauncherResult, SentryExclusion};
|
||||
pub use game::{GameError, LaunchedGame, get_loader_versions, launch_cluster};
|
||||
pub use images::ImageCacheStore;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue