Add idle tab auto archive policy
This commit is contained in:
@@ -56,6 +56,11 @@ pub(crate) fn move_tab_space_name(command: &str) -> Option<&str> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn archive_idle_days(command: &str) -> Option<u16> {
|
||||
command_argument(command, &["archive-idle-tabs ", "archive idle tabs "])
|
||||
.and_then(|value| value.parse().ok())
|
||||
}
|
||||
|
||||
pub(crate) fn new_profile_name(command: &str) -> Option<&str> {
|
||||
command_argument(command, &["new-profile ", "new profile "])
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, time::SystemTime};
|
||||
|
||||
use ely_domain::{
|
||||
ArchivedTab, BookmarkEntry, BrowserTab, DomainError, DownloadEntry, DownloadPolicy,
|
||||
HistoryEntry, Profile, ProfileId, ProfileKind, ReadingListEntry, SitePermissionAuditEvent,
|
||||
SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, TabId, UrlText,
|
||||
ArchivePolicy, ArchivedTab, BookmarkEntry, BrowserTab, DomainError, DownloadEntry,
|
||||
DownloadPolicy, HistoryEntry, Profile, ProfileId, ProfileKind, ReadingListEntry,
|
||||
SitePermissionAuditEvent, SitePermissionEntry, Space, SpaceId, SplitLayout, SyncStatus, TabId,
|
||||
UrlText,
|
||||
};
|
||||
|
||||
use crate::CoreError;
|
||||
@@ -194,6 +195,28 @@ impl BrowserCore {
|
||||
Ok(tab_id)
|
||||
}
|
||||
|
||||
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_command_query(&mut self, query: impl Into<String>) {
|
||||
self.command_query = query.into();
|
||||
}
|
||||
@@ -264,4 +287,10 @@ impl BrowserCore {
|
||||
fn visible_tabs(&self) -> Vec<BrowserTab> {
|
||||
self.tabs.iter().filter(|tab| tab.space_id() == &self.active_space_id).cloned().collect()
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use ely_domain::{CommandIntent, CommandScope, ProfileId, ProfileKind, SpaceId};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use ely_domain::{ArchivePolicy, CommandIntent, CommandScope, ProfileId, ProfileKind, SpaceId};
|
||||
|
||||
use crate::{
|
||||
CoreError,
|
||||
navigation::{
|
||||
about_url, archive_url, bookmarks_url, downloads_url, history_url, 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, settings_url, space_icon,
|
||||
switch_profile_name, sync_status_url, task_manager_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,
|
||||
plugin_detail_url, plugins_url, reading_list_url, search_url, settings_page_url,
|
||||
settings_url, space_icon, switch_profile_name, sync_status_url, task_manager_url,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -115,6 +117,11 @@ impl BrowserCore {
|
||||
self.select_profile(&profile_id)?;
|
||||
return Ok(true);
|
||||
}
|
||||
if let Some(idle_days) = archive_idle_days(command) {
|
||||
self.set_active_space_archive_policy(ArchivePolicy::IdleDays(idle_days))?;
|
||||
self.archive_idle_tabs(SystemTime::now())?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
match command.to_ascii_lowercase().as_str() {
|
||||
"new-tab" => {
|
||||
@@ -185,6 +192,10 @@ impl BrowserCore {
|
||||
"close-split-view" | "close split view" => {
|
||||
Ok(self.close_active_saved_split_view()?.is_some())
|
||||
}
|
||||
"archive-idle-tabs" | "archive idle tabs" => {
|
||||
self.archive_idle_tabs(SystemTime::now())?;
|
||||
Ok(true)
|
||||
}
|
||||
"favorite" | "toggle-favorite" => {
|
||||
self.toggle_active_tab_favorite()?;
|
||||
Ok(true)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use ely_domain::{ArchiveSource, ArchivedTab, BrowserTab, ProfileId, SpaceId, TabId, UrlText};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use ely_domain::{
|
||||
ArchivePolicy, ArchiveSource, ArchivedTab, BrowserTab, ProfileId, SpaceId, TabId, UrlText,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
CoreError,
|
||||
@@ -168,6 +172,23 @@ impl BrowserCore {
|
||||
self.restore_archived_tab_at_index(index).map(Some)
|
||||
}
|
||||
|
||||
pub fn archive_idle_tabs(&mut self, now: SystemTime) -> Result<usize, CoreError> {
|
||||
let ArchivePolicy::IdleDays(idle_days) = self.active_space()?.archive_policy() else {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
let active_space_id = self.active_space_id.clone();
|
||||
let idle_after = Duration::from_secs(u64::from(*idle_days) * 86_400);
|
||||
let tab_ids = self.idle_archive_candidates(&active_space_id, now, idle_after);
|
||||
let archived_count = tab_ids.len();
|
||||
|
||||
for tab_id in tab_ids {
|
||||
self.archive_idle_tab(&tab_id)?;
|
||||
}
|
||||
|
||||
Ok(archived_count)
|
||||
}
|
||||
|
||||
pub fn select_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
||||
let tab = self
|
||||
.tabs
|
||||
@@ -184,6 +205,7 @@ impl BrowserCore {
|
||||
self.active_tabs_by_space_profile
|
||||
.insert((active_space_id.clone(), active_profile_id), active_tab_id.clone());
|
||||
self.active_tabs_by_space.insert(active_space_id, active_tab_id);
|
||||
self.record_tab_activity(tab_id, SystemTime::now());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -348,4 +370,62 @@ impl BrowserCore {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn idle_archive_candidates(
|
||||
&self,
|
||||
space_id: &SpaceId,
|
||||
now: SystemTime,
|
||||
idle_after: Duration,
|
||||
) -> Vec<TabId> {
|
||||
self.tabs
|
||||
.iter()
|
||||
.filter(|tab| tab.space_id() == space_id)
|
||||
.filter(|tab| tab.id() != &self.active_tab_id)
|
||||
.filter(|tab| !tab.flags().favorite && !tab.flags().pinned)
|
||||
.filter(|tab| tab.split_id().is_none())
|
||||
.filter(|tab| tab_is_idle(tab, now, idle_after))
|
||||
.map(|tab| tab.id().clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn archive_idle_tab(&mut self, tab_id: &TabId) -> Result<(), CoreError> {
|
||||
let tab_index = self
|
||||
.tabs
|
||||
.iter()
|
||||
.position(|tab| tab.id() == tab_id)
|
||||
.ok_or_else(|| CoreError::TabNotFound { id: tab_id.clone() })?;
|
||||
let mut tab = self.tabs.remove(tab_index);
|
||||
let space_id = tab.space_id().clone();
|
||||
let profile_id = tab.profile_id().clone();
|
||||
|
||||
tab.clear_split_id();
|
||||
self.archived_tabs.push(ArchivedTab::new(tab, ArchiveSource::AutoArchive));
|
||||
self.refresh_space_profile_active_tab(&space_id, &profile_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn refresh_space_profile_active_tab(&mut self, space_id: &SpaceId, profile_id: &ProfileId) {
|
||||
let key = (space_id.clone(), profile_id.clone());
|
||||
if self.active_tabs_by_space_profile.get(&key).is_some_and(|tab_id| {
|
||||
self.tabs.iter().any(|tab| {
|
||||
tab.id() == tab_id && tab.space_id() == space_id && tab.profile_id() == profile_id
|
||||
})
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.active_tabs_by_space_profile.remove(&key);
|
||||
if let Some(tab_id) = self
|
||||
.tabs
|
||||
.iter()
|
||||
.find(|tab| tab.space_id() == space_id && tab.profile_id() == profile_id)
|
||||
.map(|tab| tab.id().clone())
|
||||
{
|
||||
self.active_tabs_by_space_profile.insert(key, tab_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_is_idle(tab: &BrowserTab, now: SystemTime, idle_after: Duration) -> bool {
|
||||
now.duration_since(tab.last_active_at()).is_ok_and(|idle_time| idle_time >= idle_after)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use ely_browser_core::{BrowserCore, InitialBrowserConfig};
|
||||
use ely_domain::{ArchivePolicy, ArchiveSource, CommandIntent, UrlText};
|
||||
|
||||
#[test]
|
||||
fn archive_idle_tabs_uses_active_space_policy() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let active_tab_id = core.active_tab()?.id().clone();
|
||||
let idle_tab_id = core.open_tab(UrlText::parse("https://example.com/idle")?);
|
||||
let pinned_tab_id = core.open_tab(UrlText::parse("https://example.com/pinned")?);
|
||||
core.toggle_active_tab_pinned()?;
|
||||
core.select_tab(&active_tab_id)?;
|
||||
core.set_active_space_archive_policy(ArchivePolicy::IdleDays(1))?;
|
||||
|
||||
let archived_count = core.archive_idle_tabs(days_from_now(2))?;
|
||||
let snapshot = core.snapshot()?;
|
||||
|
||||
assert_eq!(archived_count, 1);
|
||||
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &active_tab_id));
|
||||
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &pinned_tab_id));
|
||||
assert_eq!(snapshot.archived_tabs.len(), 1);
|
||||
assert_eq!(snapshot.archived_tabs[0].tab().id(), &idle_tab_id);
|
||||
assert_eq!(snapshot.archived_tabs[0].source(), &ArchiveSource::AutoArchive);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_idle_tabs_manual_policy_leaves_tabs_open() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let active_tab_id = core.active_tab()?.id().clone();
|
||||
let idle_tab_id = core.open_tab(UrlText::parse("https://example.com/idle")?);
|
||||
core.select_tab(&active_tab_id)?;
|
||||
|
||||
let archived_count = core.archive_idle_tabs(days_from_now(30))?;
|
||||
let snapshot = core.snapshot()?;
|
||||
|
||||
assert_eq!(archived_count, 0);
|
||||
assert!(snapshot.tabs.iter().any(|tab| tab.id() == &idle_tab_id));
|
||||
assert!(snapshot.archived_tabs.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn archive_idle_tabs_command_sets_days_policy() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let active_tab_id = core.active_tab()?.id().clone();
|
||||
let archived_tab_id = core.open_tab(UrlText::parse("https://example.com/command")?);
|
||||
core.select_tab(&active_tab_id)?;
|
||||
|
||||
core.set_command_query(">archive-idle-tabs 0");
|
||||
let intent = core.submit_command()?;
|
||||
let snapshot = core.snapshot()?;
|
||||
|
||||
assert_eq!(intent, Some(CommandIntent::Command("archive-idle-tabs 0".to_string())));
|
||||
assert_eq!(snapshot.active_tab_id, active_tab_id);
|
||||
assert_eq!(snapshot.archived_tabs.len(), 1);
|
||||
assert_eq!(snapshot.archived_tabs[0].tab().id(), &archived_tab_id);
|
||||
assert_eq!(snapshot.archived_tabs[0].source(), &ArchiveSource::AutoArchive);
|
||||
assert_eq!(snapshot.command_query, "");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn days_from_now(days: u64) -> SystemTime {
|
||||
SystemTime::now() + Duration::from_secs(days * 86_400)
|
||||
}
|
||||
Reference in New Issue
Block a user