// SPDX-License-Identifier: GPL-3.0-or-later // src/document/manager.rs // // Document Manager – single entry point via DocumentCommand. use crate::document::loader::{load_document, scan_directory_into}; use crate::document::session::{SessionData, SessionKind}; use crate::document::store; use crate::document::DocumentContent; use crate::error::Error; use log::warn; use std::path::{Path, PathBuf}; #[allow(dead_code)] #[derive(Debug)] pub enum DocumentState { Empty, Loading, Loaded(DocumentContent), Error(Error), } /// All actions the UI can request from the manager. /// The manager executes; the UI decides. #[derive(Debug)] pub enum DocumentCommand { /// Create a new session. Does nothing if a session with that name already exists. CreateSession { name: String, kind: SessionKind, path: Option, }, /// Scan a directory into an existing session, replacing its current items. ScanDirectory { session: String, dir: PathBuf }, /// Load a file and make it the active item in the given session. AddFileToSession { session: String, path: PathBuf }, /// Set the active session by name. SetActive(String), /// Open a session file (.ron): loads all sessions and activates the DirectoryBrowser. OpenSession(PathBuf), /// Navigate to the next item in the active session. NavigateNext, /// Navigate to the previous item in the active session. NavigatePrevious, /// Select a specific item by index in the active session. SelectItem(usize), /// Copy an item from one session to another. CopyItemToSession { source_session: String, target_session: String, source_item_index: usize, target_item_index: Option, }, /// Delete an item from a session. DeleteItemFromSession { session: String, item_index: Option, }, } pub struct DocumentManager { sessions: Vec, pub active_session_name: Option, pub state: DocumentState, } impl Default for DocumentManager { fn default() -> Self { Self { sessions: Vec::new(), active_session_name: None, state: DocumentState::Empty, } } } impl DocumentManager { pub fn new() -> Self { Self::default() } /// The single public entry point for all state changes. pub fn handle(&mut self, command: DocumentCommand) { match command { DocumentCommand::CreateSession { name, kind, path } => { if self.session(&name).is_none() { self.sessions .push(SessionData::new(&name, kind, path, Vec::new())); } } DocumentCommand::ScanDirectory { session, dir } => { if let Some(s) = self.session_mut(&session) { scan_directory_into(s, &dir); } } DocumentCommand::AddFileToSession { session, path } => { self.active_session_name = Some(session); self.load_file(&path); } DocumentCommand::SetActive(name) => { if self.session(&name).is_some() { self.active_session_name = Some(name); } } DocumentCommand::OpenSession(path) => { self.load_all_sessions(&path); } DocumentCommand::NavigateNext => self.next(), DocumentCommand::NavigatePrevious => self.previous(), DocumentCommand::SelectItem(index) => self.select_item(index), DocumentCommand::CopyItemToSession { source_session, target_session, source_item_index, target_item_index, } => { self.copy_item_to_session( &source_session, &target_session, source_item_index, target_item_index, ); } DocumentCommand::DeleteItemFromSession { session, item_index, } => { self.delete_item_from_session(&session, item_index); } } } /// Read-only access to a session by name. pub fn session(&self, name: &str) -> Option<&SessionData> { self.sessions.iter().find(|s| s.name == name) } // Mutable access to a session by name. fn session_mut(&mut self, name: &str) -> Option<&mut SessionData> { self.sessions.iter_mut().find(|s| s.name == name) } // Mutable access to the currently active session. // The name is cloned to satisfy the borrow checker – it is a cheap String copy. fn active_session_mut(&mut self) -> Option<&mut SessionData> { let name = self.active_session_name.clone()?; self.sessions.iter_mut().find(|s| s.name == name) } // Loads all sessions from a RON file and activates the DirectoryBrowser. fn load_all_sessions(&mut self, path: &Path) { match store::load_sessions(path) { Ok(mut loaded) => { // Re-scan every DirectoryBrowser's source directory on load. // Validate every DocumentCollection: remove items whose files no longer exist. for session in loaded.iter_mut() { match session.kind { SessionKind::DirectoryBrowser => { if let Some(dir) = session.path.clone() { scan_directory_into(session, &dir); } } SessionKind::DocumentCollection => { session.items.retain(|item| { if item.path.exists() { true } else { warn!( "Session '{}': {}", session.name, Error::NotFound(item.path.display().to_string()) ); false } }); // Reset index if it pointed to a now-removed item. session.current_index = match session.current_index { Some(i) if i < session.items.len() => Some(i), _ if !session.items.is_empty() => Some(0), _ => None, }; } } } // Activate the DirectoryBrowser as the default entry point. let browser_name = loaded .iter() .find(|s| s.kind == SessionKind::DirectoryBrowser) .map(|s| s.name.clone()); self.sessions = loaded; self.active_session_name = browser_name; self.state = DocumentState::Empty; } Err(e) => { self.state = DocumentState::Error(e); } } } fn select_item(&mut self, index: usize) { if let Some(session) = self.active_session_mut() { if index < session.items.len() { session.current_index = Some(index); } } } fn next(&mut self) { if let Some(session) = self.active_session_mut() { if let Some(idx) = session.current_index { if idx + 1 < session.items.len() { session.current_index = Some(idx + 1); } } } } fn previous(&mut self) { if let Some(session) = self.active_session_mut() { if let Some(idx) = session.current_index { if idx > 0 { session.current_index = Some(idx - 1); } } } } fn load_file(&mut self, path: &Path) { self.state = DocumentState::Loading; match load_document(path) { Ok(content) => { // Align current_index to the loaded file's position in the session. if let Some(session) = self.active_session_mut() { if let Some(idx) = session.items.iter().position(|item| item.path == path) { session.current_index = Some(idx); } } self.state = DocumentState::Loaded(content); } Err(e) => { self.state = DocumentState::Error(e); } } } fn copy_item_to_session( &mut self, source_session: &str, target_session: &str, source_item_index: usize, target_item_index: Option, ) { let source = match self.session(source_session) { Some(s) => s, None => { self.state = DocumentState::Error(Error::Session(format!( "Session '{}' not found", source_session ))); return; } }; let target = match self.session(target_session) { Some(s) => s, None => { self.state = DocumentState::Error(Error::Session(format!( "Session '{}' not found", target_session ))); return; } }; if target.kind != SessionKind::DocumentCollection { self.state = DocumentState::Error(Error::Session( "Target must be a DocumentCollection".to_string(), )); return; } let item = match source.items.get(source_item_index) { Some(item) => item.clone(), None => { return; } }; if target .items .iter() .any(|i| i.path == item.path && i.page_index == item.page_index) { self.state = DocumentState::Error(Error::Session("Item already exists".to_string())); return; } let insert_index = match target_item_index { Some(idx) if idx <= target.items.len() => idx, _ => target.items.len(), }; if let Some(target) = self.session_mut(target_session) { target.items.insert(insert_index, item); } } fn delete_item_from_session(&mut self, session: &str, item_index: Option) { let target = match self.session(session) { Some(s) => s, None => { self.state = DocumentState::Error(Error::Session(format!( "Session '{}' not found", session ))); return; } }; if target.kind != SessionKind::DocumentCollection { self.state = DocumentState::Error(Error::Session( "Session must be a DocumentCollection".to_string(), )); return; } let idx = match item_index { Some(i) => i, None => match target.current_index { Some(i) => i, None => return, }, }; if let Some(target) = self.session_mut(session) { if idx < target.items.len() { target.items.remove(idx); let new_len = target.items.len(); if new_len == 0 { target.current_index = None; } else if let Some(current) = target.current_index { if current >= new_len { target.current_index = Some(new_len - 1); } } } } } }