# Noctua – Complete Migration Plan **Ziel:** Vollständige Trennung von TEA (UI) und Business Logic nach Clean Architecture **Status:** `src/app/` ist Altlast, `src/ui/` + `src/application/` + `src/domain/` sind das Ziel --- ## File Mapping: src/app/document/ → Ziel-Layer ### ✅ Domain Layer: src/domain/document/ | Quelle | Ziel | Aktion | |--------|------|--------| | `raster.rs` | `domain/document/types/raster.rs` | Features ergänzen (crop, dimensions, extract_meta) | | `vector.rs` | `domain/document/types/vector.rs` | Features ergänzen falls nötig | | `portable.rs` | `domain/document/types/portable.rs` | Features ergänzen (thumbnails) | | `mod.rs` (Traits) | `domain/document/core/document.rs` | Vergleichen & konsolidieren | | `mod.rs` (DocumentContent) | `domain/document/core/content.rs` | Methoden ergänzen (handle, dimensions, crop) | | `meta.rs` | `domain/document/core/metadata.rs` | Merge mit existierender Datei | ### ✅ Infrastructure Layer: src/infrastructure/ | Quelle | Ziel | Aktion | |--------|------|--------| | `file.rs::open_document()` | `loaders/document_loader.rs` | **Bereits vorhanden!** (DocumentLoaderFactory::load) | | `file.rs::collect_supported_files()` | `filesystem/file_ops.rs` | **Bereits vorhanden!** | | `file.rs::file_size()` | `filesystem/file_ops.rs` | **Bereits vorhanden!** | | `file.rs::read_file_bytes()` | `filesystem/file_ops.rs` | **Bereits vorhanden!** | | `cache.rs` | `cache/thumbnail_cache.rs` | **Neu erstellen** | | `utils.rs::set_as_wallpaper()` | `system/wallpaper.rs` | **Neu erstellen** | ### ✅ Application Layer: src/application/ | Quelle | Ziel | Aktion | |--------|------|--------| | `file.rs::navigate_next()` | `document_manager.rs` | **Bereits vorhanden!** (next_document) | | `file.rs::navigate_prev()` | `document_manager.rs` | **Bereits vorhanden!** (previous_document) | | `file.rs::open_initial_path()` | `document_manager.rs` | In open_document() integrieren | | `file.rs::save_crop_as()` | `commands/crop_document.rs` | In Command integrieren | ### ❌ Wird gelöscht (keine Migration nötig) - `file.rs::load_document_into_model()` → War UI-spezifisch, wird durch sync_model_from_manager() ersetzt - `file.rs::refresh_folder_entries()` → Intern in DocumentManager --- ## Phase 1: Domain Layer Konsolidierung ### Schritt 1.1: Feature-Vergleich (90 Min) **Für jeden Dokumenttyp:** ```bash # RasterDocument diff src/app/document/raster.rs src/domain/document/types/raster.rs > /tmp/raster-diff.txt # VectorDocument diff src/app/document/vector.rs src/domain/document/types/vector.rs > /tmp/vector-diff.txt # PortableDocument diff src/app/document/portable.rs src/domain/document/types/portable.rs > /tmp/portable-diff.txt ``` **Checkliste erstellen:** | Feature | RasterDocument | VectorDocument | PortableDocument | |---------|----------------|----------------|------------------| | `open()` | ✅ Beide | ✅ Beide | ✅ Beide | | `render()` | ✅ Beide | ✅ Beide | ✅ Beide | | `rotate()` | ✅ Beide | ✅ Beide | ✅ Beide | | `flip()` | ✅ Beide | ✅ Beide | ✅ Beide | | `dimensions()` | ❌ Nur app | ❓ Prüfen | ❓ Prüfen | | `crop()` | ❌ Nur app | N/A | N/A | | `crop_to_image()` | ❌ Nur app | N/A | N/A | | `extract_meta()` | ❌ Nur app | ❓ Prüfen | ❓ Prüfen | | `handle` (public) | ❌ Nur app | ❓ Prüfen | ❓ Prüfen | | Thumbnails | N/A | N/A | ❓ Prüfen | ### Schritt 1.2: RasterDocument Features portieren (60 Min) **Datei:** `src/domain/document/types/raster.rs` ```rust impl RasterDocument { /// Get current dimensions after transformations. pub fn dimensions(&self) -> (u32, u32) { let (w, h) = self.document.dimensions(); match self.transform.rotation { Rotation::Cw90 | Rotation::Cw270 => (h, w), _ => (w, h), } } /// Crop the document to the specified rectangle (in-place). pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> DocResult<()> { self.document = self.document.crop_imm(x, y, width, height); self.refresh_handle(); Ok(()) } /// Crop to a new DynamicImage (non-destructive). pub fn crop_to_image(&self, x: u32, y: u32, width: u32, height: u32) -> DocResult { let cropped = self.document.crop_imm(x, y, width, height); Ok(cropped) } /// Make handle field public or add getter pub fn handle(&self) -> ImageHandle { self.handle.clone() } } ``` ### Schritt 1.3: VectorDocument Features portieren (30 Min) **Datei:** `src/domain/document/types/vector.rs` ```rust impl VectorDocument { pub fn dimensions(&self) -> (u32, u32) { // Implementation based on transform state } pub fn handle(&self) -> ImageHandle { self.handle.clone() } } ``` ### Schritt 1.4: PortableDocument Features portieren (30 Min) **Datei:** `src/domain/document/types/portable.rs` ```rust impl PortableDocument { pub fn dimensions(&self) -> (u32, u32) { // Implementation based on current page } pub fn handle(&self) -> ImageHandle { self.handle.clone() } } ``` ### Schritt 1.5: DocumentContent Methoden ergänzen (45 Min) **Datei:** `src/domain/document/core/content.rs` ```rust impl DocumentContent { /// Get current image handle for rendering. pub fn handle(&self) -> ImageHandle { match self { Self::Raster(doc) => doc.handle(), Self::Vector(doc) => doc.handle(), Self::Portable(doc) => doc.handle(), } } /// Get current dimensions. pub fn dimensions(&self) -> (u32, u32) { match self { Self::Raster(doc) => doc.dimensions(), Self::Vector(doc) => doc.dimensions(), Self::Portable(doc) => doc.dimensions(), } } /// Crop the document (only supported for raster). pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> DocResult<()> { match self { Self::Raster(doc) => doc.crop(x, y, width, height), Self::Vector(_) => Err(anyhow::anyhow!("Crop not supported for vector documents")), Self::Portable(_) => Err(anyhow::anyhow!("Crop not supported for PDF documents")), } } } ``` ### Schritt 1.6: Metadata konsolidieren (30 Min) **Vergleichen:** - `src/app/document/meta.rs` - `src/domain/document/core/metadata.rs` **Aktion:** Fehlende Methoden aus app/meta.rs nach domain/core/metadata.rs portieren. ### Schritt 1.7: Traits & Enums konsolidieren (30 Min) **Vergleichen:** - `src/app/document/mod.rs` (Traits, Enums) - `src/domain/document/core/document.rs` (Traits, Enums) **Prüfen ob identisch:** Rotation, FlipDirection, TransformState, Renderable, Transformable, MultiPage **Falls Unterschiede:** Domain-Version als Master verwenden. --- ## Phase 2: Infrastructure Layer Migration ### Schritt 2.1: Thumbnail Cache erstellen (45 Min) **Neue Datei:** `src/infrastructure/cache/thumbnail_cache.rs` ```rust // SPDX-License-Identifier: GPL-3.0-or-later // src/infrastructure/cache/thumbnail_cache.rs // // Disk cache for document thumbnails. use std::fs; use std::path::{Path, PathBuf}; use image::DynamicImage; use sha2::{Digest, Sha256}; use cosmic::widget::image::Handle as ImageHandle; use crate::constant::{CACHE_DIR, THUMBNAIL_EXT}; pub struct ThumbnailCache; impl ThumbnailCache { pub fn load(file_path: &Path, page: usize) -> Option { // Copy from app/document/cache.rs } pub fn save(file_path: &Path, page: usize, image: &DynamicImage) { // Copy from app/document/cache.rs } pub fn clear_cache() { // Copy from app/document/cache.rs } } ``` **Neue Datei:** `src/infrastructure/cache/mod.rs` ```rust pub mod thumbnail_cache; pub use thumbnail_cache::ThumbnailCache; ``` **Neue Datei:** `src/infrastructure/mod.rs` (falls nicht vorhanden) ```rust pub mod cache; pub mod filesystem; pub mod loaders; ``` ### Schritt 2.2: Wallpaper System erstellen (30 Min) **Neue Datei:** `src/infrastructure/system/wallpaper.rs` ```rust // SPDX-License-Identifier: GPL-3.0-or-later // src/infrastructure/system/wallpaper.rs // // Set desktop wallpaper across different desktop environments. use std::path::Path; pub fn set_as_wallpaper(path: &Path) { // Copy entire implementation from app/document/utils.rs } fn try_cosmic_wallpaper(path_str: &str) -> bool { ... } fn try_wallpaper_crate(path_str: &str) -> bool { ... } fn try_gsettings_wallpaper(path_str: &str) -> bool { ... } fn try_feh_wallpaper(path_str: &str) -> bool { ... } ``` **Neue Datei:** `src/infrastructure/system/mod.rs` ```rust pub mod wallpaper; pub use wallpaper::set_as_wallpaper; ``` **Update:** `src/infrastructure/mod.rs` ```rust pub mod cache; pub mod filesystem; pub mod loaders; pub mod system; ``` --- ## Phase 3: Application Layer Integration ### Schritt 3.1: Sync-Funktion implementieren (45 Min) **Neue Datei:** `src/ui/sync.rs` ```rust // SPDX-License-Identifier: GPL-3.0-or-later // src/ui/sync.rs // // Synchronize UI model from DocumentManager state. use crate::application::DocumentManager; use crate::ui::model::AppModel; /// Synchronize AppModel from DocumentManager. /// /// Updates UI state with current document info, but does NOT copy /// the entire document (would break Clean Architecture). pub fn sync_model_from_manager(model: &mut AppModel, manager: &DocumentManager) { // Update cached render data if let Some(doc) = manager.current_document() { model.current_image_handle = Some(doc.handle()); model.current_dimensions = Some(doc.dimensions()); model.current_page = doc.current_page(); model.page_count = doc.page_count(); } else { model.current_image_handle = None; model.current_dimensions = None; model.current_page = None; model.page_count = None; } // Update navigation state model.current_path = manager.current_path().map(|p| p.to_path_buf()); model.folder_count = manager.folder_entries().len(); model.current_index = manager.current_index(); // Metadata model.metadata = manager.current_metadata().cloned(); } ``` ### Schritt 3.2: DocumentManager in NoctuaApp aktivieren (30 Min) **Datei:** `src/ui/app.rs` Status prüfen: - ✅ DocumentManager bereits als Feld vorhanden - ✅ DocumentManager wird in init() erstellt - ✅ Initial document wird geladen **Was fehlt:** - Model-Sync nach init - Model-Sync nach jedem Command **Änderungen:** ```rust impl cosmic::Application for NoctuaApp { fn init(mut core: Core, flags: Self::Flags) -> (Self, Task>) { // ... existing code ... let mut document_manager = DocumentManager::new(); if let Some(path) = initial_path { if let Err(e) = document_manager.open_document(&path) { log::error!("Failed to open initial path {}: {}", path.display(), e); } } // ✅ NEU: Sync model from manager let mut model = AppModel::new(config.clone()); sync::sync_model_from_manager(&mut model, &document_manager); // ... rest of init ... } } ``` --- ## Phase 4: UI Layer Migration ### Schritt 4.1: AppModel bereinigen (60 Min) **Datei:** `src/ui/model.rs` **Aktuell:** ```rust pub struct AppModel { pub document: Option, // ❌ Raus pub metadata: Option, pub current_path: Option, pub folder_entries: Vec, // ❌ Raus pub current_index: Option, pub view_mode: ViewMode, pub pan_x: f32, pub pan_y: f32, pub tool_mode: ToolMode, pub crop_selection: CropSelection, pub error: Option, pub tick: u64, } ``` **Neu:** ```rust pub struct AppModel { // ✅ Cached rendering data (read-only from DocumentManager) pub current_image_handle: Option, pub current_dimensions: Option<(u32, u32)>, pub current_page: Option, pub page_count: Option, // ✅ Cached metadata (read-only) pub metadata: Option, // ✅ Navigation info (read-only) pub current_path: Option, pub current_index: Option, pub folder_count: usize, // ✅ View state (UI controls these) pub view_mode: ViewMode, pub pan_x: f32, pub pan_y: f32, // ✅ Tool state (UI controls these) pub tool_mode: ToolMode, pub crop_selection: CropSelection, // ✅ UI state pub error: Option, pub tick: u64, } ``` ### Schritt 4.2: Update-Logik umschreiben (120 Min) **Datei:** `src/ui/update.rs` **Pattern für alle Messages:** ```rust // ❌ Alt AppMessage::RotateCW => { if let Some(doc) = &mut model.document { doc.rotate_cw(); } } // ✅ Neu AppMessage::RotateCW => { use crate::application::commands::TransformDocumentCommand; use crate::domain::document::operations::transform::TransformOperation; let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw); if let Err(e) = cmd.execute(&mut app.document_manager) { model.set_error(format!("Rotation failed: {e}")); return UpdateResult::None; } sync::sync_model_from_manager(&mut app.model, &app.document_manager); UpdateResult::None } ``` **Alle Messages umschreiben:** - OpenPath → DocumentManager::open_document() - NextDocument → DocumentManager::next_document() - PrevDocument → DocumentManager::previous_document() - RotateCW/CCW → TransformDocumentCommand - FlipHorizontal/Vertical → TransformDocumentCommand - ApplyCrop → CropDocumentCommand - GotoPage → DocumentManager.current_document_mut().go_to_page() - SetAsWallpaper → infrastructure::system::set_as_wallpaper() ### Schritt 4.3: Views anpassen (90 Min) **Pattern für alle Views:** ```rust // ❌ Alt (src/app/view/canvas.rs) if let Some(doc) = &model.document { let handle = doc.handle(); let (width, height) = doc.dimensions(); } // ✅ Neu (src/ui/views/canvas.rs) if let Some(handle) = &model.current_image_handle { let (width, height) = model.current_dimensions.unwrap_or((0, 0)); } ``` **Dateien prüfen und anpassen:** - canvas.rs - footer.rs - header.rs - panels.rs - pages_panel.rs **Konsolidieren:** - Falls `src/app/view/` und `src/ui/views/` beide existieren: neuere Version behalten - Imports anpassen: `use crate::ui::model::AppModel;` --- ## Phase 5: Main Entry Point ### Schritt 5.1: main.rs umstellen (15 Min) **Datei:** `src/main.rs` ```rust // ❌ Entfernen // mod app; // use crate::app::Noctua; // ✅ Hinzufügen mod ui; mod application; mod domain; mod infrastructure; mod config; mod constant; mod i18n; use crate::ui::NoctuaApp; fn main() -> Result<()> { // ... logging setup ... cosmic::app::run::( Settings::default(), ui::Flags::Args(args) ).map_err(|e| anyhow::anyhow!(e)) } ``` ### Schritt 5.2: Module-Exports prüfen (30 Min) **src/ui/mod.rs:** ```rust pub mod app; pub mod model; pub mod message; pub mod update; pub mod views; pub mod components; pub(crate) mod sync; // Internal: Sync from DocumentManager ``` **src/application/mod.rs:** ```rust pub mod commands; pub mod document_manager; pub mod queries; pub mod services; pub use document_manager::DocumentManager; ``` **src/domain/mod.rs:** ```rust pub mod document; pub mod errors; pub use document::{DocumentContent, DocumentMeta}; ``` **src/infrastructure/mod.rs:** ```rust pub mod cache; pub mod filesystem; pub mod loaders; pub mod system; pub use loaders::DocumentLoaderFactory; ``` --- ## Phase 6: Testing & Validation ### Schritt 6.1: Kompilierung (30 Min) ```bash # Check ohne build cargo check --all-features # Erwartete Errors beheben: # - Import paths # - Missing methods # - Type mismatches ``` ### Schritt 6.2: Build (15 Min) ```bash cargo build --all-features ``` ### Schritt 6.3: Funktionale Tests (60 Min) **Testplan:** - [ ] Bild öffnen (CLI argument) - [ ] Ordner öffnen - [ ] Navigation (Pfeiltasten: Links/Rechts) - [ ] Rotation (R / Shift+R) - [ ] Flip (H / V) - [ ] Zoom (+ / - / 1 / F) - [ ] Pan (Ctrl+Arrows, 0 zum Reset) - [ ] Crop-Mode (C, Enter zum Apply, Esc zum Cancel) - [ ] PDF öffnen (mehrseitig) - [ ] Seiten-Navigation (Pages Panel) - [ ] Thumbnail-Generation - [ ] Properties-Panel (I) - [ ] Wallpaper setzen (W) ### Schritt 6.4: Warnings beheben (45 Min) ```bash cargo clippy --all-features -- -W clippy::pedantic ``` **Fokus:** - Unused imports - Dead code - Visibility warnings --- ## Phase 7: Cleanup ### Schritt 7.1: src/app/ löschen (5 Min) ```bash # Backup erstellen cp -r src/app /tmp/app-backup # Löschen rm -rf src/app/ # Nochmal kompilieren cargo check --all-features ``` ### Schritt 7.2: Dokumentation aktualisieren (60 Min) **AGENTS.md:** - Projektstruktur korrigieren (src/app/ entfernen) - Workflow aktualisieren - Migration Status: 100% ✅ **DEVNOTE/Workflow.md:** - Korrekten Workflow dokumentieren - Diagramme aktualisieren **DEVNOTE/Tree.md:** - Finale Struktur ohne src/app/ **README.md:** - Architecture-Sektion hinzufügen - Build-Instructions prüfen --- ## Timeline ### Tag 1: Domain + Infrastructure (5-6h) | Zeit | Schritt | Dauer | |------|---------|-------| | 09:00-10:30 | 1.1-1.2: Feature-Vergleich & RasterDocument | 90 Min | | 10:30-11:00 | **Pause** | 30 Min | | 11:00-12:00 | 1.3-1.4: Vector/Portable Features | 60 Min | | 12:00-13:00 | **Mittagspause** | 60 Min | | 13:00-14:00 | 1.5-1.7: DocumentContent, Metadata, Traits | 60 Min | | 14:00-14:15 | **Pause** | 15 Min | | 14:15-15:45 | 2.1-2.2: Infrastructure Layer (Cache, Wallpaper) | 90 Min | ### Tag 2: Application + UI (7-8h) | Zeit | Schritt | Dauer | |------|---------|-------| | 09:00-10:15 | 3.1-3.2: Sync-Funktion & DocumentManager | 75 Min | | 10:15-10:30 | **Pause** | 15 Min | | 10:30-12:30 | 4.1-4.2: Model bereinigen & Update umschreiben | 120 Min | | 12:30-13:30 | **Mittagspause** | 60 Min | | 13:30-15:00 | 4.3: Views anpassen | 90 Min | | 15:00-15:15 | **Pause** | 15 Min | | 15:15-16:00 | 5.1-5.2: Main Entry & Module-Exports | 45 Min | ### Tag 3: Testing + Cleanup (4-5h) | Zeit | Schritt | Dauer | |------|---------|-------| | 09:00-10:15 | 6.1-6.2: Kompilierung & Build | 75 Min | | 10:15-10:30 | **Pause** | 15 Min | | 10:30-11:30 | 6.3: Funktionale Tests | 60 Min | | 11:30-12:15 | 6.4: Warnings beheben | 45 Min | | 12:15-13:15 | **Mittagspause** | 60 Min | | 13:15-13:20 | 7.1: src/app/ löschen | 5 Min | | 13:20-14:20 | 7.2: Dokumentation | 60 Min | --- ## Success Criteria ✅ **Migration erfolgreich wenn:** 1. [ ] `cargo build --release` kompiliert ohne Errors 2. [ ] Alle 13 funktionalen Tests bestehen 3. [ ] `src/app/` existiert nicht mehr 4. [ ] `AppModel` enthält keine `DocumentContent` 5. [ ] Alle Updates gehen über `DocumentManager` 6. [ ] Views nutzen nur `model.current_image_handle` 7. [ ] < 50 Warnings (down from 121) 8. [ ] AGENTS.md ist aktuell 9. [ ] Workflow.md ist korrekt 10. [ ] Code folgt Clean Architecture --- ## Rollback-Plan Falls kritische Probleme auftreten: ```bash # Option 1: Git Reset git reset --hard HEAD # Option 2: Backup wiederherstellen cp -r /tmp/app-backup src/app/ ``` --- ## Nächster Schritt **START:** ```bash # Branch erstellen git checkout -b migration/clean-architecture # Backup erstellen cp -r src/app /tmp/app-backup # Tag 1, Schritt 1.1 starten diff src/app/document/raster.rs src/domain/document/types/raster.rs ``` --- **Status:** Ready for Execution **Geschätzte Dauer:** 16-19 Stunden (3 Tage) **Letzte Änderung:** 2025-01-XX