Recompose plugin marketplace as hero + grid + editor's pick
The marketplace landing now matches the design's two-row hero (serif
"Quiet tools. Everyday magic." headline + native-to-ELY tagline on the
left, an Editor's Pick card on the right that promotes the first
enabled plugin or, when none are installed, an empty-state CTA), a row
of category chips with sandbox audit count, and a 4-column card grid.
Each plugin card uses real manifest data (name, author, description,
permission counts, sandbox flag) and routes to ely://plugin/{id} on
click. Empty state surfaces an honest "drop a signed .rplug" message
plus the install action. Nothing on the page is fabricated catalog data.
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
use ely_browser_core::{BrowserSnapshot, InstalledPlugin};
|
use ely_browser_core::{BrowserSnapshot, InstalledPlugin};
|
||||||
use ely_design_system::colors;
|
use ely_design_system::colors;
|
||||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
use ely_domain::ProfileKind;
|
||||||
use gpui_component::{
|
use gpui::{
|
||||||
IconName, Sizable, StyledExt,
|
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||||
button::{Button, ButtonVariants},
|
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, px, rgb,
|
||||||
scroll::ScrollableElement,
|
rgba,
|
||||||
};
|
};
|
||||||
|
use gpui_component::{IconName, scroll::ScrollableElement};
|
||||||
|
|
||||||
use super::{ElyShell, render_canvas_surface};
|
use super::{ElyShell, render_canvas_surface};
|
||||||
|
|
||||||
@@ -18,243 +19,474 @@ impl ElyShell {
|
|||||||
render_canvas_surface(
|
render_canvas_surface(
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.p_8()
|
.pt(px(40.0))
|
||||||
|
.px(px(56.0))
|
||||||
|
.pb(px(40.0))
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_5()
|
.gap(px(24.0))
|
||||||
.child(render_plugin_catalog_header(snapshot, cx))
|
.child(render_hero(snapshot, cx))
|
||||||
.child(render_plugin_catalog_summary(snapshot))
|
.child(render_summary(snapshot))
|
||||||
.child(render_plugin_catalog_list(snapshot, cx)),
|
.child(render_grid(snapshot, cx)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_plugin_catalog_header(
|
fn render_hero(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.grid()
|
||||||
|
.grid_cols(2)
|
||||||
|
.gap(px(28.0))
|
||||||
|
.child(render_hero_left(snapshot, cx))
|
||||||
|
.child(render_editors_pick(snapshot, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_hero_left(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
let total = snapshot.installed_plugins.len();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
|
.child("PLUGINS · NATIVE TO ELY"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(48.0))
|
||||||
|
.font_weight(FontWeight(400.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("Quiet tools. Everyday magic."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w(px(520.0))
|
||||||
|
.text_size(px(13.5))
|
||||||
|
.text_color(rgb(colors::INK_2))
|
||||||
|
.child(
|
||||||
|
"ELY plugins are sandboxed, theme-aware, and ship with their own controls in \
|
||||||
|
your sidebar — no Chrome extension framework required.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(render_install_button(total, cx))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_install_button(total: usize, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div().flex().child(
|
||||||
|
div()
|
||||||
|
.id(SharedString::from("plugin-install"))
|
||||||
|
.h(px(42.0))
|
||||||
|
.px(px(16.0))
|
||||||
|
.rounded(px(12.0))
|
||||||
|
.bg(rgba(SEARCH_BG))
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(10.0))
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(SEARCH_BG_HOVER)))
|
||||||
|
.active(|style| style.opacity(0.85))
|
||||||
|
.on_click(cx.listener(|shell, _, window, cx| {
|
||||||
|
shell.choose_plugin_package(window, cx);
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(IconName::Plus),
|
||||||
|
)
|
||||||
|
.child(format!(
|
||||||
|
"Install plugin · {} active",
|
||||||
|
total
|
||||||
|
)))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_editors_pick(
|
||||||
snapshot: &BrowserSnapshot,
|
snapshot: &BrowserSnapshot,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let featured = featured_plugin(snapshot);
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.p(px(20.0))
|
||||||
.items_end()
|
.rounded(px(16.0))
|
||||||
.justify_between()
|
.bg(linear_gradient(
|
||||||
.gap_4()
|
135.0,
|
||||||
.child(
|
linear_color_stop(hsla(0.0, 0.0, 1.0, 0.94), 0.0),
|
||||||
div()
|
linear_color_stop(hsla(263.0 / 360.0, 0.6, 0.86, 0.65), 1.0),
|
||||||
|
))
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_2()
|
.gap(px(14.0))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(px(26.0))
|
.text_size(px(11.0))
|
||||||
.text_color(rgb(colors::INK))
|
.text_color(rgb(colors::INK_3))
|
||||||
.child("Plugin Marketplace"),
|
.child("EDITOR'S PICK"),
|
||||||
)
|
|
||||||
.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);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
.child(render_featured_body(featured))
|
||||||
|
.child(render_featured_actions(featured, cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_plugin_catalog_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
fn render_featured_body(featured: Option<&InstalledPlugin>) -> AnyElement {
|
||||||
|
let (name, desc) = match featured {
|
||||||
|
Some(plugin) => (
|
||||||
|
plugin.manifest().name().to_string(),
|
||||||
|
plugin.manifest().description().to_string(),
|
||||||
|
),
|
||||||
|
None => (
|
||||||
|
"Install your first plugin".to_string(),
|
||||||
|
"Drop a signed .rplug package onto ELY to extend the browser with sandboxed tools."
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.border_t_1()
|
.flex()
|
||||||
.border_b_1()
|
.gap(px(14.0))
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
.child(
|
||||||
.py_3()
|
div()
|
||||||
|
.size(px(56.0))
|
||||||
|
.rounded(px(14.0))
|
||||||
|
.bg(linear_gradient(
|
||||||
|
135.0,
|
||||||
|
linear_color_stop(hsla(341.0 / 360.0, 0.78, 0.85, 1.0), 0.0),
|
||||||
|
linear_color_stop(hsla(228.0 / 360.0, 1.0, 0.86, 1.0), 1.0),
|
||||||
|
))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_center()
|
||||||
.gap_4()
|
.text_size(px(28.0))
|
||||||
.children([
|
.text_color(rgb(0xffffff))
|
||||||
plugin_metric("Installed", snapshot.installed_plugins.len()),
|
.child(name.chars().next().unwrap_or('✦').to_uppercase().to_string()),
|
||||||
plugin_metric("Enabled", enabled_plugin_count(snapshot)),
|
)
|
||||||
plugin_metric("High Risk", high_risk_plugin_count(snapshot)),
|
.child(
|
||||||
plugin_metric("Audit Events", snapshot.plugin_audit_events.len()),
|
|
||||||
])
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn plugin_metric(label: &'static str, value: usize) -> AnyElement {
|
|
||||||
div()
|
div()
|
||||||
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(div().text_xs().text_color(rgb(colors::MUTED)).child(label))
|
|
||||||
.child(
|
.child(
|
||||||
div().text_sm().font_semibold().text_color(rgb(colors::INK)).child(value.to_string()),
|
div()
|
||||||
|
.text_size(px(15.0))
|
||||||
|
.font_weight(FontWeight(600.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child(name),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(desc),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_plugin_catalog_list(
|
fn render_featured_actions(
|
||||||
snapshot: &BrowserSnapshot,
|
featured: Option<&InstalledPlugin>,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
if let Some(plugin) = featured {
|
||||||
|
let detail_route = format!("ely://plugin/{}", plugin.id().as_str());
|
||||||
|
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.child(action_button(
|
||||||
|
"featured-open",
|
||||||
|
"Open detail",
|
||||||
|
cx,
|
||||||
|
move |shell, window, cx| {
|
||||||
|
shell.open_internal_tab(&detail_route, window, cx);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.ml_auto()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(format!(
|
||||||
|
"{} permissions",
|
||||||
|
plugin.manifest().permissions().len()
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
} else {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.child(action_button(
|
||||||
|
"featured-install",
|
||||||
|
"Install",
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.choose_plugin_package(window, cx),
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_button<F>(
|
||||||
|
id: &'static str,
|
||||||
|
label: &'static str,
|
||||||
|
cx: &mut Context<ElyShell>,
|
||||||
|
handler: F,
|
||||||
|
) -> AnyElement
|
||||||
|
where
|
||||||
|
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
|
||||||
|
{
|
||||||
|
div()
|
||||||
|
.id(SharedString::from(id))
|
||||||
|
.px(px(14.0))
|
||||||
|
.py(px(7.0))
|
||||||
|
.rounded(px(8.0))
|
||||||
|
.bg(rgb(colors::INK))
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.opacity(0.92))
|
||||||
|
.active(|style| style.opacity(0.78))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx)))
|
||||||
|
.child(label)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(6.0))
|
||||||
|
.child(category_chip("All", true))
|
||||||
|
.child(category_chip("Installed", false))
|
||||||
|
.child(category_chip(
|
||||||
|
sandbox_chip_label(snapshot),
|
||||||
|
false,
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.ml_auto()
|
||||||
|
.text_size(px(11.5))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(format!(
|
||||||
|
"{} signed · {} high-risk",
|
||||||
|
snapshot.installed_plugins.len(),
|
||||||
|
high_risk_plugin_count(snapshot)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn category_chip(label: &'static str, active: bool) -> AnyElement {
|
||||||
|
let bg = if active { 0x1d1c1aff } else { 0xffffffb3 };
|
||||||
|
let text_color = if active { 0xffffff } else { colors::INK_2 };
|
||||||
|
|
||||||
|
div()
|
||||||
|
.px(px(12.0))
|
||||||
|
.py(px(6.0))
|
||||||
|
.rounded(px(999.0))
|
||||||
|
.bg(rgba(bg))
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(text_color))
|
||||||
|
.child(label)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_grid(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
if snapshot.installed_plugins.is_empty() {
|
if snapshot.installed_plugins.is_empty() {
|
||||||
return div()
|
return render_empty_state(cx);
|
||||||
.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()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.overflow_y_scrollbar()
|
.overflow_y_scrollbar()
|
||||||
.border_t_1()
|
.grid()
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
.grid_cols(4)
|
||||||
.children(snapshot.installed_plugins.iter().enumerate().map(|(index, plugin)| {
|
.gap(px(14.0))
|
||||||
render_plugin_catalog_row(index, plugin, &snapshot.active_profile_kind, cx)
|
.children(
|
||||||
}))
|
snapshot
|
||||||
|
.installed_plugins
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, plugin)| {
|
||||||
|
render_plugin_card(index, plugin, &snapshot.active_profile_kind, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_plugin_catalog_row(
|
fn render_empty_state(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(12.0))
|
||||||
|
.py(px(60.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(15.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(colors::INK))
|
||||||
|
.child("No plugins yet."),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w(px(420.0))
|
||||||
|
.text_size(px(13.0))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.child(
|
||||||
|
"Drop a signed .rplug package on ELY to extend the browser with sandboxed \
|
||||||
|
tools. Plugins ship with their own sidebar controls.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(action_button(
|
||||||
|
"empty-install",
|
||||||
|
"Install plugin",
|
||||||
|
cx,
|
||||||
|
|shell, window, cx| shell.choose_plugin_package(window, cx),
|
||||||
|
))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_plugin_card(
|
||||||
index: usize,
|
index: usize,
|
||||||
plugin: &InstalledPlugin,
|
plugin: &InstalledPlugin,
|
||||||
profile_kind: &ely_domain::ProfileKind,
|
profile_kind: &ProfileKind,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let detail_route = format!("ely://plugin/{}", plugin.id().as_str());
|
let detail_route = format!("ely://plugin/{}", plugin.id().as_str());
|
||||||
let high_risk_count = plugin.manifest().high_risk_permissions().count();
|
let enabled = plugin.enabled_for_profile(profile_kind);
|
||||||
let status_color =
|
let status = if enabled { "Enabled" } else { "Disabled" };
|
||||||
if plugin.enabled_for_profile(profile_kind) { colors::SUCCESS } else { colors::MUTED };
|
let status_color = if enabled { colors::SUCCESS } else { colors::INK_4 };
|
||||||
let status_label =
|
let high_risk = plugin.manifest().high_risk_permissions().count();
|
||||||
if plugin.enabled_for_profile(profile_kind) { "Enabled" } else { "Disabled" };
|
let glyph = plugin.manifest().name().chars().next().unwrap_or('◇').to_string();
|
||||||
|
let initial = glyph.to_uppercase().to_string();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.py_3()
|
.id(SharedString::from(format!("plugin-card-{index}")))
|
||||||
.border_b_1()
|
.p(px(14.0))
|
||||||
.border_color(rgb(colors::HAIRLINE))
|
.rounded(px(16.0))
|
||||||
|
.bg(rgba(CARD_BG))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.flex_col()
|
||||||
.justify_between()
|
.gap(px(10.0))
|
||||||
.gap_4()
|
.cursor_pointer()
|
||||||
|
.hover(|style| style.bg(rgba(CARD_BG_HOVER)))
|
||||||
|
.active(|style| style.opacity(0.85))
|
||||||
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
||||||
|
shell.open_internal_tab(&detail_route, window, cx);
|
||||||
|
}))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.min_w_0()
|
.h(px(108.0))
|
||||||
|
.rounded(px(10.0))
|
||||||
|
.bg(linear_gradient(
|
||||||
|
135.0,
|
||||||
|
linear_color_stop(hsla(341.0 / 360.0, 0.78, 0.85, 1.0), 0.0),
|
||||||
|
linear_color_stop(hsla(228.0 / 360.0, 1.0, 0.86, 1.0), 1.0),
|
||||||
|
))
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_3()
|
.justify_center()
|
||||||
.child(div().text_color(rgb(colors::MUTED_SOFT)).child(IconName::Asterisk))
|
.text_size(px(36.0))
|
||||||
|
.text_color(rgb(0xffffff))
|
||||||
|
.child(initial),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
|
.flex()
|
||||||
|
.items_start()
|
||||||
|
.gap(px(8.0))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_size(px(13.5))
|
||||||
.font_semibold()
|
.font_weight(FontWeight(600.0))
|
||||||
.truncate()
|
|
||||||
.text_color(rgb(colors::INK))
|
.text_color(rgb(colors::INK))
|
||||||
|
.truncate()
|
||||||
.child(plugin.manifest().name().to_string()),
|
.child(plugin.manifest().name().to_string()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_size(px(11.0))
|
||||||
|
.text_color(rgb(colors::INK_4))
|
||||||
.truncate()
|
.truncate()
|
||||||
.text_color(rgb(colors::MUTED))
|
.child(plugin.manifest().author().to_string()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.px(px(9.0))
|
||||||
|
.py(px(3.0))
|
||||||
|
.rounded(px(6.0))
|
||||||
|
.bg(rgba(STATUS_BG))
|
||||||
|
.text_size(px(11.0))
|
||||||
|
.font_weight(FontWeight(500.0))
|
||||||
|
.text_color(rgb(status_color))
|
||||||
|
.child(status),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.0))
|
||||||
|
.text_color(rgb(colors::INK_3))
|
||||||
|
.max_h(px(48.0))
|
||||||
|
.overflow_hidden()
|
||||||
.child(plugin.manifest().description().to_string()),
|
.child(plugin.manifest().description().to_string()),
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.gap(px(8.0))
|
||||||
.gap_3()
|
.pt(px(8.0))
|
||||||
.text_xs()
|
.border_t_1()
|
||||||
.text_color(rgb(colors::MUTED))
|
.border_color(rgba(colors::DIVIDER))
|
||||||
.child(format!("{} permissions", plugin.manifest().permissions().len()))
|
.text_size(px(10.5))
|
||||||
.child(format!("{high_risk_count} high risk"))
|
.text_color(rgb(colors::INK_4))
|
||||||
.child(div().text_color(rgb(status_color)).child(status_label))
|
.child(format!(
|
||||||
|
"{} permissions",
|
||||||
|
plugin.manifest().permissions().len()
|
||||||
|
))
|
||||||
|
.child("·")
|
||||||
|
.child(format!("{high_risk} high risk"))
|
||||||
.child(
|
.child(
|
||||||
Button::new(("plugin-market-details", index))
|
div()
|
||||||
.ghost()
|
.ml_auto()
|
||||||
.xsmall()
|
.flex()
|
||||||
.icon(IconName::ExternalLink)
|
.items_center()
|
||||||
.label("Details")
|
.gap(px(3.0))
|
||||||
.tooltip("Open Plugin Details")
|
.text_color(rgb(colors::SUCCESS))
|
||||||
.on_click(cx.listener(move |shell, _, window, cx| {
|
.child("Sandboxed"),
|
||||||
shell.open_internal_tab(&detail_route, window, cx);
|
|
||||||
})),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enabled_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
fn featured_plugin(snapshot: &BrowserSnapshot) -> Option<&InstalledPlugin> {
|
||||||
snapshot
|
snapshot
|
||||||
.installed_plugins
|
.installed_plugins
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|plugin| plugin.enabled_for_profile(&snapshot.active_profile_kind))
|
.find(|plugin| plugin.enabled_for_profile(&snapshot.active_profile_kind))
|
||||||
.count()
|
.or_else(|| snapshot.installed_plugins.first())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn high_risk_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
fn high_risk_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
||||||
@@ -264,3 +496,21 @@ fn high_risk_plugin_count(snapshot: &BrowserSnapshot) -> usize {
|
|||||||
.filter(|plugin| plugin.manifest().high_risk_permissions().next().is_some())
|
.filter(|plugin| plugin.manifest().high_risk_permissions().next().is_some())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sandbox_chip_label(snapshot: &BrowserSnapshot) -> &'static str {
|
||||||
|
if snapshot
|
||||||
|
.installed_plugins
|
||||||
|
.iter()
|
||||||
|
.all(|plugin| plugin.manifest().high_risk_permissions().next().is_none())
|
||||||
|
{
|
||||||
|
"Sandboxed"
|
||||||
|
} else {
|
||||||
|
"Audit needed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEARCH_BG: u32 = 0xffffffd9;
|
||||||
|
const SEARCH_BG_HOVER: u32 = 0xffffffeb;
|
||||||
|
const CARD_BG: u32 = 0xffffffc7;
|
||||||
|
const CARD_BG_HOVER: u32 = 0xffffffeb;
|
||||||
|
const STATUS_BG: u32 = 0xffffffd9;
|
||||||
|
|||||||
Reference in New Issue
Block a user