diff --git a/DEVNOTE/Feature-Comparison.md b/DEVNOTE/Feature-Comparison.md deleted file mode 100644 index 6bffc21..0000000 --- a/DEVNOTE/Feature-Comparison.md +++ /dev/null @@ -1,167 +0,0 @@ -# Feature-Vergleich: app/document vs domain/document/types - -**Ziel:** Identifizieren welche Features von `src/app/document/` nach `src/domain/document/types/` portiert werden müssen. - ---- - -## RasterDocument - -### Struct-Felder - -| Feld | app/ | domain/ | Status | -|------|------|---------|--------| -| `document: DynamicImage` | ✅ | ✅ | OK | -| `native_width: u32` | ✅ | ✅ | OK | -| `native_height: u32` | ✅ | ✅ | OK | -| `transform: TransformState` | ✅ | ✅ | OK | -| `handle: ImageHandle` | ✅ pub | ✅ private | **⚠️ domain: public machen oder getter** | -| `fine_rotation_angle: f32` | ❌ | ✅ | ℹ️ Extra feature in domain | -| `interpolation_quality` | ❌ | ✅ | ℹ️ Extra feature in domain | - -**Entscheidung:** Domain-Version hat mehr Features → Domain behalten, `handle` public machen - ---- - -### Methoden-Vergleich - -| Methode | app/ | domain/ | Aktion | -|---------|------|---------|--------| -| **Core Operations** | | | | -| `open()` | ✅ | ✅ | ✅ OK | -| `render()` | ✅ | ✅ | ✅ OK | -| `save()` | ✅ | ✅ | ✅ OK | -| | | | | -| **Transformations (Trait)** | | | | -| `rotate()` | ✅ | ✅ | ✅ OK | -| `flip()` | ✅ | ✅ | ✅ OK | -| `transform_state()` | ✅ | ✅ | ✅ OK | -| | | | | -| **Renderable (Trait)** | | | | -| `info()` | ✅ | ✅ | ✅ OK | -| | | | | -| **Dimensions** | | | | -| `dimensions()` | ✅ | ✅ | ✅ OK (beide haben es!) | -| `native_dimensions()` | ❌ | ✅ | ℹ️ Extra in domain | -| | | | | -| **Crop** | | | | -| `crop()` | ✅ | ✅ | ✅ OK (beide haben es!) | -| `crop_to_image()` | ✅ | ❌ | 📋 **Portieren nach domain/** | -| | | | | -| **Handle/Image Access** | | | | -| `handle` (field pub) | ✅ | ❌ | 📋 **Public machen oder getter** | -| `handle()` (getter) | ❌ | ✅ | ✅ OK (domain hat getter) | -| `image()` | ❌ | ✅ | ℹ️ Extra in domain | -| `get_rendered_image()` | ❌ | ✅ | ℹ️ Extra in domain | -| | | | | -| **Metadata** | | | | -| `extract_meta()` | ✅ | ❌ | 📋 **Portieren nach domain/** | -| | | | | -| **Internal Helpers** | | | | -| `refresh_handle()` | ✅ private | ❌ | ℹ️ Evtl. bereits integriert | -| `apply_rotation()` | ❌ | ✅ | ℹ️ Extra in domain | -| `apply_flip()` | ❌ | ✅ | ℹ️ Extra in domain | -| `create_image_handle_from_image()` | ❌ | ✅ | ℹ️ Extra in domain | -| | | | | -| **Extra Features (domain)** | | | | -| `rotate_fine()` | ❌ | ✅ | ℹ️ Feature in domain | -| `reset_fine_rotation()` | ❌ | ✅ | ℹ️ Feature in domain | -| `set_interpolation_quality()` | ❌ | ✅ | ℹ️ Feature in domain | -| `resize_to_format()` | ❌ | ✅ | ℹ️ Feature in domain | - ---- - -### Zusammenfassung RasterDocument - -**Domain-Version ist fortgeschrittener** ✅ -- Mehr Features (fine rotation, interpolation quality, resize) -- Bessere API (getter statt public fields) -- Saubere Helper-Funktionen - -**Aus app/ portieren:** -1. ✅ `crop_to_image()` - Nicht-destruktives Crop -2. ✅ `extract_meta()` - Metadaten-Extraktion -3. ✅ `handle` public machen ODER getter `handle()` nutzen (bereits vorhanden!) - -**Entscheidung:** Domain-Version als Basis, nur 2 Methoden fehlen - ---- - -## VectorDocument - -### Methoden-Vergleich - -| Methode | app/ | domain/ | Aktion | -|---------|------|---------|--------| -| `open()` | ✅ | ✅ | ✅ OK | -| `render()` | ✅ | ✅ | ✅ OK | -| `dimensions()` | ✅ | ❌ | 📋 **Portieren** | -| `handle` (pub field) | ✅ | ❌ private | 📋 **Public machen oder getter** | -| `extract_meta()` | ✅ | ❌ | 📋 **Portieren** | -| `crop()` | ❌ | ❌ | 📋 **Neu implementieren** (Design-Entscheidung) | - -**Aus app/ portieren:** -1. `dimensions()` -2. `extract_meta()` -3. `handle()` getter oder public -4. NEU: `crop()` implementieren (render-based) - ---- - -## PortableDocument - -### Methoden-Vergleich - -| Methode | app/ | domain/ | Aktion | -|---------|------|---------|--------| -| `open()` | ✅ | ✅ | ✅ OK | -| `render()` | ✅ | ✅ | ✅ OK | -| `dimensions()` | ✅ | ❌ | 📋 **Portieren** | -| `handle` (pub field) | ✅ | ❌ private | 📋 **Public machen oder getter** | -| `extract_meta()` | ✅ | ❌ | 📋 **Portieren** | -| `crop()` | ❌ | ❌ | 📋 **Neu implementieren** (Design-Entscheidung) | -| Thumbnails | ✅ | ✅ | ℹ️ Prüfen ob identisch | - -**Aus app/ portieren:** -1. `dimensions()` -2. `extract_meta()` -3. `handle()` getter oder public -4. NEU: `crop()` implementieren (render-based) - ---- - -## Action Items für Schritt 1.2-1.4 - -### Schritt 1.2: RasterDocument (60 Min) -- [x] `crop()` - Bereits vorhanden! ✅ -- [x] `dimensions()` - Bereits vorhanden! ✅ -- [x] `crop_to_image()` hinzufügen ✅ -- [x] `extract_meta()` hinzufügen ✅ (oder in core/metadata.rs) -- [x] `handle()` getter - Bereits vorhanden! ✅ - -### Schritt 1.3: VectorDocument (45 Min) -- [x] `dimensions()` - Bereits vorhanden ✅ -- [x] `handle()` getter - Bereits vorhanden ✅ -- [x] `extract_meta()` implementieren ✅ -- [x] `crop()` implementieren (render-based) ✅ - -### Schritt 1.4: PortableDocument (45 Min) -- [x] `dimensions()` - Bereits vorhanden ✅ -- [x] `handle()` getter - Bereits vorhanden ✅ -- [x] `extract_meta()` implementieren ✅ -- [x] `crop()` implementieren (render-based) ✅ - ---- - -## Überraschende Erkenntnisse - -1. **Domain hat bereits crop() für Raster!** ✅ -2. **Domain hat bereits dimensions()!** ✅ -3. **Domain hat bereits handle() getter!** ✅ -4. **Domain hat MEHR Features** (fine rotation, interpolation) ✅ - -**→ Domain-Implementierung ist besser! Nur 2-3 Methoden fehlen pro Type.** - ---- - -**Status:** Vergleich abgeschlossen -**Nächster Schritt:** 1.2 - RasterDocument ergänzen diff --git a/DEVNOTE/Metadata-Comparison.md b/DEVNOTE/Metadata-Comparison.md deleted file mode 100644 index 76a219f..0000000 --- a/DEVNOTE/Metadata-Comparison.md +++ /dev/null @@ -1,57 +0,0 @@ -# Metadata Konsolidierung - Vergleich - -## Status: ✅ Strukturen sind identisch - -### Strukturen - -| Struktur | app/meta.rs | domain/core/metadata.rs | Status | -|----------|-------------|-------------------------|--------| -| `BasicMeta` | ✅ 7 fields | ✅ 7 fields | ✅ Identisch | -| `ExifMeta` | ✅ 9 fields | ✅ 9 fields | ✅ Identisch | -| `DocumentMeta` | ✅ | ✅ | ✅ Identisch | - -### Methoden - -| Methode | app/ | domain/ | Status | -|---------|------|---------|--------| -| `BasicMeta::file_size_display()` | ✅ | ✅ | ✅ Identisch | -| `BasicMeta::resolution_display()` | ✅ | ✅ | ✅ Identisch | -| `ExifMeta::camera_display()` | ✅ | ✅ | ✅ Identisch | -| `ExifMeta::gps_display()` | ✅ | ✅ | ✅ Identisch | -| `ExifMeta::from_bytes()` | ❌ private fn | ✅ pub fn | ✅ Domain besser | - -### Helper-Funktionen - -**App hat:** -- `build_raster_meta()` - Wird nicht außerhalb app/ verwendet -- `build_vector_meta()` - Wird nicht außerhalb app/ verwendet -- `build_portable_meta()` - Wird nicht außerhalb app/ verwendet -- `extract_exif_from_bytes()` - Private Funktion - -**Domain hat:** -- `ExifMeta::from_bytes()` - Public Methode (sauberer) -- Document-Typen haben eigene `extract_meta()` Methoden - -### Unterschiede - -**Organisation:** -- **App:** Helper-Funktionen `build_*_meta()` außerhalb der Structs -- **Domain:** `extract_meta()` Methoden direkt in Document-Typen (RasterDocument, VectorDocument, PortableDocument) - -**Vorteile Domain:** -- ✅ Sauberer: `doc.extract_meta(path)` statt `build_raster_meta(path, doc, ...)` -- ✅ Type-safe: Compiler weiß welcher Typ -- ✅ Erweiterbar: Jeder Document-Typ kontrolliert eigene Metadaten - -## Entscheidung - -✅ **Keine Änderungen nötig!** - -- Domain-Version ist vollständig und sogar besser organisiert -- Strukturen sind identisch -- `ExifMeta::from_bytes()` ist bereits in Domain als public Methode -- `extract_meta()` Methoden in Document-Typen sind bereits implementiert - -## Nächster Schritt - -Schritt 1.7: Traits & Enums konsolidieren diff --git a/DEVNOTE/Migration-Plan.md b/DEVNOTE/Migration-Plan.md deleted file mode 100644 index b937edf..0000000 --- a/DEVNOTE/Migration-Plan.md +++ /dev/null @@ -1,925 +0,0 @@ -# Noctua – Complete Migration Plan - -**Ziel:** Vollständige Trennung von TEA (UI) und Business Logic nach Clean Architecture - -**Status:** ✅ **MIGRATION ABGESCHLOSSEN** (100%) - -- ✅ `src/app/` wurde gelöscht -- ✅ `src/ui/` + `src/application/` + `src/domain/` + `src/infrastructure/` sind aktiv -- ✅ Clean Architecture vollständig implementiert -- ✅ DocumentManager ist Single Source of Truth -- ✅ Command Pattern durchgängig implementiert -- ✅ Views nutzen gecachte Daten aus AppModel -- ✅ Sync-Mechanismus aktiv - ---- - -## 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` - - -**Design-Entscheidung:** Crop wird für alle Dokumenttypen unterstützt, da alle als Raster gerendert werden. - -```rust -impl VectorDocument { - pub fn dimensions(&self) -> (u32, u32) { - // Implementation based on transform state - } - - - /// Crop the document to the specified rectangle. - /// Works on rendered output (raster). - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> DocResult<()> { - // Render to raster with current transform - let rendered = self.render_to_image()?; - let cropped = rendered.crop_imm(x, y, width, height); - self.handle = create_image_handle_from_image(&cropped); - self.width = width; - self.height = height; - Ok(()) - } - 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 - } - - - /// Crop the current page to the specified rectangle. - /// Works on rendered output (raster). - pub fn crop(&mut self, x: u32, y: u32, width: u32, height: u32) -> DocResult<()> { - // Crop current page - let rendered = self.render_current_page()?; - let cropped = rendered.crop_imm(x, y, width, height); - self.handle = create_image_handle_from_image(&cropped); - self.width = width; - self.height = height; - Ok(()) - } - 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 (supported for all types - works on rendered output). - 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(doc) => doc.crop(x, y, width, height), - Self::Portable(doc) => doc.crop(x, y, width, height), - } - } -} -``` - -### 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 -``` - ---- - -## Migration Completion Summary - -**Status:** ✅ **VOLLSTÄNDIG ABGESCHLOSSEN** -**Datum:** 2024 (Session-based Migration) -**Tatsächliche Dauer:** ~6 Phasen in einer Session - -### Durchgeführte Phasen: - -#### Phase 1: Domain Layer Konsolidierung ✅ -- ✅ Feature-Vergleich durchgeführt -- ✅ RasterDocument, VectorDocument, PortableDocument Features portiert -- ✅ DocumentContent Methoden ergänzt -- ✅ Metadata konsolidiert -- ✅ Traits & Enums konsolidiert - -#### Phase 2: Infrastructure Layer Migration ✅ -- ✅ ThumbnailCache erstellt und strukturiert -- ✅ Wallpaper System implementiert (Multi-DE Support: COSMIC, KDE, GNOME, feh) -- ✅ System Integration vollständig - -#### Phase 3: Application Layer Integration ✅ -- ✅ Sync-Funktion implementiert (sync_model_from_manager, sync_render_data, sync_navigation) -- ✅ DocumentManager bereits in NoctuaApp integriert -- ✅ AppModel erweitert mit cached render data - -#### Phase 4: UI Layer Migration ✅ -- ✅ AppModel bereinigt (nur UI state + cached data) -- ✅ Update-Logik umgeschrieben (alle Operations über DocumentManager + Commands) -- ✅ Views angepasst (nutzen AppModel Cache statt direkten Document-Zugriff) - - canvas.rs, footer.rs, header.rs, panels.rs, pages_panel.rs, mod.rs - -#### Phase 5: Main Entry Point ✅ -- ✅ main.rs umgestellt (ui::NoctuaApp statt app::Noctua) -- ✅ Module-Exports korrekt (ui, application, domain, infrastructure) -- ✅ i18n Keys hinzugefügt (format-section-title, menu-main, etc.) - -#### Phase 6: Testing & Validation ✅ -- ✅ Kompilierung erfolgreich (0 Errors) -- ✅ Build erfolgreich (Release Build: 29s) -- ✅ Funktionale Tests validiert (alle kritischen Code-Pfade vorhanden) -- ✅ Warnings reduziert (von 62 auf 53) - -#### Phase 7: Cleanup ✅ -- ✅ src/app/ gelöscht (Backup in /tmp/app-backup) -- ✅ AGENTS.md aktualisiert (100% Status, neue Struktur dokumentiert) -- ✅ README.md erweitert (Architecture-Sektion hinzugefügt) -- ✅ Workflow.md aktualisiert (Migration Complete Status) - -### Finale Struktur: - -``` -src/ -├── main.rs # Entry point (ui::NoctuaApp) -├── config.rs -├── constant.rs -├── i18n.rs -│ -├── ui/ # UI Layer (COSMIC) -│ ├── app.rs -│ ├── model.rs # UI state + cached render data -│ ├── message.rs -│ ├── update.rs -│ ├── sync.rs # Model ↔ DocumentManager sync -│ ├── views/ -│ └── components/ -│ -├── application/ # Application Layer -│ ├── document_manager.rs -│ ├── commands/ -│ ├── queries/ -│ └── services/ -│ -├── domain/ # Domain Layer -│ ├── document/ -│ │ ├── core/ -│ │ ├── types/ -│ │ ├── operations/ -│ │ └── collection.rs -│ ├── errors.rs -│ └── viewport/ -│ -└── infrastructure/ # Infrastructure Layer - ├── loaders/ - ├── cache/ # ThumbnailCache - ├── filesystem/ - └── system/ # Wallpaper support -``` - -### Build-Status: - -- **Compilation:** ✅ 0 Errors -- **Warnings:** 53 (hauptsächlich "unused" für zukünftigen Code) -- **Binary:** target/release/noctua (~18MB) -- **Tests:** Alle kritischen Code-Pfade validiert - -### Architektur-Validierung: - -✅ **Clean Architecture vollständig:** -- Dependency Flow: ui → application → domain ← infrastructure -- Keine zirkulären Abhängigkeiten -- Single Source of Truth: DocumentManager -- Command Pattern: Alle Operationen über Commands -- Type Erasure: DocumentContent enum -- Cached Rendering: AppModel cacht Handle/Dimensions -- Sync-Mechanismus: Explizit nach jeder Operation - -### Bekannte Einschränkungen: - -1. **Fine Rotation:** Temporär deaktiviert (imageproc Dependency fehlt) -2. **Deprecated Functions:** canvas_to_image_coords (bereits migriert zu CropCommand) -3. **Unused Code:** ~30 Items für zukünftige Features reserviert - -### Erfolgreiche Features: - -- ✅ Multi-Format Support (Raster, SVG, PDF) -- ✅ Document Navigation (Folder-Browse mit Wrap-Around) -- ✅ Transformationen (Rotate, Flip, Crop) -- ✅ Zoom & Pan -- ✅ Multi-Page Support (PDF Thumbnails) -- ✅ Metadata Display (EXIF) -- ✅ Wallpaper Setting (COSMIC, KDE, GNOME, feh) - ---- - -**Migration erfolgreich abgeschlossen!** 🎉 - -Die Anwendung nutzt jetzt vollständig die neue Clean Architecture. -Alte `src/app/` Struktur wurde entfernt. -Alle Tests bestanden, Build erfolgreich. -**Letzte Änderung:** 2025-01-XX \ No newline at end of file diff --git a/DEVNOTE/Migration-Plan.md.backup b/DEVNOTE/Migration-Plan.md.backup deleted file mode 100644 index dbb8cb2..0000000 --- a/DEVNOTE/Migration-Plan.md.backup +++ /dev/null @@ -1,763 +0,0 @@ -# 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 \ No newline at end of file diff --git a/DEVNOTE/Traits-Enums-Comparison.md b/DEVNOTE/Traits-Enums-Comparison.md deleted file mode 100644 index f9a9ef3..0000000 --- a/DEVNOTE/Traits-Enums-Comparison.md +++ /dev/null @@ -1,160 +0,0 @@ -# Traits & Enums Konsolidierung - Vergleich - -## Status: ✅ Domain-Version ist vollständig und erweitert - -### Enums - -| Enum | app/ | domain/ | Status | -|------|------|---------|--------| -| `Rotation` | ✅ | ✅ | ✅ Identisch | -| `FlipDirection` | ✅ | ✅ | ✅ Identisch | -| `RotationMode` | ❌ | ✅ | ℹ️ Extra in domain (fine rotation support) | -| `InterpolationQuality` | ❌ | ✅ | ℹ️ Extra in domain (quality control) | -| `DocumentKind` | ✅ | ✅ | ✅ Identisch (in content.rs) | - -### Structs - -| Struct | app/ | domain/ | Status | -|--------|------|---------|--------| -| `TransformState` | ✅ `rotation: Rotation` | ✅ `rotation: RotationMode` | ⚠️ Domain erweitert (RotationMode statt Rotation) | -| `RenderOutput` | ✅ | ✅ | ✅ Identisch | -| `DocumentInfo` | ✅ | ✅ | ✅ Identisch | - -### Traits - -#### Renderable - -| Methode | app/ | domain/ | Status | -|---------|------|---------|--------| -| `render()` | ✅ | ✅ | ✅ Identisch | -| `info()` | ✅ | ✅ | ✅ Identisch | - -#### Transformable - -| Methode | app/ | domain/ | Status | -|---------|------|---------|--------| -| `rotate()` | ✅ | ✅ | ✅ Identisch | -| `flip()` | ✅ | ✅ | ✅ Identisch | -| `transform_state()` | ✅ | ✅ | ✅ Identisch | -| `rotate_fine()` | ❌ | ✅ | ℹ️ Extra in domain (default impl) | -| `reset_fine_rotation()` | ❌ | ✅ | ℹ️ Extra in domain (default impl) | -| `set_interpolation_quality()` | ❌ | ✅ | ℹ️ Extra in domain (default impl) | - -#### MultiPage - -| Methode | app/ | domain/ | Status | -|---------|------|---------|--------| -| `page_count()` | ✅ | ✅ | ✅ Identisch | -| `current_page()` | ✅ | ✅ | ✅ Identisch | -| `go_to_page()` | ✅ | ✅ | ✅ Identisch | - -#### MultiPageThumbnails - -| Methode | app/ | domain/ | Status | -|---------|------|---------|--------| -| `get_thumbnail()` | ✅ `Option` | ✅ `Result>` | ⚠️ Domain hat error handling | -| `thumbnails_ready()` | ✅ | ✅ | ✅ Identisch | -| `thumbnails_loaded()` | ✅ | ✅ | ✅ Identisch | -| `generate_thumbnail_page()` | ✅ `Option` | ✅ `Result<()>` | ⚠️ Domain hat error handling | -| `generate_all_thumbnails()` | ✅ | ✅ | ℹ️ Beide vorhanden | - -### Unterschiede - -#### 1. RotationMode (Domain-Erweiterung) - -**App:** -```rust -pub struct TransformState { - pub rotation: Rotation, // Nur 90° Schritte -} -``` - -**Domain:** -```rust -pub enum RotationMode { - Standard(Rotation), // 90° Schritte - Fine(f32), // Beliebige Winkel -} - -pub struct TransformState { - pub rotation: RotationMode, // Flexibel! -} -``` - -**Vorteil:** Domain unterstützt fine rotation (RasterDocument nutzt das) - -#### 2. Transformable Erweiterungen - -**Domain hat zusätzlich:** -- `rotate_fine()` - Für beliebige Rotationswinkel -- `reset_fine_rotation()` - Reset zu 90° Schritten -- `set_interpolation_quality()` - Qualitätskontrolle - -**Default Implementierungen:** Alle haben Default-Impls (no-op), daher backward-compatible. - -#### 3. Error Handling in Thumbnails - -**App:** -```rust -fn get_thumbnail(&self, page: usize) -> Option; -``` - -**Domain:** -```rust -fn get_thumbnail(&mut self, page: usize) -> DocResult>; -``` - -**Vorteil:** Domain kann Fehler melden statt silent failure. - -## Kompatibilität - -### ⚠️ Potenzielle Breaking Changes - -1. **TransformState::rotation** ist jetzt `RotationMode` statt `Rotation` - - Alt: `state.rotation == Rotation::Cw90` - - Neu: `state.rotation == RotationMode::Standard(Rotation::Cw90)` - -2. **MultiPageThumbnails Signatur** unterscheidet sich - - Kann zu Kompilierungsfehlern führen wenn app/ Code darauf zugreift - -### ✅ Lösungen - -**Option 1:** `RotationMode` bietet `From` an: -```rust -impl From for RotationMode { - fn from(rot: Rotation) -> Self { - RotationMode::Standard(rot) - } -} -``` - -**Option 2:** Helper-Methode in TransformState: -```rust -impl TransformState { - pub fn standard_rotation(&self) -> Option { - match self.rotation { - RotationMode::Standard(rot) => Some(rot), - _ => None, - } - } -} -``` - -## Entscheidung - -✅ **Domain-Version nutzen - ist besser!** - -**Begründung:** -- Alle app/ Features sind vorhanden -- Zusätzliche Features (fine rotation, interpolation) -- Besseres Error Handling -- Backward-compatible durch Default-Impls - -**Aktion:** -- Keine Änderungen nötig -- Domain ist bereits vollständig -- Bei Integration: Helper für RotationMode-Kompatibilität hinzufügen falls nötig - -## Nächster Schritt - -Phase 2: Infrastructure Layer Migration diff --git a/src/application/commands/transform_document.rs b/src/application/commands/transform_document.rs index 2b34dd5..d2f5bc4 100644 --- a/src/application/commands/transform_document.rs +++ b/src/application/commands/transform_document.rs @@ -9,6 +9,7 @@ use crate::domain::document::operations::transform; /// Transformation operation. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] pub enum TransformOperation { /// Rotate clockwise by 90 degrees. RotateCw, diff --git a/src/domain/document/core/document.rs b/src/domain/document/core/document.rs index ccb6cf9..3e1fdd7 100644 --- a/src/domain/document/core/document.rs +++ b/src/domain/document/core/document.rs @@ -126,6 +126,7 @@ impl RotationMode { /// Interpolation quality for fine rotation and resizing operations. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[allow(dead_code)] pub enum InterpolationQuality { /// Fast, nearest neighbor interpolation. Fast, diff --git a/src/domain/document/core/page.rs b/src/domain/document/core/page.rs index 3a4e55b..98929e6 100644 --- a/src/domain/document/core/page.rs +++ b/src/domain/document/core/page.rs @@ -7,6 +7,7 @@ use cosmic::widget::image::Handle as ImageHandle; /// Represents a single page in a multi-page document. #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct Page { /// Page index (0-based). pub index: usize, diff --git a/src/domain/document/operations/export.rs b/src/domain/document/operations/export.rs index ffb9750..aa78ffa 100644 --- a/src/domain/document/operations/export.rs +++ b/src/domain/document/operations/export.rs @@ -66,6 +66,7 @@ impl ExportFormat { /// Export options for image formats. #[derive(Debug, Clone)] +#[allow(dead_code)] pub struct ImageExportOptions { /// Quality setting (0-100) for lossy formats. pub quality: u8, diff --git a/src/ui/app.rs b/src/ui/app.rs index d48ad23..64e5a85 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -97,9 +97,6 @@ impl cosmic::Application for NoctuaApp { } } - // Sync model from document manager after loading initial document - crate::ui::sync::sync_model_from_manager(&mut model, &mut document_manager); - // Initialize nav bar model (required for COSMIC to show toggle icon). let nav = nav_bar::Model::default(); @@ -131,7 +128,7 @@ impl cosmic::Application for NoctuaApp { fn update(&mut self, message: Self::Message) -> Task> { match &message { AppMessage::ToggleNavBar => { - use crate::ui::model::NavPanel; + use crate::ui::model::LeftPanel; self.core.nav_bar_toggle(); let is_visible = self.core.nav_bar_active(); @@ -139,36 +136,26 @@ impl cosmic::Application for NoctuaApp { self.save_config(); if is_visible { - // Opening nav bar - restore last panel or default to Pages for multi-page docs - if let Some(last_panel) = self.model.last_nav_panel { - self.model.active_nav_panel = last_panel; - } else if let Some(doc) = self.document_manager.current_document() + // Opening nav bar - show thumbnails for multi-page docs + if let Some(doc) = self.document_manager.current_document() && doc.is_multi_page() { - self.model.active_nav_panel = NavPanel::Pages; + self.model.panels.left = Some(LeftPanel::Thumbnails); } - return start_thumbnail_generation_task(&self.model); + } else { + // Closing nav bar - hide left panel + self.model.panels.left = None; } - // Closing nav bar - remember current panel - if self.model.active_nav_panel != NavPanel::None { - self.model.last_nav_panel = Some(self.model.active_nav_panel); - } - self.model.active_nav_panel = NavPanel::None; return Task::none(); } AppMessage::OpenFormatPanel => { - use crate::ui::model::NavPanel; - - // Set active panel to Format - self.model.active_nav_panel = NavPanel::Format; - - // Open nav bar if not already open - if !self.core.nav_bar_active() { - self.core.nav_bar_toggle(); - self.config.nav_bar_visible = true; - self.save_config(); - } + // Format panel is now part of Transform mode + // Switch to Transform mode which shows format tools in right panel + self.model.mode = crate::ui::model::AppMode::Transform { + paper_format: None, + orientation: crate::ui::model::Orientation::default(), + }; return Task::none(); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d2c4796..96a0990 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -11,9 +11,6 @@ pub mod components; pub mod views; pub mod widgets; -// Internal module for syncing model from DocumentManager -pub(crate) mod sync; - // Re-export main types pub use app::NoctuaApp; pub use message::AppMessage; diff --git a/src/ui/model.rs b/src/ui/model.rs index 0071aa5..ef22573 100644 --- a/src/ui/model.rs +++ b/src/ui/model.rs @@ -2,6 +2,9 @@ // src/ui/model.rs // // UI state (view, tools, panels). +// +// AppModel contains ONLY UI-specific state. +// Document state lives in DocumentManager (application layer). use cosmic::iced::Size; @@ -9,29 +12,20 @@ use crate::ui::widgets::CropSelection; use crate::config::AppConfig; // ============================================================================= -// Enums +// View Mode // ============================================================================= -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum ViewMode { + #[default] Fit, ActualSize, Custom, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ToolMode { - None, - Crop, - Scale, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum NavPanel { - None, - Pages, - Format, -} +// ============================================================================= +// Paper Format (for export/transform) +// ============================================================================= #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum PaperFormat { @@ -75,109 +69,212 @@ impl PaperFormat { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Orientation { Horizontal, + #[default] Vertical, } // ============================================================================= -// Model +// Application Mode (combines tool + panel state) +// ============================================================================= + +/// Application mode - unified tool and panel state. +/// +/// Each mode determines: +/// - Active tool behavior +/// - Right panel content +/// - Available shortcuts +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum AppMode { + /// Normal viewing mode - no active tool + View, + + /// Crop mode with selection + Crop { selection: CropSelection }, + + /// Transform/export mode + Transform { + paper_format: Option, + orientation: Orientation, + }, + + /// Fullscreen mode (all panels hidden) + Fullscreen, +} + +impl Default for AppMode { + fn default() -> Self { + Self::View + } +} + +impl AppMode { + /// Get the right panel that should be shown for this mode + pub fn right_panel(&self) -> Option { + match self { + Self::View => Some(RightPanel::Properties), + Self::Crop { .. } => Some(RightPanel::CropTools), + Self::Transform { .. } => Some(RightPanel::TransformTools), + Self::Fullscreen => None, + } + } + + /// Check if mode is an active tool (not View/Fullscreen) + pub fn is_tool_active(&self) -> bool { + matches!(self, Self::Crop { .. } | Self::Transform { .. }) + } +} + +// ============================================================================= +// Viewport (zoom, pan, canvas) +// ============================================================================= + +/// Viewport state - zoom, pan, canvas dimensions. +#[derive(Debug, Clone)] +pub struct Viewport { + /// Current scale factor + pub scale: f32, + + /// Pan offset X + pub pan_x: f32, + + /// Pan offset Y + pub pan_y: f32, + + /// Canvas size (container) + pub canvas_size: Size, + + /// Image size (after scaling) + pub image_size: Size, + + /// Fit mode + pub fit_mode: ViewMode, + + /// Scroll container ID + pub scroll_id: cosmic::widget::Id, + + /// Cached image handle for rendering (updated when document or scale changes) + pub cached_image_handle: Option, +} + +impl Default for Viewport { + fn default() -> Self { + Self { + scale: 1.0, + pan_x: 0.0, + pan_y: 0.0, + canvas_size: Size::ZERO, + image_size: Size::ZERO, + fit_mode: ViewMode::Fit, + scroll_id: cosmic::widget::Id::new("canvas-scroll"), + cached_image_handle: None, + } + } +} + +impl Viewport { + /// Reset pan to center + pub fn reset_pan(&mut self) { + self.pan_x = 0.0; + self.pan_y = 0.0; + } +} + +// ============================================================================= +// Panel State +// ============================================================================= + +/// Panel visibility state. +#[derive(Debug, Clone, Default)] +pub struct PanelState { + /// Left panel (thumbnails for multi-page) + pub left: Option, + + /// Right panel (context-dependent tools/properties) + pub right: Option, +} + +/// Left panel types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LeftPanel { + /// Thumbnail navigation for multi-page documents + Thumbnails, +} + +/// Right panel types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow(dead_code)] +pub enum RightPanel { + /// Document properties and metadata + Properties, + + /// Crop mode tools + CropTools, + + /// Transform/export tools + TransformTools, +} + +// ============================================================================= +// AppModel (UI State Only) // ============================================================================= /// UI state for the application. /// -/// This struct holds only UI-related state (view, tools, panels). -/// Document data is managed by DocumentManager in the application layer. -/// Cached render data is stored here for performance. +/// Contains ONLY UI-specific state: +/// - Current mode (view/tool) +/// - Viewport (zoom/pan) +/// - Panel visibility +/// - Transient UI state (errors, menu) +/// +/// Document state (current file, metadata, etc.) lives in DocumentManager! 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, + /// Current application mode + pub mode: AppMode, - // Cached metadata (read-only) - pub metadata: Option, + /// Viewport state + pub viewport: Viewport, - // Navigation info (read-only) - pub current_path: Option, - pub current_index: Option, - pub folder_count: usize, + /// Panel visibility + pub panels: PanelState, - // View state - pub view_mode: ViewMode, - pub pan_x: f32, - pub pan_y: f32, - pub scale: f32, - pub canvas_size: Size, - pub image_size: Size, - pub scroll_id: cosmic::widget::Id, + /// Error message to display + pub error: Option, - // Tool state - pub tool_mode: ToolMode, - pub crop_selection: CropSelection, - - // Format settings (for export) - pub paper_format: Option, - pub orientation: Orientation, - - // UI panels - pub active_nav_panel: NavPanel, - pub last_nav_panel: Option, + /// Is main menu open? pub menu_open: bool, - // UI feedback - pub error: Option, + /// Tick counter for animations pub tick: u64, } impl AppModel { pub fn new(_config: AppConfig) -> Self { Self { - // Cached data - current_image_handle: None, - current_dimensions: None, - current_page: None, - page_count: None, - metadata: None, - current_path: None, - current_index: None, - folder_count: 0, - // View state - view_mode: ViewMode::Fit, - pan_x: 0.0, - pan_y: 0.0, - scale: 1.0, - canvas_size: Size::ZERO, - image_size: Size::ZERO, - scroll_id: cosmic::widget::Id::new("canvas-scroll"), - // Tool state - tool_mode: ToolMode::None, - crop_selection: CropSelection::default(), - // Format settings - paper_format: None, - orientation: Orientation::Vertical, - // UI panels - active_nav_panel: NavPanel::None, - last_nav_panel: None, - menu_open: false, - // UI feedback + mode: AppMode::default(), + viewport: Viewport::default(), + panels: PanelState::default(), error: None, + menu_open: false, tick: 0, } } + /// Set error message pub fn set_error>(&mut self, msg: S) { self.error = Some(msg.into()); } + /// Clear error message pub fn clear_error(&mut self) { self.error = None; } + /// Reset viewport pan to center pub fn reset_pan(&mut self) { - self.pan_x = 0.0; - self.pan_y = 0.0; + self.viewport.reset_pan(); } } diff --git a/src/ui/sync.rs b/src/ui/sync.rs deleted file mode 100644 index 70672e1..0000000 --- a/src/ui/sync.rs +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/sync.rs -// -// Synchronize UI model from DocumentManager state. - -use crate::application::DocumentManager; -use crate::domain::document::core::document::Renderable; -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). -/// Only caches render-related data for performance. -pub fn sync_model_from_manager(model: &mut AppModel, manager: &mut DocumentManager) { - // Update cached render data - if let Some(doc) = manager.current_document_mut() { - // Cache image handle for rendering - if let Ok(render_output) = doc.render(1.0) { - model.current_image_handle = Some(render_output.handle); - } else { - model.current_image_handle = None; - } - - // Cache dimensions - let info = doc.info(); - model.current_dimensions = Some((info.width, info.height)); - - // Cache page info - model.current_page = Some(doc.current_page()); - model.page_count = Some(doc.page_count()); - } else { - // No document loaded - clear cached data - 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(); - - // Update metadata - model.metadata = manager.current_metadata().cloned(); -} - -/// Synchronize only render data without full document info. -/// -/// Useful when only the rendered image has changed (e.g., after transform). -pub fn sync_render_data(model: &mut AppModel, manager: &mut DocumentManager) { - if let Some(doc) = manager.current_document_mut() { - // Re-render at current scale to get updated image handle - if let Ok(render_output) = doc.render(model.scale as f64) { - model.current_image_handle = Some(render_output.handle); - } - - // Update dimensions (may have changed after rotation) - let info = doc.info(); - model.current_dimensions = Some((info.width, info.height)); - - // Update page info (in case page changed) - model.current_page = Some(doc.current_page()); - } -} - -/// Synchronize only navigation state without render data. -/// -/// Useful when switching documents in a folder. -#[allow(dead_code)] -pub fn sync_navigation(model: &mut AppModel, manager: &DocumentManager) { - model.current_path = manager.current_path().map(|p| p.to_path_buf()); - model.current_index = manager.current_index(); - model.folder_count = manager.folder_entries().len(); -} diff --git a/src/ui/update.rs b/src/ui/update.rs index 8cdb9fd..5d3290f 100644 --- a/src/ui/update.rs +++ b/src/ui/update.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// src/ui/app/update.rs +// src/ui/update.rs // // Application update loop: applies messages to the global model state. @@ -7,11 +7,11 @@ use cosmic::{Action, Task}; use super::NoctuaApp; use super::message::AppMessage; -use super::model::{AppModel, ToolMode, ViewMode}; +use super::model::{AppMode, ViewMode}; use crate::application::commands::transform_document::{TransformDocumentCommand, TransformOperation}; use crate::application::commands::crop_document::CropDocumentCommand; - -use crate::ui::widgets::DragHandle; +use crate::domain::document::core::document::Renderable; +use crate::ui::widgets::{CropSelection, DragHandle}; // ============================================================================= // Update Result @@ -35,38 +35,35 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { app.model.set_error(format!("Failed to open document: {e}")); } else { app.model.reset_pan(); - app.model.view_mode = ViewMode::Fit; - app.model.scale = 1.0; - // Sync model from document manager - crate::ui::sync::sync_model_from_manager(&mut app.model, &mut app.document_manager); + app.model.viewport.fit_mode = ViewMode::Fit; + app.model.viewport.scale = 1.0; + cache_render(&mut app.model, &mut app.document_manager); } } AppMessage::NextDocument => { // Ignore navigation in Crop mode - if app.model.tool_mode != ToolMode::Crop + if !matches!(app.model.mode, AppMode::Crop { .. }) && let Some(_path) = app.document_manager.next_document() { // Reset zoom when navigating to new document - app.model.scale = 1.0; - app.model.view_mode = ViewMode::ActualSize; + app.model.viewport.scale = 1.0; + app.model.viewport.fit_mode = ViewMode::ActualSize; app.model.reset_pan(); - // Sync model from document manager - crate::ui::sync::sync_model_from_manager(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } AppMessage::PrevDocument => { // Ignore navigation in Crop mode - if app.model.tool_mode != ToolMode::Crop + if !matches!(app.model.mode, AppMode::Crop { .. }) && let Some(_path) = app.document_manager.previous_document() { // Reset zoom when navigating to new document - app.model.scale = 1.0; - app.model.view_mode = ViewMode::ActualSize; + app.model.viewport.scale = 1.0; + app.model.viewport.fit_mode = ViewMode::ActualSize; app.model.reset_pan(); - // Sync model from document manager - crate::ui::sync::sync_model_from_manager(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } @@ -75,23 +72,15 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { if let Err(e) = doc.go_to_page(*page) { log::error!("Failed to navigate to page {page}: {e}"); } else { - // Sync render data after page change - crate::ui::sync::sync_render_data(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } } // ---- Thumbnail generation ------------------------------------------------- AppMessage::GenerateThumbnailPage(_page) => { - // TODO: Re-enable when model.document is synced from DocumentManager - // Currently disabled because DocumentContent doesn't implement Clone - // if let Some(doc) = &mut model.document { - // if let Ok(()) = doc.generate_thumbnail_page(*page) { - // return UpdateResult::Task(Task::batch([ - // Task::done(Action::App(AppMessage::RefreshView)), - // ])); - // } - // } + // TODO: Thumbnail generation via DocumentManager + // Currently handled by DocumentManager.open_document() } AppMessage::RefreshView => { @@ -100,29 +89,23 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { // ---- View / zoom --------------------------------------------------------- AppMessage::ZoomIn => { - let current = app.model.scale; - let new_zoom = - (current * app.config.scale_step).clamp(app.config.min_scale, app.config.max_scale); - app.model.scale = new_zoom; - app.model.view_mode = ViewMode::Custom; + app.model.viewport.scale = (app.model.viewport.scale * 1.2).min(10.0); + app.model.viewport.fit_mode = ViewMode::Custom; } AppMessage::ZoomOut => { - let current = app.model.scale; - let new_zoom = - (current / app.config.scale_step).clamp(app.config.min_scale, app.config.max_scale); - app.model.scale = new_zoom; - app.model.view_mode = ViewMode::Custom; + app.model.viewport.scale = (app.model.viewport.scale / 1.2).max(0.1); + app.model.viewport.fit_mode = ViewMode::Custom; } AppMessage::ZoomReset => { - app.model.scale = 1.0; - app.model.view_mode = ViewMode::ActualSize; + app.model.viewport.scale = 1.0; + app.model.viewport.fit_mode = ViewMode::ActualSize; app.model.reset_pan(); } AppMessage::ZoomFit => { - app.model.view_mode = ViewMode::Fit; + app.model.viewport.fit_mode = ViewMode::Fit; app.model.reset_pan(); } @@ -134,34 +117,35 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { image_size, } => { // Detect scale changes (zoom vs just pan) - let old_scale = app.model.scale; + let old_scale = app.model.viewport.scale; // Update model from viewer state - app.model.scale = *scale; - app.model.pan_x = *offset_x; - app.model.pan_y = *offset_y; - app.model.canvas_size = *canvas_size; - app.model.image_size = *image_size; + app.model.viewport.scale = *scale; + app.model.viewport.pan_x = *offset_x; + app.model.viewport.pan_y = *offset_y; + app.model.viewport.canvas_size = *canvas_size; + app.model.viewport.image_size = *image_size; - // If scale changed, user zoomed -> switch to Custom mode + // If scale changed, user zoomed -> switch to Custom mode and re-render // (Fit mode is only maintained when explicitly set via ZoomFit button) - if old_scale != *scale { - app.model.view_mode = ViewMode::Custom; + if (old_scale - *scale).abs() > 0.001 { + app.model.viewport.fit_mode = ViewMode::Custom; + cache_render(&mut app.model, &mut app.document_manager); } } // ---- Pan control --------------------------------------------------------- AppMessage::PanLeft => { - app.model.pan_x -= app.config.pan_step; + app.model.viewport.pan_x -= 50.0; } AppMessage::PanRight => { - app.model.pan_x += app.config.pan_step; + app.model.viewport.pan_x += 50.0; } AppMessage::PanUp => { - app.model.pan_y -= app.config.pan_step; + app.model.viewport.pan_y -= 50.0; } AppMessage::PanDown => { - app.model.pan_y += app.config.pan_step; + app.model.viewport.pan_y += 50.0; } AppMessage::PanReset => { app.model.reset_pan(); @@ -169,46 +153,56 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { // ---- Tool modes ---------------------------------------------------------- AppMessage::ToggleCropMode => { - app.model.tool_mode = if app.model.tool_mode == ToolMode::Crop { - ToolMode::None - } else { - ToolMode::Crop + app.model.mode = match &app.model.mode { + AppMode::Crop { .. } => AppMode::View, + _ => AppMode::Crop { + selection: CropSelection::default(), + }, }; } + AppMessage::ToggleScaleMode => { - app.model.tool_mode = if app.model.tool_mode == ToolMode::Scale { - ToolMode::None - } else { - ToolMode::Scale + // Scale mode -> Transform mode + app.model.mode = match &app.model.mode { + AppMode::Transform { .. } => AppMode::View, + _ => AppMode::Transform { + paper_format: None, + orientation: Default::default(), + }, }; } // ---- Crop operations ----------------------------------------------------- AppMessage::StartCrop => { if app.document_manager.current_document().is_some() { - app.model.tool_mode = ToolMode::Crop; - app.model.crop_selection.reset(); + app.model.mode = AppMode::Crop { + selection: CropSelection::default(), + }; } } + AppMessage::CancelCrop => { // Only cancel if actually in Crop mode - if app.model.tool_mode == ToolMode::Crop { - app.model.tool_mode = ToolMode::None; - app.model.crop_selection.reset(); + if matches!(app.model.mode, AppMode::Crop { .. }) { + app.model.mode = AppMode::View; } } + AppMessage::ApplyCrop => { - if app.model.tool_mode == ToolMode::Crop { + if let AppMode::Crop { selection } = &app.model.mode { // Get crop selection region - if let Some(crop_region) = app.model.crop_selection.to_crop_region() { + if let Some(crop_region) = selection.to_crop_region() { // Create crop command from canvas selection - let pan_offset = cosmic::iced::Vector::new(app.model.pan_x, app.model.pan_y); + let pan_offset = cosmic::iced::Vector::new( + app.model.viewport.pan_x, + app.model.viewport.pan_y, + ); match CropDocumentCommand::from_canvas_selection( &crop_region, - app.model.canvas_size, - app.model.image_size, - app.model.scale, + app.model.viewport.canvas_size, + app.model.viewport.image_size, + app.model.viewport.scale, pan_offset, ) { Ok(cmd) => { @@ -216,18 +210,13 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { if let Err(e) = cmd.execute(&mut app.document_manager) { app.model.set_error(format!("Crop failed: {e}")); } else { - // Success - exit crop mode and reset selection - app.model.tool_mode = ToolMode::None; - app.model.crop_selection.reset(); + // Success - exit crop mode + app.model.mode = AppMode::View; // Reset view to fit the cropped image - app.model.scale = 1.0; - app.model.view_mode = ViewMode::Fit; + app.model.viewport.scale = 1.0; + app.model.viewport.fit_mode = ViewMode::Fit; app.model.reset_pan(); - // Sync model after crop - crate::ui::sync::sync_model_from_manager( - &mut app.model, - &mut app.document_manager, - ); + cache_render(&mut app.model, &mut app.document_manager); } } Err(e) => { @@ -239,23 +228,26 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { } } } + AppMessage::CropDragStart { x, y, handle } => { - if app.model.tool_mode == ToolMode::Crop { + if let AppMode::Crop { selection } = &mut app.model.mode { if *handle == DragHandle::None { - app.model.crop_selection.start_new_selection(*x, *y); + selection.start_new_selection(*x, *y); } else { - app.model.crop_selection.start_handle_drag(*handle, *x, *y); + selection.start_handle_drag(*handle, *x, *y); } } } + AppMessage::CropDragMove { x, y, max_x, max_y } => { - if app.model.tool_mode == ToolMode::Crop { - app.model.crop_selection.update_drag(*x, *y, *max_x, *max_y); + if let AppMode::Crop { selection } = &mut app.model.mode { + selection.update_drag(*x, *y, *max_x, *max_y); } } + AppMessage::CropDragEnd => { - if app.model.tool_mode == ToolMode::Crop { - app.model.crop_selection.end_drag(); + if let AppMode::Crop { selection } = &mut app.model.mode { + selection.end_drag(); } } @@ -267,71 +259,72 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { // ---- Document transformations -------------------------------------------- AppMessage::FlipHorizontal => { // Ignore transformations in Crop mode (would invalidate selection) - if app.model.tool_mode != ToolMode::Crop { + if !matches!(app.model.mode, AppMode::Crop { .. }) { let cmd = TransformDocumentCommand::new(TransformOperation::FlipHorizontal); if let Err(e) = cmd.execute(&mut app.document_manager) { app.model.set_error(format!("Flip horizontal failed: {e}")); } else { - // Sync render data after transform - crate::ui::sync::sync_render_data(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } } + AppMessage::FlipVertical => { // Ignore transformations in Crop mode (would invalidate selection) - if app.model.tool_mode != ToolMode::Crop { + if !matches!(app.model.mode, AppMode::Crop { .. }) { let cmd = TransformDocumentCommand::new(TransformOperation::FlipVertical); if let Err(e) = cmd.execute(&mut app.document_manager) { app.model.set_error(format!("Flip vertical failed: {e}")); } else { - // Sync render data after transform - crate::ui::sync::sync_render_data(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } } + AppMessage::RotateCW => { // Ignore transformations in Crop mode (would invalidate selection) - if app.model.tool_mode != ToolMode::Crop { + if !matches!(app.model.mode, AppMode::Crop { .. }) { let cmd = TransformDocumentCommand::new(TransformOperation::RotateCw); if let Err(e) = cmd.execute(&mut app.document_manager) { app.model.set_error(format!("Rotate clockwise failed: {e}")); } else { - // Sync render data after transform - crate::ui::sync::sync_render_data(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } } + AppMessage::RotateCCW => { // Ignore transformations in Crop mode (would invalidate selection) - if app.model.tool_mode != ToolMode::Crop { + if !matches!(app.model.mode, AppMode::Crop { .. }) { let cmd = TransformDocumentCommand::new(TransformOperation::RotateCcw); if let Err(e) = cmd.execute(&mut app.document_manager) { app.model.set_error(format!("Rotate CCW failed: {e}")); } else { - // Sync render data after transform - crate::ui::sync::sync_render_data(&mut app.model, &mut app.document_manager); + cache_render(&mut app.model, &mut app.document_manager); } } } // ---- Metadata ------------------------------------------------------------ AppMessage::RefreshMetadata => { - // Metadata is already synced via DocumentManager - // Nothing to do here - } - - // ---- Wallpaper ----------------------------------------------------------- - AppMessage::SetAsWallpaper => { - set_as_wallpaper(&mut app.model, &app.document_manager); + // Metadata is managed by DocumentManager + // Nothing to do here - views access it directly } // ---- Format operations --------------------------------------------------- AppMessage::SetPaperFormat(format) => { - app.model.paper_format = Some(*format); + if let AppMode::Transform { paper_format, .. } = &mut app.model.mode { + *paper_format = Some(*format); + } } AppMessage::SetOrientation(orientation) => { - app.model.orientation = *orientation; + if let AppMode::Transform { + orientation: ori, .. + } = &mut app.model.mode + { + *ori = *orientation; + } } // ---- Menu ---------------------------------------------------------------- @@ -339,23 +332,31 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { app.model.menu_open = !app.model.menu_open; } - // ---- Format Panel -------------------------------------------------------- - AppMessage::OpenFormatPanel => { - // Close menu if open - app.model.menu_open = false; - // This is also handled in app.rs for nav bar toggling + // ---- Wallpaper ----------------------------------------------------------- + AppMessage::SetAsWallpaper => { + if let Some(path) = app.document_manager.current_path() { + log::info!("Setting wallpaper to: {}", path.display()); + crate::infrastructure::system::set_as_wallpaper(path); + } else { + app.model.set_error("No image loaded".to_string()); + } } // ---- Error handling ------------------------------------------------------ AppMessage::ShowError(msg) => { app.model.set_error(msg.clone()); } + AppMessage::ClearError => { app.model.clear_error(); } // ---- Handled elsewhere --------------------------------------------------- - AppMessage::ToggleContextPage(_) | AppMessage::ToggleNavBar => {} + AppMessage::ToggleContextPage(_) + | AppMessage::ToggleNavBar + | AppMessage::OpenFormatPanel => { + // These are handled in app.rs + } AppMessage::NoOp => {} } @@ -367,17 +368,27 @@ pub fn update(app: &mut NoctuaApp, msg: &AppMessage) -> UpdateResult { // Helper Functions // ============================================================================= -fn set_as_wallpaper(model: &mut AppModel, manager: &crate::application::DocumentManager) { - let Some(path) = manager.current_path() else { - model.set_error("No image loaded".to_string()); - return; - }; - - log::info!("Setting wallpaper to: {}", path.display()); - crate::infrastructure::system::set_as_wallpaper(path); +/// Cache rendered image handle in viewport for view performance. +fn cache_render( + model: &mut super::model::AppModel, + manager: &mut crate::application::DocumentManager, +) { + if let Some(doc) = manager.current_document_mut() { + match doc.render(model.viewport.scale as f64) { + Ok(output) => { + model.viewport.cached_image_handle = Some(output.handle); + } + Err(e) => { + log::error!("Failed to cache render: {e}"); + model.viewport.cached_image_handle = None; + } + } + } else { + model.viewport.cached_image_handle = None; + } } -fn save_as(model: &mut AppModel) { +fn save_as(model: &mut super::model::AppModel) { // TODO: Implement file dialog for save path // For now, show error that this needs UI integration model.set_error("Save As: File dialog not yet implemented".to_string()); diff --git a/src/ui/views/canvas.rs b/src/ui/views/canvas.rs index 3f9cdf5..b140374 100644 --- a/src/ui/views/canvas.rs +++ b/src/ui/views/canvas.rs @@ -10,7 +10,7 @@ use cosmic::widget::{container, text}; use cosmic::Element; use crate::ui::widgets::{crop_overlay, Viewer}; -use crate::ui::model::{ToolMode, ViewMode}; +use crate::ui::model::{AppMode, ViewMode}; use crate::ui::{AppMessage, AppModel}; use crate::application::DocumentManager; use crate::config::AppConfig; @@ -22,14 +22,24 @@ pub fn view<'a>( _manager: &'a DocumentManager, config: &'a AppConfig, ) -> Element<'a, AppMessage> { - if let Some(handle) = &model.current_image_handle { - let content_fit = match model.view_mode { + // Use cached image handle from viewport + if let Some(handle) = &model.viewport.cached_image_handle { + // Determine content fit mode + let content_fit = match model.viewport.fit_mode { ViewMode::Fit => ContentFit::Contain, ViewMode::ActualSize | ViewMode::Custom => ContentFit::None, }; - let img_viewer = Viewer::new(handle) - .with_state(model.scale, model.pan_x, model.pan_y) + // Check if we're in crop mode (to disable pan) + let disable_pan = matches!(model.mode, AppMode::Crop { .. }); + + // Create image viewer + let img_viewer = Viewer::new(handle.clone()) + .with_state( + model.viewport.scale, + model.viewport.pan_x, + model.viewport.pan_y, + ) .on_state_change(|scale, offset_x, offset_y, canvas_size, image_size| { AppMessage::ViewerStateChanged { scale, @@ -46,11 +56,11 @@ pub fn view<'a>( .min_scale(config.min_scale) .max_scale(config.max_scale) .scale_step(config.scale_step - 1.0) - .disable_pan(model.tool_mode == ToolMode::Crop); + .disable_pan(disable_pan); // Overlay crop UI when in crop mode - if model.tool_mode == ToolMode::Crop { - let overlay = crop_overlay(&model.crop_selection, config.crop_show_grid); + if let AppMode::Crop { selection } = &model.mode { + let overlay = crop_overlay(selection, config.crop_show_grid); stack![img_viewer, overlay].into() } else { container(img_viewer) @@ -59,6 +69,7 @@ pub fn view<'a>( .into() } } else { + // No document loaded container(text(fl!("no-document"))) .width(Length::Fill) .height(Length::Fill) diff --git a/src/ui/views/footer.rs b/src/ui/views/footer.rs index 267bfb5..b9770ea 100644 --- a/src/ui/views/footer.rs +++ b/src/ui/views/footer.rs @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/footer.rs +// src/ui/views/footer.rs // // Footer bar with zoom controls and document info. @@ -10,32 +10,34 @@ use cosmic::Element; use crate::ui::model::{AppModel, ViewMode}; use crate::ui::AppMessage; use crate::application::DocumentManager; +use crate::domain::document::core::document::Renderable; use crate::fl; /// Build the footer element with zoom controls and document info. -pub fn view<'a>(model: &'a AppModel, _manager: &'a DocumentManager) -> Element<'a, AppMessage> { - // Zoom level display - use scale as single source of truth. - let zoom_text = if model.view_mode == ViewMode::Fit { +pub fn view<'a>(model: &'a AppModel, manager: &'a DocumentManager) -> Element<'a, AppMessage> { + // Zoom level display + let zoom_text = if model.viewport.fit_mode == ViewMode::Fit { fl!("status-zoom-fit") } else { - // Use scale directly for accurate zoom display - let percent = (model.scale * 100.0).round() as i32; + let percent = (model.viewport.scale * 100.0).round() as i32; fl!("status-zoom-percent", percent: percent) }; - // Document dimensions (current after transformations). - let doc_info = if let Some((w, h)) = model.current_dimensions { - fl!("status-doc-dimensions", width: w, height: h) + // Document dimensions (from DocumentManager) + let doc_info = if let Some(doc) = manager.current_document() { + let info = doc.info(); + fl!("status-doc-dimensions", width: info.width, height: info.height) } else { String::new() }; - // Navigation position (e.g., "3 / 42"). - let nav_info = if model.folder_count == 0 { + // Navigation position (from DocumentManager) + let folder_count = manager.folder_entries().len(); + let nav_info = if folder_count == 0 { String::new() } else { - let current = model.current_index.map_or(0, |i| i + 1); - let total = model.folder_count; + let current = manager.current_index().map_or(0, |i| i + 1); + let total = folder_count; fl!("status-nav-position", current: current, total: total) }; @@ -43,37 +45,43 @@ pub fn view<'a>(model: &'a AppModel, _manager: &'a DocumentManager) -> Element<' .spacing(8) .align_y(Alignment::Center) .padding([4, 12]) - // Zoom out button. + // Zoom out button .push( button::icon(icon::from_name("zoom-out-symbolic")) .on_press(AppMessage::ZoomOut) .padding(4), ) - // Zoom level display. - .push(text::body(zoom_text)) - // Zoom in button. + // Zoom level text + .push(text(zoom_text)) + // Zoom in button .push( button::icon(icon::from_name("zoom-in-symbolic")) .on_press(AppMessage::ZoomIn) .padding(4), ) - // Fit button. + // Zoom reset button + .push( + button::icon(icon::from_name("zoom-original-symbolic")) + .on_press(AppMessage::ZoomReset) + .padding(4), + ) + // Zoom fit button .push( button::icon(icon::from_name("zoom-fit-best-symbolic")) .on_press(AppMessage::ZoomFit) .padding(4), ) - // Spacer. - .push(cosmic::widget::horizontal_space()) - // Document dimensions. - .push(text::body(doc_info)) - // Separator. - .push_maybe(if model.folder_count == 0 { + // Document dimensions + .push_maybe(if !doc_info.is_empty() { + Some(text(doc_info)) + } else { + None + }) + // Navigation info + .push_maybe(if folder_count == 0 { None } else { - Some(text::body(fl!("status-separator"))) + Some(text(nav_info)) }) - // Navigation position. - .push(text::body(nav_info)) .into() } diff --git a/src/ui/views/format_panel.rs b/src/ui/views/format_panel.rs index 0c63f6d..7863204 100644 --- a/src/ui/views/format_panel.rs +++ b/src/ui/views/format_panel.rs @@ -6,12 +6,21 @@ use cosmic::widget::{column, radio, text}; use cosmic::Element; -use crate::ui::model::{AppModel, Orientation, PaperFormat}; +use crate::ui::model::{AppMode, AppModel, Orientation, PaperFormat}; use crate::ui::AppMessage; use crate::fl; /// Build the format panel view for the navigation bar. pub fn view(model: &AppModel) -> Element<'static, AppMessage> { + // Extract values from Transform mode + let (paper_format, orientation) = match &model.mode { + AppMode::Transform { + paper_format, + orientation, + } => (*paper_format, *orientation), + _ => (None, Orientation::default()), + }; + let mut content = column::with_capacity(20).spacing(12).padding(16); // --- Format Section --- @@ -24,7 +33,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( "US Letter (216 × 279 mm)", PaperFormat::UsLetter, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -37,7 +46,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA0.display_name(), PaperFormat::IsoA0, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -46,7 +55,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA1.display_name(), PaperFormat::IsoA1, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -55,7 +64,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA2.display_name(), PaperFormat::IsoA2, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -64,7 +73,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA3.display_name(), PaperFormat::IsoA3, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -73,7 +82,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA4.display_name(), PaperFormat::IsoA4, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -82,7 +91,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA5.display_name(), PaperFormat::IsoA5, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -91,7 +100,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( PaperFormat::IsoA6.display_name(), PaperFormat::IsoA6, - model.paper_format, + paper_format, AppMessage::SetPaperFormat, ) .size(16), @@ -107,7 +116,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( "Horizontal", Orientation::Horizontal, - Some(model.orientation), + Some(orientation), AppMessage::SetOrientation, ) .size(16), @@ -118,7 +127,7 @@ pub fn view(model: &AppModel) -> Element<'static, AppMessage> { radio( "Vertical", Orientation::Vertical, - Some(model.orientation), + Some(orientation), AppMessage::SetOrientation, ) .size(16), diff --git a/src/ui/views/header.rs b/src/ui/views/header.rs index e0b51a3..ac65910 100644 --- a/src/ui/views/header.rs +++ b/src/ui/views/header.rs @@ -15,10 +15,10 @@ use crate::fl; /// Build the start (left) side of the header bar. pub fn start<'a>( - model: &'a AppModel, - _manager: &'a DocumentManager, + _model: &'a AppModel, + manager: &'a DocumentManager, ) -> Vec> { - let has_doc = model.current_image_handle.is_some(); + let has_doc = manager.current_document().is_some(); // Left section: Panel toggle + Menu + Navigation let left_controls = row() diff --git a/src/ui/views/meta_panel.rs b/src/ui/views/meta_panel.rs new file mode 100644 index 0000000..025d4fa --- /dev/null +++ b/src/ui/views/meta_panel.rs @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// src/ui/views/meta_panel.rs +// +// Metadata and properties panel for document information. + +use cosmic::iced::{Alignment, Length}; +use cosmic::widget::{button, column, divider, horizontal_space, icon, row, text}; +use cosmic::Element; + +use crate::application::DocumentManager; +use crate::domain::document::core::document::Renderable; +use crate::ui::{AppMessage, AppModel}; +use crate::fl; + +/// Build the metadata/properties panel view. +pub fn view(_model: &AppModel, manager: &DocumentManager) -> Element<'static, AppMessage> { + let mut content = column::with_capacity(16).spacing(8).padding(12); + + // Header with action icons + content = content.push(panel_header(manager)); + + // Display document metadata if available + if let Some(meta) = manager.current_metadata() { + // --- Basic Information Section --- + content = content + .push(section_header(fl!("meta-section-file"))) + .push(meta_row(fl!("meta-filename"), meta.basic.file_name.clone())) + .push(meta_row(fl!("meta-format"), meta.basic.format.clone())); + + // Show dimensions - original from metadata, current if transformed + let original_dims = (meta.basic.width, meta.basic.height); + let current_dims = if let Some(doc) = manager.current_document() { + let info = doc.info(); + (info.width, info.height) + } else { + (0, 0) + }; + + if original_dims != current_dims && current_dims != (0, 0) { + // Dimensions changed (e.g., rotation) - show both + content = content.push(meta_row( + fl!("meta-dimensions"), + format!( + "{} × {} (original: {} × {})", + current_dims.0, current_dims.1, original_dims.0, original_dims.1 + ), + )); + } else { + // No transformation or no document loaded yet + content = content.push(meta_row( + fl!("meta-dimensions"), + meta.basic.resolution_display(), + )); + } + + content = content + .push(meta_row( + fl!("meta-filesize"), + meta.basic.file_size_display(), + )) + .push(meta_row( + fl!("meta-colortype"), + meta.basic.color_type.clone(), + )); + + // --- EXIF Section (if available) --- + if let Some(ref exif) = meta.exif { + let has_exif_data = exif.camera_display().is_some() + || exif.date_time.is_some() + || exif.exposure_time.is_some() + || exif.f_number.is_some() + || exif.iso.is_some() + || exif.focal_length.is_some() + || exif.gps_display().is_some(); + + if has_exif_data { + content = content + .push(divider::horizontal::light()) + .push(section_header(fl!("meta-section-exif"))); + + if let Some(camera) = exif.camera_display() { + content = content.push(meta_row(fl!("meta-camera"), camera)); + } + + if let Some(ref date) = exif.date_time { + content = content.push(meta_row(fl!("meta-datetime"), date.clone())); + } + + if let Some(ref exposure) = exif.exposure_time { + content = content.push(meta_row(fl!("meta-exposure"), exposure.clone())); + } + + if let Some(ref fnumber) = exif.f_number { + content = content.push(meta_row(fl!("meta-aperture"), fnumber.clone())); + } + + if let Some(iso) = exif.iso { + content = content.push(meta_row(fl!("meta-iso"), format!("ISO {}", iso))); + } + + if let Some(ref focal) = exif.focal_length { + content = content.push(meta_row(fl!("meta-focal"), focal.clone())); + } + + if let Some(gps) = exif.gps_display() { + content = content.push(meta_row(fl!("meta-gps"), gps)); + } + } + } + + // --- File Path (at the bottom, less prominent) --- + content = content + .push(divider::horizontal::light()) + .push(meta_row_small( + fl!("meta-path"), + meta.basic.file_path.clone(), + )); + } else { + // No document loaded + content = content + .push(vertical_space()) + .push(text::body(fl!("no-document"))) + .push(vertical_space()); + } + + content.into() +} + +// ============================================================================= +// Helper Components +// ============================================================================= + +/// Panel header with title and action buttons. +fn panel_header(manager: &DocumentManager) -> Element<'static, AppMessage> { + let has_doc = manager.current_document().is_some(); + + row::with_capacity(5) + .spacing(4) + .align_y(Alignment::Center) + .padding([0, 0, 8, 0]) + .push(text::title4(fl!("panel-properties"))) + .push(horizontal_space().width(Length::Fill)) + .push( + button::icon(icon::from_name("image-x-generic-symbolic")) + .tooltip(fl!("action-set-wallpaper")) + .padding(4) + .on_press_maybe(has_doc.then_some(AppMessage::SetAsWallpaper)), + ) + .into() +} + +/// Section header for grouping metadata. +fn section_header(label: String) -> Element<'static, AppMessage> { + text::heading(label).size(14).into() +} + +/// Key-value metadata row. +fn meta_row(label: String, value: String) -> Element<'static, AppMessage> { + column::with_capacity(2) + .spacing(2) + .push(text::caption(format!("{}:", label))) + .push(text::body(value)) + .into() +} + +/// Less prominent metadata row (smaller text). +fn meta_row_small(label: String, value: String) -> Element<'static, AppMessage> { + column::with_capacity(2) + .spacing(2) + .push(text::caption(format!("{}:", label))) + .push(text::caption(value)) + .into() +} + +/// Vertical spacer helper. +fn vertical_space() -> Element<'static, AppMessage> { + cosmic::widget::vertical_space() + .height(Length::Fixed(32.0)) + .into() +} diff --git a/src/ui/views/mod.rs b/src/ui/views/mod.rs index 0fd30a5..439ae5d 100644 --- a/src/ui/views/mod.rs +++ b/src/ui/views/mod.rs @@ -7,6 +7,7 @@ pub mod canvas; pub mod footer; pub mod format_panel; pub mod header; +pub mod meta_panel; pub mod pages_panel; pub mod panels; @@ -14,7 +15,7 @@ use cosmic::iced::Length; use cosmic::widget::container; use cosmic::{Action, Element}; -use crate::ui::model::NavPanel; +use crate::ui::model::LeftPanel; use crate::ui::{AppMessage, AppModel}; use crate::application::DocumentManager; use crate::config::AppConfig; @@ -30,39 +31,21 @@ pub fn view<'a>( /// Navigation bar content (left panel). /// -/// Shows different panels based on `active_nav_panel` state: -/// - `NavPanel::Format`: Format and orientation selection -/// - `NavPanel::Pages`: Page thumbnails (multi-page documents) -/// - `NavPanel::None`: Hidden +/// Shows different panels based on panel state: +/// - `LeftPanel::Thumbnails`: Page thumbnails (multi-page documents) +/// - `None`: Hidden pub fn nav_bar<'a>( model: &'a AppModel, manager: &'a DocumentManager, ) -> Option>> { - match model.active_nav_panel { - NavPanel::None => None, - NavPanel::Format => { - let panel = format_panel::view(model); - Some( - container(panel.map(Action::App)) - .width(Length::Shrink) - .height(Length::Fill) - .max_width(250) - .into(), - ) - } - NavPanel::Pages => { - // Check if document has multiple pages using cached data - if model.page_count.unwrap_or(1) <= 1 { - return None; - } - - pages_panel::view(model, manager).map(|panel| { - container(panel.map(Action::App)) - .width(Length::Shrink) - .height(Length::Fill) - .max_width(200) - .into() - }) - } + match model.panels.left { + None => None, + Some(LeftPanel::Thumbnails) => pages_panel::view(model, manager).map(|panel| { + container(panel.map(Action::App)) + .width(Length::Shrink) + .height(Length::Fill) + .max_width(250) + .into() + }), } } diff --git a/src/ui/views/pages_panel.rs b/src/ui/views/pages_panel.rs index 2f0d6f6..c2ab024 100644 --- a/src/ui/views/pages_panel.rs +++ b/src/ui/views/pages_panel.rs @@ -19,19 +19,18 @@ use crate::fl; /// Build the page navigation panel view. /// Returns None if the current document doesn't support multiple pages. pub fn view<'a>( - model: &'a AppModel, + _model: &'a AppModel, manager: &'a DocumentManager, ) -> Option> { - // Only show for multi-page documents. - let page_count = model.page_count?; + // Get document and check if it's multi-page + let doc = manager.current_document()?; + let page_count = doc.page_count(); + if page_count <= 1 { return None; } - let current_page = model.current_page.unwrap_or(0); - - // Get document for thumbnail loading status - let doc = manager.current_document()?; + let current_page = doc.current_page(); let loaded = doc.thumbnails_loaded(); let mut content = column::with_capacity(page_count + 1) diff --git a/src/ui/views/panels.rs b/src/ui/views/panels.rs index 224a26d..5879d8e 100644 --- a/src/ui/views/panels.rs +++ b/src/ui/views/panels.rs @@ -1,167 +1,43 @@ // SPDX-License-Identifier: GPL-3.0-or-later -// src/app/view/panels.rs +// src/ui/views/panels.rs // -// Properties panel content for COSMIC context drawer. +// Panel router - delegates to specific panel views. -use cosmic::iced::Length; -use cosmic::widget::{button, column, divider, horizontal_space, icon, row, text}; use cosmic::Element; -use crate::ui::{AppMessage, AppModel}; -use crate::fl; use crate::application::DocumentManager; +use crate::ui::model::{AppModel, RightPanel}; +use crate::ui::AppMessage; -/// Build the properties panel view. +use super::{format_panel, meta_panel}; + +/// Build the right panel view based on current panel state. +/// +/// Returns the appropriate panel content: +/// - `RightPanel::Properties`: Metadata and document properties (default) +/// - `RightPanel::CropTools`: Crop tool controls (TODO) +/// - `RightPanel::TransformTools`: Transform/export controls +/// +/// Defaults to Properties panel if no panel is explicitly set. pub fn view(model: &AppModel, manager: &DocumentManager) -> Element<'static, AppMessage> { - let mut content = column::with_capacity(16).spacing(8); - - // Header with action icons - content = content.push(panel_header(model, manager)); - - // Display document metadata if available (cached in model). - if let Some(meta) = manager.current_metadata() { - // --- Basic Information Section --- - content = content - .push(section_header(fl!("meta-section-file"))) - .push(meta_row(fl!("meta-filename"), meta.basic.file_name.clone())) - .push(meta_row(fl!("meta-format"), meta.basic.format.clone())); - - // Show dimensions - original from metadata, current if transformed - let original_dims = (meta.basic.width, meta.basic.height); - let current_dims = model.current_dimensions.unwrap_or((0, 0)); - - if original_dims != current_dims && current_dims != (0, 0) { - // Dimensions changed (e.g., rotation) - show both - content = content.push(meta_row( - fl!("meta-dimensions"), - format!( - "{} × {} (original: {} × {})", - current_dims.0, current_dims.1, original_dims.0, original_dims.1 - ), - )); - } else { - // No transformation or no document loaded yet - content = content.push(meta_row( - fl!("meta-dimensions"), - meta.basic.resolution_display(), - )); - } - - content = content - .push(meta_row( - fl!("meta-filesize"), - meta.basic.file_size_display(), - )) - .push(meta_row( - fl!("meta-colortype"), - meta.basic.color_type.clone(), - )); - - // --- EXIF Section (if available) --- - if let Some(ref exif) = meta.exif { - let has_exif_data = exif.camera_display().is_some() - || exif.date_time.is_some() - || exif.exposure_time.is_some() - || exif.f_number.is_some() - || exif.iso.is_some() - || exif.focal_length.is_some() - || exif.gps_display().is_some(); - - if has_exif_data { - content = content - .push(divider::horizontal::light()) - .push(section_header(fl!("meta-section-exif"))); - - if let Some(camera) = exif.camera_display() { - content = content.push(meta_row(fl!("meta-camera"), camera)); - } - - if let Some(ref date) = exif.date_time { - content = content.push(meta_row(fl!("meta-datetime"), date.clone())); - } - - if let Some(ref exposure) = exif.exposure_time { - content = content.push(meta_row(fl!("meta-exposure"), exposure.clone())); - } - - if let Some(ref fnumber) = exif.f_number { - content = content.push(meta_row(fl!("meta-aperture"), fnumber.clone())); - } - - if let Some(iso) = exif.iso { - content = content.push(meta_row(fl!("meta-iso"), fl!("meta-iso", iso: iso))); - } - - if let Some(ref focal) = exif.focal_length { - content = content.push(meta_row(fl!("meta-focal"), focal.clone())); - } - - if let Some(gps) = exif.gps_display() { - content = content.push(meta_row(fl!("meta-gps"), gps)); - } - } - } - - // --- File Path (at the bottom, less prominent) --- - content = content - .push(divider::horizontal::light()) - .push(meta_row_small( - fl!("meta-path"), - meta.basic.file_path.clone(), - )); - } else { - content = content.push(text::body(fl!("no-document"))); + match model.panels.right.as_ref() { + Some(RightPanel::Properties) | None => meta_panel::view(model, manager), + Some(RightPanel::CropTools) => crop_tools_panel(model, manager), + Some(RightPanel::TransformTools) => format_panel::view(model), } - - content.into() } -/// Section header for grouping metadata. -fn section_header(label: String) -> Element<'static, AppMessage> { - text::body(label).into() -} +/// Crop tools panel (TODO: implement dedicated crop controls). +fn crop_tools_panel(_model: &AppModel, _manager: &DocumentManager) -> Element<'static, AppMessage> { + use cosmic::widget::{column, text}; -/// Helper to create a key-value metadata row. -fn meta_row(label: String, value: String) -> Element<'static, AppMessage> { - row::with_capacity(2) - .spacing(8) - .push(text::body(format!("{label}:"))) - .push(text::body(value)) - .into() -} - -/// Helper for less prominent metadata (smaller text, e.g., file path). -fn meta_row_small(label: String, value: String) -> Element<'static, AppMessage> { - column::with_capacity(2) - .spacing(2) - .push(text::caption(format!("{label}:"))) - .push(text::caption(value)) - .into() -} - -/// Panel header with title and action icon buttons. -fn panel_header(model: &AppModel, _manager: &DocumentManager) -> Element<'static, AppMessage> { - let has_doc = model.current_image_handle.is_some(); - - row::with_capacity(5) - .spacing(4) - .align_y(cosmic::iced::Alignment::Center) - .push(text::title4(fl!("panel-properties"))) - .push(horizontal_space().width(Length::Fill)) - .push( - button::icon(icon::from_name("image-x-generic-symbolic")) - .tooltip(fl!("action-set-wallpaper")) - .on_press_maybe(has_doc.then_some(AppMessage::SetAsWallpaper)), - ) - // .push( - // button::icon(icon::from_name("system-run-symbolic")) - // .on_press_maybe(has_doc.then_some(AppMessage::NoOp)) // TODO: Implement - // .tooltip(fl!("action-open-with")) - // ) - // .push( - // button::icon(icon::from_name("system-file-manager-symbolic")) - // .on_press_maybe(has_doc.then_some(AppMessage::NoOp)) // TODO: Implement - // .tooltip(fl!("action-show-in-folder")) - // ) + column::with_capacity(4) + .spacing(12) + .padding(12) + .push(text::title4("Crop Tools")) + .push(text::body("Crop controls will be implemented here.")) + .push(text::caption( + "For now, use the crop overlay on the canvas.", + )) .into() }