noctua/src/domain/document/collection.rs
wfx fc73e4b76b Complete Clean Architecture migration
Phase 1-7: Full migration from src/app/ to Clean Architecture

BREAKING CHANGES:
- Removed src/app/ (old TEA-style implementation)
- Removed src/constant.rs (constants now local to modules)
- Removed deprecated canvas_to_image_coords functions

NEW STRUCTURE:
- src/ui/           - UI Layer (COSMIC interface)
- src/application/  - Application Layer (DocumentManager, Commands)
- src/domain/       - Domain Layer (Document types, Operations)
- src/infrastructure/ - Infrastructure Layer (Loaders, Cache, System)

FEATURES:
- DocumentManager as Single Source of Truth
- Command Pattern for all operations
- Model caching for render data (performance)
- Sync mechanism between DocumentManager and UI Model
- Wallpaper support (COSMIC, KDE, GNOME, feh)
- Thumbnail cache with disk persistence

IMPROVEMENTS:
- Warnings: 62 → 43 (-31%)
- Deprecated warnings: 2 → 0 (-100%)
- Code removed: src/app/ (~2000 lines), constant.rs, deprecated functions
- Better Locality of Reference (constants local to modules)
- Clean separation of concerns
- No circular dependencies

DOCUMENTATION:
- Updated AGENTS.md (100% migration status)
- Updated README.md (architecture section)
- Updated Workflow.md
- Added Migration-Plan.md with full completion summary

TESTS:
- All 41 tests passing
- Build successful (0 errors, 43 warnings)
- Release build verified

Migration Status:  100% Complete
2026-02-03 08:43:21 +01:00

275 lines
7.9 KiB
Rust

// SPDX-License-Identifier: GPL-3.0-or-later
// src/domain/document/collection.rs
//
// Document collection for managing multiple documents.
use std::path::PathBuf;
use crate::domain::document::core::content::DocumentContent;
/// A collection of documents with navigation support.
///
/// This abstraction is useful for:
/// - Browsing through folders of images
/// - Batch operations on multiple documents
/// - Comparison views (showing multiple documents side-by-side)
#[derive(Debug)]
pub struct DocumentCollection {
/// List of document paths in the collection.
paths: Vec<PathBuf>,
/// Currently active document index.
current_index: Option<usize>,
/// Currently loaded document (lazy-loaded).
current_document: Option<DocumentContent>,
}
impl DocumentCollection {
/// Create an empty collection.
#[must_use]
pub fn new() -> Self {
Self {
paths: Vec::new(),
current_index: None,
current_document: None,
}
}
/// Create a collection from a list of paths.
#[must_use]
pub fn from_paths(paths: Vec<PathBuf>) -> Self {
let current_index = if paths.is_empty() { None } else { Some(0) };
Self {
paths,
current_index,
current_document: None,
}
}
/// Get the number of documents in the collection.
#[must_use]
pub fn len(&self) -> usize {
self.paths.len()
}
/// Check if the collection is empty.
#[must_use]
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
/// Get the current document index (0-based).
#[must_use]
pub fn current_index(&self) -> Option<usize> {
self.current_index
}
/// Get the current document path.
#[must_use]
pub fn current_path(&self) -> Option<&PathBuf> {
self.current_index.and_then(|idx| self.paths.get(idx))
}
/// Get all paths in the collection.
#[must_use]
pub fn paths(&self) -> &[PathBuf] {
&self.paths
}
/// Get a reference to the currently loaded document.
#[must_use]
pub fn current_document(&self) -> Option<&DocumentContent> {
self.current_document.as_ref()
}
/// Get a mutable reference to the currently loaded document.
#[must_use]
pub fn current_document_mut(&mut self) -> Option<&mut DocumentContent> {
self.current_document.as_mut()
}
/// Set the currently loaded document.
pub fn set_current_document(&mut self, document: DocumentContent) {
self.current_document = Some(document);
}
/// Clear the currently loaded document.
pub fn clear_current_document(&mut self) {
self.current_document = None;
}
/// Navigate to the next document in the collection.
///
/// Returns the new index if successful, None if already at the end.
pub fn next(&mut self) -> Option<usize> {
if let Some(current) = self.current_index
&& current + 1 < self.paths.len() {
self.current_index = Some(current + 1);
self.current_document = None; // Clear document (needs reload)
return self.current_index;
}
None
}
/// Navigate to the previous document in the collection.
///
/// Returns the new index if successful, None if already at the start.
pub fn previous(&mut self) -> Option<usize> {
if let Some(current) = self.current_index
&& current > 0 {
self.current_index = Some(current - 1);
self.current_document = None; // Clear document (needs reload)
return self.current_index;
}
None
}
/// Navigate to a specific index.
///
/// Returns true if the index is valid and navigation succeeded.
pub fn goto(&mut self, index: usize) -> bool {
if index < self.paths.len() {
self.current_index = Some(index);
self.current_document = None; // Clear document (needs reload)
true
} else {
false
}
}
/// Add a document path to the collection.
pub fn add_path(&mut self, path: PathBuf) {
self.paths.push(path);
if self.current_index.is_none() {
self.current_index = Some(0);
}
}
/// Remove a document path at the given index.
///
/// Returns the removed path if successful.
pub fn remove_at(&mut self, index: usize) -> Option<PathBuf> {
if index < self.paths.len() {
let removed = self.paths.remove(index);
// Update current index if needed
if let Some(current) = self.current_index {
if current == index {
// Removed current document
self.current_document = None;
if self.paths.is_empty() {
self.current_index = None;
} else if current >= self.paths.len() {
self.current_index = Some(self.paths.len() - 1);
}
} else if current > index {
// Adjust index after removal
self.current_index = Some(current - 1);
}
}
Some(removed)
} else {
None
}
}
/// Clear the entire collection.
pub fn clear(&mut self) {
self.paths.clear();
self.current_index = None;
self.current_document = None;
}
/// Check if there is a next document available.
#[must_use]
pub fn has_next(&self) -> bool {
if let Some(current) = self.current_index {
current + 1 < self.paths.len()
} else {
false
}
}
/// Check if there is a previous document available.
#[must_use]
pub fn has_previous(&self) -> bool {
if let Some(current) = self.current_index {
current > 0
} else {
false
}
}
/// Get the path at a specific index.
#[must_use]
pub fn path_at(&self, index: usize) -> Option<&PathBuf> {
self.paths.get(index)
}
}
impl Default for DocumentCollection {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_collection() {
let collection = DocumentCollection::new();
assert!(collection.is_empty());
assert_eq!(collection.len(), 0);
assert_eq!(collection.current_index(), None);
}
#[test]
fn test_navigation() {
let paths = vec![
PathBuf::from("a.png"),
PathBuf::from("b.png"),
PathBuf::from("c.png"),
];
let mut collection = DocumentCollection::from_paths(paths);
assert_eq!(collection.current_index(), Some(0));
assert_eq!(collection.next(), Some(1));
assert_eq!(collection.next(), Some(2));
assert_eq!(collection.next(), None); // At end
assert_eq!(collection.previous(), Some(1));
assert_eq!(collection.previous(), Some(0));
assert_eq!(collection.previous(), None); // At start
}
#[test]
fn test_goto() {
let paths = vec![
PathBuf::from("a.png"),
PathBuf::from("b.png"),
PathBuf::from("c.png"),
];
let mut collection = DocumentCollection::from_paths(paths);
assert!(collection.goto(2));
assert_eq!(collection.current_index(), Some(2));
assert!(!collection.goto(10)); // Invalid index
}
#[test]
fn test_remove() {
let paths = vec![
PathBuf::from("a.png"),
PathBuf::from("b.png"),
PathBuf::from("c.png"),
];
let mut collection = DocumentCollection::from_paths(paths);
collection.goto(1);
assert_eq!(collection.remove_at(1), Some(PathBuf::from("b.png")));
assert_eq!(collection.len(), 2);
assert_eq!(collection.current_index(), Some(1)); // Now points to c.png
}
}