From 4bdbf5e0c5ed47f7a5ff9e10d81254bc9ce35943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 8 May 2026 03:13:37 -0400 Subject: [PATCH] Add general new tab settings --- crates/ely_app/src/shell/internal_pages.rs | 2 + .../src/shell/internal_pages/general.rs | 215 ++++++++++++++++++ .../src/shell/internal_pages/settings.rs | 6 + crates/ely_app/src/shell/mod.rs | 20 +- crates/ely_browser_core/src/navigation.rs | 4 +- crates/ely_browser_core/src/state.rs | 41 +++- crates/ely_browser_core/src/state/commands.rs | 2 +- crates/ely_browser_core/src/state/profiles.rs | 2 +- crates/ely_browser_core/src/state/splits.rs | 4 +- crates/ely_browser_core/src/state/tabs.rs | 9 +- .../ely_browser_core/tests/settings_routes.rs | 21 ++ crates/ely_browser_core/tests/tabs.rs | 19 +- crates/ely_domain/src/lib.rs | 2 + crates/ely_domain/src/new_tab.rs | 44 ++++ 14 files changed, 369 insertions(+), 22 deletions(-) create mode 100644 crates/ely_app/src/shell/internal_pages/general.rs create mode 100644 crates/ely_domain/src/new_tab.rs diff --git a/crates/ely_app/src/shell/internal_pages.rs b/crates/ely_app/src/shell/internal_pages.rs index f5c970b..4f2f637 100644 --- a/crates/ely_app/src/shell/internal_pages.rs +++ b/crates/ely_app/src/shell/internal_pages.rs @@ -3,6 +3,7 @@ mod bookmarks; mod download_actions; mod download_labels; mod downloads; +mod general; mod plugin_catalog; mod plugin_details; mod plugins; @@ -51,6 +52,7 @@ impl ElyShell { } "ely://about" => self.render_about_page(snapshot), "ely://settings" => self.render_settings_page(snapshot, cx), + "ely://settings/general" => self.render_general_page(snapshot, cx), "ely://settings/sidebar-tabs" => self.render_sidebar_tabs_page(snapshot, cx), "ely://settings/search" => self.render_search_page(snapshot, cx), "ely://settings/spaces" => self.render_spaces_page(snapshot, cx), diff --git a/crates/ely_app/src/shell/internal_pages/general.rs b/crates/ely_app/src/shell/internal_pages/general.rs new file mode 100644 index 0000000..ec79bd8 --- /dev/null +++ b/crates/ely_app/src/shell/internal_pages/general.rs @@ -0,0 +1,215 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::colors; +use ely_domain::NewTabDestination; +use gpui::{AnyElement, Context, IntoElement, ParentElement, Styled, div, px, rgb}; +use gpui_component::{ + IconName, Selectable, Sizable, StyledExt, + button::{Button, ButtonVariants}, + scroll::ScrollableElement, +}; + +use super::{ElyShell, render_canvas_surface}; + +impl ElyShell { + pub(super) fn render_general_page( + &mut self, + snapshot: &BrowserSnapshot, + cx: &mut Context, + ) -> AnyElement { + render_canvas_surface( + div() + .size_full() + .p_8() + .flex() + .flex_col() + .gap_5() + .child(render_general_header(snapshot)) + .child(render_general_summary(snapshot.new_tab_destination)) + .child(render_new_tab_destinations(snapshot.new_tab_destination, cx)), + ) + } +} + +fn render_general_header(snapshot: &BrowserSnapshot) -> AnyElement { + div() + .flex() + .items_end() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_2() + .child(div().text_size(px(26.0)).text_color(rgb(colors::INK)).child("General")) + .child( + div() + .text_sm() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(format!("Profile: {}", snapshot.active_profile_name)), + ), + ) + .child( + div() + .flex() + .items_center() + .gap_2() + .text_xs() + .font_semibold() + .text_color(rgb(colors::MUTED)) + .child(IconName::Settings2) + .child(snapshot.new_tab_destination.name()), + ) + .into_any_element() +} + +fn render_general_summary(destination: NewTabDestination) -> AnyElement { + div() + .rounded_md() + .border_1() + .border_color(rgb(colors::HAIRLINE)) + .bg(rgb(colors::CANVAS_SOFT)) + .px_4() + .py_3() + .flex() + .items_center() + .justify_between() + .gap_4() + .child( + div() + .min_w_0() + .flex() + .items_center() + .gap_3() + .child(div().text_color(rgb(colors::PRIMARY)).child(destination_icon(destination))) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .text_color(rgb(colors::INK)) + .child(format!("New Tab opens {}", destination.name())), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(destination.detail()), + ), + ), + ) + .child( + div().text_xs().font_semibold().text_color(rgb(colors::SUCCESS)).child("Saved locally"), + ) + .into_any_element() +} + +fn render_new_tab_destinations( + active_destination: NewTabDestination, + 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(NewTabDestination::ALL.iter().copied().enumerate().map(|(index, destination)| { + render_new_tab_destination_row(index, destination, active_destination, cx) + })) + .into_any_element() +} + +fn render_new_tab_destination_row( + index: usize, + destination: NewTabDestination, + active_destination: NewTabDestination, + cx: &mut Context, +) -> AnyElement { + let selected = destination == active_destination; + + 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(destination_icon_color(selected))) + .child(destination_status_icon(destination, selected)), + ) + .child( + div() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_semibold() + .truncate() + .text_color(rgb(colors::INK)) + .child(destination.name()), + ) + .child( + div() + .text_xs() + .truncate() + .text_color(rgb(colors::MUTED)) + .child(destination.detail()), + ), + ), + ) + .child( + Button::new(("new-tab-destination", index)) + .ghost() + .xsmall() + .selected(selected) + .label(destination_button_label(selected)) + .tooltip(destination.name()) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_new_tab_destination(destination, cx); + })), + ) + .into_any_element() +} + +fn destination_icon(destination: NewTabDestination) -> IconName { + match destination { + NewTabDestination::ElyNewTab => IconName::Plus, + NewTabDestination::Bookmarks => IconName::BookOpen, + NewTabDestination::ReadingList => IconName::Inbox, + } +} + +fn destination_status_icon(destination: NewTabDestination, selected: bool) -> IconName { + if selected { IconName::CircleCheck } else { destination_icon(destination) } +} + +fn destination_icon_color(selected: bool) -> u32 { + if selected { colors::PRIMARY } else { colors::MUTED_SOFT } +} + +fn destination_button_label(selected: bool) -> &'static str { + if selected { "Active" } else { "Select" } +} diff --git a/crates/ely_app/src/shell/internal_pages/settings.rs b/crates/ely_app/src/shell/internal_pages/settings.rs index e2850ac..ad67272 100644 --- a/crates/ely_app/src/shell/internal_pages/settings.rs +++ b/crates/ely_app/src/shell/internal_pages/settings.rs @@ -18,6 +18,12 @@ struct SettingsRoute { } const SETTINGS_ROUTES: &[SettingsRoute] = &[ + SettingsRoute { + icon: IconName::Settings2, + title: "General", + detail: "New Tab destination and browser startup defaults.", + route: "ely://settings/general", + }, SettingsRoute { icon: IconName::LayoutDashboard, title: "Sidebar & Tabs", diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index ffd3231..3588e2d 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -6,7 +6,10 @@ mod site_permissions; mod splits; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{ArchivePolicy, CommandIntent, ProfileId, SearchEngine, SpaceId, TabId, UrlText}; +use ely_domain::{ + ArchivePolicy, CommandIntent, NewTabDestination, ProfileId, SearchEngine, SpaceId, TabId, + UrlText, +}; use gpui::{App, AppContext, Context, Entity, FocusHandle, Focusable, Subscription, Window}; use gpui_component::input::{InputEvent, InputState, SelectAll}; @@ -101,7 +104,13 @@ impl ElyShell { } fn open_new_tab(&mut self, window: &mut Window, cx: &mut Context) { - self.open_internal_tab("ely://new-tab", window, cx); + if let ShellState::Ready(core) = &mut self.state + && core.open_new_tab().is_ok() + { + self.sync_address_input(window, cx); + self.focus_address_bar(window, cx); + cx.notify(); + } } fn open_downloads(&mut self, window: &mut Window, cx: &mut Context) { @@ -271,6 +280,13 @@ impl ElyShell { } } + fn set_new_tab_destination(&mut self, destination: NewTabDestination, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state { + core.set_new_tab_destination(destination); + cx.notify(); + } + } + 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/navigation.rs b/crates/ely_browser_core/src/navigation.rs index fb5c008..aef7464 100644 --- a/crates/ely_browser_core/src/navigation.rs +++ b/crates/ely_browser_core/src/navigation.rs @@ -25,6 +25,7 @@ fn internal_page_title(url: &str) -> Option<&'static str> { url if SiteOrigin::from_site_route(url).ok().flatten().is_some() => Some("Site Settings"), "ely://about" => Some("About ELY Browser"), "ely://settings" => Some("Settings"), + "ely://settings/general" => Some("General Settings"), "ely://settings/sidebar-tabs" => Some("Sidebar & Tabs Settings"), "ely://settings/search" => Some("Search Settings"), "ely://settings/spaces" => Some("Space Settings"), @@ -154,7 +155,8 @@ pub(crate) fn settings_page_url(query: &str) -> Result, CoreErro fn settings_page_route(query: &str) -> Option<&'static str> { match query { - "settings" | "general" | "browser" => Some("ely://settings"), + "settings" => Some("ely://settings"), + "general" | "browser" | "new tab" | "new-tab" | "startup" => Some("ely://settings/general"), "about" | "about ely browser" => Some("ely://about"), "sidebar" | "tabs" | "sidebar tabs" | "sidebar & tabs" => { Some("ely://settings/sidebar-tabs") diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index 006946a..58cd8aa 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -2,12 +2,12 @@ use std::{collections::BTreeMap, time::SystemTime}; use ely_domain::{ ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DomainError, DownloadEntry, - DownloadPolicy, HistoryEntry, Profile, ProfileId, ProfileKind, ReadingListEntry, SearchEngine, - SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, TabId, - UrlText, + DownloadPolicy, HistoryEntry, NewTabDestination, Profile, ProfileId, ProfileKind, + ReadingListEntry, SearchEngine, SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, + SplitLayout, SyncStatus, TabId, UrlText, }; -use crate::CoreError; +use crate::{CoreError, navigation::tab_title}; mod bookmarks; mod commands; @@ -28,7 +28,7 @@ pub struct InitialBrowserConfig { pub space_name: String, pub space_icon: String, pub profile_name: String, - pub initial_url: UrlText, + pub new_tab_destination: NewTabDestination, } impl InitialBrowserConfig { @@ -37,7 +37,7 @@ impl InitialBrowserConfig { space_name: "Work".to_string(), space_icon: "W".to_string(), profile_name: "Default".to_string(), - initial_url: UrlText::parse("ely://new-tab")?, + new_tab_destination: NewTabDestination::default(), }) } } @@ -67,6 +67,7 @@ pub struct BrowserSnapshot { pub active_profile_name: String, pub active_download_policy: DownloadPolicy, pub search_engine: SearchEngine, + pub new_tab_destination: NewTabDestination, pub command_query: String, } @@ -92,22 +93,24 @@ pub struct BrowserCore { active_tabs_by_space: BTreeMap, active_tabs_by_space_profile: BTreeMap<(SpaceId, ProfileId), TabId>, search_engine: SearchEngine, + new_tab_destination: NewTabDestination, command_query: String, - new_tab_url: UrlText, } impl BrowserCore { pub fn new(config: InitialBrowserConfig) -> Result { let space = Space::new(config.space_name, config.space_icon, 0xf54e00); let profile = Profile::new(config.profile_name, 0x26251e, ProfileKind::Standard); - let new_tab_url = config.initial_url; + 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 active_profile_id = profile.id().clone(); let tab = BrowserTab::new( TabId::new(), active_space_id.clone(), active_profile_id.clone(), - "New Tab", + new_tab_title, new_tab_url.clone(), ); let active_tab_id = tab.id().clone(); @@ -124,6 +127,7 @@ impl BrowserCore { active_tabs_by_space, active_tabs_by_space_profile, search_engine: SearchEngine::default(), + new_tab_destination, spaces: vec![space], profiles: vec![profile], tabs: vec![tab], @@ -139,7 +143,6 @@ impl BrowserCore { installed_plugins: Vec::new(), plugin_audit_events: Vec::new(), command_query: String::new(), - new_tab_url, }) } @@ -154,7 +157,7 @@ impl BrowserCore { let tab = self.build_tab_for( space_id.clone(), self.active_profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let tab_id = tab.id().clone(); @@ -190,7 +193,7 @@ impl BrowserCore { let tab = self.build_tab_for( space_id.clone(), self.active_profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let tab_id = tab.id().clone(); self.tabs.push(tab); @@ -229,6 +232,15 @@ impl BrowserCore { self.search_engine } + pub fn set_new_tab_destination(&mut self, destination: NewTabDestination) { + self.new_tab_destination = destination; + } + + #[must_use] + pub fn new_tab_destination(&self) -> NewTabDestination { + self.new_tab_destination + } + pub fn set_command_query(&mut self, query: impl Into) { self.command_query = query.into(); } @@ -266,10 +278,15 @@ impl BrowserCore { active_profile_name: active_profile.name().to_string(), active_download_policy: active_profile.download_policy().clone(), search_engine: self.search_engine, + new_tab_destination: self.new_tab_destination, 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() diff --git a/crates/ely_browser_core/src/state/commands.rs b/crates/ely_browser_core/src/state/commands.rs index 8a61038..3c06fc3 100644 --- a/crates/ely_browser_core/src/state/commands.rs +++ b/crates/ely_browser_core/src/state/commands.rs @@ -126,7 +126,7 @@ impl BrowserCore { match command.to_ascii_lowercase().as_str() { "new-tab" => { - self.open_tab(self.new_tab_url.clone()); + self.open_new_tab()?; Ok(true) } "split-right" | "split right" => { diff --git a/crates/ely_browser_core/src/state/profiles.rs b/crates/ely_browser_core/src/state/profiles.rs index 4d017bc..c9aed06 100644 --- a/crates/ely_browser_core/src/state/profiles.rs +++ b/crates/ely_browser_core/src/state/profiles.rs @@ -53,7 +53,7 @@ impl BrowserCore { let tab = self.build_tab_for( self.active_space_id.clone(), profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let tab_id = tab.id().clone(); let insert_index = self diff --git a/crates/ely_browser_core/src/state/splits.rs b/crates/ely_browser_core/src/state/splits.rs index b81933d..ddc022b 100644 --- a/crates/ely_browser_core/src/state/splits.rs +++ b/crates/ely_browser_core/src/state/splits.rs @@ -75,7 +75,7 @@ impl BrowserCore { let active_profile_id = self.tabs[active_index].profile_id().clone(); let split_id = self.split_id_for_new_pane(&active_tab_id)?; let mut new_tab = - self.build_tab_for(active_space_id, active_profile_id, self.new_tab_url.clone()); + self.build_tab_for(active_space_id, active_profile_id, self.new_tab_url()?); let new_tab_id = new_tab.id().clone(); new_tab.set_split_id(split_id.clone()); @@ -276,7 +276,7 @@ impl BrowserCore { let replacement = self.build_tab_for( closed_space_id.clone(), closed_profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let replacement_id = replacement.id().clone(); self.tabs.insert(start_index.min(self.tabs.len()), replacement); diff --git a/crates/ely_browser_core/src/state/tabs.rs b/crates/ely_browser_core/src/state/tabs.rs index f8c8149..085c455 100644 --- a/crates/ely_browser_core/src/state/tabs.rs +++ b/crates/ely_browser_core/src/state/tabs.rs @@ -14,6 +14,11 @@ use super::BrowserCore; const DEFAULT_FAVORITE_LIMIT: usize = 12; impl BrowserCore { + pub fn open_new_tab(&mut self) -> Result { + let url = self.new_tab_url()?; + Ok(self.open_tab(url)) + } + pub fn open_tab(&mut self, url: UrlText) -> TabId { let tab = self.build_tab(url); let tab_id = tab.id().clone(); @@ -62,7 +67,7 @@ impl BrowserCore { let tab = self.build_tab_for( source_space_id.clone(), self.active_profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let replacement_id = tab.id().clone(); self.tabs.insert(tab_index, tab); @@ -125,7 +130,7 @@ impl BrowserCore { let tab = self.build_tab_for( closed_space_id.clone(), closed_profile_id.clone(), - self.new_tab_url.clone(), + self.new_tab_url()?, ); let replacement_id = tab.id().clone(); self.tabs.insert(close_index.min(self.tabs.len()), tab); diff --git a/crates/ely_browser_core/tests/settings_routes.rs b/crates/ely_browser_core/tests/settings_routes.rs index c13e7e7..22b6671 100644 --- a/crates/ely_browser_core/tests/settings_routes.rs +++ b/crates/ely_browser_core/tests/settings_routes.rs @@ -24,6 +24,27 @@ fn settings_scoped_search_opens_sidebar_tabs_page() -> Result<(), Box Ok(()) } +#[test] +fn settings_scoped_search_opens_general_page() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + + core.set_command_query("@settings general"); + let intent = core.submit_command()?; + let active_tab = core.active_tab()?; + + assert_eq!( + intent, + Some(CommandIntent::ScopedSearch { + scope: CommandScope::Settings, + query: "general".to_string(), + }) + ); + assert_eq!(active_tab.title(), "General Settings"); + assert_eq!(active_tab.url().as_str(), "ely://settings/general"); + assert_eq!(core.snapshot()?.command_query, ""); + Ok(()) +} + #[test] fn settings_scoped_search_opens_shortcuts_page() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_browser_core/tests/tabs.rs b/crates/ely_browser_core/tests/tabs.rs index d6cc539..f17155e 100644 --- a/crates/ely_browser_core/tests/tabs.rs +++ b/crates/ely_browser_core/tests/tabs.rs @@ -1,7 +1,7 @@ use std::error::Error; use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig}; -use ely_domain::{CommandIntent, CommandScope, SearchEngine, TabState, UrlText}; +use ely_domain::{CommandIntent, CommandScope, NewTabDestination, SearchEngine, TabState, UrlText}; #[test] fn opens_new_tab_below_active_tab() -> Result<(), Box> { @@ -260,6 +260,23 @@ fn search_command_uses_selected_search_engine() -> Result<(), Box> { Ok(()) } +#[test] +fn new_tab_command_uses_selected_destination() -> Result<(), Box> { + let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; + core.set_new_tab_destination(NewTabDestination::Bookmarks); + + core.set_command_query(">new-tab"); + let intent = core.submit_command()?; + + let active_tab = core.active_tab()?; + assert_eq!(intent, Some(CommandIntent::Command("new-tab".to_string()))); + assert_eq!(active_tab.title(), "Bookmarks"); + assert_eq!(active_tab.url().as_str(), "ely://bookmarks"); + assert_eq!(core.snapshot()?.new_tab_destination, NewTabDestination::Bookmarks); + assert_eq!(core.command_query(), ""); + Ok(()) +} + #[test] fn switching_spaces_restores_each_space_active_tab() -> Result<(), Box> { let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?; diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index 3d40369..dde8152 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -5,6 +5,7 @@ mod download; mod error; mod history; mod identifiers; +mod new_tab; mod plugin; mod profile; mod reading_list; @@ -28,6 +29,7 @@ pub use history::HistoryEntry; pub use identifiers::{ BookmarkId, DownloadId, ProfileId, ReadingListId, SpaceId, SplitId, TabId, WebViewId, }; +pub use new_tab::NewTabDestination; pub use plugin::{ PluginContributionPoint, PluginId, PluginManifest, PluginPermission, PluginPermissionRisk, PluginSignature, PluginSignatureAlgorithm, diff --git a/crates/ely_domain/src/new_tab.rs b/crates/ely_domain/src/new_tab.rs new file mode 100644 index 0000000..416c57e --- /dev/null +++ b/crates/ely_domain/src/new_tab.rs @@ -0,0 +1,44 @@ +use crate::{DomainError, UrlText}; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum NewTabDestination { + #[default] + ElyNewTab, + Bookmarks, + ReadingList, +} + +impl NewTabDestination { + pub const ALL: &[Self] = &[Self::ElyNewTab, Self::Bookmarks, Self::ReadingList]; + + #[must_use] + pub fn name(self) -> &'static str { + match self { + Self::ElyNewTab => "ELY New Tab", + Self::Bookmarks => "Bookmarks", + Self::ReadingList => "Reading List", + } + } + + #[must_use] + pub fn detail(self) -> &'static str { + match self { + Self::ElyNewTab => "Open the quiet browser start surface.", + Self::Bookmarks => "Open saved pages first.", + Self::ReadingList => "Open the saved reading queue first.", + } + } + + #[must_use] + pub fn route(self) -> &'static str { + match self { + Self::ElyNewTab => "ely://new-tab", + Self::Bookmarks => "ely://bookmarks", + Self::ReadingList => "ely://reading-list", + } + } + + pub fn url(self) -> Result { + UrlText::parse(self.route()) + } +}