use std::{collections::BTreeMap, time::SystemTime}; use ely_domain::{ AppearanceSettings, ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DEFAULT_SIDEBAR_WIDTH_PX, DiagnosticEvent, DiagnosticsReportingPolicy, DomainError, DownloadEntry, DownloadPolicy, FavoriteLimit, HistoryEntry, HistoryRecordingPolicy, NewTabDestination, NoteEntry, Profile, ProfileId, ProfileKind, ReadingListEntry, SearchEngine, SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, TabGroup, TabId, ThemeMode, UpdatePolicy, UrlText, WallpaperTheme, }; use crate::{CoreError, navigation::tab_title}; use sync::SyncObjectPolicies; mod bookmarks; mod commands; mod diagnostics; mod downloads; mod history; mod local_data_export_records; mod local_data_exports; mod notes; mod plugins; mod privacy; mod profiles; mod reading_list; mod site_data; mod site_permissions; mod space_exports; mod spaces; mod splits; mod sync; mod sync_context; mod sync_notes; mod sync_profiles; mod sync_reading_list; mod tab_group_order; mod tab_groups; mod tab_lifecycle; mod tab_order; mod tab_selection; mod tabs; mod visible_content; pub use bookmarks::{ BookmarkImportSummary, ELYBOOKMARKS_FILE_EXTENSION, ELYBOOKMARKS_SCHEMA_VERSION, ElyBookmarksPackage, }; pub use local_data_exports::{ELYDATA_FILE_EXTENSION, ELYDATA_SCHEMA_VERSION, ElyLocalDataPackage}; pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent}; pub use privacy::LocalDataInventory; pub use site_data::SiteDataClearance; pub use space_exports::{ ELYSPACE_FILE_EXTENSION, ELYSPACE_SCHEMA_VERSION, ElySpacePackage, SpaceImportProfileMapping, }; pub use spaces::TrashedSpace; #[derive(Clone, Debug)] pub struct InitialBrowserConfig { pub space_name: String, pub space_icon: String, pub space_color_hex: u32, pub profile_name: String, pub profile_color_hex: u32, pub profile_kind: ProfileKind, pub new_tab_destination: NewTabDestination, } impl InitialBrowserConfig { pub fn ely_defaults() -> Result { Ok(Self { space_name: "Work".to_string(), space_icon: "W".to_string(), space_color_hex: 0xf54e00, profile_name: "Default".to_string(), profile_color_hex: 0x26251e, profile_kind: ProfileKind::Standard, new_tab_destination: NewTabDestination::default(), }) } pub fn private_window() -> Result { Ok(Self { space_name: "Private".to_string(), space_icon: "P".to_string(), space_color_hex: 0x807d72, profile_name: "Private".to_string(), profile_color_hex: 0x807d72, profile_kind: ProfileKind::Private, new_tab_destination: NewTabDestination::default(), }) } } #[derive(Clone, Debug)] pub struct BrowserSnapshot { pub tabs: Vec, pub favorites: Vec, pub pinned_tabs: Vec, pub archived_tabs: Vec, pub bookmarks: Vec, pub notes: Vec, pub reading_list: Vec, pub site_permissions: Vec, pub site_permission_audit_events: Vec, pub download_entries: Vec, pub history_entries: Vec, pub active_profile_history_entry_count: usize, pub local_data_inventory: LocalDataInventory, pub tab_groups: Vec, pub split_layouts: Vec, pub installed_plugins: Vec, pub plugin_audit_events: Vec, pub diagnostic_events: Vec, pub spaces: Vec, pub trashed_spaces: Vec, pub profiles: Vec, pub sync_status: SyncStatus, 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_profile_kind: ProfileKind, pub active_download_policy: DownloadPolicy, pub search_engine: SearchEngine, pub new_tab_destination: NewTabDestination, pub history_recording_policy: HistoryRecordingPolicy, pub diagnostics_reporting_policy: DiagnosticsReportingPolicy, pub favorite_limit: FavoriteLimit, pub update_policy: UpdatePolicy, pub appearance: AppearanceSettings, pub command_query: String, } #[derive(Debug)] pub struct BrowserCore { spaces: Vec, profiles: Vec, tabs: Vec, archived_tabs: Vec, bookmarks: Vec, notes: Vec, reading_list: Vec, site_permissions: Vec, site_permission_audit_events: Vec, download_entries: Vec, history_entries: Vec, tab_groups: Vec, split_layouts: Vec, archived_split_layouts: Vec, trashed_spaces: Vec, installed_plugins: Vec, plugin_audit_events: Vec, diagnostic_events: Vec, active_space_id: SpaceId, active_profile_id: ProfileId, active_tab_id: TabId, active_tabs_by_space: BTreeMap, active_tabs_by_space_profile: BTreeMap<(SpaceId, ProfileId), TabId>, search_engine: SearchEngine, new_tab_destination: NewTabDestination, history_recording_policy: HistoryRecordingPolicy, diagnostics_reporting_policy: DiagnosticsReportingPolicy, favorite_limit: FavoriteLimit, update_policy: UpdatePolicy, appearance: AppearanceSettings, sync_object_policies: SyncObjectPolicies, sync_connection_state: ely_domain::SyncConnectionState, command_query: String, } impl BrowserCore { pub fn new(config: InitialBrowserConfig) -> Result { let profile = Profile::new(config.profile_name, config.profile_color_hex, config.profile_kind); let active_profile_id = profile.id().clone(); let space = Space::new( config.space_name, config.space_icon, config.space_color_hex, active_profile_id.clone(), 0, ); let new_tab_destination = config.new_tab_destination; let new_tab_url = new_tab_destination.url()?; let new_tab_title = tab_title(&new_tab_url); let active_space_id = space.id().clone(); let tab = BrowserTab::new( TabId::new(), active_space_id.clone(), active_profile_id.clone(), new_tab_title, new_tab_url.clone(), ) .with_sort_key(0); let active_tab_id = tab.id().clone(); let mut active_tabs_by_space = BTreeMap::new(); let mut active_tabs_by_space_profile = BTreeMap::new(); active_tabs_by_space.insert(active_space_id.clone(), active_tab_id.clone()); active_tabs_by_space_profile .insert((active_space_id.clone(), active_profile_id.clone()), active_tab_id.clone()); Ok(Self { active_space_id, active_profile_id, active_tab_id, active_tabs_by_space, active_tabs_by_space_profile, search_engine: SearchEngine::default(), new_tab_destination, history_recording_policy: HistoryRecordingPolicy::default(), diagnostics_reporting_policy: DiagnosticsReportingPolicy::default(), favorite_limit: FavoriteLimit::default(), update_policy: UpdatePolicy::default(), appearance: AppearanceSettings::default(), sync_object_policies: SyncObjectPolicies::default(), sync_connection_state: ely_domain::SyncConnectionState::SignedOut, spaces: vec![space], profiles: vec![profile], tabs: vec![tab], archived_tabs: Vec::new(), bookmarks: Vec::new(), notes: Vec::new(), reading_list: Vec::new(), site_permissions: Vec::new(), site_permission_audit_events: Vec::new(), download_entries: Vec::new(), history_entries: Vec::new(), tab_groups: Vec::new(), split_layouts: Vec::new(), archived_split_layouts: Vec::new(), trashed_spaces: Vec::new(), installed_plugins: Vec::new(), plugin_audit_events: Vec::new(), diagnostic_events: vec![DiagnosticEvent::startup_success(SystemTime::now())], command_query: String::new(), }) } pub fn set_active_space_archive_policy( &mut self, archive_policy: ArchivePolicy, ) -> Result<(), CoreError> { let active_space_id = self.active_space_id.clone(); self.set_space_archive_policy(&active_space_id, archive_policy) } pub fn set_space_archive_policy( &mut self, space_id: &SpaceId, archive_policy: ArchivePolicy, ) -> Result<(), CoreError> { let space = self .spaces .iter_mut() .find(|space| space.id() == space_id) .ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?; space.set_archive_policy(archive_policy); Ok(()) } pub fn set_space_default_profile( &mut self, space_id: &SpaceId, profile_id: &ProfileId, ) -> Result<(), CoreError> { let profile = self .profiles .iter() .find(|profile| profile.id() == profile_id) .ok_or_else(|| CoreError::ProfileNotFound { id: profile_id.clone() })?; if profile.kind() == &ProfileKind::Private { return Err(CoreError::PrivateProfileDefaultLocked { id: profile_id.clone() }); } let space = self .spaces .iter_mut() .find(|space| space.id() == space_id) .ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?; space.set_default_profile_id(profile_id.clone()); Ok(()) } pub fn set_active_space_default_profile( &mut self, profile_id: &ProfileId, ) -> Result<(), CoreError> { let active_space_id = self.active_space_id.clone(); self.set_space_default_profile(&active_space_id, profile_id) } pub fn set_space_sidebar_width( &mut self, space_id: &SpaceId, sidebar_width_px: u16, ) -> Result<(), CoreError> { let space = self .spaces .iter_mut() .find(|space| space.id() == space_id) .ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?; space.set_sidebar_width_px(sidebar_width_px); Ok(()) } pub fn set_space_sort_key( &mut self, space_id: &SpaceId, sort_key: u64, ) -> Result<(), CoreError> { let space = self .spaces .iter_mut() .find(|space| space.id() == space_id) .ok_or_else(|| CoreError::SpaceNotFound { id: space_id.clone() })?; space.set_sort_key(sort_key); Ok(()) } pub fn set_search_engine(&mut self, search_engine: SearchEngine) { self.search_engine = search_engine; } pub fn reset_search_settings(&mut self) { self.set_search_engine(SearchEngine::default()); } #[must_use] pub fn search_engine(&self) -> SearchEngine { self.search_engine } pub fn set_new_tab_destination(&mut self, destination: NewTabDestination) { self.new_tab_destination = destination; } pub fn reset_general_settings(&mut self) { self.set_new_tab_destination(NewTabDestination::default()); } #[must_use] pub fn new_tab_destination(&self) -> NewTabDestination { self.new_tab_destination } pub fn set_favorite_limit(&mut self, favorite_limit: FavoriteLimit) { self.favorite_limit = favorite_limit; } pub fn reset_sidebar_tabs_settings(&mut self) -> Result<(), CoreError> { let active_space_id = self.active_space_id.clone(); self.set_space_archive_policy(&active_space_id, ArchivePolicy::Manual)?; self.set_space_sidebar_width(&active_space_id, DEFAULT_SIDEBAR_WIDTH_PX)?; self.set_favorite_limit(FavoriteLimit::default()); Ok(()) } #[must_use] pub fn favorite_limit(&self) -> FavoriteLimit { 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 } #[must_use] pub fn appearance(&self) -> AppearanceSettings { self.appearance } pub fn set_wallpaper_theme(&mut self, wallpaper: WallpaperTheme) { self.appearance.set_wallpaper(wallpaper); } pub fn set_theme_mode(&mut self, theme_mode: ThemeMode) { self.appearance.set_theme_mode(theme_mode); } pub fn set_reduce_motion(&mut self, reduce_motion: bool) { self.appearance.set_reduce_motion(reduce_motion); } pub fn set_translucency_pct(&mut self, value: u8) { self.appearance.set_translucency_pct(value); } pub fn reset_appearance(&mut self) { self.appearance = AppearanceSettings::default(); } pub fn set_command_query(&mut self, query: impl Into) { self.command_query = query.into(); } #[must_use] pub fn command_query(&self) -> &str { &self.command_query } pub fn snapshot(&self) -> Result { let active_space = self.active_space()?; let active_profile = self.active_profile()?; let tabs = self.visible_tabs(); let pinned_tabs = tabs .iter() .filter(|tab| tab.flags().pinned && !tab.flags().favorite) .cloned() .collect(); Ok(BrowserSnapshot { favorites: self.favorites(), pinned_tabs, archived_tabs: self.archived_tabs.clone(), bookmarks: self.visible_bookmarks(), notes: self.visible_notes(), reading_list: self.visible_reading_list(), site_permissions: self.visible_site_permissions(), site_permission_audit_events: self.visible_site_permission_audit_events(), download_entries: self.visible_downloads(), history_entries: self.visible_history(), active_profile_history_entry_count: self.active_profile_history_count(), local_data_inventory: self.active_profile_local_data_inventory(), tab_groups: self.visible_tab_groups(), split_layouts: self.visible_split_layouts(), installed_plugins: self.installed_plugins.clone(), plugin_audit_events: self.plugin_audit_events.clone(), diagnostic_events: self.diagnostic_events.clone(), spaces: self.sorted_spaces(), trashed_spaces: self.trashed_spaces.clone(), profiles: self.profiles.clone(), sync_status: self.sync_status(), 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_profile_kind: active_profile.kind().clone(), active_download_policy: active_profile.download_policy().clone(), search_engine: self.search_engine, new_tab_destination: self.new_tab_destination, history_recording_policy: self.history_recording_policy, diagnostics_reporting_policy: self.diagnostics_reporting_policy, favorite_limit: self.favorite_limit, update_policy: self.update_policy, appearance: self.appearance, command_query: self.command_query.clone(), }) } pub(super) fn new_tab_url(&self) -> Result { self.new_tab_destination.url().map_err(CoreError::from) } fn active_profile(&self) -> Result<&Profile, CoreError> { self.profiles .iter() .find(|profile| profile.id() == &self.active_profile_id) .ok_or_else(|| CoreError::ProfileNotFound { id: self.active_profile_id.clone() }) } fn active_space(&self) -> Result<&Space, CoreError> { self.spaces .iter() .find(|space| space.id() == &self.active_space_id) .ok_or_else(|| CoreError::SpaceNotFound { id: self.active_space_id.clone() }) } #[must_use] pub fn active_space_sidebar_width(&self) -> Option { self.active_space().ok().map(|space| space.sidebar_width_px()) } fn favorites(&self) -> Vec { self.tabs.iter().filter(|tab| tab.flags().favorite).cloned().collect() } fn visible_tabs(&self) -> Vec { tab_order::sorted_tabs( self.tabs.iter().filter(|tab| tab.space_id() == &self.active_space_id), ) } fn record_tab_activity(&mut self, tab_id: &TabId, active_at: SystemTime) { if let Some(tab) = self.tabs.iter_mut().find(|tab| tab.id() == tab_id) { tab.record_activity(active_at); } } }