From 38a847c27e07446852653de10e35d454d5b04a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 03:39:18 -0400 Subject: [PATCH] Add sync object pause controls --- crates/ely_app/src/shell/internal_pages.rs | 4 +- .../ely_app/src/shell/internal_pages/sync.rs | 102 +++++++++++++++--- crates/ely_app/src/shell/mod.rs | 15 ++- crates/ely_browser_core/src/state.rs | 3 + crates/ely_browser_core/src/state/sync.rs | 97 +++++++++++++++-- crates/ely_browser_core/tests/sync.rs | 33 +++++- crates/ely_domain/src/lib.rs | 3 +- crates/ely_domain/src/sync.rs | 38 +++++-- 8 files changed, 260 insertions(+), 35 deletions(-) diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index b3c0c5f..b2af90a 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -63,8 +63,8 @@ impl ElyShell { "ely://settings/shortcuts" => self.render_shortcuts_page(snapshot), "ely://settings/plugins" => self.render_plugins_page(snapshot, cx), "ely://settings/profiles" => self.render_profiles_page(snapshot, cx), - "ely://settings/sync" => self.render_sync_page(snapshot), - "ely://sync/status" => self.render_sync_page(snapshot), + "ely://settings/sync" => self.render_sync_page(snapshot, cx), + "ely://sync/status" => self.render_sync_page(snapshot, cx), _ => render_default_page(tab), } } diff --git a/crates/ely_app/src/shell/internal_pages/sync.rs b/crates/ely_app/src/shell/internal_pages/sync.rs index 54f8ef4..c407d6f 100644 --- a/crates/ely_app/src/shell/internal_pages/sync.rs +++ b/crates/ely_app/src/shell/internal_pages/sync.rs @@ -1,13 +1,23 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; -use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus}; -use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb}; +use ely_domain::{ + SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, +}; +use gpui::{ + AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, + px, rgb, +}; +use gpui::{StatefulInteractiveElement, prelude::FluentBuilder}; use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; use super::{ElyShell, render_canvas_surface}; impl ElyShell { - pub(super) fn render_sync_page(&mut self, snapshot: &BrowserSnapshot) -> AnyElement { + pub(super) fn render_sync_page( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut Context, + ) -> AnyElement { render_canvas_surface( div() .size_full() @@ -17,7 +27,7 @@ impl ElyShell { .gap_5() .child(render_sync_header(snapshot)) .child(render_sync_queue(snapshot)) - .child(render_sync_objects(snapshot)), + .child(render_sync_objects(snapshot, cx)), ) } } @@ -87,7 +97,7 @@ fn metric_block(label: &'static str, value: usize, color: u32) -> AnyElement { .into_any_element() } -fn render_sync_objects(snapshot: &BrowserSnapshot) -> AnyElement { +fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { div() .flex_1() .min_h_0() @@ -96,11 +106,22 @@ fn render_sync_objects(snapshot: &BrowserSnapshot) -> AnyElement { .overflow_y_scrollbar() .border_t_1() .border_color(rgb(colors::HAIRLINE)) - .children(snapshot.sync_status.objects().iter().map(render_sync_object_row)) + .children( + snapshot + .sync_status + .objects() + .iter() + .enumerate() + .map(|(index, status)| render_sync_object_row(index, status, cx)), + ) .into_any_element() } -fn render_sync_object_row(status: &SyncObjectStatus) -> AnyElement { +fn render_sync_object_row( + index: usize, + status: &SyncObjectStatus, + cx: &mut Context, +) -> AnyElement { div() .py_3() .border_b_1() @@ -133,21 +154,64 @@ fn render_sync_object_row(status: &SyncObjectStatus) -> AnyElement { ) .child( div() - .text_xs() - .font_semibold() - .text_color(rgb(sync_object_state_color(status.state()))) - .child(sync_object_state_label(status.state())), + .flex() + .items_center() + .gap_3() + .child( + div() + .text_xs() + .font_semibold() + .text_color(rgb(sync_object_state_color(status.state()))) + .child(sync_object_state_label(status.state())), + ) + .child(render_sync_policy_toggle(index, status, cx)), ) .into_any_element() } +fn render_sync_policy_toggle( + index: usize, + status: &SyncObjectStatus, + cx: &mut Context, +) -> AnyElement { + let enabled = status.policy() == SyncObjectPolicy::Enabled; + let next_policy = if enabled { SyncObjectPolicy::Paused } else { SyncObjectPolicy::Enabled }; + let kind = status.kind(); + + div() + .id(SharedString::from(format!("sync-policy-toggle-{index}"))) + .w(px(38.0)) + .h(px(22.0)) + .rounded_full() + .border_1() + .border_color(rgb(sync_policy_border_color(enabled))) + .bg(rgb(sync_policy_track_color(enabled))) + .p(px(2.0)) + .cursor_pointer() + .hover(|style| style.opacity(0.9)) + .active(|style| style.opacity(0.78)) + .child( + div() + .w(px(16.0)) + .h(px(16.0)) + .rounded_full() + .bg(rgb(colors::SURFACE_CARD)) + .shadow_sm() + .when(enabled, |this| this.ml(px(16.0))), + ) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_sync_object_policy(kind, next_policy, cx); + })) + .into_any_element() +} + fn connection_label(connection: &SyncConnectionState) -> &'static str { match connection { SyncConnectionState::SignedOut => "Signed out", } } -fn sync_object_kind_label(kind: &SyncObjectKind) -> &'static str { +fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str { match kind { SyncObjectKind::Spaces => "Spaces", SyncObjectKind::Tabs => "Tabs", @@ -160,16 +224,26 @@ fn sync_object_kind_label(kind: &SyncObjectKind) -> &'static str { } } -fn sync_object_state_label(state: &SyncObjectState) -> &'static str { +fn sync_object_state_label(state: SyncObjectState) -> &'static str { match state { SyncObjectState::LocalOnly => "Local only", + SyncObjectState::Paused => "Paused", SyncObjectState::PrivacyControlled => "Privacy controlled", } } -fn sync_object_state_color(state: &SyncObjectState) -> u32 { +fn sync_object_state_color(state: SyncObjectState) -> u32 { match state { SyncObjectState::LocalOnly => colors::MUTED, + SyncObjectState::Paused => colors::PRIMARY, SyncObjectState::PrivacyControlled => colors::PRIMARY, } } + +fn sync_policy_track_color(enabled: bool) -> u32 { + if enabled { colors::SUCCESS } else { colors::CANVAS_SOFT } +} + +fn sync_policy_border_color(enabled: bool) -> u32 { + if enabled { colors::SUCCESS } else { colors::HAIRLINE_STRONG } +} diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index a03a802..8f5fbb5 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -8,7 +8,8 @@ mod splits; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_domain::{ ArchivePolicy, CommandIntent, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, - NewTabDestination, ProfileId, SearchEngine, SpaceId, TabId, UrlText, + NewTabDestination, ProfileId, SearchEngine, SpaceId, SyncObjectKind, SyncObjectPolicy, TabId, + UrlText, }; use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window}; use gpui_component::input::{InputEvent, InputState, SelectAll}; @@ -317,6 +318,18 @@ impl ElyShell { } } + fn set_sync_object_policy( + &mut self, + kind: SyncObjectKind, + policy: SyncObjectPolicy, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state { + core.set_sync_object_policy(kind, policy); + cx.notify(); + } + } + fn archive_idle_tabs_now(&mut self, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state && core.archive_idle_tabs(std::time::SystemTime::now()).is_ok() diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 80b7b82..139efc6 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -8,6 +8,7 @@ use ely_domain::{ }; use crate::{CoreError, navigation::tab_title}; +use sync::SyncObjectPolicies; mod bookmarks; mod commands; @@ -98,6 +99,7 @@ pub struct BrowserCore { new_tab_destination: NewTabDestination, history_recording_policy: HistoryRecordingPolicy, favorite_limit: FavoriteLimit, + sync_object_policies: SyncObjectPolicies, command_query: String, } @@ -134,6 +136,7 @@ impl BrowserCore { new_tab_destination, history_recording_policy: HistoryRecordingPolicy::default(), favorite_limit: FavoriteLimit::default(), + sync_object_policies: SyncObjectPolicies::default(), spaces: vec![space], profiles: vec![profile], tabs: vec![tab], diff --git a/crates/ely_browser_core/src/state/sync.rs b/crates/ely_browser_core/src/state/sync.rs index e8f85a8..d9171c3 100644 --- a/crates/ely_browser_core/src/state/sync.rs +++ b/crates/ely_browser_core/src/state/sync.rs @@ -1,50 +1,129 @@ -use ely_domain::{SyncObjectKind, SyncObjectState, SyncObjectStatus, SyncStatus}; +use ely_domain::{SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, SyncStatus}; use super::BrowserCore; +#[derive(Clone, Debug)] +pub(super) struct SyncObjectPolicies { + spaces: SyncObjectPolicy, + tabs: SyncObjectPolicy, + bookmarks: SyncObjectPolicy, + reading_list: SyncObjectPolicy, + profiles: SyncObjectPolicy, + site_permissions: SyncObjectPolicy, + history: SyncObjectPolicy, + plugin_settings: SyncObjectPolicy, +} + +impl Default for SyncObjectPolicies { + fn default() -> Self { + Self { + spaces: SyncObjectPolicy::Enabled, + tabs: SyncObjectPolicy::Enabled, + bookmarks: SyncObjectPolicy::Enabled, + reading_list: SyncObjectPolicy::Enabled, + profiles: SyncObjectPolicy::Enabled, + site_permissions: SyncObjectPolicy::Enabled, + history: SyncObjectPolicy::Enabled, + plugin_settings: SyncObjectPolicy::Enabled, + } + } +} + +impl SyncObjectPolicies { + fn get(&self, kind: SyncObjectKind) -> SyncObjectPolicy { + match kind { + SyncObjectKind::Spaces => self.spaces, + SyncObjectKind::Tabs => self.tabs, + SyncObjectKind::Bookmarks => self.bookmarks, + SyncObjectKind::ReadingList => self.reading_list, + SyncObjectKind::Profiles => self.profiles, + SyncObjectKind::SitePermissions => self.site_permissions, + SyncObjectKind::History => self.history, + SyncObjectKind::PluginSettings => self.plugin_settings, + } + } + + fn set(&mut self, kind: SyncObjectKind, policy: SyncObjectPolicy) { + match kind { + SyncObjectKind::Spaces => self.spaces = policy, + SyncObjectKind::Tabs => self.tabs = policy, + SyncObjectKind::Bookmarks => self.bookmarks = policy, + SyncObjectKind::ReadingList => self.reading_list = policy, + SyncObjectKind::Profiles => self.profiles = policy, + SyncObjectKind::SitePermissions => self.site_permissions = policy, + SyncObjectKind::History => self.history = policy, + SyncObjectKind::PluginSettings => self.plugin_settings = policy, + } + } +} + impl BrowserCore { + pub fn set_sync_object_policy(&mut self, kind: SyncObjectKind, policy: SyncObjectPolicy) { + self.sync_object_policies.set(kind, policy); + } + + #[must_use] + pub fn sync_object_policy(&self, kind: SyncObjectKind) -> SyncObjectPolicy { + self.sync_object_policies.get(kind) + } + pub(super) fn sync_status(&self) -> SyncStatus { SyncStatus::signed_out(vec![ - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::Spaces, self.spaces.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::Tabs, self.tabs.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::Bookmarks, self.bookmarks.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::ReadingList, self.reading_list.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::Profiles, self.profiles.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::SitePermissions, self.site_permissions.len(), SyncObjectState::LocalOnly, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::History, self.history_entries.len(), SyncObjectState::PrivacyControlled, ), - SyncObjectStatus::new( + self.sync_object_status( SyncObjectKind::PluginSettings, self.installed_plugins.len(), SyncObjectState::LocalOnly, ), ]) } + + fn sync_object_status( + &self, + kind: SyncObjectKind, + local_count: usize, + enabled_state: SyncObjectState, + ) -> SyncObjectStatus { + let policy = self.sync_object_policies.get(kind); + let state = match policy { + SyncObjectPolicy::Enabled => enabled_state, + SyncObjectPolicy::Paused => SyncObjectState::Paused, + }; + + SyncObjectStatus::with_policy(kind, local_count, state, policy) + } } diff --git a/crates/ely_browser_core/tests/sync.rs b/crates/ely_browser_core/tests/sync.rs index b202140..94ba7ac 100644 --- a/crates/ely_browser_core/tests/sync.rs +++ b/crates/ely_browser_core/tests/sync.rs @@ -1,7 +1,10 @@ use std::error::Error; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus, UrlText}; +use ely_domain::{ + SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, + UrlText, +}; #[test] fn default_sync_status_reflects_local_browser_state() -> Result<(), Box> { @@ -32,3 +35,31 @@ fn default_sync_status_reflects_local_browser_state() -> Result<(), Box Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.set_sync_object_policy(SyncObjectKind::Tabs, SyncObjectPolicy::Paused); + + let snapshot = core.snapshot()?; + let Some(tabs_status) = + snapshot.sync_status.objects().iter().find(|status| status.kind() == SyncObjectKind::Tabs) + else { + return Err("missing tabs sync status".into()); + }; + let Some(spaces_status) = snapshot + .sync_status + .objects() + .iter() + .find(|status| status.kind() == SyncObjectKind::Spaces) + else { + return Err("missing spaces sync status".into()); + }; + + assert_eq!(core.sync_object_policy(SyncObjectKind::Tabs), SyncObjectPolicy::Paused); + assert_eq!(tabs_status.policy(), SyncObjectPolicy::Paused); + assert_eq!(tabs_status.state(), SyncObjectState::Paused); + assert_eq!(spaces_status.policy(), SyncObjectPolicy::Enabled); + assert_eq!(spaces_status.state(), SyncObjectState::LocalOnly); + Ok(()) +} diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index e492f5b..843c484 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -48,7 +48,8 @@ pub use site_permission::{ pub use space::{ArchivePolicy, Space}; pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane}; pub use sync::{ - SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus, SyncStatus, + SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, + SyncStatus, }; pub use tab::{BrowserTab, TabFlags, TabState}; pub use url_text::UrlText; diff --git a/crates/ely_domain/src/sync.rs b/crates/ely_domain/src/sync.rs index a7f671e..d7aa834 100644 --- a/crates/ely_domain/src/sync.rs +++ b/crates/ely_domain/src/sync.rs @@ -3,7 +3,7 @@ pub enum SyncConnectionState { SignedOut, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SyncObjectKind { Spaces, Tabs, @@ -15,9 +15,17 @@ pub enum SyncObjectKind { PluginSettings, } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum SyncObjectPolicy { + #[default] + Enabled, + Paused, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SyncObjectState { LocalOnly, + Paused, PrivacyControlled, } @@ -26,6 +34,7 @@ pub struct SyncObjectStatus { kind: SyncObjectKind, local_count: usize, state: SyncObjectState, + policy: SyncObjectPolicy, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -39,12 +48,22 @@ pub struct SyncStatus { impl SyncObjectStatus { #[must_use] pub fn new(kind: SyncObjectKind, local_count: usize, state: SyncObjectState) -> Self { - Self { kind, local_count, state } + Self::with_policy(kind, local_count, state, SyncObjectPolicy::Enabled) } #[must_use] - pub fn kind(&self) -> &SyncObjectKind { - &self.kind + pub fn with_policy( + kind: SyncObjectKind, + local_count: usize, + state: SyncObjectState, + policy: SyncObjectPolicy, + ) -> Self { + Self { kind, local_count, state, policy } + } + + #[must_use] + pub fn kind(&self) -> SyncObjectKind { + self.kind } #[must_use] @@ -53,8 +72,13 @@ impl SyncObjectStatus { } #[must_use] - pub fn state(&self) -> &SyncObjectState { - &self.state + pub fn state(&self) -> SyncObjectState { + self.state + } + + #[must_use] + pub fn policy(&self) -> SyncObjectPolicy { + self.policy } }