Add page notes panel

This commit is contained in:
2026-05-08 06:53:31 -04:00
parent bae3c7789f
commit 7e68857eb8
15 changed files with 844 additions and 7 deletions
+2
View File
@@ -6,6 +6,7 @@ pub enum CommandScope {
Spaces,
Tabs,
Bookmarks,
Notes,
ReadingList,
History,
Settings,
@@ -50,6 +51,7 @@ fn parse_scope(value: &str) -> Option<(CommandScope, &str)> {
"@spaces" => CommandScope::Spaces,
"@tabs" => CommandScope::Tabs,
"@bookmarks" => CommandScope::Bookmarks,
"@notes" => CommandScope::Notes,
"@reading-list" | "@reading" => CommandScope::ReadingList,
"@history" => CommandScope::History,
"@settings" => CommandScope::Settings,
+1
View File
@@ -41,3 +41,4 @@ entity_id!(WebViewId, "webview");
entity_id!(DownloadId, "download");
entity_id!(BookmarkId, "bookmark");
entity_id!(ReadingListId, "reading");
entity_id!(NoteId, "note");
+3 -1
View File
@@ -7,6 +7,7 @@ mod favorite;
mod history;
mod identifiers;
mod new_tab;
mod note;
mod plugin;
mod privacy;
mod profile;
@@ -30,9 +31,10 @@ pub use error::DomainError;
pub use favorite::FavoriteLimit;
pub use history::HistoryEntry;
pub use identifiers::{
BookmarkId, DownloadId, ProfileId, ReadingListId, SpaceId, SplitId, TabId, WebViewId,
BookmarkId, DownloadId, NoteId, ProfileId, ReadingListId, SpaceId, SplitId, TabId, WebViewId,
};
pub use new_tab::NewTabDestination;
pub use note::{NoteEntry, NoteTarget};
pub use plugin::{
PluginContributionPoint, PluginId, PluginManifest, PluginPermission, PluginPermissionRisk,
PluginSignature, PluginSignatureAlgorithm,
+228
View File
@@ -0,0 +1,228 @@
use std::time::SystemTime;
use crate::{DomainError, NoteId, ProfileId, SpaceId, TabId, UrlText};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum NoteTarget {
Url(UrlText),
Tab(TabId),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NoteEntry {
id: NoteId,
profile_id: ProfileId,
space_id: SpaceId,
target: NoteTarget,
title: String,
source_url: UrlText,
body: String,
created_at: SystemTime,
updated_at: SystemTime,
}
impl NoteEntry {
pub fn new(
profile_id: ProfileId,
space_id: SpaceId,
target: NoteTarget,
title: impl Into<String>,
source_url: UrlText,
body: impl Into<String>,
now: SystemTime,
) -> Result<Self, DomainError> {
let title = non_empty_text("note title", title.into())?;
let body = normalize_body(body.into())?;
Ok(Self {
id: NoteId::new(),
profile_id,
space_id,
target,
title,
source_url,
body,
created_at: now,
updated_at: now,
})
}
#[must_use]
pub fn id(&self) -> &NoteId {
&self.id
}
#[must_use]
pub fn profile_id(&self) -> &ProfileId {
&self.profile_id
}
#[must_use]
pub fn space_id(&self) -> &SpaceId {
&self.space_id
}
#[must_use]
pub fn target(&self) -> &NoteTarget {
&self.target
}
#[must_use]
pub fn target_label(&self) -> &'static str {
match &self.target {
NoteTarget::Url(_) => "URL note",
NoteTarget::Tab(_) => "Tab note",
}
}
#[must_use]
pub fn title(&self) -> &str {
&self.title
}
#[must_use]
pub fn source_url(&self) -> &UrlText {
&self.source_url
}
#[must_use]
pub fn display_url(&self) -> String {
self.source_url.display_url()
}
#[must_use]
pub fn body(&self) -> &str {
&self.body
}
#[must_use]
pub fn created_at(&self) -> SystemTime {
self.created_at
}
#[must_use]
pub fn updated_at(&self) -> SystemTime {
self.updated_at
}
pub fn update(
&mut self,
title: impl Into<String>,
source_url: UrlText,
body: impl Into<String>,
updated_at: SystemTime,
) -> Result<(), DomainError> {
self.title = non_empty_text("note title", title.into())?;
self.source_url = source_url;
self.body = normalize_body(body.into())?;
self.updated_at = updated_at;
Ok(())
}
}
fn normalize_body(value: String) -> Result<String, DomainError> {
let normalized = value.replace("\r\n", "\n").replace('\r', "\n");
non_empty_text("note body", normalized)
}
fn non_empty_text(field: &'static str, value: String) -> Result<String, DomainError> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(DomainError::EmptyField { field });
}
Ok(trimmed.to_string())
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::{NoteEntry, NoteTarget};
use crate::{DomainError, ProfileId, SpaceId, TabId, UrlText};
#[test]
fn creates_url_note_with_markdown_body() -> Result<(), DomainError> {
let now = SystemTime::UNIX_EPOCH + Duration::from_secs(10);
let source_url = UrlText::parse("https://example.com/article")?;
let note = NoteEntry::new(
ProfileId::new(),
SpaceId::new(),
NoteTarget::Url(source_url.clone()),
"Example",
source_url,
" # Heading\r\n- item ",
now,
)?;
assert_eq!(note.title(), "Example");
assert_eq!(note.body(), "# Heading\n- item");
assert_eq!(note.target_label(), "URL note");
assert_eq!(note.created_at(), now);
assert_eq!(note.updated_at(), now);
Ok(())
}
#[test]
fn creates_tab_note_target() -> Result<(), DomainError> {
let tab_id = TabId::new();
let note = NoteEntry::new(
ProfileId::new(),
SpaceId::new(),
NoteTarget::Tab(tab_id.clone()),
"Example",
UrlText::parse("https://example.com/tab")?,
"- detail",
SystemTime::UNIX_EPOCH,
)?;
assert_eq!(note.target(), &NoteTarget::Tab(tab_id));
assert_eq!(note.target_label(), "Tab note");
Ok(())
}
#[test]
fn rejects_empty_body() -> Result<(), DomainError> {
let source_url = UrlText::parse("https://example.com")?;
let result = NoteEntry::new(
ProfileId::new(),
SpaceId::new(),
NoteTarget::Url(source_url.clone()),
"Example",
source_url,
" ",
SystemTime::UNIX_EPOCH,
);
assert_eq!(result, Err(DomainError::EmptyField { field: "note body" }));
Ok(())
}
#[test]
fn updates_body_and_timestamp() -> Result<(), DomainError> {
let created_at = SystemTime::UNIX_EPOCH + Duration::from_secs(10);
let updated_at = created_at + Duration::from_secs(30);
let mut note = NoteEntry::new(
ProfileId::new(),
SpaceId::new(),
NoteTarget::Url(UrlText::parse("https://example.com/old")?),
"Old",
UrlText::parse("https://example.com/old")?,
"first",
created_at,
)?;
note.update(
"New",
UrlText::parse("https://example.com/new")?,
" second\rline ",
updated_at,
)?;
assert_eq!(note.title(), "New");
assert_eq!(note.source_url().as_str(), "https://example.com/new");
assert_eq!(note.body(), "second\nline");
assert_eq!(note.created_at(), created_at);
assert_eq!(note.updated_at(), updated_at);
Ok(())
}
}
+1
View File
@@ -8,6 +8,7 @@ pub enum SyncObjectKind {
Spaces,
Tabs,
Bookmarks,
Notes,
ReadingList,
Profiles,
SitePermissions,