use ely_browser_core::{BrowserSnapshot, InstalledPlugin}; use ely_design_system::colors; use ely_domain::{PluginId, PluginManifest, PluginPermission, PluginPermissionRisk, ProfileKind}; use gpui::prelude::FluentBuilder; use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; use gpui_component::{ IconName, Sizable, StyledExt, button::{Button, ButtonVariants}, scroll::ScrollableElement, }; use super::{ElyShell, render_canvas_surface}; impl ElyShell { pub(super) fn render_plugin_detail_page( &mut self, snapshot: &BrowserSnapshot, route: &str, cx: &mut Context, ) -> AnyElement { let plugin_id = parse_plugin_detail_route(route); let plugin = plugin_id.as_ref().and_then(|plugin_id| find_installed_plugin(snapshot, plugin_id)); render_canvas_surface( div() .size_full() .p_8() .flex() .flex_col() .gap_5() .when_some(plugin, |this, plugin| { this.child(self.render_plugin_detail_header( plugin, &snapshot.active_profile_kind, cx, )) .child(render_plugin_security_summary(plugin)) .child(render_plugin_manifest_rows(plugin.manifest())) .child(render_plugin_permission_list(plugin.manifest())) }) .when(plugin.is_none(), |this| { this.child(render_missing_plugin_detail(plugin_id.as_ref(), cx)) }), ) } fn render_plugin_detail_header( &mut self, plugin: &InstalledPlugin, profile_kind: &ProfileKind, cx: &mut Context, ) -> AnyElement { let plugin_id = plugin.id().clone(); let target_enabled = !plugin.enabled(); let private_allowed = plugin.private_window_allowed(); let status_label = plugin_status_label(plugin, profile_kind); let status_color = if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED }; let action_label = if plugin.enabled() { "Disable" } else { "Enable" }; let action_icon = if plugin.enabled() { IconName::CircleX } else { IconName::Check }; let private_action_label = if private_allowed { "Block Private" } else { "Allow Private" }; let private_action_icon = if private_allowed { IconName::CircleX } else { IconName::Check }; div() .flex() .items_end() .justify_between() .gap_4() .child( div() .min_w_0() .flex() .flex_col() .gap_2() .child( div() .text_size(px(26.0)) .truncate() .text_color(rgb(colors::INK)) .child(plugin.manifest().name().to_string()), ) .child(div().text_sm().truncate().text_color(rgb(colors::MUTED)).child( format!("{} - {}", plugin.manifest().author(), plugin.id().as_str()), )), ) .child( div() .flex() .items_center() .gap_3() .text_xs() .child(div().font_semibold().text_color(rgb(status_color)).child(status_label)) .child( Button::new("toggle-plugin-detail-private") .ghost() .xsmall() .icon(private_action_icon) .label(private_action_label) .tooltip("Set Private Window Permission") .on_click(cx.listener({ let plugin_id = plugin_id.clone(); move |shell, _, _, cx| { shell.set_plugin_private_window_allowed( plugin_id.clone(), !private_allowed, cx, ); } })), ) .child( Button::new("toggle-plugin-detail-enabled") .xsmall() .icon(action_icon) .label(action_label) .tooltip("Set Plugin State") .on_click(cx.listener(move |shell, _, _, cx| { shell.set_plugin_enabled(plugin_id.clone(), target_enabled, cx); })), ) .child( Button::new("open-plugin-settings") .ghost() .xsmall() .icon(IconName::Info) .label("Settings") .tooltip("Open Plugin Settings") .on_click(cx.listener(|shell, _, window, cx| { shell.open_internal_tab("ely://settings/plugins", window, cx); })), ), ) .into_any_element() } } fn plugin_status_label(plugin: &InstalledPlugin, profile_kind: &ProfileKind) -> &'static str { if !plugin.enabled() { return "Disabled"; } match profile_kind { ProfileKind::Standard => "Enabled", ProfileKind::Private if plugin.private_window_allowed() => "Private Enabled", ProfileKind::Private => "Private Off", } } fn render_plugin_security_summary(plugin: &InstalledPlugin) -> AnyElement { let manifest = plugin.manifest(); div() .border_t_1() .border_b_1() .border_color(rgb(colors::HAIRLINE)) .py_3() .flex() .items_center() .justify_between() .gap_4() .children([ detail_metric("Signature", manifest.signature().algorithm().as_str()), detail_metric("Key", manifest.signature().key_id()), detail_metric("High Risk", manifest.high_risk_permissions().count().to_string()), detail_metric("Sync", sync_participation_label(manifest)), ]) .into_any_element() } fn detail_metric(label: &'static str, value: impl Into) -> AnyElement { div() .min_w_0() .flex() .flex_col() .gap_1() .child(div().text_xs().text_color(rgb(colors::MUTED)).child(label)) .child( div() .text_sm() .font_semibold() .truncate() .text_color(rgb(colors::INK)) .child(value.into()), ) .into_any_element() } fn render_plugin_manifest_rows(manifest: &PluginManifest) -> AnyElement { div() .flex() .flex_col() .border_b_1() .border_color(rgb(colors::HAIRLINE)) .child(plugin_detail_row( IconName::Info, "Description", manifest.description(), "Manifest summary", )) .child(plugin_detail_row(IconName::User, "Author", manifest.author(), "Publisher")) .child(plugin_detail_row(IconName::Globe, "Homepage", manifest.homepage(), "Publisher URL")) .child(plugin_detail_row( IconName::CircleCheck, "Minimum Build", manifest.min_ely_build().to_string(), "Required ELY version", )) .child(plugin_detail_row( IconName::BookOpen, "Checksum", manifest.checksum(), "Wasm package checksum", )) .into_any_element() } fn plugin_detail_row( icon: IconName, label: &'static str, value: impl Into, detail: &'static str, ) -> AnyElement { let value = value.into(); div() .py_3() .border_t_1() .border_color(rgb(colors::HAIRLINE)) .flex() .items_center() .justify_between() .gap_4() .child( div() .min_w_0() .flex() .items_center() .gap_3() .child(div().text_color(rgb(colors::MUTED_SOFT)).child(icon)) .child( div() .min_w_0() .flex() .flex_col() .gap_1() .child( div() .text_sm() .font_semibold() .truncate() .text_color(rgb(colors::INK)) .child(label), ) .child(div().text_xs().text_color(rgb(colors::MUTED)).child(detail)), ), ) .child( div() .max_w(px(380.0)) .truncate() .text_sm() .font_semibold() .text_color(rgb(colors::INK)) .child(value), ) .into_any_element() } fn render_plugin_permission_list(manifest: &PluginManifest) -> AnyElement { if manifest.permissions().is_empty() { return div() .text_sm() .text_color(rgb(colors::MUTED)) .child("This plugin declares no permissions.") .into_any_element(); } div() .flex_1() .min_h_0() .flex() .flex_col() .gap_3() .child(div().text_xs().font_semibold().text_color(rgb(colors::MUTED)).child("Permissions")) .child( div() .flex_1() .min_h_0() .flex() .flex_col() .overflow_y_scrollbar() .children(manifest.permissions().iter().map(render_permission_row)), ) .into_any_element() } fn render_permission_row(permission: &PluginPermission) -> AnyElement { let risk = permission.risk(); let risk_color = match risk { PluginPermissionRisk::Standard => colors::MUTED, PluginPermissionRisk::High => colors::ERROR, }; div() .py_3() .border_b_1() .border_color(rgb(colors::HAIRLINE)) .flex() .items_center() .justify_between() .gap_4() .child( div() .min_w_0() .flex() .items_center() .gap_3() .child(div().text_color(rgb(risk_color)).child(permission_icon(permission))) .child( div() .min_w_0() .flex() .flex_col() .gap_1() .child( div() .text_sm() .font_semibold() .truncate() .text_color(rgb(colors::INK)) .child(permission.as_str()), ) .child( div() .text_xs() .text_color(rgb(colors::MUTED)) .child(permission_scope_label(permission)), ), ), ) .child( div() .text_xs() .font_semibold() .text_color(rgb(risk_color)) .child(permission_risk_label(risk)), ) .into_any_element() } fn render_missing_plugin_detail( plugin_id: Option<&PluginId>, cx: &mut Context, ) -> AnyElement { let detail = plugin_id .map(|plugin_id| plugin_id.as_str().to_string()) .unwrap_or_else(|| "Invalid plugin route".to_string()); div() .size_full() .flex() .flex_col() .gap_5() .child( div() .flex() .flex_col() .gap_2() .child( div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Plugin Details"), ) .child(div().text_sm().text_color(rgb(colors::MUTED)).child(detail)), ) .child( div() .border_t_1() .border_color(rgb(colors::HAIRLINE)) .pt_5() .flex() .items_center() .justify_between() .gap_4() .child( div() .text_sm() .text_color(rgb(colors::MUTED)) .child("Plugin details are available for installed plugins."), ) .child( Button::new("open-plugin-marketplace") .primary() .small() .icon(IconName::ExternalLink) .label("Open Plugins") .tooltip("Open Plugin Marketplace") .on_click(cx.listener(|shell, _, window, cx| { shell.open_internal_tab("ely://plugins", window, cx); })), ), ) .into_any_element() } fn parse_plugin_detail_route(route: &str) -> Option { let plugin_id = route.strip_prefix("ely://plugin/")?; PluginId::parse(plugin_id).ok() } fn find_installed_plugin<'a>( snapshot: &'a BrowserSnapshot, plugin_id: &PluginId, ) -> Option<&'a InstalledPlugin> { snapshot.installed_plugins.iter().find(|plugin| plugin.id() == plugin_id) } fn sync_participation_label(manifest: &PluginManifest) -> &'static str { if manifest .permissions() .iter() .any(|permission| matches!(permission, PluginPermission::SyncPlugin)) { "Participates" } else { "Local" } } fn permission_risk_label(risk: PluginPermissionRisk) -> &'static str { match risk { PluginPermissionRisk::Standard => "Standard", PluginPermissionRisk::High => "High", } } fn permission_icon(permission: &PluginPermission) -> IconName { if permission.requires_separate_confirmation() { IconName::TriangleAlert } else { IconName::CircleCheck } } fn permission_scope_label(permission: &PluginPermission) -> &'static str { match permission { PluginPermission::TabsRead => "Reads tab metadata.", PluginPermission::TabsWrite => "Creates, moves, or closes tabs.", PluginPermission::SpacesRead => "Reads Space metadata.", PluginPermission::SpacesWrite => "Creates or changes Spaces.", PluginPermission::BookmarksRead => "Reads bookmarks.", PluginPermission::BookmarksWrite => "Writes bookmarks.", PluginPermission::HistoryRead => "Reads browsing history.", PluginPermission::DownloadsRead => "Reads download entries.", PluginPermission::DownloadsWrite => "Controls downloads.", PluginPermission::PageMetadata => "Reads active page metadata.", PluginPermission::PageScreenshot => "Captures page screenshots.", PluginPermission::PageScript => "Runs scoped page scripts.", PluginPermission::ClipboardRead => "Reads clipboard content.", PluginPermission::ClipboardWrite => "Writes clipboard content.", PluginPermission::FilesystemRead => "Reads user-selected files.", PluginPermission::FilesystemWrite => "Writes user-selected files.", PluginPermission::NetworkFetch => "Performs plugin network requests.", PluginPermission::SettingsRead => "Reads plugin settings.", PluginPermission::SettingsWrite => "Writes plugin settings.", PluginPermission::SyncPlugin => "Syncs plugin configuration.", PluginPermission::UiPanel => "Registers sidebar panels.", PluginPermission::UiCommand => "Registers command bar actions.", PluginPermission::UiContextMenu => "Registers context menu actions.", } }