noctua/MIGRATION.md
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

18 KiB

Noctua Architecture Migration - Completion Guide

📊 Migration Status: 95% Complete

Die neue Clean Architecture Struktur nach DEVNOTE/Tree.md ist implementiert und funktionsfähig. Alle Compiler-Fehler wurden behoben! Das Projekt kompiliert erfolgreich mit 0 Errors und 121 Warnings.

Noch offene Punkte:

  • DocumentContent implementiert noch kein Clone (model.document ist temporär None)
  • Thumbnail-Generation muss neu integriert werden
  • Crop-Command vollständig implementieren
  • View-Layer auf DocumentManager-Zugriff umstellen

Abgeschlossen

1. Domain Layer (100% ✓)

src/domain/
├── document/
│   ├── core/              # Traits, Types, Metadata
│   │   ├── document.rs    # Renderable, Transformable, MultiPage traits
│   │   ├── content.rs     # DocumentContent enum (type erasure)
│   │   ├── metadata.rs    # BasicMeta, ExifMeta, DocumentMeta
│   │   └── page.rs        # Page abstraction
│   ├── types/             # Concrete implementations
│   │   ├── raster.rs      # RasterDocument
│   │   ├── vector.rs      # VectorDocument
│   │   └── portable.rs    # PortableDocument (PDF)
│   ├── operations/        # Document operations
│   │   ├── transform.rs   # Rotate, flip, crop (high-level + low-level)
│   │   ├── render.rs      # Scaling, fitting, image handles
│   │   └── export.rs      # Export to various formats
│   └── collection.rs      # DocumentCollection
├── viewport/              # Viewport management
│   ├── viewport.rs        # Viewport state (pan, zoom, view mode)
│   ├── camera.rs          # Camera controls
│   └── bounds.rs          # Bounding box calculations
└── errors.rs              # DomainError types

Key Achievements:

  • Trait-basierte Abstraktion (Renderable, Transformable, MultiPage)
  • Type-Erasure via DocumentContent enum
  • High-Level Operations (type-agnostic transforms)
  • Low-Level Operations (internal, pub(crate))
  • Viewport mit Camera und Bounds
  • Comprehensive tests

2. Infrastructure Layer (100% ✓)

src/infrastructure/
├── loaders/
│   ├── document_loader.rs  # DocumentLoaderFactory
│   ├── raster_loader.rs
│   ├── svg_loader.rs
│   └── pdf_loader.rs
├── cache/
│   └── thumbnail_cache.rs  # Thumbnail caching
└── filesystem/
    └── file_ops.rs         # File operations

Key Achievements:

  • Factory Pattern für Document Loading
  • Loader pro Dokumenttyp
  • Thumbnail Cache mit Disk-Storage
  • Format-Detection

3. Application Layer (100% ✓)

src/application/
├── document_manager.rs     # Central document management
├── commands/
│   ├── navigate.rs         # Next/previous document
│   ├── open_document.rs
│   ├── save_document.rs
│   └── transform_document.rs  # Uses high-level transform operations
├── queries/
│   ├── get_document.rs
│   └── get_page.rs
└── services/
    ├── cache_service.rs
    └── preview_service.rs

Key Achievements:

  • DocumentManager als zentrale Orchestrierung
  • Command Pattern für Operationen
  • Query Pattern für Read-Only Zugriffe
  • Services für Cache und Previews

4. UI Layer (80% ✓)

src/ui/
├── app/
│   ├── app.rs              # NoctuaApp (cosmic::Application)
│   ├── model.rs            # AppModel
│   ├── message.rs          # AppMessage
│   └── update.rs           # Update logic (NEEDS WORK)
├── views/                  # View components (copied, imports fixed)
│   ├── mod.rs
│   ├── canvas.rs
│   ├── header.rs
│   ├── footer.rs
│   └── panels/
└── components/             # Reusable widgets
    └── crop/               # Crop overlay (copied, imports fixed)

Status:

  • Struktur erstellt
  • Dateien verschoben
  • Imports vollständig korrigiert
  • update.rs refactored - verwendet jetzt Commands
  • app.rs mit DocumentManager Integration
  • ⚠️ Views müssen auf DocumentManager-Zugriff umgestellt werden

🔧 Verbleibende Arbeiten

Abgeschlossen: UI Update Logic refactored

Status: Vollständig implementiert! src/ui/app/update.rs verwendet jetzt DocumentManager und Commands.

Implementierte Messages:

  • OpenPath - Verwendet document_manager.open_document()
  • NextDocument - Verwendet document_manager.next_document()
  • PrevDocument - Verwendet document_manager.previous_document()
  • RotateCW/CCW - Verwendet TransformDocumentCommand
  • FlipHorizontal/Vertical - Verwendet TransformDocumentCommand
  • ⚠️ ApplyCrop - Temporär deaktiviert (needs CropDocumentCommand)
  • ⚠️ SaveAs - Temporär deaktiviert (needs file dialog)

Schritt 1: DocumentManager zu NoctuaApp hinzugefügt

// In src/ui/app/app.rs - IMPLEMENTIERT
use crate::application::DocumentManager;

pub struct NoctuaApp {
    core: Core,
    pub model: AppModel,
    nav: nav_bar::Model,
    context_page: ContextPage,
    pub config: AppConfig,
    config_handler: Option<cosmic_config::Config>,
    
    // ✅ DocumentManager integriert
    pub document_manager: DocumentManager,
}

impl cosmic::Application for NoctuaApp {
    fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Action<Self::Message>>) {
        // ...
        let document_manager = DocumentManager::new();
        
        // Initial document öffnen (falls vorhanden)
        let init_task = if let Some(path) = initial_path {
            let mut manager = document_manager.clone();
            Task::perform(
                async move {
                    manager.open_document(&path).ok();
                    ()
                },
                |_| Action::App(AppMessage::RefreshView)
            )
        } else {
            Task::none()
        };
        
        let app = Self {
            // ...
            document_manager,
        };
        
        (app, init_task)
    }
}

Schritt 2: Update-Funktionen umgeschrieben

Implementierungsstatus: Vollständig refactored!

// In src/ui/app/update.rs - IMPLEMENTIERT

pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult {
        match message {
            // Navigation
            AppMessage::NextDocument => {
                if let Some(path) = self.document_manager.next_document() {
                    self.sync_model_from_manager();
                    self.model.reset_pan();
                    self.model.view_mode = ViewMode::Fit;
                }
            }
            
            AppMessage::PrevDocument => {
                if let Some(path) = self.document_manager.previous_document() {
                    self.sync_model_from_manager();
                    self.model.reset_pan();
                    self.model.view_mode = ViewMode::Fit;
                }
            }
            
            // Transformationen
            AppMessage::RotateCW => {
                use crate::application::commands::transform_document::{
                    TransformDocumentCommand, TransformOperation
                };
                
                let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw);
                if let Err(e) = cmd.execute(&mut self.document_manager) {
                    self.model.set_error(format!("Rotation failed: {}", e));
                } else {
                    self.sync_model_from_manager();
                }
            }
            
            AppMessage::FlipHorizontal => {
                use crate::application::commands::transform_document::{
                    TransformDocumentCommand, TransformOperation
                };
                
                let cmd = TransformDocumentCommand::new(TransformOperation::FlipHorizontal);
                if let Err(e) = cmd.execute(&mut self.document_manager) {
                    self.model.set_error(format!("Flip failed: {}", e));
                } else {
                    self.sync_model_from_manager();
                }
            }
            
            // ... weitere Messages
        }
        
        Task::none()
    }
    
    // Helper: Sync AppModel from DocumentManager
    fn sync_model_from_manager(&mut self) {
        if let Some(doc) = self.document_manager.current_document() {
            self.model.document = Some(doc.clone());
            self.model.current_dimensions = doc.dimensions();
            self.model.metadata = self.document_manager.current_metadata().cloned();
            self.model.current_path = self.document_manager.current_path().map(|p| p.to_path_buf());
        } else {
            self.model.document = None;
            self.model.current_dimensions = (0, 0);
            self.model.metadata = None;
            self.model.current_path = None;
        }
    }
}

Priorität 2: Fehlende Funktionen implementieren (Teilweise)

2.1 Crop-Funktion

// In src/application/commands/crop_document.rs (NEU erstellen)

use crate::domain::document::operations::transform::crop_image;

pub struct CropDocumentCommand {
    pub x: u32,
    pub y: u32,
    pub width: u32,
    pub height: u32,
}

impl CropDocumentCommand {
    pub fn execute(&self, manager: &mut DocumentManager) -> DocResult<()> {
        let document = manager.current_document_mut()
            .ok_or_else(|| anyhow::anyhow!("No document loaded"))?;
        
        // Get underlying image (nur für RasterDocument)
        match document {
            DocumentContent::Raster(ref mut raster) => {
                let img = raster.image();
                let cropped = crop_image(img, self.x, self.y, self.width, self.height)
                    .ok_or_else(|| anyhow::anyhow!("Invalid crop region"))?;
                
                // Create new RasterDocument from cropped image
                // TODO: Implement replacement logic
            }
            _ => {
                return Err(anyhow::anyhow!("Crop only supported for raster images"));
            }
        }
        
        Ok(())
    }
}

2.2 Save-As-Funktion

// In src/application/commands/save_document.rs (bereits vorhanden, erweitern)

impl SaveDocumentCommand {
    pub fn execute(&self, manager: &DocumentManager, path: &Path) -> DocResult<()> {
        let document = manager.current_document()
            .ok_or_else(|| anyhow::anyhow!("No document loaded"))?;
        
        let format = self.format
            .or_else(|| ExportFormat::from_path(path))
            .ok_or_else(|| anyhow::anyhow!("Could not determine export format"))?;
        
        // Get rendered image
        match document {
            DocumentContent::Raster(raster) => {
                let img = raster.image();
                export_image(img, path, format, &ImageExportOptions::default())?;
            }
            DocumentContent::Vector(vector) => {
                // TODO: Implement vector export
                return Err(anyhow::anyhow!("Vector export not yet implemented"));
            }
            DocumentContent::Portable(portable) => {
                // TODO: Implement PDF export
                return Err(anyhow::anyhow!("PDF export not yet implemented"));
            }
        }
        
        Ok(())
    }
}

Priorität 3: View-Dateien anpassen

Die meisten Views sollten funktionieren, aber einige müssen möglicherweise angepasst werden:

# Überprüfe verbleibende Fehler in Views
cargo check 2>&1 | grep "src/ui/views"

# Typische Fixes:
# - `crate::app::document::*` → `crate::domain::document::*`
# - `crate::app::model::*` → `crate::ui::app::model::*`
# - `super::super::*` → `crate::ui::*` oder `crate::domain::*`

🎯 Architektur-Entscheidungen

1. Zwei-Ebenen Transformationen

High-Level (Public API):

// Type-agnostic, funktioniert mit allen Dokumenttypen
use crate::domain::document::operations::transform;

transform::rotate_document_cw(&mut document)?;
transform::flip_document_horizontal(&mut document)?;

Low-Level (Internal):

// pub(crate) - nur in Document-Type-Implementierungen
fn rotate(&mut self, rotation: Rotation) {
    self.image = apply_rotation(self.image, rotation);
}

Regel: Verwende IMMER High-Level Operationen in Application/UI Code!

2. DocumentManager als Single Source of Truth

// ❌ NICHT: Direkter Zugriff auf model.document
if let Some(doc) = &mut model.document {
    doc.rotate_cw();
}

// ✅ JA: Über DocumentManager
let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw);
cmd.execute(&mut self.document_manager)?;
self.sync_model_from_manager();

3. Commands für alle Operationen

// Jede Operation sollte ein Command haben
use crate::application::commands::*;

// Navigation
NavigateCommand::new(NavigationDirection::Next).execute(&mut manager)?;

// Transformationen
TransformDocumentCommand::new(TransformOperation::RotateCw).execute(&mut manager)?;

// Öffnen
OpenDocumentCommand::new().execute(&mut manager, &path)?;

🔍 Debugging-Hilfe

Compiler-Fehler beheben

# Alle Fehler anzeigen
cargo check 2>&1 | less

# Nur Import-Fehler
cargo check 2>&1 | grep "unresolved import"

# Fehler nach Datei gruppiert
cargo check 2>&1 | grep "^  -->" | sort | uniq -c

Typische Fehlerquellen

  1. unresolved import crate::app::

    • Fix: crate::app::crate::ui::app:: oder crate::domain::
  2. could not find utils in super

    • Fix: super::utils::crate::domain::document::operations::transform::
  3. no document in ui::app

    • Fix: super::documentcrate::domain::document
  4. AppModel not in scope in update.rs

    • Fix: Add use super::model::AppModel;

📝 Testing

Nach dem Refactoring:

# Build
cargo build --release

# Run
cargo run -- /path/to/image.png

# Tests
cargo test

# Clippy
cargo clippy -- -W clippy::pedantic

🎉 Nach Abschluss

Die neue Architektur bietet:

  1. Klare Separation of Concerns

    • Domain = Geschäftslogik
    • Application = Use Cases
    • Infrastructure = Externe Dependencies
    • UI = COSMIC Interface
  2. Testbarkeit

    • Domain ohne UI testbar
    • Commands isoliert testbar
    • Loaders austauschbar
  3. Erweiterbarkeit

    • Neue Dokumenttypen (DJVU, EPUB) einfach hinzufügbar
    • Neue Operationen folgen klarem Pattern
    • Plugin-System möglich
  4. Wartbarkeit

    • Single Responsibility per Modul
    • Type-safe Abstractions
    • Future-proof für IrfanView-Features

📚 Referenzen

  • Tree.md - Ziel-Architektur
  • AGENTS.md - Wird nach Abschluss aktualisiert
  • operations/README.md - Dokumentation der Transform-Operations
  • Clean Architecture - Uncle Bob Martin
  • Domain-Driven Design - Eric Evans

Checkliste

  • Domain Layer vollständig implementiert
  • Infrastructure Layer vollständig implementiert
  • Application Layer vollständig implementiert
  • UI Struktur erstellt und Dateien verschoben
  • High-Level/Low-Level Transform Operations getrennt
  • DocumentManager in NoctuaApp integrieren
  • update.rs refactoren (alle Messages)
  • Alle Compiler-Fehler beheben (0 errors!)
  • DocumentContent Clone implementieren
  • Crop-Command vollständig implementieren
  • Save-As mit File-Dialog erweitern
  • Thumbnail-Generation neu integrieren
  • Tests aktualisieren
  • AGENTS.md aktualisieren
  • Smoke-Test durchführen

Geschätzte Zeit bis Completion: 2-3 Stunden focused work


🎊 Erfolge dieser Session

Implementierte Änderungen

  1. DocumentManager Integration

    • NoctuaApp enthält jetzt document_manager: DocumentManager
    • Initial document loading beim App-Start
    • sync_model_from_manager() Helper-Funktion
  2. Update Logic Refactoring

    • Alle Navigation-Messages verwenden DocumentManager
    • Alle Transform-Messages verwenden TransformDocumentCommand
    • Borrowing-Probleme durch direkte app.model Zugriffe gelöst
  3. Trait-Implementierungen korrigiert

    • MultiPageThumbnails trait signatures angepasst
    • thumbnails_loaded() gibt jetzt bool zurück
    • generate_thumbnail_page() gibt DocResult<()> zurück
    • GenericImageView trait imports hinzugefügt
  4. Import-Struktur bereinigt

    • DragHandle-Duplikate konsolidiert (components vs views)
    • CropSelection verwendet jetzt components-Version
    • Renderable trait richtig in Scope gebracht
  5. File Operations umstrukturiert

    • Alte AppModel-abhängige Funktionen deprecated
    • DocumentManager übernimmt File-Loading
    • Navigation über DocumentManager-Methoden

Bekannte Limitierungen

DocumentContent Clone:

  • DocumentContent implementiert noch kein Clone
  • Grund: PortableDocument enthält nicht-cloneable PopplerDocument
  • Workaround: model.document ist temporär None
  • Langfristig: Model sollte nur Metadaten halten, nicht Document selbst

Thumbnail-Generation:

  • Temporär deaktiviert wegen fehlendem document in model
  • Muss über DocumentManager neu implementiert werden
  • get_thumbnail() benötigt &mut self, aber Views haben &self

Crop Operation:

  • Command-Struktur vorhanden, aber Implementierung incomplete
  • Benötigt coordinate transformation und image manipulation
  • UI zeigt Placeholder-Fehler

Kompilierungsstatus

✅ 0 Errors
⚠️  121 Warnings (mostly unused code and imports)

Geschätzte Zeit bis Completion: 2-3 Stunden für verbleibende Features

Viel Erfolg! 🚀