noctua/DEVNOTE/Migration-Plan.md.backup
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

763 lines
No EOL
19 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<DynamicImage> {
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<ImageHandle> {
// 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<Action<Self::Message>>) {
// ... 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<DocumentContent>, // ❌ Raus
pub metadata: Option<DocumentMeta>,
pub current_path: Option<PathBuf>,
pub folder_entries: Vec<PathBuf>, // ❌ Raus
pub current_index: Option<usize>,
pub view_mode: ViewMode,
pub pan_x: f32,
pub pan_y: f32,
pub tool_mode: ToolMode,
pub crop_selection: CropSelection,
pub error: Option<String>,
pub tick: u64,
}
```
**Neu:**
```rust
pub struct AppModel {
// ✅ Cached rendering data (read-only from DocumentManager)
pub current_image_handle: Option<ImageHandle>,
pub current_dimensions: Option<(u32, u32)>,
pub current_page: Option<usize>,
pub page_count: Option<usize>,
// ✅ Cached metadata (read-only)
pub metadata: Option<DocumentMeta>,
// ✅ Navigation info (read-only)
pub current_path: Option<PathBuf>,
pub current_index: Option<usize>,
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<String>,
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::<NoctuaApp>(
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