Add plugin marketplace routes
This commit is contained in:
@@ -3,6 +3,8 @@ mod bookmarks;
|
||||
mod download_actions;
|
||||
mod download_labels;
|
||||
mod downloads;
|
||||
mod plugin_catalog;
|
||||
mod plugin_details;
|
||||
mod plugins;
|
||||
mod profiles;
|
||||
mod reading_list;
|
||||
@@ -34,6 +36,10 @@ impl ElyShell {
|
||||
"ely://history" => self.render_history_page(snapshot, cx),
|
||||
"ely://archive" => self.render_archive_page(snapshot, cx),
|
||||
"ely://task-manager" => self.render_task_manager_page(snapshot),
|
||||
"ely://plugins" => self.render_plugin_catalog_page(snapshot, cx),
|
||||
url if url.starts_with("ely://plugin/") => {
|
||||
self.render_plugin_detail_page(snapshot, url, cx)
|
||||
}
|
||||
"ely://about" => self.render_about_page(snapshot),
|
||||
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
|
||||
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
use ely_browser_core::{BrowserSnapshot, InstalledPlugin};
|
||||
use ely_design_system::colors;
|
||||
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_catalog_page(
|
||||
&mut self,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
render_canvas_surface(
|
||||
div()
|
||||
.size_full()
|
||||
.p_8()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_plugin_catalog_header(snapshot, cx))
|
||||
.child(render_plugin_catalog_summary(snapshot))
|
||||
.child(render_plugin_catalog_list(snapshot, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_plugin_catalog_header(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_end()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(26.0))
|
||||
.text_color(rgb(colors::INK))
|
||||
.child("Plugin Marketplace"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child("Signed local .rplug packages"),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child(format!("{} installed", snapshot.installed_plugins.len()))
|
||||
.child(
|
||||
Button::new("plugin-market-install")
|
||||
.primary()
|
||||
.small()
|
||||
.icon(IconName::Plus)
|
||||
.label("Install")
|
||||
.tooltip("Install Plugin from File")
|
||||
.on_click(cx.listener(|shell, _, window, cx| {
|
||||
shell.choose_plugin_package(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_plugin_catalog_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(rgb(colors::HAIRLINE))
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.children([
|
||||
plugin_metric("Installed", snapshot.installed_plugins.len()),
|
||||
plugin_metric("Enabled", enabled_plugin_count(snapshot)),
|
||||
plugin_metric("High Risk", high_risk_plugin_count(snapshot)),
|
||||
plugin_metric("Audit Events", snapshot.plugin_audit_events.len()),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn plugin_metric(label: &'static str, value: usize) -> 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().text_color(rgb(colors::INK)).child(value.to_string()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_plugin_catalog_list(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
if snapshot.installed_plugins.is_empty() {
|
||||
return div()
|
||||
.flex_1()
|
||||
.border_t_1()
|
||||
.border_color(rgb(colors::HAIRLINE))
|
||||
.pt_5()
|
||||
.flex()
|
||||
.items_start()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::INK))
|
||||
.child("No plugins installed."),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child("Install a signed .rplug package from local disk."),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("plugin-market-empty-install")
|
||||
.primary()
|
||||
.small()
|
||||
.icon(IconName::Plus)
|
||||
.label("Install")
|
||||
.tooltip("Install Plugin from File")
|
||||
.on_click(cx.listener(|shell, _, window, cx| {
|
||||
shell.choose_plugin_package(window, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_y_scrollbar()
|
||||
.border_t_1()
|
||||
.border_color(rgb(colors::HAIRLINE))
|
||||
.children(
|
||||
snapshot
|
||||
.installed_plugins
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, plugin)| render_plugin_catalog_row(index, plugin, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_plugin_catalog_row(
|
||||
index: usize,
|
||||
plugin: &InstalledPlugin,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let detail_route = format!("ely://plugin/{}", plugin.id().as_str());
|
||||
let high_risk_count = plugin.manifest().high_risk_permissions().count();
|
||||
let status_color = if plugin.enabled() { colors::SUCCESS } else { colors::MUTED };
|
||||
let status_label = if plugin.enabled() { "Enabled" } else { "Disabled" };
|
||||
|
||||
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(colors::MUTED_SOFT)).child(IconName::Asterisk))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::INK))
|
||||
.child(plugin.manifest().name().to_string()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child(plugin.manifest().description().to_string()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.text_color(rgb(colors::MUTED))
|
||||
.child(format!("{} permissions", plugin.manifest().permissions().len()))
|
||||
.child(format!("{high_risk_count} high risk"))
|
||||
.child(div().text_color(rgb(status_color)).child(status_label))
|
||||
.child(
|
||||
Button::new(("plugin-market-details", index))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::ExternalLink)
|
||||
.label("Details")
|
||||
.tooltip("Open Plugin Details")
|
||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||
shell.open_internal_tab(&detail_route, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn enabled_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
||||
snapshot.installed_plugins.iter().filter(|plugin| plugin.enabled()).count()
|
||||
}
|
||||
|
||||
fn high_risk_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
||||
snapshot
|
||||
.installed_plugins
|
||||
.iter()
|
||||
.filter(|plugin| plugin.manifest().high_risk_permissions().next().is_some())
|
||||
.count()
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
use ely_browser_core::{BrowserSnapshot, InstalledPlugin};
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::{PluginId, PluginManifest, PluginPermission, PluginPermissionRisk};
|
||||
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<Self>,
|
||||
) -> 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, 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,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let plugin_id = plugin.id().clone();
|
||||
let target_enabled = !plugin.enabled();
|
||||
let status_label = if plugin.enabled() { "Enabled" } else { "Disabled" };
|
||||
let status_color = if plugin.enabled() { 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 };
|
||||
|
||||
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-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 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<String>) -> 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<String>,
|
||||
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<ElyShell>,
|
||||
) -> 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<PluginId> {
|
||||
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.",
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use ely_domain::{BrowserTab, DomainError, UrlText};
|
||||
use ely_domain::{BrowserTab, DomainError, PluginId, UrlText};
|
||||
use url::Url;
|
||||
|
||||
use crate::CoreError;
|
||||
@@ -22,6 +22,8 @@ fn internal_page_title(url: &str) -> Option<&'static str> {
|
||||
"ely://history" => Some("History"),
|
||||
"ely://archive" => Some("Archived Tabs"),
|
||||
"ely://task-manager" => Some("Task Manager"),
|
||||
"ely://plugins" => Some("Plugin Marketplace"),
|
||||
url if plugin_detail_route_id(url).is_some() => Some("Plugin Details"),
|
||||
"ely://about" => Some("About ELY Browser"),
|
||||
"ely://settings" => Some("Settings"),
|
||||
"ely://settings/plugins" => Some("Plugin Settings"),
|
||||
@@ -103,6 +105,15 @@ pub(crate) fn task_manager_url() -> Result<UrlText, CoreError> {
|
||||
internal_page_url("ely://task-manager")
|
||||
}
|
||||
|
||||
pub(crate) fn plugins_url() -> Result<UrlText, CoreError> {
|
||||
internal_page_url("ely://plugins")
|
||||
}
|
||||
|
||||
pub(crate) fn plugin_detail_url(plugin_id: &PluginId) -> Result<UrlText, CoreError> {
|
||||
let route = format!("ely://plugin/{}", plugin_id.as_str());
|
||||
internal_page_url(&route)
|
||||
}
|
||||
|
||||
pub(crate) fn about_url() -> Result<UrlText, CoreError> {
|
||||
internal_page_url("ely://about")
|
||||
}
|
||||
@@ -142,3 +153,8 @@ fn settings_page_route(query: &str) -> Option<&'static str> {
|
||||
fn internal_page_url(value: &str) -> Result<UrlText, CoreError> {
|
||||
UrlText::parse(value).map_err(CoreError::from)
|
||||
}
|
||||
|
||||
fn plugin_detail_route_id(url: &str) -> Option<&str> {
|
||||
let plugin_id = url.strip_prefix("ely://plugin/")?;
|
||||
(!plugin_id.is_empty() && !plugin_id.contains('/')).then_some(plugin_id)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ use crate::{
|
||||
CoreError,
|
||||
navigation::{
|
||||
about_url, bookmarks_url, downloads_url, history_url, move_tab_space_name,
|
||||
new_profile_name, new_space_name, reading_list_url, search_url, settings_page_url,
|
||||
settings_url, space_icon, switch_profile_name, sync_status_url, task_manager_url,
|
||||
new_profile_name, new_space_name, plugin_detail_url, plugins_url, reading_list_url,
|
||||
search_url, settings_page_url, settings_url, space_icon, switch_profile_name,
|
||||
sync_status_url, task_manager_url,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -69,6 +70,12 @@ impl BrowserCore {
|
||||
self.command_query.clear();
|
||||
}
|
||||
}
|
||||
CommandIntent::ScopedSearch { scope: CommandScope::Plugins, query } => {
|
||||
if let Some(url) = self.find_plugin_match(query)? {
|
||||
self.open_tab(url);
|
||||
self.command_query.clear();
|
||||
}
|
||||
}
|
||||
CommandIntent::ScopedSearch { scope: CommandScope::Archive, query }
|
||||
if self.restore_archived_tab_match(query)?.is_some() =>
|
||||
{
|
||||
@@ -130,6 +137,15 @@ impl BrowserCore {
|
||||
self.open_tab(task_manager_url()?);
|
||||
Ok(true)
|
||||
}
|
||||
"plugins"
|
||||
| "open-plugins"
|
||||
| "open plugins"
|
||||
| "plugin-marketplace"
|
||||
| "open-plugin-marketplace"
|
||||
| "open plugin marketplace" => {
|
||||
self.open_tab(plugins_url()?);
|
||||
Ok(true)
|
||||
}
|
||||
"about" | "open-about" | "open about" => {
|
||||
self.open_tab(about_url()?);
|
||||
Ok(true)
|
||||
@@ -186,4 +202,21 @@ impl BrowserCore {
|
||||
.find(|profile| profile.name().to_lowercase().contains(&query))
|
||||
.map(|profile| profile.id().clone())
|
||||
}
|
||||
|
||||
fn find_plugin_match(&self, query: &str) -> Result<Option<ely_domain::UrlText>, CoreError> {
|
||||
let query = query.trim().to_lowercase();
|
||||
if query.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
self.installed_plugins
|
||||
.iter()
|
||||
.find(|plugin| {
|
||||
plugin.id().as_str().to_lowercase().contains(&query)
|
||||
|| plugin.manifest().name().to_lowercase().contains(&query)
|
||||
|| plugin.manifest().description().to_lowercase().contains(&query)
|
||||
})
|
||||
.map(|plugin| plugin_detail_url(plugin.id()))
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,21 @@ fn open_task_manager_command_opens_task_manager_page() -> Result<(), Box<dyn Err
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_plugins_command_opens_plugin_marketplace_page() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
core.set_command_query(">plugins");
|
||||
let intent = core.submit_command()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("plugins".to_string())));
|
||||
assert_eq!(active_tab.title(), "Plugin Marketplace");
|
||||
assert_eq!(active_tab.url().as_str(), "ely://plugins");
|
||||
assert_eq!(core.snapshot()?.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_about_command_opens_about_page() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig, PluginAuditAction};
|
||||
use ely_domain::{PluginId, PluginManifest};
|
||||
use ely_domain::{CommandIntent, CommandScope, PluginId, PluginManifest};
|
||||
|
||||
#[test]
|
||||
fn installs_standard_plugin_and_records_audit_event() -> Result<(), Box<dyn Error>> {
|
||||
@@ -146,6 +146,48 @@ fn rejects_unknown_plugin_state_change() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_scoped_search_opens_installed_plugin_detail() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
core.install_plugin(plugin_manifest("com.elydora.reader", &["page:metadata"])?, false)?;
|
||||
|
||||
core.set_command_query("@plugins reader");
|
||||
let intent = core.submit_command()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(
|
||||
intent,
|
||||
Some(CommandIntent::ScopedSearch {
|
||||
scope: CommandScope::Plugins,
|
||||
query: "reader".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(active_tab.title(), "Plugin Details");
|
||||
assert_eq!(active_tab.url().as_str(), "ely://plugin/com.elydora.reader");
|
||||
assert_eq!(core.snapshot()?.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_scoped_search_preserves_query_without_match() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
core.set_command_query("@plugins missing");
|
||||
let intent = core.submit_command()?;
|
||||
let active_tab = core.active_tab()?;
|
||||
|
||||
assert_eq!(
|
||||
intent,
|
||||
Some(CommandIntent::ScopedSearch {
|
||||
scope: CommandScope::Plugins,
|
||||
query: "missing".to_string()
|
||||
})
|
||||
);
|
||||
assert_eq!(active_tab.url().as_str(), "ely://new-tab");
|
||||
assert_eq!(core.snapshot()?.command_query, "@plugins missing");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_error(
|
||||
core: &mut BrowserCore,
|
||||
manifest: PluginManifest,
|
||||
|
||||
Reference in New Issue
Block a user