feat: added deleting browser and local mods (#853)
* feat: added deleting browser and local mods by context menu and on list view by trashcan icon * fix: only confirm shared deletes that reach other clusters * fix: confirm shared removals from the browser package page too * fix: keep "View in folder" alive after the context menu closes --------- Co-authored-by: LynithDev <61880709+LynithDev@users.noreply.github.com>
This commit is contained in:
parent
ac66752b99
commit
abc452f294
12 changed files with 247 additions and 52 deletions
|
|
@ -27,6 +27,7 @@ mod navbar;
|
|||
mod notifications;
|
||||
mod optional_mods_popup;
|
||||
mod overlay_popup;
|
||||
mod package_delete_dialog;
|
||||
mod package_row;
|
||||
mod package_update_popup;
|
||||
mod pagination;
|
||||
|
|
@ -78,6 +79,7 @@ pub use navbar::{AppNavbar, OnboardingNavbar};
|
|||
pub use notifications::NotificationCenter;
|
||||
pub use optional_mods_popup::OptionalModsPopup;
|
||||
pub use overlay_popup::{OVERLAY_BASE_LEVEL, OVERLAY_MAX_LEVEL, OverlayPopup, overlay_is_topmost};
|
||||
pub use package_delete_dialog::use_shared_delete;
|
||||
pub(crate) use package_row::{CARD_GRID_H, GRID_GAP, GRID_MIN_W, grid_card, package_icon};
|
||||
pub use package_row::{CardLayout, PackageEntry, PackageRow, package_context_menu, provider_badge};
|
||||
pub use package_update_popup::PackageUpdatePopup;
|
||||
|
|
|
|||
139
packages/oneclient_app/src/components/package_delete_dialog.rs
Normal file
139
packages/oneclient_app/src/components/package_delete_dialog.rs
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
use freya::prelude::*;
|
||||
|
||||
use crate::components::{Button, Icon, IconType, OverlayPopup};
|
||||
use crate::theme::colors;
|
||||
use crate::ui::border_all_color;
|
||||
|
||||
const CARD_BG: Color = Color::from_rgb(26, 34, 41);
|
||||
|
||||
pub fn use_shared_delete(
|
||||
delete: impl Into<EventHandler<(String, String)>>,
|
||||
) -> (EventHandler<(String, String)>, Option<Element>) {
|
||||
let mut pending = use_state(|| None::<(String, String, usize)>);
|
||||
let delete = delete.into();
|
||||
|
||||
let request = {
|
||||
let delete = delete.clone();
|
||||
EventHandler::new_current(move |(name, hash): (String, String)| {
|
||||
let delete = delete.clone();
|
||||
spawn(async move {
|
||||
let clusters = match crate::launcher::state() {
|
||||
Ok(state) => oneclient_core::clusters_sharing_artifact(
|
||||
&hash,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
tracing::warn!(%err, "could not check whether this package is shared");
|
||||
None
|
||||
}),
|
||||
Err(_) => None,
|
||||
};
|
||||
match clusters {
|
||||
Some(clusters) if clusters > 1 => pending.set(Some((name, hash, clusters))),
|
||||
_ => delete.call((name, hash)),
|
||||
}
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
let dialog = pending.read().clone().map(|(name, hash, clusters)| {
|
||||
SharedPackageDeleteDialog {
|
||||
name: name.clone(),
|
||||
clusters,
|
||||
on_cancel: (move |()| pending.set(None)).into(),
|
||||
on_confirm: (move |()| {
|
||||
delete.call((name.clone(), hash.clone()));
|
||||
pending.set(None);
|
||||
})
|
||||
.into(),
|
||||
}
|
||||
.into_element()
|
||||
});
|
||||
|
||||
(request, dialog)
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct SharedPackageDeleteDialog {
|
||||
name: String,
|
||||
clusters: usize,
|
||||
on_cancel: EventHandler<()>,
|
||||
on_confirm: EventHandler<()>,
|
||||
}
|
||||
|
||||
impl Component for SharedPackageDeleteDialog {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let title = format!("Delete {} from every cluster?", self.name);
|
||||
let body = format!(
|
||||
"Resource packs and shaders are shared, so this deletes it from all {} clusters that use it.",
|
||||
self.clusters
|
||||
);
|
||||
let close = self.on_cancel.clone();
|
||||
let cancel = self.on_cancel.clone();
|
||||
let confirm = self.on_confirm.clone();
|
||||
|
||||
OverlayPopup::new().on_close(move |_| close.call(())).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(
|
||||
label()
|
||||
.text(body)
|
||||
.font_size(12.)
|
||||
.width(Size::fill())
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.main_align(Alignment::End)
|
||||
.spacing(8.)
|
||||
.child(
|
||||
Button::new()
|
||||
.secondary()
|
||||
.on_press(move |_| cancel.call(()))
|
||||
.text("Cancel"),
|
||||
)
|
||||
.child(
|
||||
Button::new()
|
||||
.danger()
|
||||
.on_press(move |_| confirm.call(()))
|
||||
.child(Icon::new(IconType::Trash01).size(14.))
|
||||
.text("Delete"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
use freya::prelude::*;
|
||||
use freya::query::UseMutation;
|
||||
use freya::router::RouterContext;
|
||||
use oneclient_content::packages::ProviderId;
|
||||
use oneclient_core::SeenStatus;
|
||||
|
||||
use crate::components::{ContextMenu, Icon, IconType, toggle_controlled};
|
||||
use crate::hooks::{
|
||||
ClusterAction, ClusterMutation, loaded_image, use_cached_image, use_cluster_mutation,
|
||||
};
|
||||
use crate::hooks::{ClusterAction, loaded_image, use_cached_image, use_cluster_mutation};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::ui::{ImageFallbackExt, border_all_color};
|
||||
|
|
@ -194,7 +191,7 @@ pub fn package_context_menu(
|
|||
item: &PackageEntry,
|
||||
cluster_id: i64,
|
||||
package_type: &'static str,
|
||||
cluster: UseMutation<ClusterMutation>,
|
||||
on_delete: EventHandler<(String, String)>,
|
||||
) -> ContextMenu {
|
||||
let mut menu = ContextMenu::new(x, y).title(item.name.clone());
|
||||
|
||||
|
|
@ -212,21 +209,21 @@ pub fn package_context_menu(
|
|||
}
|
||||
|
||||
if let Some(hash) = item.hash.clone() {
|
||||
menu = menu.action(IconType::Folder, "View in folder", move |()| {
|
||||
reveal_in_store(hash.clone());
|
||||
});
|
||||
menu = menu.action(
|
||||
IconType::Folder,
|
||||
"View in folder",
|
||||
EventHandler::new_current(move |()| reveal_in_store(hash.clone())),
|
||||
);
|
||||
}
|
||||
|
||||
if item.installed && !item.in_bundle() {
|
||||
let hash = item.hash.clone();
|
||||
let name = item.name.clone();
|
||||
menu = menu
|
||||
.separator()
|
||||
.danger_action(IconType::Trash01, "Delete", move |()| {
|
||||
if let Some(hash) = &hash {
|
||||
cluster.mutate(ClusterAction::RemoveArtifact {
|
||||
cluster_id,
|
||||
hash: hash.clone(),
|
||||
});
|
||||
on_delete.call((name.clone(), hash.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1194,13 +1194,8 @@ impl Actions {
|
|||
};
|
||||
|
||||
let events = state.services.events.clone();
|
||||
match oneclient_core::remove_artifact_from_cluster(
|
||||
cluster_id,
|
||||
&hash,
|
||||
true,
|
||||
&state.services.content(),
|
||||
)
|
||||
.await
|
||||
match oneclient_core::delete_artifact(cluster_id, &hash, &state.services.content())
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
events.notify("Removed").body(display_name).send();
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ impl MutationCapability for ClusterMutation {
|
|||
}
|
||||
}),
|
||||
ClusterAction::RemoveArtifact { cluster_id, hash } => {
|
||||
oneclient_core::remove_artifact_from_cluster(*cluster_id, hash, true, content).await
|
||||
oneclient_core::delete_artifact(*cluster_id, hash, content).await
|
||||
}
|
||||
ClusterAction::RemoveBundlePackageFromDisk { cluster_id, hash } => {
|
||||
oneclient_core::remove_artifact_from_cluster(*cluster_id, hash, false, content).await
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use freya::prelude::*;
|
||||
use oneclient_content::packages::{ContentType, ProviderId};
|
||||
|
||||
use crate::components::ScrollArea;
|
||||
use crate::components::{ScrollArea, use_shared_delete};
|
||||
use crate::hooks::use_cluster;
|
||||
use crate::hooks::{
|
||||
bundles_with_status_items, cluster_content_items, content_type_for_slug, project_detail,
|
||||
|
|
@ -148,6 +148,21 @@ impl Component for BrowserPackage {
|
|||
.remove(&(provider, project_id.clone()));
|
||||
|
||||
let project = project_detail(&project_query);
|
||||
|
||||
let remove_id = project.as_ref().map(|p| p.id.clone());
|
||||
let remove_dispatch = dispatch.clone();
|
||||
let (on_remove, remove_dialog) = use_shared_delete(move |(name, hash)| {
|
||||
if let Some(project_id) = &remove_id {
|
||||
remove_dispatch.remove_package_version(
|
||||
cluster_id,
|
||||
provider,
|
||||
project_id.clone(),
|
||||
hash,
|
||||
name,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let versions = version_list(&versions_query);
|
||||
let total_versions = versions_total(&versions_query);
|
||||
let latest_version = versions.first().map(|v| v.version_id.clone());
|
||||
|
|
@ -171,6 +186,7 @@ impl Component for BrowserPackage {
|
|||
project.id.clone(),
|
||||
cluster_id,
|
||||
dispatch.clone(),
|
||||
on_remove,
|
||||
installed.clone(),
|
||||
installing,
|
||||
)
|
||||
|
|
@ -217,6 +233,7 @@ impl Component for BrowserPackage {
|
|||
.padding(Gaps::new(0., SCROLLBAR_GUTTER, 0., 0.))
|
||||
.children([row]),
|
||||
)
|
||||
.maybe_child(remove_dialog)
|
||||
.into_element()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ pub(super) fn versions_panel(
|
|||
project_id: String,
|
||||
cluster_id: i64,
|
||||
dispatch: Actions,
|
||||
on_remove: EventHandler<(String, String)>,
|
||||
installed: Option<Installed>,
|
||||
installing: bool,
|
||||
) -> impl IntoElement {
|
||||
|
|
@ -148,6 +149,7 @@ pub(super) fn versions_panel(
|
|||
project_id.clone(),
|
||||
cluster_id,
|
||||
dispatch.clone(),
|
||||
on_remove.clone(),
|
||||
tag,
|
||||
duplicated,
|
||||
installing,
|
||||
|
|
@ -222,6 +224,7 @@ fn version_row(
|
|||
project_id: String,
|
||||
cluster_id: i64,
|
||||
dispatch: Actions,
|
||||
on_remove: EventHandler<(String, String)>,
|
||||
installed: Option<InstalledVersion>,
|
||||
// Saying which version is live only tells the user anything when there are several
|
||||
duplicated: bool,
|
||||
|
|
@ -285,7 +288,8 @@ fn version_row(
|
|||
.map(|installed| activity_badge(installed.enabled).into_element()),
|
||||
)
|
||||
.child(version_button(
|
||||
installed, v.name, provider, project_id, version_id, cluster_id, dispatch, installing,
|
||||
installed, v.name, provider, project_id, version_id, cluster_id, dispatch, on_remove,
|
||||
installing,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -299,6 +303,7 @@ fn version_button(
|
|||
version_id: String,
|
||||
cluster_id: i64,
|
||||
dispatch: Actions,
|
||||
on_remove: EventHandler<(String, String)>,
|
||||
busy: bool,
|
||||
) -> impl IntoElement {
|
||||
let Some(installed) = installed else {
|
||||
|
|
@ -329,15 +334,7 @@ fn version_button(
|
|||
.danger()
|
||||
.small()
|
||||
.enabled(!busy)
|
||||
.on_press(move |_| {
|
||||
dispatch.remove_package_version(
|
||||
cluster_id,
|
||||
provider,
|
||||
project_id.clone(),
|
||||
hash.clone(),
|
||||
version_name.clone(),
|
||||
);
|
||||
})
|
||||
.on_press(move |_| on_remove.call((version_name.clone(), hash.clone())))
|
||||
.text("Remove")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use oneclient_core::settings::ViewLayout;
|
|||
|
||||
use crate::components::{
|
||||
Button, CardLayout, Icon, IconType, PackageEntry, PackageRow, ScrollArea, Segment,
|
||||
SegmentedControl, TextInput, package_context_menu,
|
||||
SegmentedControl, TextInput, package_context_menu, use_shared_delete,
|
||||
};
|
||||
use crate::hooks::{use_cluster_mutation, use_dispatch, use_overlay_claim};
|
||||
use crate::hooks::{ClusterAction, use_cluster_mutation, use_dispatch, use_overlay_claim};
|
||||
use crate::routes::Route;
|
||||
use crate::theme::colors;
|
||||
use crate::{Actions, utils};
|
||||
|
|
@ -742,6 +742,9 @@ impl Component for ContentBox {
|
|||
let dispatch = use_dispatch();
|
||||
let cluster = use_cluster_mutation();
|
||||
let mut menu = use_state(|| None::<(f32, f32, PackageEntry)>);
|
||||
let (on_delete, delete_dialog) = use_shared_delete(move |(_, hash)| {
|
||||
cluster.mutate(ClusterAction::RemoveArtifact { cluster_id, hash });
|
||||
});
|
||||
|
||||
let row = {
|
||||
let items = items.clone();
|
||||
|
|
@ -773,7 +776,7 @@ impl Component for ContentBox {
|
|||
});
|
||||
|
||||
let menu_overlay = menu.read().clone().map(|(x, y, item)| {
|
||||
package_context_menu(x, y, &item, cluster_id, package_type, cluster)
|
||||
package_context_menu(x, y, &item, cluster_id, package_type, on_delete)
|
||||
.on_close(move |_| menu.set(None))
|
||||
.into_element()
|
||||
});
|
||||
|
|
@ -821,6 +824,7 @@ impl Component for ContentBox {
|
|||
.maybe_child(scroll)
|
||||
.maybe_child(empty)
|
||||
.maybe_child(menu_overlay)
|
||||
.maybe_child(delete_dialog)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -641,12 +641,7 @@ pub async fn set_artifact_enabled_to(
|
|||
|
||||
// which clusters have to record what the user just did
|
||||
async fn override_scope(cluster_id: i64, hash: &str, ctx: &ContentCtx) -> ContentResult<Vec<i64>> {
|
||||
let global = artifact_dao::get_artifact_by_hash(&ctx.db, hash)
|
||||
.await?
|
||||
.and_then(|artifact| ContentType::from_repr(artifact.content_type as u8))
|
||||
.is_some_and(ContentType::is_global);
|
||||
|
||||
if !global {
|
||||
if !is_shared_artifact(hash, ctx).await? {
|
||||
return Ok(vec![cluster_id]);
|
||||
}
|
||||
|
||||
|
|
@ -785,6 +780,46 @@ pub async fn remove_artifact_from_cluster(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_shared_artifact(hash: &str, ctx: &ContentCtx) -> ContentResult<bool> {
|
||||
Ok(artifact_dao::get_artifact_by_hash(&ctx.db, hash)
|
||||
.await?
|
||||
.and_then(|artifact| ContentType::from_repr(artifact.content_type as u8))
|
||||
.is_some_and(ContentType::is_global))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(ctx))]
|
||||
pub async fn clusters_sharing_artifact(
|
||||
hash: &str,
|
||||
ctx: &ContentCtx,
|
||||
) -> ContentResult<Option<usize>> {
|
||||
if !is_shared_artifact(hash, ctx).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(
|
||||
artifact_dao::list_clusters_linking(&ctx.db, hash)
|
||||
.await?
|
||||
.len(),
|
||||
))
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(ctx))]
|
||||
pub async fn delete_artifact(cluster_id: i64, hash: &str, ctx: &ContentCtx) -> ContentResult<()> {
|
||||
if !is_shared_artifact(hash, ctx).await? {
|
||||
return remove_artifact_from_cluster(cluster_id, hash, true, ctx).await;
|
||||
}
|
||||
|
||||
let mut first_error = None;
|
||||
for linked_cluster in artifact_dao::list_clusters_linking(&ctx.db, hash).await? {
|
||||
if let Err(err) = remove_artifact_from_cluster(linked_cluster, hash, true, ctx).await {
|
||||
tracing::warn!(cluster_id = linked_cluster, hash, error = %err, "failed to delete shared package from a cluster");
|
||||
first_error.get_or_insert(err);
|
||||
}
|
||||
}
|
||||
|
||||
first_error.map_or(Ok(()), Err)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ mod updates;
|
|||
|
||||
pub use error::BundleError;
|
||||
pub use install::{
|
||||
effective_enabled, enabled_bundle_bytes, extract_bundle_overrides_for_cluster,
|
||||
heal_bundle_activity, install_bundle, install_cluster_bundles, install_enabled_bundle_files,
|
||||
install_package_from_bundle, list_cluster_bundle_overrides, on_user_disable_artifact,
|
||||
on_user_enable_artifact, on_user_remove_artifact, remove_artifact_from_cluster,
|
||||
set_artifact_enabled_to, set_bundle_package_enabled, set_bundle_package_opt_in,
|
||||
set_bundle_package_override, set_bundle_package_overrides,
|
||||
clusters_sharing_artifact, delete_artifact, effective_enabled, enabled_bundle_bytes,
|
||||
extract_bundle_overrides_for_cluster, heal_bundle_activity, install_bundle,
|
||||
install_cluster_bundles, install_enabled_bundle_files, install_package_from_bundle,
|
||||
list_cluster_bundle_overrides, on_user_disable_artifact, on_user_enable_artifact,
|
||||
on_user_remove_artifact, remove_artifact_from_cluster, set_artifact_enabled_to,
|
||||
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
||||
set_bundle_package_overrides,
|
||||
};
|
||||
pub use manager::{Bundle, BundlesManager};
|
||||
pub use manifest::BundleManifest as RemoteBundleManifest;
|
||||
|
|
|
|||
|
|
@ -45,12 +45,13 @@ pub use oneclient_content::bundles::{
|
|||
ApplyBundleUpdatesResult, Bundle, BundleArchive, BundleError, BundleFile, BundleFileKind,
|
||||
BundleManifest, BundleOptionalPackage, BundleUpdateCheckResult, BundleWithUpdateStatus,
|
||||
BundlesManager, FileUpdateStatus, PendingOptionalMod, apply_bundle_updates,
|
||||
apply_bundle_updates_with, check_bundle_updates, cluster_has_bundle_content, effective_enabled,
|
||||
get_bundles_with_update_status, install_bundle, install_cluster_bundles,
|
||||
install_package_from_bundle, list_cluster_bundle_overrides, pending_optional_mods,
|
||||
remove_artifact_from_cluster, resolve_optional_mods, set_artifact_enabled_to,
|
||||
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
|
||||
set_bundle_package_overrides, skip_optional_mods,
|
||||
apply_bundle_updates_with, check_bundle_updates, cluster_has_bundle_content,
|
||||
clusters_sharing_artifact, delete_artifact, effective_enabled, get_bundles_with_update_status,
|
||||
install_bundle, install_cluster_bundles, install_package_from_bundle,
|
||||
list_cluster_bundle_overrides, pending_optional_mods, remove_artifact_from_cluster,
|
||||
resolve_optional_mods, set_artifact_enabled_to, set_bundle_package_enabled,
|
||||
set_bundle_package_opt_in, set_bundle_package_override, set_bundle_package_overrides,
|
||||
skip_optional_mods,
|
||||
};
|
||||
pub use oneclient_content::packages::LinkedArtifactInfo;
|
||||
pub use oneclient_content::packages::updates::{
|
||||
|
|
|
|||
|
|
@ -312,6 +312,13 @@ pub async fn is_cluster_linked(
|
|||
Ok(row.is_some())
|
||||
}
|
||||
|
||||
pub async fn list_clusters_linking(pool: &SqlitePool, hash: &str) -> Result<Vec<i64>, sqlx::Error> {
|
||||
sqlx::query_scalar("SELECT cluster_id FROM cluster_artifacts WHERE hash = ?")
|
||||
.bind(hash)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unlink_cluster_artifact(
|
||||
pool: &SqlitePool,
|
||||
cluster_id: i64,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue