Add reading list progress percent
This commit is contained in:
@@ -192,7 +192,7 @@ fn reading_list_space_name(snapshot: &BrowserSnapshot, entry: &ReadingListEntry)
|
|||||||
.map(|space| space.name().to_string())
|
.map(|space| space.name().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn progress_label(progress: &ReadingProgress) -> &'static str {
|
fn progress_label(progress: &ReadingProgress) -> String {
|
||||||
progress.label()
|
progress.label()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +204,7 @@ fn render_progress_action(
|
|||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let next_progress = progress.toggled();
|
let next_progress = progress.toggled();
|
||||||
let icon = match progress {
|
let icon = match progress {
|
||||||
ReadingProgress::Unread => IconName::CircleCheck,
|
ReadingProgress::Unread | ReadingProgress::InProgress(_) => IconName::CircleCheck,
|
||||||
ReadingProgress::Finished => IconName::Undo2,
|
ReadingProgress::Finished => IconName::Undo2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use ely_domain::{BrowserTab, PluginId, SearchEngine, SiteOrigin, UrlText};
|
use ely_domain::{
|
||||||
|
BrowserTab, DomainError, PluginId, ReadingProgressPercent, SearchEngine, SiteOrigin, UrlText,
|
||||||
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::CoreError;
|
use crate::CoreError;
|
||||||
@@ -67,6 +69,28 @@ pub(crate) fn archive_idle_days(command: &str) -> Option<u16> {
|
|||||||
.and_then(|value| value.parse().ok())
|
.and_then(|value| value.parse().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reading_progress_percent(
|
||||||
|
command: &str,
|
||||||
|
) -> Result<Option<ReadingProgressPercent>, CoreError> {
|
||||||
|
let Some(value) = command_argument(
|
||||||
|
command,
|
||||||
|
&[
|
||||||
|
"reading-progress ",
|
||||||
|
"reading progress ",
|
||||||
|
"set-reading-progress ",
|
||||||
|
"set reading progress ",
|
||||||
|
],
|
||||||
|
) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let percent_text = value.trim().strip_suffix('%').unwrap_or(value.trim()).trim();
|
||||||
|
let percent = percent_text.parse::<u8>().map_err(|_| {
|
||||||
|
DomainError::InvalidReadingProgressPercent { value: value.trim().to_string() }
|
||||||
|
})?;
|
||||||
|
ReadingProgressPercent::new(percent).map(Some).map_err(CoreError::from)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn new_profile_name(command: &str) -> Option<&str> {
|
pub(crate) fn new_profile_name(command: &str) -> Option<&str> {
|
||||||
command_argument(command, &["new-profile ", "new profile "])
|
command_argument(command, &["new-profile ", "new profile "])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use crate::{
|
|||||||
navigation::{
|
navigation::{
|
||||||
about_url, archive_idle_days, archive_url, bookmarks_url, downloads_url, history_url,
|
about_url, archive_idle_days, archive_url, bookmarks_url, downloads_url, history_url,
|
||||||
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name,
|
move_tab_space_name, new_private_profile_name, new_profile_name, new_space_name,
|
||||||
plugin_detail_url, plugins_url, reading_list_url, search_url, settings_page_url,
|
plugin_detail_url, plugins_url, reading_list_url, reading_progress_percent, search_url,
|
||||||
settings_url, shortcut_settings_url, space_icon, switch_profile_name, sync_status_url,
|
settings_page_url, settings_url, shortcut_settings_url, space_icon, switch_profile_name,
|
||||||
task_manager_url,
|
sync_status_url, task_manager_url,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,6 +123,10 @@ impl BrowserCore {
|
|||||||
self.archive_idle_tabs(SystemTime::now())?;
|
self.archive_idle_tabs(SystemTime::now())?;
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
if let Some(percent) = reading_progress_percent(command)? {
|
||||||
|
self.set_active_tab_reading_progress(percent)?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
match command.to_ascii_lowercase().as_str() {
|
match command.to_ascii_lowercase().as_str() {
|
||||||
"new-tab" => {
|
"new-tab" => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use ely_domain::{ReadingListEntry, ReadingListId, ReadingProgress, UrlText};
|
use ely_domain::{
|
||||||
|
ReadingListEntry, ReadingListId, ReadingProgress, ReadingProgressPercent, UrlText,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::CoreError;
|
use crate::CoreError;
|
||||||
|
|
||||||
@@ -36,6 +38,15 @@ impl BrowserCore {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_active_tab_reading_progress(
|
||||||
|
&mut self,
|
||||||
|
percent: ReadingProgressPercent,
|
||||||
|
) -> Result<ReadingListId, CoreError> {
|
||||||
|
let entry_id = self.save_active_tab_to_reading_list()?;
|
||||||
|
self.set_reading_list_progress(&entry_id, ReadingProgress::InProgress(percent))?;
|
||||||
|
Ok(entry_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_reading_list_entry(&mut self, entry_id: &ReadingListId) -> Result<(), CoreError> {
|
pub fn remove_reading_list_entry(&mut self, entry_id: &ReadingListId) -> Result<(), CoreError> {
|
||||||
let index = self.reading_list_entry_index(entry_id)?;
|
let index = self.reading_list_entry_index(entry_id)?;
|
||||||
self.reading_list.remove(index);
|
self.reading_list.remove(index);
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
CommandIntent, CommandScope, ProfileKind, ReadingListId, ReadingProgress, UrlText,
|
CommandIntent, CommandScope, DomainError, ProfileKind, ReadingListId, ReadingProgress,
|
||||||
|
ReadingProgressPercent, UrlText,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -49,6 +50,18 @@ fn reading_list_progress_updates_entry() -> Result<(), Box<dyn Error>> {
|
|||||||
core.open_tab(UrlText::parse("https://example.com/long-read")?);
|
core.open_tab(UrlText::parse("https://example.com/long-read")?);
|
||||||
let entry_id = core.save_active_tab_to_reading_list()?;
|
let entry_id = core.save_active_tab_to_reading_list()?;
|
||||||
|
|
||||||
|
core.set_reading_list_progress(
|
||||||
|
&entry_id,
|
||||||
|
ReadingProgress::InProgress(ReadingProgressPercent::new(42)?),
|
||||||
|
)?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(snapshot.reading_list[0].id(), &entry_id);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.reading_list[0].progress(),
|
||||||
|
&ReadingProgress::InProgress(ReadingProgressPercent::new(42)?)
|
||||||
|
);
|
||||||
|
|
||||||
core.set_reading_list_progress(&entry_id, ReadingProgress::Finished)?;
|
core.set_reading_list_progress(&entry_id, ReadingProgress::Finished)?;
|
||||||
let snapshot = core.snapshot()?;
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
@@ -57,6 +70,59 @@ fn reading_list_progress_updates_entry() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_tab_reading_progress_saves_partial_progress() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/long-read")?);
|
||||||
|
|
||||||
|
let entry_id = core.set_active_tab_reading_progress(ReadingProgressPercent::new(47)?)?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(snapshot.reading_list.len(), 1);
|
||||||
|
assert_eq!(snapshot.reading_list[0].id(), &entry_id);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.reading_list[0].progress(),
|
||||||
|
&ReadingProgress::InProgress(ReadingProgressPercent::new(47)?)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_progress_command_updates_active_page_entry() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/long-read")?);
|
||||||
|
|
||||||
|
core.set_command_query(">reading-progress 42%");
|
||||||
|
let intent = core.submit_command()?;
|
||||||
|
let snapshot = core.snapshot()?;
|
||||||
|
|
||||||
|
assert_eq!(intent, Some(CommandIntent::Command("reading-progress 42%".to_string())));
|
||||||
|
assert_eq!(snapshot.command_query, "");
|
||||||
|
assert_eq!(snapshot.reading_list.len(), 1);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot.reading_list[0].progress(),
|
||||||
|
&ReadingProgress::InProgress(ReadingProgressPercent::new(42)?)
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_progress_command_rejects_terminal_percent() -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
core.open_tab(UrlText::parse("https://example.com/long-read")?);
|
||||||
|
|
||||||
|
core.set_command_query(">reading-progress 100");
|
||||||
|
let Err(error) = core.submit_command() else {
|
||||||
|
return Err("expected invalid reading progress error".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
error,
|
||||||
|
CoreError::Domain(DomainError::InvalidReadingProgressPercent { value: "100".to_string() })
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_reading_list_progress_update_returns_error() -> Result<(), Box<dyn Error>> {
|
fn missing_reading_list_progress_update_returns_error() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ pub enum DomainError {
|
|||||||
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
#[error("download progress {received_bytes} exceeds total {total_bytes}")]
|
||||||
InvalidDownloadProgress { received_bytes: u64, total_bytes: u64 },
|
InvalidDownloadProgress { received_bytes: u64, total_bytes: u64 },
|
||||||
|
|
||||||
|
#[error("invalid reading progress percent: {value}")]
|
||||||
|
InvalidReadingProgressPercent { value: String },
|
||||||
|
|
||||||
#[error("invalid plugin manifest: {reason}")]
|
#[error("invalid plugin manifest: {reason}")]
|
||||||
InvalidPluginManifest { reason: String },
|
InvalidPluginManifest { reason: String },
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub use plugin::{
|
|||||||
};
|
};
|
||||||
pub use privacy::HistoryRecordingPolicy;
|
pub use privacy::HistoryRecordingPolicy;
|
||||||
pub use profile::{Profile, ProfileKind, ProfileSyncPolicy};
|
pub use profile::{Profile, ProfileKind, ProfileSyncPolicy};
|
||||||
pub use reading_list::{ReadingListEntry, ReadingProgress};
|
pub use reading_list::{ReadingListEntry, ReadingProgress, ReadingProgressPercent};
|
||||||
pub use search::SearchEngine;
|
pub use search::SearchEngine;
|
||||||
pub use site_permission::{
|
pub use site_permission::{
|
||||||
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
SiteOrigin, SitePermissionAuditAction, SitePermissionAuditEvent, SitePermissionDecision,
|
||||||
|
|||||||
@@ -2,25 +2,44 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
use crate::{DomainError, ProfileId, ReadingListId, SpaceId, UrlText};
|
use crate::{DomainError, ProfileId, ReadingListId, SpaceId, UrlText};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ReadingProgressPercent(u8);
|
||||||
|
|
||||||
|
impl ReadingProgressPercent {
|
||||||
|
pub fn new(value: u8) -> Result<Self, DomainError> {
|
||||||
|
if !(1..=99).contains(&value) {
|
||||||
|
return Err(DomainError::InvalidReadingProgressPercent { value: value.to_string() });
|
||||||
|
}
|
||||||
|
Ok(Self(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn value(self) -> u8 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ReadingProgress {
|
pub enum ReadingProgress {
|
||||||
Unread,
|
Unread,
|
||||||
|
InProgress(ReadingProgressPercent),
|
||||||
Finished,
|
Finished,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReadingProgress {
|
impl ReadingProgress {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn label(self) -> &'static str {
|
pub fn label(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Self::Unread => "Unread",
|
Self::Unread => "Unread".to_string(),
|
||||||
Self::Finished => "Read",
|
Self::InProgress(percent) => format!("{}% read", percent.value()),
|
||||||
|
Self::Finished => "Read".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn action_label(self) -> &'static str {
|
pub fn action_label(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Unread => "Mark Read",
|
Self::Unread | Self::InProgress(_) => "Mark Read",
|
||||||
Self::Finished => "Mark Unread",
|
Self::Finished => "Mark Unread",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,7 +47,7 @@ impl ReadingProgress {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn toggled(self) -> Self {
|
pub fn toggled(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Unread => Self::Finished,
|
Self::Unread | Self::InProgress(_) => Self::Finished,
|
||||||
Self::Finished => Self::Unread,
|
Self::Finished => Self::Unread,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,3 +137,30 @@ fn non_empty_text(field: &'static str, value: String) -> Result<String, DomainEr
|
|||||||
}
|
}
|
||||||
Ok(trimmed.to_string())
|
Ok(trimmed.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{ReadingProgress, ReadingProgressPercent};
|
||||||
|
use crate::DomainError;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_progress_percent_accepts_partial_progress() -> Result<(), DomainError> {
|
||||||
|
let percent = ReadingProgressPercent::new(42)?;
|
||||||
|
|
||||||
|
assert_eq!(percent.value(), 42);
|
||||||
|
assert_eq!(ReadingProgress::InProgress(percent).label(), "42% read");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reading_progress_percent_rejects_terminal_values() {
|
||||||
|
assert_eq!(
|
||||||
|
ReadingProgressPercent::new(0),
|
||||||
|
Err(DomainError::InvalidReadingProgressPercent { value: "0".to_string() })
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ReadingProgressPercent::new(100),
|
||||||
|
Err(DomainError::InvalidReadingProgressPercent { value: "100".to_string() })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user