Fix shell regressions and reset Servo embedding direction

This commit is contained in:
2026-05-16 11:23:41 -04:00
parent 7823ecd0a3
commit d076dad356
36 changed files with 683 additions and 1919 deletions
+1 -6
View File
@@ -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)
);
}
}
+31 -20
View File
@@ -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);
+4
View File
@@ -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()
}
+6 -24
View File
@@ -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>,
+69 -296
View File
@@ -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
)
}
+28 -11
View File
@@ -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()
}
+9 -2
View File
@@ -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) => {
+35
View File
@@ -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())
}
+54 -4
View File
@@ -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 })
}
}