Fix shell regressions and reset Servo embedding direction
This commit is contained in:
@@ -11,7 +11,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use ely_design_system::spacing;
|
||||
use ely_domain::UrlText;
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Application, Bounds, Entity, Focusable, Menu, MenuItem,
|
||||
@@ -19,6 +18,7 @@ use gpui::{
|
||||
};
|
||||
use gpui_component_assets::Assets;
|
||||
use shell::ElyShell;
|
||||
use shell::chrome::{TRAFFIC_LIGHT_ORIGIN_X, TRAFFIC_LIGHT_ORIGIN_Y};
|
||||
use shortcuts::bind_shortcuts;
|
||||
|
||||
use crate::brand::{DEEP_LINK_PREFIX, PRODUCT_NAME};
|
||||
@@ -52,11 +52,6 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
// Measured from the window's top-left to the close button origin.
|
||||
// Places the macOS traffic lights inside the calm part of the corner curve.
|
||||
const TRAFFIC_LIGHT_ORIGIN_X: f32 = spacing::SHELL_INSET + 34.0;
|
||||
const TRAFFIC_LIGHT_ORIGIN_Y: f32 = spacing::SHELL_INSET + 22.0;
|
||||
|
||||
fn main() {
|
||||
init_tracing();
|
||||
let pending_deep_links = PendingDeepLinks::default();
|
||||
|
||||
@@ -5,11 +5,12 @@ use std::{
|
||||
};
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use ely_domain::ProfileId;
|
||||
use ely_domain::{ProfileId, ProfileKind};
|
||||
|
||||
const ELY_QUALIFIER: &str = "com";
|
||||
const ELY_ORGANIZATION: &str = "elydora";
|
||||
const ELY_APPLICATION: &str = "ELY Browser";
|
||||
const DEFAULT_STANDARD_PROFILE_DIR: &str = "default";
|
||||
|
||||
pub(crate) fn default_profile_data_root() -> Option<PathBuf> {
|
||||
ProjectDirs::from(ELY_QUALIFIER, ELY_ORGANIZATION, ELY_APPLICATION)
|
||||
@@ -20,6 +21,18 @@ pub(crate) fn profile_data_dir(profile_data_root: &Path, profile_id: &ProfileId)
|
||||
profile_data_root.join(profile_id.as_str()).join("servo")
|
||||
}
|
||||
|
||||
pub(crate) fn sync_profile_data_dir(
|
||||
profile_data_root: &Path,
|
||||
profile_id: &ProfileId,
|
||||
profile_name: &str,
|
||||
profile_kind: &ProfileKind,
|
||||
) -> PathBuf {
|
||||
if profile_name == "Default" && matches!(profile_kind, ProfileKind::Standard) {
|
||||
return profile_data_root.join(DEFAULT_STANDARD_PROFILE_DIR).join("servo");
|
||||
}
|
||||
profile_data_dir(profile_data_root, profile_id)
|
||||
}
|
||||
|
||||
pub(crate) fn transient_profile_data_dir(
|
||||
profile_id: &ProfileId,
|
||||
) -> Result<PathBuf, SystemTimeError> {
|
||||
@@ -36,3 +49,32 @@ pub(crate) enum ProfileDataMode {
|
||||
Persistent,
|
||||
Transient,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{profile_data_dir, sync_profile_data_dir};
|
||||
use ely_domain::{ProfileId, ProfileKind};
|
||||
|
||||
#[test]
|
||||
fn default_standard_sync_profile_dir_is_stable() {
|
||||
let root = std::path::Path::new("/profiles");
|
||||
let first_id = ProfileId::new();
|
||||
let second_id = ProfileId::new();
|
||||
|
||||
assert_eq!(
|
||||
sync_profile_data_dir(root, &first_id, "Default", &ProfileKind::Standard),
|
||||
sync_profile_data_dir(root, &second_id, "Default", &ProfileKind::Standard)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_sync_profile_dir_keeps_profile_identity() {
|
||||
let root = std::path::Path::new("/profiles");
|
||||
let profile_id = ProfileId::new();
|
||||
|
||||
assert_eq!(
|
||||
sync_profile_data_dir(root, &profile_id, "Personal", &ProfileKind::Standard),
|
||||
profile_data_dir(root, &profile_id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
use ely_browser_core::SyncEngine;
|
||||
use ely_domain::{ProfileId, ProfileKind};
|
||||
use ely_sync_client::{ApiClientConfig, BearerToken, send_email_otp, verify_email_otp};
|
||||
use gpui::Context;
|
||||
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, sync_profile_data_dir};
|
||||
|
||||
use super::sync_state::{SyncStateUpdate, sync_platform_label};
|
||||
use super::{ElyShell, ShellState};
|
||||
@@ -44,16 +45,6 @@ pub(crate) enum AuthFlowPhase {
|
||||
}
|
||||
|
||||
impl AuthFlowPhase {
|
||||
pub(crate) fn email(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Idle => None,
|
||||
Self::SendingCode { email }
|
||||
| Self::AwaitingOtp { email }
|
||||
| Self::Verifying { email }
|
||||
| Self::Error { email, .. } => Some(email),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn error_message(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Error { message, .. } => Some(message.as_str()),
|
||||
@@ -102,8 +93,8 @@ impl ElyShell {
|
||||
AuthFlowPhase::Error { email, message: "Enter the code you received.".to_string() };
|
||||
return;
|
||||
}
|
||||
let active_profile_id = match active_profile_id_for(&self.state) {
|
||||
Some(id) => id,
|
||||
let active_profile = match active_profile_sync_context_for(&self.state) {
|
||||
Some(profile) => profile,
|
||||
None => return,
|
||||
};
|
||||
let Some(profile_root) = default_profile_data_root() else {
|
||||
@@ -113,7 +104,12 @@ impl ElyShell {
|
||||
};
|
||||
return;
|
||||
};
|
||||
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
||||
let profile_dir = sync_profile_data_dir(
|
||||
&profile_root,
|
||||
&active_profile.id,
|
||||
&active_profile.name,
|
||||
&active_profile.kind,
|
||||
);
|
||||
self.auth_flow_phase = AuthFlowPhase::Verifying { email: email.clone() };
|
||||
let tx = self.sync_inbox_tx.clone();
|
||||
spawn_verify_otp(email, normalized_otp, profile_dir, tx);
|
||||
@@ -124,14 +120,19 @@ impl ElyShell {
|
||||
/// call to make, the token is the only artefact we own.
|
||||
pub(crate) fn submit_sign_out(&mut self, _cx: &mut Context<Self>) {
|
||||
self.auth_flow_phase = AuthFlowPhase::Idle;
|
||||
let active_profile_id = match active_profile_id_for(&self.state) {
|
||||
Some(id) => id,
|
||||
let active_profile = match active_profile_sync_context_for(&self.state) {
|
||||
Some(profile) => profile,
|
||||
None => return,
|
||||
};
|
||||
let Some(profile_root) = default_profile_data_root() else {
|
||||
return;
|
||||
};
|
||||
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
||||
let profile_dir = sync_profile_data_dir(
|
||||
&profile_root,
|
||||
&active_profile.id,
|
||||
&active_profile.name,
|
||||
&active_profile.kind,
|
||||
);
|
||||
match SyncEngine::for_profile_dir(&profile_dir, "ELY", sync_platform_label()) {
|
||||
Ok(mut engine) => {
|
||||
let _ = engine.install_bearer("");
|
||||
@@ -162,11 +163,22 @@ fn normalize_email(raw: &str) -> Option<String> {
|
||||
Some(trimmed.to_lowercase())
|
||||
}
|
||||
|
||||
fn active_profile_id_for(state: &ShellState) -> Option<ely_domain::ProfileId> {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ActiveProfileSyncContext {
|
||||
id: ProfileId,
|
||||
name: String,
|
||||
kind: ProfileKind,
|
||||
}
|
||||
|
||||
fn active_profile_sync_context_for(state: &ShellState) -> Option<ActiveProfileSyncContext> {
|
||||
let ShellState::Ready(core) = state else {
|
||||
return None;
|
||||
};
|
||||
core.snapshot().ok().map(|snapshot| snapshot.active_profile_id.clone())
|
||||
core.snapshot().ok().map(|snapshot| ActiveProfileSyncContext {
|
||||
id: snapshot.active_profile_id,
|
||||
name: snapshot.active_profile_name,
|
||||
kind: snapshot.active_profile_kind,
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_send_otp(email: String, tx: Sender<SyncStateUpdate>) {
|
||||
@@ -253,7 +265,6 @@ mod tests {
|
||||
#[test]
|
||||
fn auth_phase_helpers() {
|
||||
let phase = AuthFlowPhase::Verifying { email: "you@there".to_string() };
|
||||
assert_eq!(phase.email(), Some("you@there"));
|
||||
assert!(phase.is_busy());
|
||||
assert_eq!(phase.error_message(), None);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) mod split_pane;
|
||||
pub(crate) mod topbar;
|
||||
pub(crate) mod typography;
|
||||
pub(crate) mod wallpaper;
|
||||
pub(crate) mod window_traffic_lights;
|
||||
|
||||
pub(crate) use appearance_form::render_appearance_form;
|
||||
pub(crate) use brand_glyph::{accent_color_for_host, render_glyph_for};
|
||||
@@ -34,3 +35,6 @@ pub(crate) use split_pane::{
|
||||
pub(crate) use topbar::render_topbar;
|
||||
pub(crate) use typography::{SANS_FAMILY, SERIF_FAMILY, register_serif_fonts};
|
||||
pub(crate) use wallpaper::render_wallpaper;
|
||||
pub(crate) use window_traffic_lights::{
|
||||
TRAFFIC_LIGHT_ORIGIN_X, TRAFFIC_LIGHT_ORIGIN_Y, render_macos_traffic_light_hitboxes,
|
||||
};
|
||||
|
||||
@@ -100,7 +100,7 @@ const NAV_GROUPS: &[NavGroup] = &[
|
||||
/// right swapped" instead of "the layout disappeared and a new tab
|
||||
/// opened" — the prior behavior that misread to users as a tab spawn.
|
||||
pub(crate) fn render_settings_shell(
|
||||
snapshot: &BrowserSnapshot,
|
||||
_snapshot: &BrowserSnapshot,
|
||||
active_route: &str,
|
||||
content: AnyElement,
|
||||
cx: &mut Context<ElyShell>,
|
||||
@@ -109,16 +109,12 @@ pub(crate) fn render_settings_shell(
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.flex()
|
||||
.child(render_nav_column(snapshot, active_route, cx))
|
||||
.child(render_nav_column(active_route, cx))
|
||||
.child(content)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_nav_column(
|
||||
snapshot: &BrowserSnapshot,
|
||||
active_route: &str,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
fn render_nav_column(active_route: &str, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.w(px(232.0))
|
||||
.h_full()
|
||||
@@ -132,7 +128,7 @@ fn render_nav_column(
|
||||
.flex_col()
|
||||
.gap(px(2.0))
|
||||
.overflow_y_scrollbar()
|
||||
.child(render_nav_brand(snapshot))
|
||||
.child(render_nav_brand())
|
||||
.children(
|
||||
NAV_GROUPS
|
||||
.iter()
|
||||
@@ -142,13 +138,10 @@ fn render_nav_column(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_nav_brand(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_nav_brand() -> AnyElement {
|
||||
div()
|
||||
.px(px(12.0))
|
||||
.pb(px(12.0))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(18.0))
|
||||
@@ -156,12 +149,6 @@ fn render_nav_brand(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Settings"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(rgb(colors::ink_4()))
|
||||
.child(format!("ELY 0.42 · Profile: {}", snapshot.active_profile_name)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::{colors, spacing};
|
||||
use ely_domain::BrowserTab;
|
||||
use gpui::{
|
||||
AnyElement, Context, FontWeight, ImageSource, InteractiveElement, IntoElement, ObjectFit,
|
||||
ParentElement, SharedString, StatefulInteractiveElement, Styled, StyledImage, div, hsla, img,
|
||||
linear_color_stop, linear_gradient, prelude::FluentBuilder, px, rgb, rgba,
|
||||
AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||
StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient,
|
||||
prelude::FluentBuilder, px, rgb, rgba,
|
||||
};
|
||||
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
||||
|
||||
@@ -464,30 +464,12 @@ where
|
||||
chrome_motion_feedback(press_id, selection_id, false, element)
|
||||
}
|
||||
|
||||
/// Resolve the favicon glyph for a tab row. Prefers the favicon URL
|
||||
/// the Servo runtime derived from the loaded URL; falls back to the
|
||||
/// initial-letter chip used everywhere else when the tab has no live
|
||||
/// favicon (yet to load, internal page, file URL, etc.).
|
||||
/// Resolve the favicon glyph for a tab row. Favicon metadata stays as
|
||||
/// a local key; rows render the host-derived glyph so the sidebar never
|
||||
/// blocks on network image assets.
|
||||
fn render_tab_favicon(tab: &BrowserTab, initial: &str) -> AnyElement {
|
||||
if let Some(favicon_url) = tab.favicon_key()
|
||||
&& favicon_url.starts_with("http")
|
||||
{
|
||||
return div()
|
||||
.size(px(FAVICON_SIZE))
|
||||
.flex_shrink_0()
|
||||
.rounded(px(FAVICON_RADIUS))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
img(ImageSource::from(favicon_url.to_string()))
|
||||
.size(px(FAVICON_SIZE))
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let host = tab.url().host();
|
||||
render_glyph_for(host.as_deref(), initial, FAVICON_SIZE)
|
||||
}
|
||||
|
||||
const FAVICON_SIZE: f32 = 16.0;
|
||||
const FAVICON_RADIUS: f32 = 4.0;
|
||||
|
||||
@@ -197,6 +197,12 @@ pub(crate) fn render_workspace_disclosure(
|
||||
.border_1()
|
||||
.border_color(rgba(disclosure_border()))
|
||||
.shadow(soft_shadow())
|
||||
.on_mouse_down(
|
||||
gpui::MouseButton::Left,
|
||||
cx.listener(|_, _: &gpui::MouseDownEvent, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.children(
|
||||
snapshot
|
||||
.spaces
|
||||
@@ -333,8 +339,7 @@ fn render_new_workspace_row(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
.hover(|style| style.bg(rgba(disclosure_row_hover_bg())).text_color(rgb(colors::ink())))
|
||||
.active(|style| style.opacity(0.85))
|
||||
.on_click(cx.listener(|shell, _, window, cx| {
|
||||
shell.close_workspace_picker(cx);
|
||||
shell.open_internal_tab("ely://settings/spaces", window, cx);
|
||||
shell.create_workspace_from_picker(window, cx);
|
||||
}))
|
||||
.child(div().text_color(rgb(colors::ink_3())).child(IconName::Plus))
|
||||
.child(div().flex_1().min_w_0().truncate().child("New workspace"))
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
use ely_design_system::spacing;
|
||||
use gpui::{AnyElement, Context, IntoElement, div};
|
||||
#[cfg(target_os = "macos")]
|
||||
use gpui::{
|
||||
App, InteractiveElement, MouseButton, ParentElement, SharedString, StatefulInteractiveElement,
|
||||
Styled, Window, px,
|
||||
};
|
||||
|
||||
use crate::shell::ElyShell;
|
||||
|
||||
// Measured from the window's top-left to the close button origin.
|
||||
// Places the macOS traffic lights inside the calm part of the corner curve.
|
||||
pub(crate) const TRAFFIC_LIGHT_ORIGIN_X: f32 = spacing::SHELL_INSET + 34.0;
|
||||
pub(crate) const TRAFFIC_LIGHT_ORIGIN_Y: f32 = spacing::SHELL_INSET + 22.0;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const TRAFFIC_LIGHT_HITBOX_SIZE: f32 = 18.0;
|
||||
#[cfg(target_os = "macos")]
|
||||
const TRAFFIC_LIGHT_HITBOX_OFFSET: f32 = -2.0;
|
||||
#[cfg(target_os = "macos")]
|
||||
const TRAFFIC_LIGHT_BUTTON_SPACING: f32 = 20.0;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[derive(Clone, Copy)]
|
||||
enum TrafficLightAction {
|
||||
Close,
|
||||
Minimize,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn render_macos_traffic_light_hitboxes(_cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.child(render_traffic_light_hitbox("macos-close-hitbox", 0.0, TrafficLightAction::Close))
|
||||
.child(render_traffic_light_hitbox(
|
||||
"macos-minimize-hitbox",
|
||||
1.0,
|
||||
TrafficLightAction::Minimize,
|
||||
))
|
||||
.child(render_traffic_light_hitbox("macos-zoom-hitbox", 2.0, TrafficLightAction::Zoom))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub(crate) fn render_macos_traffic_light_hitboxes(_cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div().into_any_element()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn render_traffic_light_hitbox(
|
||||
id: &'static str,
|
||||
index: f32,
|
||||
action: TrafficLightAction,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.id(SharedString::from(id))
|
||||
.absolute()
|
||||
.left(px(TRAFFIC_LIGHT_ORIGIN_X
|
||||
+ index * TRAFFIC_LIGHT_BUTTON_SPACING
|
||||
+ TRAFFIC_LIGHT_HITBOX_OFFSET))
|
||||
.top(px(TRAFFIC_LIGHT_ORIGIN_Y + TRAFFIC_LIGHT_HITBOX_OFFSET))
|
||||
.size(px(TRAFFIC_LIGHT_HITBOX_SIZE))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, cx| {
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(move |_, window, cx| {
|
||||
run_traffic_light_action(action, window, cx);
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn run_traffic_light_action(action: TrafficLightAction, window: &mut Window, _cx: &mut App) {
|
||||
match action {
|
||||
TrafficLightAction::Close => window.remove_window(),
|
||||
TrafficLightAction::Minimize => window.minimize_window(),
|
||||
TrafficLightAction::Zoom => window.zoom_window(),
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ mod plugin_catalog;
|
||||
mod plugin_details;
|
||||
mod plugin_editors_pick;
|
||||
mod plugins;
|
||||
mod privacy_data_inventory;
|
||||
mod privacy_security;
|
||||
mod profiles;
|
||||
mod reading_list;
|
||||
|
||||
@@ -15,89 +15,19 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_advanced_header(snapshot))
|
||||
.child(render_advanced_summary(snapshot))
|
||||
.child(render_advanced_header())
|
||||
.child(render_advanced_rows(snapshot)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_advanced_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_advanced_header() -> AnyElement {
|
||||
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)).text_color(rgb(colors::ink())).child("Advanced"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Space: {}", snapshot.active_space_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Inspector)
|
||||
.child(format!("{} policies", advanced_policy_count())),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_advanced_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().text_color(rgb(colors::primary())).child(IconName::Inspector))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Local Runtime"),
|
||||
)
|
||||
.child(div().text_xs().truncate().text_color(rgb(colors::muted())).child(
|
||||
format!(
|
||||
"{} space / {} profile",
|
||||
snapshot.active_space_name, snapshot.active_profile_name
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(div().text_xs().font_semibold().text_color(rgb(colors::success())).child("Local"))
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Advanced"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -132,24 +62,6 @@ fn render_advanced_rows(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
download_policy_label(&snapshot.active_download_policy),
|
||||
"Active Profile download destination policy",
|
||||
))
|
||||
.child(advanced_row(
|
||||
IconName::Globe,
|
||||
"Sync Objects",
|
||||
snapshot.sync_status.objects().len().to_string(),
|
||||
"Object scopes tracked by local Sync state",
|
||||
))
|
||||
.child(advanced_row(
|
||||
IconName::Asterisk,
|
||||
"Installed Plugins",
|
||||
snapshot.installed_plugins.len().to_string(),
|
||||
"Verified plugins registered in Browser Core",
|
||||
))
|
||||
.child(advanced_row(
|
||||
IconName::Inspector,
|
||||
"Audit Events",
|
||||
audit_event_count(snapshot).to_string(),
|
||||
"Plugin and site permission audit records",
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -236,11 +148,3 @@ fn archive_policy_label(policy: &ArchivePolicy) -> &'static str {
|
||||
ArchivePolicy::IdleDays(_) => "Custom",
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_event_count(snapshot: &BrowserSnapshot) -> usize {
|
||||
snapshot.plugin_audit_events.len() + snapshot.site_permission_audit_events.len()
|
||||
}
|
||||
|
||||
fn advanced_policy_count() -> usize {
|
||||
8
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
||||
use directories::UserDirs;
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::{DownloadDestination, DownloadPolicy};
|
||||
use ely_domain::DownloadPolicy;
|
||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||
use gpui_component::{
|
||||
IconName, Selectable, Sizable, StyledExt,
|
||||
@@ -11,7 +11,7 @@ use gpui_component::{
|
||||
scroll::ScrollableElement,
|
||||
};
|
||||
|
||||
use super::{ElyShell, download_labels::download_policy_label, render_canvas_surface};
|
||||
use super::{ElyShell, render_canvas_surface};
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DownloadPolicyOption {
|
||||
@@ -36,115 +36,29 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_download_settings_header(snapshot))
|
||||
.child(render_download_policy_summary(snapshot, cx))
|
||||
.child(render_download_settings_header(cx))
|
||||
.child(render_download_policy_rows(&snapshot.active_download_policy, &options, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_download_settings_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_download_settings_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
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)).text_color(rgb(colors::ink())).child("Downloads"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Folder)
|
||||
.child(download_destination_short_label(&snapshot.active_download_policy)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_download_policy_summary(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Downloads"))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().text_color(rgb(colors::primary())).child(IconName::Folder))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Download location"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(download_policy_label(&snapshot.active_download_policy)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("{} entries", snapshot.download_entries.len())),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-download-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Download Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_active_profile_download_settings(cx);
|
||||
})),
|
||||
),
|
||||
Button::new("reset-download-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Download Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_active_profile_download_settings(cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -261,13 +175,6 @@ fn user_downloads_dir() -> Option<PathBuf> {
|
||||
UserDirs::new().and_then(|dirs| dirs.download_dir().map(Path::to_path_buf))
|
||||
}
|
||||
|
||||
fn download_destination_short_label(policy: &DownloadPolicy) -> &'static str {
|
||||
match policy.destination() {
|
||||
DownloadDestination::AskEveryTime => "Ask Every Time",
|
||||
DownloadDestination::FixedDirectory(_) => "Fixed Folder",
|
||||
}
|
||||
}
|
||||
|
||||
fn download_policy_icon(option: &DownloadPolicyOption, selected: bool) -> IconName {
|
||||
if selected { IconName::CircleCheck } else { option.icon.clone() }
|
||||
}
|
||||
|
||||
@@ -23,117 +23,29 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_general_header(snapshot))
|
||||
.child(render_general_summary(snapshot.new_tab_destination, cx))
|
||||
.child(render_general_header(cx))
|
||||
.child(render_new_tab_destinations(snapshot.new_tab_destination, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_general_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_general_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
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)).text_color(rgb(colors::ink())).child("General"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Settings2)
|
||||
.child(snapshot.new_tab_destination.name()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_general_summary(
|
||||
destination: NewTabDestination,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("General"))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
div().text_color(rgb(colors::primary())).child(destination_icon(destination)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(format!("New Tab opens {}", destination.name())),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(destination.detail()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::success()))
|
||||
.child("Saved locally"),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-general-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore General Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_general_settings(cx);
|
||||
})),
|
||||
),
|
||||
Button::new("reset-general-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore General Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_general_settings(cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
use ely_browser_core::{BrowserSnapshot, LocalDataInventory};
|
||||
use ely_design_system::colors;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, rgb};
|
||||
use gpui_component::{
|
||||
IconName, Sizable, StyledExt,
|
||||
button::{Button, ButtonVariants},
|
||||
};
|
||||
|
||||
use super::ElyShell;
|
||||
|
||||
pub(super) fn render_local_data_inventory(
|
||||
snapshot: &BrowserSnapshot,
|
||||
notice: Option<&str>,
|
||||
error: Option<&str>,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let inventory = snapshot.local_data_inventory;
|
||||
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(render_inventory_header(snapshot, inventory, cx))
|
||||
.when_some(file_message(notice, error), |this, message| this.child(message))
|
||||
.child(render_inventory_rows(inventory))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_inventory_header(
|
||||
snapshot: &BrowserSnapshot,
|
||||
inventory: LocalDataInventory,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().text_color(rgb(colors::primary())).child(IconName::Inspector))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Local Data"),
|
||||
)
|
||||
.child(div().text_xs().truncate().text_color(rgb(colors::muted())).child(
|
||||
format!(
|
||||
"{} Profile inventory for review, export, and deletion.",
|
||||
snapshot.active_profile_name
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas()))
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(format!("{} items", inventory.total_items())),
|
||||
)
|
||||
.child(
|
||||
Button::new("export-local-data")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::File)
|
||||
.label("Export")
|
||||
.tooltip("Export Local Data")
|
||||
.on_click(cx.listener(|shell, _, window, cx| {
|
||||
shell.export_local_data(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn file_message(notice: Option<&str>, error: Option<&str>) -> Option<AnyElement> {
|
||||
notice
|
||||
.map(|message| render_file_message(message, colors::success()))
|
||||
.or_else(|| error.map(|message| render_file_message(message, colors::error())))
|
||||
}
|
||||
|
||||
fn render_file_message(message: &str, color: u32) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(color))
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(color))
|
||||
.child(message.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_inventory_rows(inventory: LocalDataInventory) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(inventory_row(IconName::Globe, "Open Tabs", inventory.open_tabs()))
|
||||
.child(inventory_row(IconName::Inbox, "Archived Tabs", inventory.archived_tabs()))
|
||||
.child(inventory_row(IconName::Undo2, "History", inventory.history_entries()))
|
||||
.child(inventory_row(IconName::BookOpen, "Bookmarks", inventory.bookmarks()))
|
||||
.child(inventory_row(IconName::File, "Notes", inventory.notes()))
|
||||
.child(inventory_row(IconName::CircleCheck, "Reading List", inventory.reading_list()))
|
||||
.child(inventory_row(IconName::Folder, "Downloads", inventory.downloads()))
|
||||
.child(inventory_row(IconName::Eye, "Site Permissions", inventory.site_permissions()))
|
||||
.child(inventory_row(
|
||||
IconName::Inspector,
|
||||
"Audit Events",
|
||||
inventory.site_permission_audit_events(),
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn inventory_row(icon: IconName, label: &'static str, count: usize) -> AnyElement {
|
||||
div()
|
||||
.py_2()
|
||||
.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()
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(label),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(count.to_string()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use gpui_component::{
|
||||
scroll::ScrollableElement,
|
||||
};
|
||||
|
||||
use super::{ElyShell, privacy_data_inventory::render_local_data_inventory, render_canvas_surface};
|
||||
use super::{ElyShell, render_canvas_surface};
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn render_privacy_security_page(
|
||||
@@ -27,126 +27,32 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_privacy_header(snapshot))
|
||||
.child(render_history_summary(snapshot, cx))
|
||||
.child(render_privacy_header(cx))
|
||||
.when(snapshot.active_profile_history_entry_count > 0, |this| {
|
||||
this.child(render_history_clear_controls(confirming_clear, cx))
|
||||
})
|
||||
.child(render_local_data_inventory(
|
||||
snapshot,
|
||||
self.local_data_file_notice.as_deref(),
|
||||
self.local_data_file_error.as_deref(),
|
||||
cx,
|
||||
))
|
||||
.child(render_privacy_settings_rows(snapshot, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_privacy_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_privacy_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
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))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Privacy & Security"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(privacy_icon(snapshot.history_recording_policy))
|
||||
.child(snapshot.history_recording_policy.status()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_history_summary(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Privacy & Security"))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.text_color(rgb(policy_color(snapshot.history_recording_policy)))
|
||||
.child(privacy_icon(snapshot.history_recording_policy)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(snapshot.history_recording_policy.name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(snapshot.history_recording_policy.detail()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(div().text_xs().font_semibold().text_color(rgb(colors::muted())).child(
|
||||
format!("{} Profile entries", snapshot.active_profile_history_entry_count),
|
||||
))
|
||||
.child(
|
||||
Button::new("reset-privacy-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Privacy Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_privacy_settings(cx);
|
||||
})),
|
||||
),
|
||||
Button::new("reset-privacy-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Privacy Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_privacy_settings(cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -159,14 +65,7 @@ fn render_history_clear_controls(confirming_clear: bool, cx: &mut Context<ElyShe
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child("Clear all history saved for this Profile."),
|
||||
)
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("request-clear-history")
|
||||
.danger()
|
||||
|
||||
@@ -23,112 +23,29 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_search_header(snapshot))
|
||||
.child(render_search_summary(snapshot.search_engine, cx))
|
||||
.child(render_search_header(cx))
|
||||
.child(render_search_engines(snapshot.search_engine, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_search_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_search_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
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)).text_color(rgb(colors::ink())).child("Search"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Search)
|
||||
.child(snapshot.search_engine.name()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_search_summary(search_engine: SearchEngine, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Search"))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().text_color(rgb(colors::primary())).child(IconName::Search))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(search_engine.name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(search_engine.host()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::success()))
|
||||
.child("Saved locally"),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-search-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Search Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_search_settings(cx);
|
||||
})),
|
||||
),
|
||||
Button::new("reset-search-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Search Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_search_settings(cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||
use gpui_component::{
|
||||
IconName, Sizable, StyledExt,
|
||||
@@ -18,7 +19,7 @@ const SHORTCUT_CATEGORIES: &[&str] = &["Command", "Tabs", "Library", "System", "
|
||||
impl ElyShell {
|
||||
pub(super) fn render_shortcuts_page(
|
||||
&mut self,
|
||||
snapshot: &BrowserSnapshot,
|
||||
_snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let conflicts = self.shortcut_profile.conflicts();
|
||||
@@ -30,64 +31,29 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_shortcuts_header(snapshot, conflicts.len(), cx))
|
||||
.child(render_shortcuts_header(cx))
|
||||
.child(render_shortcut_file_message(
|
||||
self.shortcut_file_notice.as_deref(),
|
||||
self.shortcut_file_error.as_deref(),
|
||||
))
|
||||
.child(render_conflict_panel(&conflicts))
|
||||
.when(!conflicts.is_empty(), |this| this.child(render_conflict_panel(&conflicts)))
|
||||
.child(render_shortcut_categories(&self.shortcut_profile, &conflicts)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_shortcuts_header(
|
||||
snapshot: &BrowserSnapshot,
|
||||
conflict_count: usize,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let status = if conflict_count == 0 {
|
||||
"Ready".to_string()
|
||||
} else {
|
||||
format!("{conflict_count} conflicts")
|
||||
};
|
||||
|
||||
fn render_shortcuts_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_end()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Shortcuts"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Shortcuts"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(shortcut_status_color(conflict_count)))
|
||||
.child(shortcut_status_icon(conflict_count))
|
||||
.child(status),
|
||||
)
|
||||
.child(
|
||||
Button::new("export-shortcuts")
|
||||
.ghost()
|
||||
@@ -311,7 +277,7 @@ fn render_shortcut_row(
|
||||
.text_xs()
|
||||
.child(shortcut_platform_label(profile, action, ShortcutPlatform::Macos))
|
||||
.child(shortcut_platform_label(profile, action, ShortcutPlatform::WindowsLinux))
|
||||
.child(shortcut_row_status(has_conflict)),
|
||||
.when(has_conflict, |this| this.child(shortcut_row_status())),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -336,19 +302,13 @@ fn shortcut_platform_label(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn shortcut_row_status(has_conflict: bool) -> AnyElement {
|
||||
let (label, color) =
|
||||
if has_conflict { ("Conflict", colors::error()) } else { ("Ready", colors::success()) };
|
||||
|
||||
div().min_w(px(72.0)).font_semibold().text_color(rgb(color)).child(label).into_any_element()
|
||||
}
|
||||
|
||||
fn shortcut_status_color(conflict_count: usize) -> u32 {
|
||||
if conflict_count == 0 { colors::success() } else { colors::error() }
|
||||
}
|
||||
|
||||
fn shortcut_status_icon(conflict_count: usize) -> IconName {
|
||||
if conflict_count == 0 { IconName::CircleCheck } else { IconName::TriangleAlert }
|
||||
fn shortcut_row_status() -> AnyElement {
|
||||
div()
|
||||
.min_w(px(72.0))
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::error()))
|
||||
.child("Conflict")
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn shortcut_row_icon(has_conflict: bool) -> IconName {
|
||||
|
||||
@@ -23,8 +23,7 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_site_permissions_header(snapshot))
|
||||
.child(render_site_permissions_summary(snapshot))
|
||||
.child(render_site_permissions_header())
|
||||
.child(render_site_permissions_controls(
|
||||
snapshot,
|
||||
self.site_permissions_clear_confirmation.as_ref()
|
||||
@@ -36,75 +35,13 @@ impl ElyShell {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_site_permissions_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_site_permissions_header() -> AnyElement {
|
||||
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))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Site Permissions"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Globe)
|
||||
.child("Profile scoped"),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_site_permissions_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([
|
||||
site_permission_metric("Configured", snapshot.site_permissions.len()),
|
||||
site_permission_metric("Allowed", allowed_count(snapshot)),
|
||||
site_permission_metric("Denied", denied_count(snapshot)),
|
||||
site_permission_metric("Audit Events", snapshot.site_permission_audit_events.len()),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn site_permission_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()),
|
||||
)
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Site Permissions"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -124,14 +61,7 @@ fn render_site_permissions_controls(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child("Clear all configured permissions for this Profile."),
|
||||
)
|
||||
.justify_end()
|
||||
.child(
|
||||
Button::new("request-clear-site-permissions")
|
||||
.danger()
|
||||
@@ -298,27 +228,6 @@ fn render_site_permission_entry(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn allowed_count(snapshot: &BrowserSnapshot) -> usize {
|
||||
snapshot
|
||||
.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
matches!(
|
||||
entry.decision(),
|
||||
SitePermissionDecision::AllowOnce | SitePermissionDecision::AllowAlways
|
||||
)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
fn denied_count(snapshot: &BrowserSnapshot) -> usize {
|
||||
snapshot
|
||||
.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| entry.decision() == SitePermissionDecision::DenyAlways)
|
||||
.count()
|
||||
}
|
||||
|
||||
fn site_settings_route(origin: &SiteOrigin) -> String {
|
||||
format!("ely://site/{}", origin.as_str())
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::{
|
||||
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
||||
SitePermissionFeature,
|
||||
};
|
||||
use ely_domain::{SiteOrigin, SitePermissionDecision, SitePermissionFeature};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||
use gpui_component::{
|
||||
@@ -40,18 +37,16 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_site_settings_header(snapshot, &origin))
|
||||
.child(render_site_permission_summary(snapshot, &origin))
|
||||
.child(render_site_permission_rows(snapshot, &origin, cx))
|
||||
.child(render_site_permission_audit(snapshot, &origin)),
|
||||
.child(render_site_settings_header(&origin))
|
||||
.child(render_site_permission_rows(snapshot, &origin, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_site_settings_header(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> AnyElement {
|
||||
fn render_site_settings_header(origin: &SiteOrigin) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_end()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
@@ -63,54 +58,13 @@ fn render_site_settings_header(snapshot: &BrowserSnapshot, origin: &SiteOrigin)
|
||||
.child(
|
||||
div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Site Settings"),
|
||||
)
|
||||
.child(div().text_sm().truncate().text_color(rgb(colors::muted())).child(format!(
|
||||
"{} / {}",
|
||||
snapshot.active_profile_name,
|
||||
origin.as_str()
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::Globe)
|
||||
.child("Profile scoped"),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_site_permission_summary(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> AnyElement {
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_b_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.children([
|
||||
site_metric("Configured", configured_count(snapshot, origin)),
|
||||
site_metric("Allowed", allowed_count(snapshot, origin)),
|
||||
site_metric("Denied", denied_count(snapshot, origin)),
|
||||
site_metric("Audit Events", audit_count(snapshot, origin)),
|
||||
])
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn site_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()),
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(origin.as_str().to_string()),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -280,47 +234,6 @@ fn permission_reset_button(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_site_permission_audit(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> AnyElement {
|
||||
let events = snapshot
|
||||
.site_permission_audit_events
|
||||
.iter()
|
||||
.filter(|event| event.origin() == origin)
|
||||
.rev()
|
||||
.take(4)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if events.is_empty() {
|
||||
return div().into_any_element();
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(div().text_xs().font_semibold().text_color(rgb(colors::muted())).child("Audit"))
|
||||
.children(events.into_iter().map(render_audit_row))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_audit_row(event: &SitePermissionAuditEvent) -> AnyElement {
|
||||
div()
|
||||
.py_2()
|
||||
.border_b_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_3()
|
||||
.text_xs()
|
||||
.child(div().min_w_0().truncate().text_color(rgb(colors::body())).child(format!(
|
||||
"{} - {}",
|
||||
event.feature().label(),
|
||||
audit_action_label(event.action())
|
||||
)))
|
||||
.child(div().text_color(rgb(colors::muted())).child("Local audit"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_invalid_site_route() -> AnyElement {
|
||||
div()
|
||||
.size_full()
|
||||
@@ -357,38 +270,6 @@ fn decision_for(
|
||||
.map(|entry| entry.decision())
|
||||
}
|
||||
|
||||
fn configured_count(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> usize {
|
||||
snapshot.site_permissions.iter().filter(|entry| entry.origin() == origin).count()
|
||||
}
|
||||
|
||||
fn allowed_count(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> usize {
|
||||
snapshot
|
||||
.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| entry.origin() == origin)
|
||||
.filter(|entry| {
|
||||
matches!(
|
||||
entry.decision(),
|
||||
SitePermissionDecision::AllowOnce | SitePermissionDecision::AllowAlways
|
||||
)
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
fn denied_count(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> usize {
|
||||
snapshot
|
||||
.site_permissions
|
||||
.iter()
|
||||
.filter(|entry| {
|
||||
entry.origin() == origin && entry.decision() == SitePermissionDecision::DenyAlways
|
||||
})
|
||||
.count()
|
||||
}
|
||||
|
||||
fn audit_count(snapshot: &BrowserSnapshot, origin: &SiteOrigin) -> usize {
|
||||
snapshot.site_permission_audit_events.iter().filter(|event| event.origin() == origin).count()
|
||||
}
|
||||
|
||||
fn decision_color(decision: SitePermissionDecision) -> u32 {
|
||||
match decision {
|
||||
SitePermissionDecision::AllowOnce | SitePermissionDecision::AllowAlways => {
|
||||
@@ -408,13 +289,6 @@ fn permission_icon(decision: Option<SitePermissionDecision>) -> IconName {
|
||||
}
|
||||
}
|
||||
|
||||
fn audit_action_label(action: &SitePermissionAuditAction) -> &'static str {
|
||||
match action {
|
||||
SitePermissionAuditAction::Set(decision) => decision.label(),
|
||||
SitePermissionAuditAction::Revoked => "Reset",
|
||||
}
|
||||
}
|
||||
|
||||
fn feature_scope_label(feature: SitePermissionFeature) -> &'static str {
|
||||
match feature {
|
||||
SitePermissionFeature::Camera => "Controls camera capture requests.",
|
||||
|
||||
@@ -28,39 +28,24 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_spaces_header(snapshot, cx))
|
||||
.child(render_spaces_header(cx))
|
||||
.child(render_space_file_message(
|
||||
self.space_file_notice.as_deref(),
|
||||
self.space_file_error.as_deref(),
|
||||
))
|
||||
.child(render_active_space_summary(snapshot))
|
||||
.child(render_spaces_list(snapshot, self.pending_space_trash.as_ref(), cx))
|
||||
.child(render_trashed_spaces_list(snapshot, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_spaces_header(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
fn render_spaces_header(cx: &mut Context<ElyShell>) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_end()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Spaces"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Spaces"))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -95,17 +80,6 @@ fn render_spaces_header(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>)
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::GalleryVerticalEnd)
|
||||
.child(format!("{} spaces", snapshot.spaces.len())),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
@@ -136,60 +110,6 @@ fn render_space_file_message(notice: Option<&str>, error: Option<&str>) -> AnyEl
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_active_space_summary(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
let Some(active_space) =
|
||||
snapshot.spaces.iter().find(|space| space.id() == &snapshot.active_space_id)
|
||||
else {
|
||||
return div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::error()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.text_sm()
|
||||
.text_color(rgb(colors::error()))
|
||||
.child("Active Space is unavailable.")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(
|
||||
div().min_w_0().flex().items_center().gap_3().child(space_avatar(active_space)).child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(active_space.name().to_string()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(space_detail_label(active_space, &snapshot.profiles)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().text_xs().font_semibold().text_color(rgb(colors::success())).child("Active"))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_spaces_list(
|
||||
snapshot: &BrowserSnapshot,
|
||||
pending_space_trash: Option<&SpaceId>,
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus};
|
||||
use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectStatus};
|
||||
use gpui::{
|
||||
AnyElement, Context, FontWeight, IntoElement, ParentElement, Styled, div, px, rgb, rgba,
|
||||
};
|
||||
use gpui_component::{IconName, input::Input, scroll::ScrollableElement};
|
||||
use gpui_component::{input::Input, scroll::ScrollableElement};
|
||||
|
||||
use crate::shell::auth::AuthFlowPhase;
|
||||
|
||||
use crate::brand::SYNC_SERVICE_NAME;
|
||||
|
||||
use super::sync_controls::{
|
||||
button_bg, render_dual_button_row, render_policy_toggle, render_primary_button,
|
||||
render_reset_button, render_sign_out_button,
|
||||
};
|
||||
use super::{ElyShell, render_canvas_surface};
|
||||
use crate::shell::chrome::SERIF_FAMILY;
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn render_sync_page(
|
||||
@@ -24,109 +21,41 @@ impl ElyShell {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
render_canvas_surface(
|
||||
div().size_full().pt(px(40.0)).px(px(56.0)).pb(px(32.0)).flex().justify_center().child(
|
||||
div()
|
||||
.max_w(px(960.0))
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap(px(32.0))
|
||||
.child(render_left_column(self, snapshot, cx))
|
||||
.child(render_right_column(self, snapshot, cx)),
|
||||
),
|
||||
div()
|
||||
.size_full()
|
||||
.p(px(40.0))
|
||||
.flex()
|
||||
.justify_center()
|
||||
.child(render_sync_body(self, snapshot, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_left_column(
|
||||
fn render_sync_body(
|
||||
shell: &mut ElyShell,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.max_w(px(860.0))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_start()
|
||||
.gap(px(20.0))
|
||||
.child(render_status_pill(snapshot))
|
||||
.child(render_serif_headline())
|
||||
.child(render_intro_paragraph())
|
||||
.child(render_account_card(shell, snapshot, cx))
|
||||
.child(render_metrics_card(shell, snapshot, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_status_pill(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.px(px(12.0))
|
||||
.py(px(5.0))
|
||||
.rounded(px(999.0))
|
||||
.bg(rgba(pill_bg()))
|
||||
.text_size(px(11.0))
|
||||
.text_color(rgb(colors::ink_3()))
|
||||
.child(div().text_color(rgb(colors::accent())).child(IconName::Globe))
|
||||
.child(format!(
|
||||
"{SYNC_SERVICE_NAME} · {}",
|
||||
connection_label(snapshot.sync_status.connection())
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_serif_headline() -> AnyElement {
|
||||
div()
|
||||
.font_family(SERIF_FAMILY)
|
||||
.text_size(px(46.0))
|
||||
.font_weight(FontWeight(400.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Your tabs, on every device.")
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_intro_paragraph() -> AnyElement {
|
||||
div()
|
||||
.max_w(px(440.0))
|
||||
.text_size(px(14.0))
|
||||
.text_color(rgb(colors::ink_2()))
|
||||
.gap(px(18.0))
|
||||
.child(
|
||||
"ELY keeps tabs, workspaces, pinned items, and history mirrored across your \
|
||||
devices — encrypted in your hands and replayed at the edge.",
|
||||
div()
|
||||
.text_size(px(26.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Sync"),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_metrics_card(
|
||||
shell: &ElyShell,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.max_w(px(380.0))
|
||||
.p(px(20.0))
|
||||
.rounded(px(16.0))
|
||||
.bg(rgba(card_bg()))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(16.0))
|
||||
.child(div().text_size(px(12.5)).text_color(rgb(colors::ink_3())).child("Local queue"))
|
||||
.child(
|
||||
div()
|
||||
.grid()
|
||||
.grid_cols(2)
|
||||
.gap(px(12.0))
|
||||
.child(render_metric(
|
||||
"Pending",
|
||||
snapshot.sync_status.pending_objects(),
|
||||
colors::ink(),
|
||||
))
|
||||
.child(render_metric(
|
||||
"Failed",
|
||||
snapshot.sync_status.failed_objects(),
|
||||
colors::error(),
|
||||
)),
|
||||
.gap(px(18.0))
|
||||
.child(render_account_card(shell, snapshot, cx))
|
||||
.child(render_data_card(shell, snapshot, cx)),
|
||||
)
|
||||
.child(render_reset_button(shell, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -135,27 +64,19 @@ fn render_account_card(
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
let card = div()
|
||||
.max_w(px(380.0))
|
||||
.p(px(20.0))
|
||||
.rounded(px(16.0))
|
||||
.bg(rgba(card_bg()))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(14.0));
|
||||
let card =
|
||||
div().p(px(18.0)).rounded(px(12.0)).bg(rgba(card_bg())).flex().flex_col().gap(px(14.0));
|
||||
|
||||
match snapshot.sync_status.connection() {
|
||||
SyncConnectionState::SignedOut => card
|
||||
.child(render_account_heading("Sign in"))
|
||||
.child(render_account_subtitle("We'll email a 6-digit code from browser@elydora.com."))
|
||||
.child(render_card_heading("Account"))
|
||||
.children(account_form(shell, cx))
|
||||
.into_any_element(),
|
||||
SyncConnectionState::SignedIn
|
||||
| SyncConnectionState::AwaitingDeviceApproval
|
||||
| SyncConnectionState::SyncReady { .. }
|
||||
| SyncConnectionState::SyncError { .. } => card
|
||||
.child(render_account_heading("Account"))
|
||||
.child(render_signed_in_chip())
|
||||
.child(render_card_heading("Account"))
|
||||
.child(render_sign_out_button(shell, cx))
|
||||
.into_any_element(),
|
||||
}
|
||||
@@ -163,12 +84,10 @@ fn render_account_card(
|
||||
|
||||
fn account_form(shell: &ElyShell, cx: &mut Context<ElyShell>) -> Vec<AnyElement> {
|
||||
let mut elements: Vec<AnyElement> = Vec::new();
|
||||
|
||||
let phase = shell.auth_flow_phase.clone();
|
||||
let prefill_email = phase.email().map(str::to_string);
|
||||
|
||||
elements.push(render_account_label("Email"));
|
||||
elements.push(render_input(&shell.auth_email_input, prefill_email.as_deref()));
|
||||
elements.push(render_field_label("Email"));
|
||||
elements.push(render_input(&shell.auth_email_input));
|
||||
|
||||
match &phase {
|
||||
AuthFlowPhase::Idle | AuthFlowPhase::Error { .. } => {
|
||||
@@ -184,18 +103,11 @@ fn account_form(shell: &ElyShell, cx: &mut Context<ElyShell>) -> Vec<AnyElement>
|
||||
));
|
||||
}
|
||||
AuthFlowPhase::SendingCode { .. } => {
|
||||
elements.push(render_primary_button(
|
||||
shell,
|
||||
"send-otp",
|
||||
"Sending...",
|
||||
true,
|
||||
cx,
|
||||
|_, _| {},
|
||||
));
|
||||
elements.push(render_primary_button(shell, "send-otp", "Sending", true, cx, |_, _| {}));
|
||||
}
|
||||
AuthFlowPhase::AwaitingOtp { .. } | AuthFlowPhase::Verifying { .. } => {
|
||||
elements.push(render_account_label("Code"));
|
||||
elements.push(render_input(&shell.auth_otp_input, None));
|
||||
elements.push(render_field_label("Code"));
|
||||
elements.push(render_input(&shell.auth_otp_input));
|
||||
elements.push(render_dual_button_row(
|
||||
shell,
|
||||
phase.is_busy(),
|
||||
@@ -213,103 +125,14 @@ fn account_form(shell: &ElyShell, cx: &mut Context<ElyShell>) -> Vec<AnyElement>
|
||||
elements
|
||||
}
|
||||
|
||||
fn render_account_heading(label: &str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(label.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_account_subtitle(text: &str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(12.0))
|
||||
.text_color(rgb(colors::ink_3()))
|
||||
.child(text.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_account_label(label: &'static str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(10.5))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink_4()))
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_input(
|
||||
state: &gpui::Entity<gpui_component::input::InputState>,
|
||||
hint: Option<&str>,
|
||||
) -> AnyElement {
|
||||
let mut wrapper = div()
|
||||
.px(px(10.0))
|
||||
.py(px(8.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(button_bg()))
|
||||
.child(Input::new(state).appearance(false).cleanable(false));
|
||||
if let Some(hint) = hint {
|
||||
wrapper = wrapper.child(
|
||||
div().text_size(px(10.0)).text_color(rgb(colors::ink_4())).child(hint.to_string()),
|
||||
);
|
||||
}
|
||||
wrapper.into_any_element()
|
||||
}
|
||||
|
||||
fn render_inline_error(message: &str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(rgb(colors::error()))
|
||||
.child(message.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_signed_in_chip() -> AnyElement {
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.text_color(rgb(colors::ink_2()))
|
||||
.child("Signed in. New sessions on this device share the same encrypted snapshot.")
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_metric(label: &'static str, value: usize, color: u32) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(div().text_size(px(10.5)).text_color(rgb(colors::ink_4())).child(label))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(20.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(color))
|
||||
.child(value.to_string()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_right_column(
|
||||
shell: &ElyShell,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(px(14.0))
|
||||
.child(render_what_syncs_card(shell, snapshot, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_what_syncs_card(
|
||||
fn render_data_card(
|
||||
shell: &ElyShell,
|
||||
snapshot: &BrowserSnapshot,
|
||||
cx: &mut Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.p(px(18.0))
|
||||
.rounded(px(16.0))
|
||||
.rounded(px(12.0))
|
||||
.bg(rgba(card_bg()))
|
||||
.flex()
|
||||
.flex_col()
|
||||
@@ -320,20 +143,10 @@ fn render_what_syncs_card(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(8.0))
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("What syncs"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(11.0))
|
||||
.text_color(rgb(colors::ink_3()))
|
||||
.child(format!("{} kinds tracked", snapshot.sync_status.objects().len())),
|
||||
),
|
||||
.justify_between()
|
||||
.gap(px(10.0))
|
||||
.child(render_card_heading("Data"))
|
||||
.child(render_reset_button(shell, cx)),
|
||||
)
|
||||
.child(
|
||||
div().flex().flex_col().gap(px(2.0)).children(
|
||||
@@ -357,86 +170,58 @@ fn render_sync_object_row(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap(px(10.0))
|
||||
.py(px(8.0))
|
||||
.justify_between()
|
||||
.gap(px(12.0))
|
||||
.py(px(9.0))
|
||||
.border_b_1()
|
||||
.border_color(rgba(colors::divider()))
|
||||
.child(render_state_dot(status.state()))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(sync_object_kind_label(status.kind())),
|
||||
)
|
||||
.child(div().text_size(px(11.0)).text_color(rgb(colors::ink_4())).child(format!(
|
||||
"{} local · {}",
|
||||
status.local_count(),
|
||||
sync_object_state_label(status.state())
|
||||
))),
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(sync_object_kind_label(status.kind())),
|
||||
)
|
||||
.child(render_policy_toggle(shell, index, status, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_state_dot(state: SyncObjectState) -> AnyElement {
|
||||
let color = match state {
|
||||
SyncObjectState::LocalOnly => colors::ink_4(),
|
||||
SyncObjectState::Paused => colors::ink_5(),
|
||||
SyncObjectState::PrivacyControlled => colors::accent(),
|
||||
SyncObjectState::Synced => colors::success(),
|
||||
};
|
||||
|
||||
div().size(px(8.0)).rounded_full().bg(rgb(color)).into_any_element()
|
||||
fn render_card_heading(label: &'static str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(13.0))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn connection_label(connection: &SyncConnectionState) -> String {
|
||||
match connection {
|
||||
SyncConnectionState::SignedOut => "Local-only · drop a session token to enable".to_string(),
|
||||
SyncConnectionState::SignedIn => "Signed in · awaiting first sync".to_string(),
|
||||
SyncConnectionState::AwaitingDeviceApproval => {
|
||||
"Signed in · waiting for device approval".to_string()
|
||||
}
|
||||
SyncConnectionState::SyncReady { last_synced_at_secs } => {
|
||||
format!("Synced · last upload {}", relative_time_since(*last_synced_at_secs))
|
||||
}
|
||||
SyncConnectionState::SyncError { message } => {
|
||||
format!("Sync error · {}", short_message(message))
|
||||
}
|
||||
}
|
||||
fn render_field_label(label: &'static str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(10.5))
|
||||
.font_weight(FontWeight(500.0))
|
||||
.text_color(rgb(colors::ink_4()))
|
||||
.child(label)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn relative_time_since(secs: u64) -> String {
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
let when = UNIX_EPOCH + Duration::from_secs(secs);
|
||||
let elapsed = SystemTime::now().duration_since(when).unwrap_or_default();
|
||||
let total_secs = elapsed.as_secs();
|
||||
if total_secs < 60 {
|
||||
return format!("{total_secs}s ago");
|
||||
}
|
||||
if total_secs < 3600 {
|
||||
return format!("{}m ago", total_secs / 60);
|
||||
}
|
||||
if total_secs < 86400 {
|
||||
return format!("{}h ago", total_secs / 3600);
|
||||
}
|
||||
format!("{}d ago", total_secs / 86400)
|
||||
fn render_input(state: &gpui::Entity<gpui_component::input::InputState>) -> AnyElement {
|
||||
div()
|
||||
.px(px(10.0))
|
||||
.py(px(8.0))
|
||||
.rounded(px(8.0))
|
||||
.bg(rgba(button_bg()))
|
||||
.child(Input::new(state).appearance(false).cleanable(false))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn short_message(message: &str) -> String {
|
||||
const MAX_LEN: usize = 72;
|
||||
if message.len() <= MAX_LEN {
|
||||
return message.to_string();
|
||||
}
|
||||
let truncated: String = message.chars().take(MAX_LEN - 1).collect();
|
||||
format!("{truncated}…")
|
||||
fn render_inline_error(message: &str) -> AnyElement {
|
||||
div()
|
||||
.text_size(px(11.5))
|
||||
.text_color(rgb(colors::error()))
|
||||
.child(message.to_string())
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
|
||||
@@ -453,18 +238,6 @@ fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_object_state_label(state: SyncObjectState) -> &'static str {
|
||||
match state {
|
||||
SyncObjectState::LocalOnly => "Local only",
|
||||
SyncObjectState::Paused => "Paused",
|
||||
SyncObjectState::PrivacyControlled => "Privacy controlled",
|
||||
SyncObjectState::Synced => "Synced",
|
||||
}
|
||||
}
|
||||
|
||||
fn pill_bg() -> u32 {
|
||||
colors::pick(0xffffffb3, 0x1f1d1bb3)
|
||||
}
|
||||
fn card_bg() -> u32 {
|
||||
colors::pick(0xffffffd9, 0x1f1d1bd9)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::env;
|
||||
|
||||
use ely_browser_core::BrowserSnapshot;
|
||||
use ely_design_system::colors;
|
||||
use ely_domain::UpdatePolicy;
|
||||
@@ -7,18 +5,10 @@ use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb};
|
||||
use gpui_component::{
|
||||
IconName, Selectable, Sizable, StyledExt,
|
||||
button::{Button, ButtonVariants},
|
||||
scroll::ScrollableElement,
|
||||
};
|
||||
|
||||
use super::{ElyShell, render_canvas_surface};
|
||||
|
||||
const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
const BUILD_REVISION: &str = env!("ELY_BUILD_REVISION");
|
||||
const RELEASE_MANIFEST_PATH: &str = "/api/releases/manifest";
|
||||
const RELEASE_SIGNATURE_PATH: &str = "/api/releases/signature";
|
||||
const RELEASE_MANIFEST_CACHE: &str = "release_manifest_cache";
|
||||
const RELEASE_INTEGRITY: &str = "SHA-256 + Ed25519";
|
||||
|
||||
impl ElyShell {
|
||||
pub(super) fn render_updates_page(
|
||||
&mut self,
|
||||
@@ -32,116 +22,29 @@ impl ElyShell {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_5()
|
||||
.child(render_updates_header(snapshot))
|
||||
.child(render_updates_summary(snapshot.update_policy, cx))
|
||||
.child(render_update_policy_rows(snapshot.update_policy, cx))
|
||||
.child(render_update_contract_rows()),
|
||||
.child(render_updates_header(cx))
|
||||
.child(render_update_policy_rows(snapshot.update_policy, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn render_updates_header(snapshot: &BrowserSnapshot) -> AnyElement {
|
||||
fn render_updates_header(cx: &mut gpui::Context<ElyShell>) -> AnyElement {
|
||||
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)).text_color(rgb(colors::ink())).child("Updates"))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(format!("Profile: {}", snapshot.active_profile_name)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(IconName::LoaderCircle)
|
||||
.child(format!("Build {BUILD_REVISION}")),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_updates_summary(
|
||||
update_policy: UpdatePolicy,
|
||||
cx: &mut gpui::Context<ElyShell>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.bg(rgb(colors::canvas_soft()))
|
||||
.px_4()
|
||||
.py_3()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.gap_4()
|
||||
.child(div().text_size(px(26.0)).text_color(rgb(colors::ink())).child("Updates"))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(div().text_color(rgb(colors::primary())).child(IconName::LoaderCircle))
|
||||
.child(
|
||||
div()
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child("Release Manifest Contract"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(update_policy.detail()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::success()))
|
||||
.child(update_policy.name()),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset-update-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Update Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_update_settings(cx);
|
||||
})),
|
||||
),
|
||||
Button::new("reset-update-settings")
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.icon(IconName::Undo2)
|
||||
.label("Reset")
|
||||
.tooltip("Restore Update Defaults")
|
||||
.on_click(cx.listener(|shell, _, _, cx| {
|
||||
shell.reset_update_settings(cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -227,44 +130,6 @@ fn render_update_policy_row(
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_update_contract_rows() -> AnyElement {
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.overflow_y_scrollbar()
|
||||
.border_t_1()
|
||||
.border_color(rgb(colors::hairline()))
|
||||
.child(update_row(IconName::Info, "Current Version", APP_VERSION, "Cargo package version"))
|
||||
.child(update_row(IconName::GitHub, "Build Revision", BUILD_REVISION, "Git revision"))
|
||||
.child(update_row(
|
||||
IconName::Globe,
|
||||
"Release Target",
|
||||
release_target(),
|
||||
"Platform and architecture",
|
||||
))
|
||||
.child(update_row(
|
||||
IconName::File,
|
||||
"Manifest API",
|
||||
RELEASE_MANIFEST_PATH,
|
||||
format!("KV namespace: {RELEASE_MANIFEST_CACHE}"),
|
||||
))
|
||||
.child(update_row(
|
||||
IconName::File,
|
||||
"Signature API",
|
||||
signature_query_path(),
|
||||
"Targeted release signature document",
|
||||
))
|
||||
.child(update_row(
|
||||
IconName::CircleCheck,
|
||||
"Artifact Integrity",
|
||||
RELEASE_INTEGRITY,
|
||||
"Release manifest requires package hash and signature",
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn policy_icon(selected: bool) -> IconName {
|
||||
if selected { IconName::CircleCheck } else { IconName::LoaderCircle }
|
||||
}
|
||||
@@ -276,74 +141,3 @@ fn policy_icon_color(selected: bool) -> u32 {
|
||||
fn policy_button_label(selected: bool) -> &'static str {
|
||||
if selected { "Active" } else { "Select" }
|
||||
}
|
||||
|
||||
fn update_row(
|
||||
icon: IconName,
|
||||
label: &'static str,
|
||||
value: impl Into<String>,
|
||||
detail: impl Into<String>,
|
||||
) -> AnyElement {
|
||||
let value = value.into();
|
||||
let detail = detail.into();
|
||||
|
||||
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(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()
|
||||
.truncate()
|
||||
.text_color(rgb(colors::muted()))
|
||||
.child(detail),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.max_w(px(360.0))
|
||||
.truncate()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(rgb(colors::ink()))
|
||||
.child(value),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn release_target() -> String {
|
||||
format!("{} / {}", env::consts::OS, env::consts::ARCH)
|
||||
}
|
||||
|
||||
fn signature_query_path() -> String {
|
||||
format!(
|
||||
"{RELEASE_SIGNATURE_PATH}?platform={}&architecture={}&version={APP_VERSION}",
|
||||
env::consts::OS,
|
||||
env::consts::ARCH
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use gpui::{
|
||||
use super::chrome::command_match::visible_command_rows;
|
||||
use super::chrome::{
|
||||
SANS_FAMILY, WorkspaceDisclosureAnchor, panel_bg, panel_shadow, render_command_overlay,
|
||||
render_topbar as render_topbar_chrome, render_wallpaper, render_workspace_disclosure,
|
||||
render_workspace_disclosure_backdrop,
|
||||
render_macos_traffic_light_hitboxes, render_topbar as render_topbar_chrome, render_wallpaper,
|
||||
render_workspace_disclosure, render_workspace_disclosure_backdrop,
|
||||
};
|
||||
use super::sidebar::collapsed_sidebar_active;
|
||||
use super::{ElyShell, ShellState};
|
||||
@@ -27,31 +27,47 @@ impl Render for ElyShell {
|
||||
match &self.state {
|
||||
ShellState::Ready(core) => match core.snapshot() {
|
||||
Ok(snapshot) => {
|
||||
colors::set_mode(resolve_color_mode(
|
||||
snapshot.appearance.theme_mode(),
|
||||
appearance,
|
||||
));
|
||||
apply_color_mode(
|
||||
resolve_color_mode(snapshot.appearance.theme_mode(), appearance),
|
||||
cx,
|
||||
);
|
||||
match active_tab_from_snapshot(&snapshot) {
|
||||
Some(active_tab) => self.render_browser(&snapshot, active_tab, window, cx),
|
||||
None => render_error("active tab missing from snapshot".to_string()),
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
colors::set_mode(resolve_color_mode(
|
||||
ely_domain::ThemeMode::default(),
|
||||
appearance,
|
||||
));
|
||||
apply_color_mode(
|
||||
resolve_color_mode(ely_domain::ThemeMode::default(), appearance),
|
||||
cx,
|
||||
);
|
||||
render_error(error.to_string())
|
||||
}
|
||||
},
|
||||
ShellState::StartupError(message) => {
|
||||
colors::set_mode(resolve_color_mode(ely_domain::ThemeMode::default(), appearance));
|
||||
apply_color_mode(
|
||||
resolve_color_mode(ely_domain::ThemeMode::default(), appearance),
|
||||
cx,
|
||||
);
|
||||
render_error(message.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_color_mode(mode: colors::Mode, cx: &mut Context<ElyShell>) {
|
||||
colors::set_mode(mode);
|
||||
|
||||
let component_mode = match mode {
|
||||
colors::Mode::Light => gpui_component::ThemeMode::Light,
|
||||
colors::Mode::Dark => gpui_component::ThemeMode::Dark,
|
||||
};
|
||||
if gpui_component::Theme::global(cx).mode != component_mode {
|
||||
gpui_component::Theme::change(component_mode, None, cx);
|
||||
}
|
||||
gpui_component::Theme::global_mut(cx).font_family = SANS_FAMILY.into();
|
||||
}
|
||||
|
||||
fn resolve_color_mode(
|
||||
theme_mode: ely_domain::ThemeMode,
|
||||
window_appearance: gpui::WindowAppearance,
|
||||
@@ -154,6 +170,7 @@ impl ElyShell {
|
||||
.child(render_workspace_disclosure(snapshot, anchor, cx))
|
||||
})
|
||||
.children(render_command_overlay(self, snapshot, cx))
|
||||
.child(render_macos_traffic_light_hitboxes(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use ely_domain::{
|
||||
use gpui::Context;
|
||||
use gpui_component::slider::SliderValue;
|
||||
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, profile_data_dir};
|
||||
use crate::services::servo_profile_data::{default_profile_data_root, sync_profile_data_dir};
|
||||
|
||||
use super::sync_state::{SyncStateUpdate, sync_platform_label};
|
||||
use super::{ElyShell, ShellState};
|
||||
@@ -269,12 +269,19 @@ impl ElyShell {
|
||||
return;
|
||||
};
|
||||
let active_profile_id = snapshot.active_profile_id.clone();
|
||||
let active_profile_name = snapshot.active_profile_name.clone();
|
||||
let active_profile_kind = snapshot.active_profile_kind.clone();
|
||||
let device_name = format!("ELY · {}", snapshot.active_profile_name);
|
||||
let Some(profile_root) = default_profile_data_root() else {
|
||||
tracing::warn!(target: "ely::sync", "profile data root is unavailable");
|
||||
return;
|
||||
};
|
||||
let profile_dir = profile_data_dir(&profile_root, &active_profile_id);
|
||||
let profile_dir = sync_profile_data_dir(
|
||||
&profile_root,
|
||||
&active_profile_id,
|
||||
&active_profile_name,
|
||||
&active_profile_kind,
|
||||
);
|
||||
let bytes = match core.build_sync_snapshot_bytes() {
|
||||
Ok(bytes) => bytes,
|
||||
Err(error) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use ely_browser_core::BrowserCore;
|
||||
use ely_domain::{ProfileId, SpaceId};
|
||||
use gpui::{Context, Window};
|
||||
|
||||
@@ -121,6 +122,22 @@ impl ElyShell {
|
||||
self.close_workspace_picker(cx);
|
||||
}
|
||||
|
||||
pub(crate) fn create_workspace_from_picker(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let ShellState::Ready(core) = &mut self.state {
|
||||
let name = next_workspace_name(core);
|
||||
if core.create_space(name.clone(), workspace_icon(&name), 0xf54e00).is_ok() {
|
||||
self.workspace_picker_open = false;
|
||||
self.sync_address_input(window, cx);
|
||||
self.schedule_cloud_sync_upload(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_select_previous_space(
|
||||
&mut self,
|
||||
_: &SelectPreviousSpace,
|
||||
@@ -135,3 +152,21 @@ impl ElyShell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_workspace_name(core: &BrowserCore) -> String {
|
||||
let Ok(snapshot) = core.snapshot() else {
|
||||
return "Workspace 1".to_string();
|
||||
};
|
||||
let mut index = snapshot.spaces.len() + 1;
|
||||
loop {
|
||||
let name = format!("Workspace {index}");
|
||||
if snapshot.spaces.iter().all(|space| space.name() != name) {
|
||||
return name;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_icon(name: &str) -> String {
|
||||
name.chars().next().map_or_else(|| "W".to_string(), |char| char.to_string())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use ely_domain::SyncConnectionState;
|
||||
use ely_domain::{ProfileKind, SyncConnectionState};
|
||||
use gpui::{Context, Timer};
|
||||
|
||||
use super::{ElyShell, ShellState, auth};
|
||||
@@ -109,15 +109,21 @@ impl ElyShell {
|
||||
let Some(snapshot) = core.snapshot().ok() else {
|
||||
return false;
|
||||
};
|
||||
let active_profile_id = snapshot.active_profile_id.clone();
|
||||
let Some(profile_root) = crate::services::servo_profile_data::default_profile_data_root()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let profile_dir = crate::services::servo_profile_data::profile_data_dir(
|
||||
let profile_dir = crate::services::servo_profile_data::sync_profile_data_dir(
|
||||
&profile_root,
|
||||
&active_profile_id,
|
||||
&snapshot.active_profile_id,
|
||||
&snapshot.active_profile_name,
|
||||
&snapshot.active_profile_kind,
|
||||
);
|
||||
if snapshot.active_profile_name == "Default"
|
||||
&& matches!(snapshot.active_profile_kind, ProfileKind::Standard)
|
||||
{
|
||||
migrate_legacy_default_sync_dir(&profile_root, &profile_dir);
|
||||
}
|
||||
let bearer_path = profile_dir.join("sync").join("bearer.token");
|
||||
let bearer_present = bearer_token_file_present(&bearer_path);
|
||||
let state = if bearer_present {
|
||||
@@ -225,6 +231,50 @@ fn bearer_token_file_present(path: &Path) -> bool {
|
||||
std::fs::metadata(path).map(|metadata| metadata.len() > 0).unwrap_or(false)
|
||||
}
|
||||
|
||||
fn migrate_legacy_default_sync_dir(profile_root: &Path, stable_profile_dir: &Path) {
|
||||
let stable_sync_dir = stable_profile_dir.join("sync");
|
||||
if stable_sync_dir.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(profile_root) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let candidate = entry.path().join("servo").join("sync");
|
||||
if candidate == stable_sync_dir {
|
||||
continue;
|
||||
}
|
||||
if !bearer_token_file_present(&candidate.join("bearer.token")) {
|
||||
continue;
|
||||
}
|
||||
if let Err(error) = copy_dir_recursive(&candidate, &stable_sync_dir) {
|
||||
tracing::warn!(
|
||||
target: "ely::sync",
|
||||
error = %error,
|
||||
source = %candidate.display(),
|
||||
"legacy sync profile migration failed",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fn copy_dir_recursive(source: &Path, destination: &Path) -> std::io::Result<()> {
|
||||
std::fs::create_dir_all(destination)?;
|
||||
for entry in std::fs::read_dir(source)? {
|
||||
let entry = entry?;
|
||||
let file_type = entry.file_type()?;
|
||||
let destination_path = destination.join(entry.file_name());
|
||||
if file_type.is_dir() {
|
||||
copy_dir_recursive(&entry.path(), &destination_path)?;
|
||||
} else if file_type.is_file() {
|
||||
std::fs::copy(entry.path(), destination_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::bearer_token_file_present;
|
||||
|
||||
@@ -224,8 +224,8 @@ impl ElyShell {
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
if let Some(favicon_url) = metadata.favicon_url
|
||||
&& let Ok(true) = core.set_tab_favicon_key(&metadata.tab_id, favicon_url)
|
||||
if let Some(favicon_key) = metadata.favicon_key
|
||||
&& let Ok(true) = core.set_tab_favicon_key(&metadata.tab_id, favicon_key)
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -28,14 +28,14 @@ impl WebSurfaceMetadataTracker {
|
||||
|
||||
/// One page's worth of metadata observed in a Ready frame. The
|
||||
/// controller applies these to the `BrowserTab` after the frame has
|
||||
/// been swapped into the surface state. Title and favicon are
|
||||
/// been swapped into the surface state. Title and favicon key are
|
||||
/// independent: navigation often settles the URL first, then Servo
|
||||
/// emits a title change a frame or two later.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct WebSurfacePageMetadata {
|
||||
pub(super) tab_id: TabId,
|
||||
pub(super) title: Option<String>,
|
||||
pub(super) favicon_url: Option<String>,
|
||||
pub(super) favicon_key: Option<String>,
|
||||
}
|
||||
|
||||
impl WebSurfacePageMetadata {
|
||||
@@ -44,14 +44,14 @@ impl WebSurfacePageMetadata {
|
||||
title: Option<String>,
|
||||
loaded_url: Option<String>,
|
||||
) -> Option<Self> {
|
||||
let favicon_url = loaded_url
|
||||
let favicon_key = loaded_url
|
||||
.as_deref()
|
||||
.and_then(|loaded| ely_domain::UrlText::parse(loaded).ok())
|
||||
.and_then(|url| url.favicon_url());
|
||||
if title.is_none() && favicon_url.is_none() {
|
||||
.and_then(|url| url.favicon_key());
|
||||
if title.is_none() && favicon_key.is_none() {
|
||||
return None;
|
||||
}
|
||||
Some(Self { tab_id: tab_id.clone(), title, favicon_url })
|
||||
Some(Self { tab_id: tab_id.clone(), title, favicon_key })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user