Add space trash recovery

This commit is contained in:
2026-05-08 09:17:59 -04:00
parent e839b05415
commit b11668e75e
8 changed files with 415 additions and 9 deletions
+6
View File
@@ -18,6 +18,12 @@ pub enum CoreError {
#[error("space not found: {id}")]
SpaceNotFound { id: SpaceId },
#[error("trashed space not found: {id}")]
TrashedSpaceNotFound { id: SpaceId },
#[error("last space cannot be moved to trash")]
LastSpaceCannotBeTrashed,
#[error("split not found: {id}")]
SplitNotFound { id: SplitId },
+1 -1
View File
@@ -5,5 +5,5 @@ mod state;
pub use error::CoreError;
pub use state::{
BrowserCore, BrowserSnapshot, InitialBrowserConfig, InstalledPlugin, PluginAuditAction,
PluginAuditEvent,
PluginAuditEvent, TrashedSpace,
};
+5
View File
@@ -28,6 +28,7 @@ mod tab_order;
mod tabs;
pub use plugins::{InstalledPlugin, PluginAuditAction, PluginAuditEvent};
pub use spaces::TrashedSpace;
#[derive(Clone, Debug)]
pub struct InitialBrowserConfig {
@@ -67,6 +68,7 @@ pub struct BrowserSnapshot {
pub installed_plugins: Vec<InstalledPlugin>,
pub plugin_audit_events: Vec<PluginAuditEvent>,
pub spaces: Vec<Space>,
pub trashed_spaces: Vec<TrashedSpace>,
pub profiles: Vec<Profile>,
pub sync_status: SyncStatus,
pub active_tab_id: TabId,
@@ -98,6 +100,7 @@ pub struct BrowserCore {
tab_groups: Vec<TabGroup>,
split_layouts: Vec<SplitLayout>,
archived_split_layouts: Vec<SplitLayout>,
trashed_spaces: Vec<TrashedSpace>,
installed_plugins: Vec<InstalledPlugin>,
plugin_audit_events: Vec<PluginAuditEvent>,
active_space_id: SpaceId,
@@ -168,6 +171,7 @@ impl BrowserCore {
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(),
command_query: String::new(),
@@ -390,6 +394,7 @@ impl BrowserCore {
installed_plugins: self.installed_plugins.clone(),
plugin_audit_events: self.plugin_audit_events.clone(),
spaces: self.sorted_spaces(),
trashed_spaces: self.trashed_spaces.clone(),
profiles: self.profiles.clone(),
sync_status: self.sync_status(),
tabs: self.visible_tabs(),
+126 -1
View File
@@ -1,9 +1,108 @@
use ely_domain::SpaceId;
use std::time::{Duration, SystemTime};
use ely_domain::{ArchivedTab, BrowserTab, Space, SpaceId};
use super::BrowserCore;
use crate::CoreError;
pub const SPACE_TRASH_RETENTION_DAYS: u64 = 30;
#[derive(Clone, Debug)]
pub struct TrashedSpace {
space: Space,
tabs: Vec<BrowserTab>,
archived_tabs: Vec<ArchivedTab>,
trashed_at: SystemTime,
purge_at: SystemTime,
}
impl TrashedSpace {
fn new(
space: Space,
tabs: Vec<BrowserTab>,
archived_tabs: Vec<ArchivedTab>,
trashed_at: SystemTime,
) -> Self {
let purge_at = trashed_at + Duration::from_secs(SPACE_TRASH_RETENTION_DAYS * 86_400);
Self { space, tabs, archived_tabs, trashed_at, purge_at }
}
#[must_use]
pub fn space(&self) -> &Space {
&self.space
}
#[must_use]
pub fn tabs(&self) -> &[BrowserTab] {
&self.tabs
}
#[must_use]
pub fn archived_tabs(&self) -> &[ArchivedTab] {
&self.archived_tabs
}
#[must_use]
pub fn trashed_at(&self) -> SystemTime {
self.trashed_at
}
#[must_use]
pub fn purge_at(&self) -> SystemTime {
self.purge_at
}
}
impl BrowserCore {
pub fn trash_space(
&mut self,
space_id: &SpaceId,
trashed_at: SystemTime,
) -> Result<bool, CoreError> {
if self.spaces.len() <= 1 {
return Err(CoreError::LastSpaceCannotBeTrashed);
}
let Some(space_index) = self.spaces.iter().position(|space| space.id() == space_id) else {
return Err(CoreError::SpaceNotFound { id: space_id.clone() });
};
let trashed_space = self.spaces.remove(space_index);
let tabs = self.remove_space_tabs(space_id);
let archived_tabs = self.remove_space_archived_tabs(space_id);
self.active_tabs_by_space.remove(space_id);
self.active_tabs_by_space_profile
.retain(|(mapped_space_id, _), _| mapped_space_id != space_id);
self.trashed_spaces.push(TrashedSpace::new(trashed_space, tabs, archived_tabs, trashed_at));
if &self.active_space_id == space_id {
let next_space_id = self
.sorted_spaces()
.first()
.map(|space| space.id().clone())
.ok_or(CoreError::MissingActiveTab)?;
self.select_space(&next_space_id)?;
}
Ok(true)
}
pub fn restore_trashed_space(&mut self, space_id: &SpaceId) -> Result<bool, CoreError> {
let Some(trash_index) =
self.trashed_spaces.iter().position(|entry| entry.space().id() == space_id)
else {
return Err(CoreError::TrashedSpaceNotFound { id: space_id.clone() });
};
let trashed_space = self.trashed_spaces.remove(trash_index);
let restored_space_id = trashed_space.space().id().clone();
self.spaces.push(trashed_space.space);
self.tabs.extend(trashed_space.tabs);
self.archived_tabs.extend(trashed_space.archived_tabs);
self.select_space(&restored_space_id)?;
Ok(true)
}
pub fn move_space_up(&mut self, space_id: &SpaceId) -> Result<bool, CoreError> {
let mut ordered_ids = self.sorted_space_ids();
let Some(index) = ordered_ids.iter().position(|id| id == space_id) else {
@@ -52,4 +151,30 @@ impl BrowserCore {
Ok(())
}
fn remove_space_tabs(&mut self, space_id: &SpaceId) -> Vec<BrowserTab> {
let mut removed_tabs = Vec::new();
self.tabs.retain(|tab| {
if tab.space_id() == space_id {
removed_tabs.push(tab.clone());
false
} else {
true
}
});
removed_tabs
}
fn remove_space_archived_tabs(&mut self, space_id: &SpaceId) -> Vec<ArchivedTab> {
let mut removed_tabs = Vec::new();
self.archived_tabs.retain(|archived| {
if archived.tab().space_id() == space_id {
removed_tabs.push(archived.clone());
false
} else {
true
}
});
removed_tabs
}
}