From e30fce921381f0ae0f563e182aa59e6d12fc5af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 04:13:41 -0400 Subject: [PATCH] Add update policy controls --- crates/ely_app/src/shell/internal_pages.rs | 2 +- .../src/shell/internal_pages/updates.rs | 154 ++++++++++++++++-- crates/ely_app/src/shell/settings_actions.rs | 20 ++- crates/ely_browser_core/src/state.rs | 19 ++- .../tests/settings_defaults.rs | 6 +- crates/ely_domain/src/lib.rs | 2 + crates/ely_domain/src/update.rs | 26 +++ 7 files changed, 212 insertions(+), 17 deletions(-) create mode 100644 crates/ely_domain/src/update.rs diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index 3db9a6e..abebcb6 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -96,7 +96,7 @@ impl ElyShell { "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, cx), - "ely://settings/updates" => self.render_updates_page(snapshot), + "ely://settings/updates" => self.render_updates_page(snapshot, cx), "ely://sync/status" => self.render_sync_page(snapshot, cx), url if super::web_surface::is_external_web_url(url) => { self.render_external_web_canvas(tab, snapshot, cx) diff --git a/crates/ely_app/src/shell/internal_pages/updates.rs b/crates/ely_app/src/shell/internal_pages/updates.rs index d48e5ca..7e1985b 100644 --- a/crates/ely_app/src/shell/internal_pages/updates.rs +++ b/crates/ely_app/src/shell/internal_pages/updates.rs @@ -2,10 +2,13 @@ use std::env; use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; +use ely_domain::UpdatePolicy; use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgb}; -use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; - -use crate::brand::SYNC_SERVICE_NAME; +use gpui_component::{ + IconName, Selectable, Sizable, StyledExt, + button::{Button, ButtonVariants}, + scroll::ScrollableElement, +}; use super::{ElyShell, render_canvas_surface}; @@ -17,7 +20,11 @@ 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, snapshot: &BrowserSnapshot) -> AnyElement { + pub(super) fn render_updates_page( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut gpui::Context, + ) -> AnyElement { render_canvas_surface( div() .size_full() @@ -26,7 +33,8 @@ impl ElyShell { .flex_col() .gap_5() .child(render_updates_header(snapshot)) - .child(render_updates_summary()) + .child(render_updates_summary(snapshot.update_policy, cx)) + .child(render_update_policy_rows(snapshot.update_policy, cx)) .child(render_update_contract_rows()), ) } @@ -67,7 +75,10 @@ fn render_updates_header(snapshot: &BrowserSnapshot) -> AnyElement { .into_any_element() } -fn render_updates_summary() -> AnyElement { +fn render_updates_summary( + update_policy: UpdatePolicy, + cx: &mut gpui::Context, +) -> AnyElement { div() .rounded_md() .border_1() @@ -99,15 +110,120 @@ fn render_updates_summary() -> AnyElement { .text_color(rgb(colors::INK)) .child("Release Manifest Contract"), ) - .child(div().text_xs().truncate().text_color(rgb(colors::MUTED)).child( - format!( - "{} target through {SYNC_SERVICE_NAME} release APIs", - release_target() - ), - )), + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(update_policy.detail()), + ), ), ) - .child(div().text_xs().font_semibold().text_color(rgb(colors::SUCCESS)).child(APP_VERSION)) + .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); + })), + ), + ) + .into_any_element() +} + +fn render_update_policy_rows( + active_policy: UpdatePolicy, + cx: &mut gpui::Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .children( + UpdatePolicy::ALL + .iter() + .copied() + .enumerate() + .map(|(index, policy)| render_update_policy_row(index, policy, active_policy, cx)), + ) + .into_any_element() +} + +fn render_update_policy_row( + index: usize, + policy: UpdatePolicy, + active_policy: UpdatePolicy, + cx: &mut gpui::Context, +) -> AnyElement { + let selected = policy == active_policy; + + 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(policy_icon_color(selected))).child(policy_icon(selected)), + ) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(policy.name()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(policy.detail()), + ), + ), + ) + .child( + Button::new(("update-policy", index)) + .ghost() + .xsmall() + .selected(selected) + .label(policy_button_label(selected)) + .tooltip(policy.name()) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_update_policy(policy, cx); + })), + ) .into_any_element() } @@ -149,6 +265,18 @@ fn render_update_contract_rows() -> AnyElement { .into_any_element() } +fn policy_icon(selected: bool) -> IconName { + if selected { IconName::CircleCheck } else { IconName::LoaderCircle } +} + +fn policy_icon_color(selected: bool) -> u32 { + if selected { colors::PRIMARY } else { colors::MUTED_SOFT } +} + +fn policy_button_label(selected: bool) -> &'static str { + if selected { "Active" } else { "Select" } +} + fn update_row( icon: IconName, label: &'static str, diff --git a/crates/ely_app/src/shell/settings_actions.rs b/crates/ely_app/src/shell/settings_actions.rs index 7c9540e..9f995d9 100644 --- a/crates/ely_app/src/shell/settings_actions.rs +++ b/crates/ely_app/src/shell/settings_actions.rs @@ -1,6 +1,6 @@ use ely_domain::{ ArchivePolicy, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, - ProfileId, ProfileSyncPolicy, SearchEngine, SyncObjectKind, SyncObjectPolicy, + ProfileId, ProfileSyncPolicy, SearchEngine, SyncObjectKind, SyncObjectPolicy, UpdatePolicy, }; use gpui::Context; @@ -151,6 +151,24 @@ impl ElyShell { } } + pub(super) fn set_update_policy( + &mut self, + update_policy: UpdatePolicy, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state { + core.set_update_policy(update_policy); + cx.notify(); + } + } + + pub(super) fn reset_update_settings(&mut self, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state { + core.reset_update_settings(); + cx.notify(); + } + } + pub(super) 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 cf1e7d1..7cb50f4 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -5,7 +5,7 @@ use ely_domain::{ DownloadEntry, DownloadPolicy, FavoriteLimit, HistoryEntry, HistoryRecordingPolicy, NewTabDestination, NoteEntry, Profile, ProfileId, ProfileKind, ReadingListEntry, SearchEngine, SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, - TabGroup, TabId, UrlText, + TabGroup, TabId, UpdatePolicy, UrlText, }; use crate::{CoreError, navigation::tab_title}; @@ -113,6 +113,7 @@ pub struct BrowserSnapshot { pub new_tab_destination: NewTabDestination, pub history_recording_policy: HistoryRecordingPolicy, pub favorite_limit: FavoriteLimit, + pub update_policy: UpdatePolicy, pub command_query: String, } @@ -144,6 +145,7 @@ pub struct BrowserCore { new_tab_destination: NewTabDestination, history_recording_policy: HistoryRecordingPolicy, favorite_limit: FavoriteLimit, + update_policy: UpdatePolicy, sync_object_policies: SyncObjectPolicies, command_query: String, } @@ -189,6 +191,7 @@ impl BrowserCore { new_tab_destination, history_recording_policy: HistoryRecordingPolicy::default(), favorite_limit: FavoriteLimit::default(), + update_policy: UpdatePolicy::default(), sync_object_policies: SyncObjectPolicies::default(), spaces: vec![space], profiles: vec![profile], @@ -348,6 +351,19 @@ impl BrowserCore { self.favorite_limit } + pub fn set_update_policy(&mut self, update_policy: UpdatePolicy) { + self.update_policy = update_policy; + } + + pub fn reset_update_settings(&mut self) { + self.set_update_policy(UpdatePolicy::default()); + } + + #[must_use] + pub fn update_policy(&self) -> UpdatePolicy { + self.update_policy + } + pub fn set_command_query(&mut self, query: impl Into) { self.command_query = query.into(); } @@ -393,6 +409,7 @@ impl BrowserCore { new_tab_destination: self.new_tab_destination, history_recording_policy: self.history_recording_policy, favorite_limit: self.favorite_limit, + update_policy: self.update_policy, command_query: self.command_query.clone(), }) } diff --git a/crates/ely_browser_core/tests/settings_defaults.rs b/crates/ely_browser_core/tests/settings_defaults.rs index 92744e3..e3b3eb8 100644 --- a/crates/ely_browser_core/tests/settings_defaults.rs +++ b/crates/ely_browser_core/tests/settings_defaults.rs @@ -4,7 +4,7 @@ use ely_browser_core::{BrowserCore, InitialBrowserConfig}; use ely_domain::{ ArchivePolicy, DEFAULT_SIDEBAR_WIDTH_PX, DownloadPolicy, FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, ProfileKind, ProfileSyncPolicy, SearchEngine, SyncObjectKind, - SyncObjectPolicy, + SyncObjectPolicy, UpdatePolicy, }; #[test] @@ -47,6 +47,10 @@ fn section_resets_restore_settings_defaults() -> Result<(), Box> { core.set_sync_object_policy(SyncObjectKind::Tabs, SyncObjectPolicy::Paused); core.reset_sync_settings(); assert_eq!(core.sync_object_policy(SyncObjectKind::Tabs), SyncObjectPolicy::Enabled); + + core.set_update_policy(UpdatePolicy::Manual); + core.reset_update_settings(); + assert_eq!(core.snapshot()?.update_policy, UpdatePolicy::Automatic); Ok(()) } diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index 983cd25..b72a4dd 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -19,6 +19,7 @@ mod split; mod sync; mod tab; mod tab_group; +mod update; mod url_text; pub use archive::{ArchiveSource, ArchivedTab}; @@ -57,4 +58,5 @@ pub use sync::{ }; pub use tab::{BrowserTab, TabFlags, TabState}; pub use tab_group::TabGroup; +pub use update::UpdatePolicy; pub use url_text::UrlText; diff --git a/crates/ely_domain/src/update.rs b/crates/ely_domain/src/update.rs new file mode 100644 index 0000000..26ee1e5 --- /dev/null +++ b/crates/ely_domain/src/update.rs @@ -0,0 +1,26 @@ +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum UpdatePolicy { + #[default] + Automatic, + Manual, +} + +impl UpdatePolicy { + pub const ALL: &[Self] = &[Self::Automatic, Self::Manual]; + + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::Automatic => "Automatic", + Self::Manual => "Manual", + } + } + + #[must_use] + pub fn detail(self) -> &'static str { + match self { + Self::Automatic => "Use release manifests as the automatic update source.", + Self::Manual => "Keep release manifest checks user-initiated.", + } + } +}