Add sync object pause controls

This commit is contained in:
2026-05-08 03:39:18 -04:00
parent 569851eae9
commit 38a847c27e
8 changed files with 260 additions and 35 deletions
+2 -2
View File
@@ -63,8 +63,8 @@ impl ElyShell {
"ely://settings/shortcuts" => self.render_shortcuts_page(snapshot), "ely://settings/shortcuts" => self.render_shortcuts_page(snapshot),
"ely://settings/plugins" => self.render_plugins_page(snapshot, cx), "ely://settings/plugins" => self.render_plugins_page(snapshot, cx),
"ely://settings/profiles" => self.render_profiles_page(snapshot, cx), "ely://settings/profiles" => self.render_profiles_page(snapshot, cx),
"ely://settings/sync" => self.render_sync_page(snapshot), "ely://settings/sync" => self.render_sync_page(snapshot, cx),
"ely://sync/status" => self.render_sync_page(snapshot), "ely://sync/status" => self.render_sync_page(snapshot, cx),
_ => render_default_page(tab), _ => render_default_page(tab),
} }
} }
+88 -14
View File
@@ -1,13 +1,23 @@
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors; use ely_design_system::colors;
use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus}; use ely_domain::{
use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb}; 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 gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
use super::{ElyShell, render_canvas_surface}; use super::{ElyShell, render_canvas_surface};
impl ElyShell { 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<Self>,
) -> AnyElement {
render_canvas_surface( render_canvas_surface(
div() div()
.size_full() .size_full()
@@ -17,7 +27,7 @@ impl ElyShell {
.gap_5() .gap_5()
.child(render_sync_header(snapshot)) .child(render_sync_header(snapshot))
.child(render_sync_queue(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() .into_any_element()
} }
fn render_sync_objects(snapshot: &BrowserSnapshot) -> AnyElement { fn render_sync_objects(snapshot: &BrowserSnapshot, cx: &mut Context<ElyShell>) -> AnyElement {
div() div()
.flex_1() .flex_1()
.min_h_0() .min_h_0()
@@ -96,11 +106,22 @@ fn render_sync_objects(snapshot: &BrowserSnapshot) -> AnyElement {
.overflow_y_scrollbar() .overflow_y_scrollbar()
.border_t_1() .border_t_1()
.border_color(rgb(colors::HAIRLINE)) .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() .into_any_element()
} }
fn render_sync_object_row(status: &SyncObjectStatus) -> AnyElement { fn render_sync_object_row(
index: usize,
status: &SyncObjectStatus,
cx: &mut Context<ElyShell>,
) -> AnyElement {
div() div()
.py_3() .py_3()
.border_b_1() .border_b_1()
@@ -133,21 +154,64 @@ fn render_sync_object_row(status: &SyncObjectStatus) -> AnyElement {
) )
.child( .child(
div() div()
.text_xs() .flex()
.font_semibold() .items_center()
.text_color(rgb(sync_object_state_color(status.state()))) .gap_3()
.child(sync_object_state_label(status.state())), .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() .into_any_element()
} }
fn render_sync_policy_toggle(
index: usize,
status: &SyncObjectStatus,
cx: &mut Context<ElyShell>,
) -> 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 { fn connection_label(connection: &SyncConnectionState) -> &'static str {
match connection { match connection {
SyncConnectionState::SignedOut => "Signed out", SyncConnectionState::SignedOut => "Signed out",
} }
} }
fn sync_object_kind_label(kind: &SyncObjectKind) -> &'static str { fn sync_object_kind_label(kind: SyncObjectKind) -> &'static str {
match kind { match kind {
SyncObjectKind::Spaces => "Spaces", SyncObjectKind::Spaces => "Spaces",
SyncObjectKind::Tabs => "Tabs", 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 { match state {
SyncObjectState::LocalOnly => "Local only", SyncObjectState::LocalOnly => "Local only",
SyncObjectState::Paused => "Paused",
SyncObjectState::PrivacyControlled => "Privacy controlled", SyncObjectState::PrivacyControlled => "Privacy controlled",
} }
} }
fn sync_object_state_color(state: &SyncObjectState) -> u32 { fn sync_object_state_color(state: SyncObjectState) -> u32 {
match state { match state {
SyncObjectState::LocalOnly => colors::MUTED, SyncObjectState::LocalOnly => colors::MUTED,
SyncObjectState::Paused => colors::PRIMARY,
SyncObjectState::PrivacyControlled => 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 }
}
+14 -1
View File
@@ -8,7 +8,8 @@ mod splits;
use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{ use ely_domain::{
ArchivePolicy, CommandIntent, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, 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::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window};
use gpui_component::input::{InputEvent, InputState, SelectAll}; 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<Self>,
) {
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<Self>) { fn archive_idle_tabs_now(&mut self, cx: &mut Context<Self>) {
if let ShellState::Ready(core) = &mut self.state if let ShellState::Ready(core) = &mut self.state
&& core.archive_idle_tabs(std::time::SystemTime::now()).is_ok() && core.archive_idle_tabs(std::time::SystemTime::now()).is_ok()
+3
View File
@@ -8,6 +8,7 @@ use ely_domain::{
}; };
use crate::{CoreError, navigation::tab_title}; use crate::{CoreError, navigation::tab_title};
use sync::SyncObjectPolicies;
mod bookmarks; mod bookmarks;
mod commands; mod commands;
@@ -98,6 +99,7 @@ pub struct BrowserCore {
new_tab_destination: NewTabDestination, new_tab_destination: NewTabDestination,
history_recording_policy: HistoryRecordingPolicy, history_recording_policy: HistoryRecordingPolicy,
favorite_limit: FavoriteLimit, favorite_limit: FavoriteLimit,
sync_object_policies: SyncObjectPolicies,
command_query: String, command_query: String,
} }
@@ -134,6 +136,7 @@ impl BrowserCore {
new_tab_destination, new_tab_destination,
history_recording_policy: HistoryRecordingPolicy::default(), history_recording_policy: HistoryRecordingPolicy::default(),
favorite_limit: FavoriteLimit::default(), favorite_limit: FavoriteLimit::default(),
sync_object_policies: SyncObjectPolicies::default(),
spaces: vec![space], spaces: vec![space],
profiles: vec![profile], profiles: vec![profile],
tabs: vec![tab], tabs: vec![tab],
+88 -9
View File
@@ -1,50 +1,129 @@
use ely_domain::{SyncObjectKind, SyncObjectState, SyncObjectStatus, SyncStatus}; use ely_domain::{SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus, SyncStatus};
use super::BrowserCore; 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 { 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 { pub(super) fn sync_status(&self) -> SyncStatus {
SyncStatus::signed_out(vec![ SyncStatus::signed_out(vec![
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::Spaces, SyncObjectKind::Spaces,
self.spaces.len(), self.spaces.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::Tabs, SyncObjectKind::Tabs,
self.tabs.len(), self.tabs.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::Bookmarks, SyncObjectKind::Bookmarks,
self.bookmarks.len(), self.bookmarks.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::ReadingList, SyncObjectKind::ReadingList,
self.reading_list.len(), self.reading_list.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::Profiles, SyncObjectKind::Profiles,
self.profiles.len(), self.profiles.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::SitePermissions, SyncObjectKind::SitePermissions,
self.site_permissions.len(), self.site_permissions.len(),
SyncObjectState::LocalOnly, SyncObjectState::LocalOnly,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::History, SyncObjectKind::History,
self.history_entries.len(), self.history_entries.len(),
SyncObjectState::PrivacyControlled, SyncObjectState::PrivacyControlled,
), ),
SyncObjectStatus::new( self.sync_object_status(
SyncObjectKind::PluginSettings, SyncObjectKind::PluginSettings,
self.installed_plugins.len(), self.installed_plugins.len(),
SyncObjectState::LocalOnly, 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)
}
} }
+32 -1
View File
@@ -1,7 +1,10 @@
use std::error::Error; use std::error::Error;
use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_browser_core::{BrowserCore, InitialBrowserConfig};
use ely_domain::{SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus, UrlText}; use ely_domain::{
SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
UrlText,
};
#[test] #[test]
fn default_sync_status_reflects_local_browser_state() -> Result<(), Box<dyn Error>> { fn default_sync_status_reflects_local_browser_state() -> Result<(), Box<dyn Error>> {
@@ -32,3 +35,31 @@ fn default_sync_status_reflects_local_browser_state() -> Result<(), Box<dyn Erro
); );
Ok(()) Ok(())
} }
#[test]
fn sync_object_policy_pauses_object_kind() -> Result<(), Box<dyn Error>> {
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(())
}
+2 -1
View File
@@ -48,7 +48,8 @@ pub use site_permission::{
pub use space::{ArchivePolicy, Space}; pub use space::{ArchivePolicy, Space};
pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane}; pub use split::{MAX_SPLIT_PANES, SplitAxis, SplitLayout, SplitPane};
pub use sync::{ pub use sync::{
SyncConnectionState, SyncObjectKind, SyncObjectState, SyncObjectStatus, SyncStatus, SyncConnectionState, SyncObjectKind, SyncObjectPolicy, SyncObjectState, SyncObjectStatus,
SyncStatus,
}; };
pub use tab::{BrowserTab, TabFlags, TabState}; pub use tab::{BrowserTab, TabFlags, TabState};
pub use url_text::UrlText; pub use url_text::UrlText;
+31 -7
View File
@@ -3,7 +3,7 @@ pub enum SyncConnectionState {
SignedOut, SignedOut,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SyncObjectKind { pub enum SyncObjectKind {
Spaces, Spaces,
Tabs, Tabs,
@@ -15,9 +15,17 @@ pub enum SyncObjectKind {
PluginSettings, 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 { pub enum SyncObjectState {
LocalOnly, LocalOnly,
Paused,
PrivacyControlled, PrivacyControlled,
} }
@@ -26,6 +34,7 @@ pub struct SyncObjectStatus {
kind: SyncObjectKind, kind: SyncObjectKind,
local_count: usize, local_count: usize,
state: SyncObjectState, state: SyncObjectState,
policy: SyncObjectPolicy,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -39,12 +48,22 @@ pub struct SyncStatus {
impl SyncObjectStatus { impl SyncObjectStatus {
#[must_use] #[must_use]
pub fn new(kind: SyncObjectKind, local_count: usize, state: SyncObjectState) -> Self { 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] #[must_use]
pub fn kind(&self) -> &SyncObjectKind { pub fn with_policy(
&self.kind 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] #[must_use]
@@ -53,8 +72,13 @@ impl SyncObjectStatus {
} }
#[must_use] #[must_use]
pub fn state(&self) -> &SyncObjectState { pub fn state(&self) -> SyncObjectState {
&self.state self.state
}
#[must_use]
pub fn policy(&self) -> SyncObjectPolicy {
self.policy
} }
} }