diff --git a/assets/icons/plus_icon_4x.png b/assets/icons/plus_icon_4x.png new file mode 100644 index 0000000..97ebadf Binary files /dev/null and b/assets/icons/plus_icon_4x.png differ diff --git a/src/account_manager.rs b/src/account_manager.rs index 52a3f46..e610394 100644 --- a/src/account_manager.rs +++ b/src/account_manager.rs @@ -87,10 +87,22 @@ impl AccountManager { self.accounts.len() } - pub fn get_currently_selected_account(&self) -> Option { + pub fn get_selected_account_index(&self) -> Option { self.currently_selected_account } + pub fn get_selected_account(&self) -> Option<&UserAccount> { + if let Some(account_index) = self.currently_selected_account { + if let Some(account) = self.get_account(account_index) { + Some(account) + } else { + None + } + } else { + None + } + } + pub fn select_account(&mut self, index: usize) { if self.accounts.get(index).is_some() { self.currently_selected_account = Some(index) diff --git a/src/colors.rs b/src/colors.rs index bd4d08a..afb0e2b 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -1,6 +1,8 @@ use egui::Color32; pub const PURPLE: Color32 = Color32::from_rgb(0xCC, 0x43, 0xC5); +// TODO: This should not be exposed publicly +pub const PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); //pub const DARK_BG: Color32 = egui::Color32::from_rgb(40, 44, 52); pub const GRAY_SECONDARY: Color32 = Color32::from_rgb(0x8A, 0x8A, 0x8A); const BLACK: Color32 = Color32::from_rgb(0x00, 0x00, 0x00); diff --git a/src/ui/account_management.rs b/src/ui/account_management.rs index 974e0ef..06b3a27 100644 --- a/src/ui/account_management.rs +++ b/src/ui/account_management.rs @@ -1,12 +1,11 @@ +use crate::colors::PINK; use crate::ui::global_popup::FromApp; use crate::{ - account_manager::{AccountManager, UserAccount}, + account_manager::AccountManager, app_style::NotedeckTextStyle, ui::{self, Preview, View}, }; -use egui::{ - Align, Button, Color32, Frame, Id, Image, Layout, Margin, RichText, ScrollArea, Sense, Vec2, -}; +use egui::{Align, Button, Frame, Image, Layout, RichText, ScrollArea, Vec2}; use super::global_popup::GlobalPopupType; use super::profile::preview::SimpleProfilePreview; @@ -176,15 +175,6 @@ impl<'a> FromApp<'a> for AccountManagementView<'a> { } } -fn simple_preview_frame(ui: &mut egui::Ui) -> Frame { - Frame::none() - .rounding(ui.visuals().window_rounding) - .fill(ui.visuals().window_fill) - .stroke(ui.visuals().window_stroke) - .outer_margin(Margin::same(2.0)) - .inner_margin(12.0) -} - fn mobile_title() -> impl egui::Widget { |ui: &mut egui::Ui| { ui.vertical_centered(|ui| { @@ -204,8 +194,6 @@ fn scroll_area() -> ScrollArea { .auto_shrink([false; 2]) } -static PINK: Color32 = Color32::from_rgb(0xE4, 0x5A, 0xC9); - fn add_account_button() -> Button<'static> { let img_data = egui::include_image!("../../assets/icons/add_account_icon_4x.png"); let img = Image::new(img_data).fit_to_exact_size(Vec2::new(48.0, 48.0)); @@ -253,60 +241,11 @@ fn selected_widget() -> impl egui::Widget { // egui::Button::new("Logout all") // } -pub struct AccountSelectionWidget<'a> { - account_manager: &'a mut AccountManager, - simple_preview_controller: SimpleProfilePreviewController<'a>, -} - -impl<'a> AccountSelectionWidget<'a> { - fn ui(&'a mut self, ui: &mut egui::Ui) -> Option<&'a UserAccount> { - let mut result: Option<&'a UserAccount> = None; - scroll_area().show(ui, |ui| { - ui.horizontal_wrapped(|ui| { - let clicked_at = self.simple_preview_controller.view_profile_previews( - self.account_manager, - ui, - |ui, preview, index| { - let resp = ui.add_sized(preview.dimensions(), |ui: &mut egui::Ui| { - simple_preview_frame(ui) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - ui.add(preview); - }); - }) - .response - }); - - ui.interact(resp.rect, Id::new(index), Sense::click()) - .clicked() - }, - ); - - if let Some(index) = clicked_at { - result = self.account_manager.get_account(index); - }; - }); - }); - result - } -} - -impl<'a> AccountSelectionWidget<'a> { - pub fn new( - account_manager: &'a mut AccountManager, - simple_preview_controller: SimpleProfilePreviewController<'a>, - ) -> Self { - AccountSelectionWidget { - account_manager, - simple_preview_controller, - } - } -} - // PREVIEWS mod preview { use nostrdb::{Config, Ndb}; + use ui::account_switcher::AccountSelectionWidget; use super::*; use crate::imgcache::ImageCache; @@ -395,14 +334,11 @@ mod preview { impl View for AccountSelectionPreview { fn ui(&mut self, ui: &mut egui::Ui) { - let mut widget = AccountSelectionWidget::new( + AccountSelectionWidget::new( &mut self.account_manager, SimpleProfilePreviewController::new(&self.ndb, &mut self.img_cache), - ); - - if let Some(account) = widget.ui(ui) { - println!("User made selection: {:?}", account.key); - } + ) + .ui(ui); } } diff --git a/src/ui/account_switcher.rs b/src/ui/account_switcher.rs new file mode 100644 index 0000000..034c22f --- /dev/null +++ b/src/ui/account_switcher.rs @@ -0,0 +1,187 @@ +use crate::{account_manager::UserAccount, colors::PINK, ui}; +use egui::{ + Align, Button, Color32, Frame, Id, Image, Layout, Margin, RichText, Rounding, ScrollArea, + Sense, Vec2, +}; + +use crate::account_manager::AccountManager; + +use super::profile::{preview::SimpleProfilePreview, SimpleProfilePreviewController}; + +pub struct AccountSelectionWidget<'a> { + account_manager: &'a mut AccountManager, + simple_preview_controller: SimpleProfilePreviewController<'a>, +} + +impl<'a> AccountSelectionWidget<'a> { + pub fn ui(&'a mut self, ui: &mut egui::Ui) { + if ui::is_mobile() { + self.show_mobile(ui); + } else { + self.show(ui); + } + } + + fn show(&mut self, ui: &mut egui::Ui) { + Frame::none().outer_margin(8.0).show(ui, |ui| { + ui.add(top_section_widget()); + scroll_area().show(ui, |ui| { + self.show_accounts(ui); + }); + ui.add_space(8.0); + ui.add(add_account_button()); + + if let Some(account_index) = self.account_manager.get_selected_account_index() { + ui.add_space(8.0); + if self.handle_sign_out(ui, account_index) { + self.account_manager.remove_account(account_index); + } + } + + ui.add_space(8.0); + }); + } + + fn handle_sign_out(&mut self, ui: &mut egui::Ui, account_index: usize) -> bool { + if let Some(account) = self.account_manager.get_account(account_index) { + if let Some(response) = self.sign_out_button(ui, account) { + return response.clicked(); + } + } + false + } + + fn show_mobile(&mut self, ui: &mut egui::Ui) -> egui::Response { + let _ = ui; + todo!() + } + + fn show_accounts(&mut self, ui: &mut egui::Ui) { + self.simple_preview_controller.view_profile_previews( + self.account_manager, + ui, + account_switcher_card_ui(), + ); + } + fn sign_out_button(&self, ui: &mut egui::Ui, account: &UserAccount) -> Option { + self.simple_preview_controller.show_with_nickname( + ui, + &account.key.pubkey, + |ui, username| { + let img_data = egui::include_image!("../../assets/icons/signout_icon_4x.png"); + let img = Image::new(img_data).fit_to_exact_size(Vec2::new(16.0, 16.0)); + let button = egui::Button::image_and_text( + img, + RichText::new(format!(" Sign out @{}", username.username())) + .color(PINK) + .size(16.0), + ) + .frame(false); + + ui.add(button) + }, + ) + } +} + +impl<'a> AccountSelectionWidget<'a> { + pub fn new( + account_manager: &'a mut AccountManager, + simple_preview_controller: SimpleProfilePreviewController<'a>, + ) -> Self { + AccountSelectionWidget { + account_manager, + simple_preview_controller, + } + } +} + +fn account_switcher_card_ui() -> fn( + ui: &mut egui::Ui, + preview: SimpleProfilePreview, + width: f32, + is_selected: bool, + index: usize, +) -> bool { + |ui, preview, width, is_selected, index| { + let resp = ui.add_sized(Vec2::new(width, 50.0), |ui: &mut egui::Ui| { + Frame::none() + .show(ui, |ui| { + ui.add_space(8.0); + ui.horizontal(|ui| { + if is_selected { + Frame::none() + .rounding(Rounding::same(8.0)) + .inner_margin(Margin::same(8.0)) + .fill(Color32::from_rgb(0x45, 0x1B, 0x59)) + .show(ui, |ui| { + ui.add(preview); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + ui.add(selection_widget()); + }); + }); + } else { + ui.add_space(8.0); + ui.add(preview); + } + }); + }) + .response + }); + + ui.interact(resp.rect, Id::new(index), Sense::click()) + .clicked() + } +} + +fn selection_widget() -> impl egui::Widget { + |ui: &mut egui::Ui| { + let img_data: egui::ImageSource = + egui::include_image!("../../assets/icons/select_icon_3x.png"); + let img = Image::new(img_data).max_size(Vec2::new(16.0, 16.0)); + ui.add(img) + } +} + +fn top_section_widget() -> impl egui::Widget { + |ui: &mut egui::Ui| { + ui.horizontal(|ui| { + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::left_to_right(egui::Align::Center), + |ui| ui.add(account_switcher_title()), + ); + + ui.allocate_ui_with_layout( + Vec2::new(ui.available_size_before_wrap().x, 32.0), + Layout::right_to_left(egui::Align::Center), + |ui| { + if ui.add(manage_accounts_button()).clicked() { + // TODO: route to AccountLoginView + } + }, + ); + }) + .response + } +} + +fn manage_accounts_button() -> egui::Button<'static> { + Button::new(RichText::new("Manage").color(PINK).size(16.0)).frame(false) +} + +fn account_switcher_title() -> impl egui::Widget { + |ui: &mut egui::Ui| ui.label(RichText::new("Account switcher").size(20.0).strong()) +} + +fn scroll_area() -> ScrollArea { + egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) +} + +fn add_account_button() -> egui::Button<'static> { + let img_data = egui::include_image!("../../assets/icons/plus_icon_4x.png"); + let img = Image::new(img_data).fit_to_exact_size(Vec2::new(16.0, 16.0)); + Button::image_and_text(img, RichText::new(" Add account").size(16.0).color(PINK)).frame(false) +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 5f05bd0..d4bd5e4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,5 +1,6 @@ pub mod account_login_view; pub mod account_management; +pub mod account_switcher; pub mod anim; pub mod global_popup; pub mod mention; @@ -11,7 +12,8 @@ pub mod side_panel; pub mod state_in_memory; pub mod username; -pub use account_management::{AccountManagementView, AccountSelectionWidget}; +pub use account_management::AccountManagementView; +pub use account_switcher::AccountSelectionWidget; pub use global_popup::DesktopGlobalPopup; pub use mention::Mention; pub use note::Note; diff --git a/src/ui/profile/preview.rs b/src/ui/profile/preview.rs index 860a7f6..3d851ae 100644 --- a/src/ui/profile/preview.rs +++ b/src/ui/profile/preview.rs @@ -3,7 +3,7 @@ use crate::imgcache::ImageCache; use crate::ui::ProfilePic; use crate::{colors, images, DisplayName}; use egui::load::TexturePoll; -use egui::{Frame, RichText, Sense, Vec2, Widget}; +use egui::{Frame, RichText, Sense, Widget}; use egui_extras::Size; use nostrdb::ProfileRecord; @@ -93,10 +93,6 @@ impl<'a, 'cache> SimpleProfilePreview<'a, 'cache> { pub fn new(profile: &'a ProfileRecord<'a>, cache: &'cache mut ImageCache) -> Self { SimpleProfilePreview { profile, cache } } - - pub fn dimensions(&self) -> Vec2 { - Vec2::new(120.0, 150.0) - } } impl<'a, 'cache> egui::Widget for SimpleProfilePreview<'a, 'cache> { @@ -152,7 +148,7 @@ mod previews { } } -fn get_display_name<'a>(profile: &'a ProfileRecord<'a>) -> DisplayName<'a> { +pub fn get_display_name<'a>(profile: &'a ProfileRecord<'a>) -> DisplayName<'a> { if let Some(name) = crate::profile::get_profile_name(profile) { name } else { diff --git a/src/ui/profile/profile_preview_controller.rs b/src/ui/profile/profile_preview_controller.rs index a6e93fb..15e9e25 100644 --- a/src/ui/profile/profile_preview_controller.rs +++ b/src/ui/profile/profile_preview_controller.rs @@ -1,8 +1,9 @@ +use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use crate::{account_manager::AccountManager, imgcache::ImageCache}; +use crate::{account_manager::AccountManager, imgcache::ImageCache, DisplayName}; -use super::preview::SimpleProfilePreview; +use super::preview::{get_display_name, SimpleProfilePreview}; pub struct SimpleProfilePreviewController<'a> { ndb: &'a Ndb, @@ -45,13 +46,12 @@ impl<'a> SimpleProfilePreviewController<'a> { if let Ok(profile) = profile { let preview = SimpleProfilePreview::new(&profile, self.img_cache); - let is_selected = if let Some(selected) = - account_manager.get_currently_selected_account() - { - i == selected - } else { - false - }; + let is_selected = + if let Some(selected) = account_manager.get_selected_account_index() { + i == selected + } else { + false + }; if let Some(op) = add_preview_ui(ui, preview, width, is_selected) { match op { @@ -74,11 +74,17 @@ impl<'a> SimpleProfilePreviewController<'a> { pub fn view_profile_previews( &mut self, - account_manager: &'a AccountManager, + account_manager: &mut AccountManager, ui: &mut egui::Ui, - add_preview_ui: fn(ui: &mut egui::Ui, preview: SimpleProfilePreview, index: usize) -> bool, - ) -> Option { - let mut clicked_at: Option = None; + add_preview_ui: fn( + ui: &mut egui::Ui, + preview: SimpleProfilePreview, + width: f32, + is_selected: bool, + index: usize, + ) -> bool, + ) { + let width = ui.available_width(); for i in 0..account_manager.num_accounts() { if let Some(account) = account_manager.get_account(i) { @@ -90,14 +96,35 @@ impl<'a> SimpleProfilePreviewController<'a> { if let Ok(profile) = profile { let preview = SimpleProfilePreview::new(&profile, self.img_cache); - if add_preview_ui(ui, preview, i) { - clicked_at = Some(i) + let is_selected = + if let Some(selected) = account_manager.get_selected_account_index() { + i == selected + } else { + false + }; + + if add_preview_ui(ui, preview, width, is_selected, i) { + account_manager.select_account(i); } } } } } + } - clicked_at + pub fn show_with_nickname( + &'a self, + ui: &mut egui::Ui, + key: &Pubkey, + ui_element: fn(ui: &mut egui::Ui, username: &DisplayName) -> egui::Response, + ) -> Option { + if let Ok(txn) = Transaction::new(self.ndb) { + let profile = self.ndb.get_profile_by_pubkey(&txn, key.bytes()); + + if let Ok(profile) = profile { + return Some(ui_element(ui, &get_display_name(&profile))); + } + } + None } }