diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index 173c8f5..7fbea60 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -2,6 +2,7 @@ mod download_actions; mod download_labels; mod downloads; mod plugins; +mod profiles; use ely_browser_core::BrowserSnapshot; use ely_design_system::{colors, spacing}; @@ -26,6 +27,7 @@ impl ElyShell { "ely://history" => self.render_history_page(snapshot, cx), "ely://archive" => self.render_archive_page(snapshot, cx), "ely://settings/plugins" => self.render_plugins_page(snapshot, cx), + "ely://settings/profiles" => self.render_profiles_page(snapshot, cx), _ => render_default_page(tab), } } diff --git a/crates/ely_app/src/shell/internal_pages/profiles.rs b/crates/ely_app/src/shell/internal_pages/profiles.rs new file mode 100644 index 0000000..a9c0e67 --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/profiles.rs @@ -0,0 +1,186 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::colors; +use ely_domain::{DownloadDestination, DownloadPolicy, Profile, ProfileId, ProfileKind}; +use gpui::{ + AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, + px, rgb, +}; +use gpui_component::{ + IconName, Sizable, StyledExt, + button::{Button, ButtonVariants}, + scroll::ScrollableElement, +}; + +use super::{ElyShell, render_canvas_surface}; + +impl ElyShell { + pub(super) fn render_profiles_page( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut Context, + ) -> AnyElement { + render_canvas_surface( + div() + .size_full() + .p_8() + .flex() + .flex_col() + .gap_5() + .child(render_profiles_header(snapshot)) + .child(render_profile_list(snapshot, cx)), + ) + } +} + +fn render_profiles_header(snapshot: &BrowserSnapshot) -> AnyElement { + div() + .flex() + .items_end() + .justify_between() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("Profiles")) + .child( + div() + .text_sm() + .text_color(rgb(colors::MUTED)) + .child(format!("Active Space: {}", snapshot.active_space_name)), + ), + ) + .child( + div() + .text_xs() + .text_color(rgb(colors::MUTED)) + .child(format!("{} profiles", snapshot.profiles.len())), + ) + .into_any_element() +} + +fn render_profile_list(snapshot: &BrowserSnapshot, cx: &mut Context) -> AnyElement { + div() + .flex_1() + .min_h_0() + .flex() + .flex_col() + .overflow_y_scrollbar() + .border_t_1() + .border_color(rgb(colors::HAIRLINE)) + .children(snapshot.profiles.iter().enumerate().map(|(index, profile)| { + render_profile_row(index, profile, profile.id() == &snapshot.active_profile_id, cx) + })) + .into_any_element() +} + +fn render_profile_row( + index: usize, + profile: &Profile, + active: bool, + cx: &mut Context, +) -> AnyElement { + let profile_id = profile.id().clone(); + + div() + .id(SharedString::from(format!("profile-{}", profile.id().as_str()))) + .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(profile_color_swatch(profile.color_hex())) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(profile.name().to_string()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(profile_detail_label(profile)), + ), + ), + ) + .child(render_profile_action(index, profile_id, active, cx)) + .into_any_element() +} + +fn profile_color_swatch(color_hex: u32) -> AnyElement { + div() + .w(px(14.0)) + .h(px(14.0)) + .rounded_full() + .border_1() + .border_color(rgb(colors::HAIRLINE_STRONG)) + .bg(rgb(color_hex)) + .into_any_element() +} + +fn render_profile_action( + index: usize, + profile_id: ProfileId, + active: bool, + cx: &mut Context, +) -> AnyElement { + if active { + return div() + .text_xs() + .font_semibold() + .text_color(rgb(colors::SUCCESS)) + .child("Active") + .into_any_element(); + } + + Button::new(("switch-profile", index)) + .small() + .primary() + .icon(IconName::Check) + .label("Switch") + .tooltip("Switch Profile") + .on_click(cx.listener(move |shell, _, window, cx| { + shell.select_profile(&profile_id, window, cx); + })) + .into_any_element() +} + +fn profile_detail_label(profile: &Profile) -> String { + format!( + "{} - {}", + profile_kind_label(profile.kind()), + download_policy_label(profile.download_policy()) + ) +} + +fn profile_kind_label(kind: &ProfileKind) -> &'static str { + match kind { + ProfileKind::Standard => "Standard", + ProfileKind::Private => "Private", + } +} + +fn download_policy_label(policy: &DownloadPolicy) -> String { + match policy.destination() { + DownloadDestination::AskEveryTime => "Ask every time".to_string(), + DownloadDestination::FixedDirectory(path) => path.display().to_string(), + } +} diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index afdf634..7fc5ee0 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -4,7 +4,7 @@ mod plugins; mod render; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{CommandIntent, SpaceId, TabId, UrlText}; +use ely_domain::{CommandIntent, ProfileId, SpaceId, TabId, UrlText}; use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window}; use gpui_component::input::{InputEvent, InputState, SelectAll}; @@ -166,6 +166,20 @@ impl ElyShell { } } + fn select_profile( + &mut self, + profile_id: &ProfileId, + window: &mut Window, + cx: &mut Context, + ) { + if let ShellState::Ready(core) = &mut self.state + && core.select_profile(profile_id).is_ok() + { + self.sync_address_input(window, cx); + cx.notify(); + } + } + fn select_next_tab(&mut self, window: &mut Window, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state && core.select_next_tab().is_ok() diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 5864d19..24b59d0 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -46,8 +46,10 @@ pub struct BrowserSnapshot { pub installed_plugins: Vec, pub plugin_audit_events: Vec, pub spaces: Vec, + pub profiles: Vec, pub active_tab_id: TabId, pub active_space_id: SpaceId, + pub active_profile_id: ProfileId, pub active_space_name: String, pub active_profile_name: String, pub active_download_policy: DownloadPolicy, @@ -198,9 +200,11 @@ impl BrowserCore { installed_plugins: self.installed_plugins.clone(), plugin_audit_events: self.plugin_audit_events.clone(), spaces: self.spaces.clone(), + profiles: self.profiles.clone(), tabs: self.visible_tabs(), active_tab_id: self.active_tab_id.clone(), active_space_id: self.active_space_id.clone(), + active_profile_id: self.active_profile_id.clone(), active_space_name: active_space.name().to_string(), active_profile_name: active_profile.name().to_string(), active_download_policy: active_profile.download_policy().clone(), diff --git a/crates/ely_browser_core/tests/commands.rs b/crates/ely_browser_core/tests/commands.rs index 73fa059..71f44f2 100644 --- a/crates/ely_browser_core/tests/commands.rs +++ b/crates/ely_browser_core/tests/commands.rs @@ -168,7 +168,9 @@ fn new_profile_command_creates_and_selects_named_profile() -> Result<(), Box