8 Commits
main ... review

Author SHA1 Message Date
02edb186bb feat: Implement photo capture functionality
Implement the logic for capturing and saving photos from the live preview stream.

- Add `save_photo` method to `MediaRepository` to handle file saving and database updates.
- `MainController` now tracks the selected color and the latest camera frame.
- The "Take Photo" button is enabled only when a color is selected.
- Pressing the button saves the current preview frame to the correct media folder and refreshes the thumbnail list.
- Fixes indentation issues in `main_controller.py` caused by previous faulty replacements.
2025-10-14 10:04:01 +02:00
47c1e6040a feat: Implement State pattern for camera logic
Refactor camera control logic in `MainController` to use the State design pattern.

- Create a new `core/camera/states.py` module with state classes (`NoCamerasState`, `DetectingState`, `ReadyToStreamState`, `StreamingState`).
- `MainController` now acts as a context, delegating actions to the current state object.
- This replaces complex conditional logic with a robust, scalable, and maintainable state machine, making it easier to manage camera behavior and add new states in the future.
2025-10-14 09:03:19 +02:00
c8d9029df7 refactor: Implement Singleton pattern for DatabaseManager
Refactor the database handling to use a Singleton pattern for `DatabaseManager`.

- A single, module-level instance `db_manager` is created in `core/database.py` to ensure one database connection is used throughout the application.
- `MediaRepository` and `MainController` are updated to use this shared instance instead of creating their own.
- This simplifies dependency injection and prevents potential issues with multiple database connections.
- Also, update `review.md` to reflect the progress.
2025-10-14 08:55:45 +02:00
96c2495a8b refactor(MainController): Simplify camera button signal handling
Refactor the signal handling for the camera start/stop button in `MainController` to simplify logic and improve reliability.

- Replaced the dynamic connect/disconnect pattern with a single, persistent signal connection to `_on_start_button_clicked`.
- Centralized UI state updates (e.g., button text) into a new `_update_start_button_state` method.
- This eliminates potential errors from mismatched signal connections and makes the control flow easier to follow.
- Also fixes major indentation errors caused by a previous faulty replacement.
2025-10-14 08:49:20 +02:00
03ab345e17 refactor(SplitView): Improve image rotation logic
Refactor the image rotation mechanism in the `SplitView` widget to prevent image quality degradation.

- The original reference pixmap is now stored in `self.original_ref_pixmap`.
- Rotations are always applied to the original, unmodified pixmap, using a cumulative rotation angle.
- This avoids sequential transformations that caused gradual quality loss.
- Also fixes indentation issues caused by previous automated replacements.
2025-10-14 08:41:47 +02:00
d62b367b47 refactor: Centralize resource path management
Create a new `settings.py` file to define and manage global application paths.

Modify UI components (`split_view_widget.py`, `view_settings_dialog.py`) to use the centralized `ICONS_DIR` path constant instead of hardcoded relative paths for icons. This improves maintainability and makes the application independent of the working directory.
2025-10-14 08:33:08 +02:00
bbdb7d3459 delete unused files 2025-10-14 08:27:40 +02:00
fa84a29ab5 review concept 2025-10-14 08:26:41 +02:00
10 changed files with 261 additions and 311 deletions

View File

@@ -1,97 +0,0 @@
# import gphoto2 as gp
import numpy as np
import cv2
from PySide6.QtCore import QObject, QThread, Signal
from PySide6.QtGui import QImage, QPixmap
# try:
# import gphoto2 as gp
# except:
from . import mock_gphoto as gp
class CameraWorker(QObject):
frameReady = Signal(QPixmap)
errorOccurred = Signal(str)
def __init__(self, fps: int = 15, parent=None):
super().__init__(parent)
self.fps = fps
self.running = False
self.camera = None
def start_camera(self):
"""Uruchom kamerę i zacznij pobierać klatki"""
try:
self.camera = gp.Camera() # type: ignore
self.camera.init()
self.running = True
self._capture_loop()
except gp.GPhoto2Error as e:
self.errorOccurred.emit(f"Błąd inicjalizacji kamery: {e}")
def stop_camera(self):
"""Zatrzymaj pobieranie"""
self.running = False
if self.camera:
try:
self.camera.exit()
except gp.GPhoto2Error:
pass
self.camera = None
def _capture_loop(self):
"""Pętla odczytu klatek w osobnym wątku"""
import time
delay = 1.0 / self.fps
while self.running:
try:
file = self.camera.capture_preview() # type: ignore
data = file.get_data_and_size()
frame = np.frombuffer(data, dtype=np.uint8)
frame = cv2.imdecode(frame, cv2.IMREAD_COLOR)
if frame is not None:
rgb_image = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
h, w, ch = rgb_image.shape
qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format.Format_RGB888)
pixmap = QPixmap.fromImage(qimg)
self.frameReady.emit(pixmap)
except gp.GPhoto2Error as e:
self.errorOccurred.emit(f"Błąd odczytu LiveView: {e}")
break
except Exception as e:
self.errorOccurred.emit(f"Nieoczekiwany błąd: {e}")
break
time.sleep(delay)
class CameraController(QObject):
frameReady = Signal(QPixmap)
errorOccurred = Signal(str)
def __init__(self, fps: int = 15, parent=None):
super().__init__(parent)
self.camera_thread = QThread()
self.worker = CameraWorker(fps)
self.worker.moveToThread(self.camera_thread )
# sygnały z workera
self.worker.frameReady.connect(self.frameReady)
self.worker.errorOccurred.connect(self.errorOccurred)
# sygnały start/stop
self.camera_thread.started.connect(self.worker.start_camera)
def start(self):
"""Start kamery w osobnym wątku"""
self.camera_thread.start()
def stop(self):
"""Stop kamery i zakończenie wątku"""
self.worker.stop_camera()
self.camera_thread.quit()
self.camera_thread.wait()

View File

@@ -3,21 +3,26 @@ from PySide6.QtCore import Slot
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QPushButton
from core.database import DatabaseManager
from core.database import db_manager
from core.media import MediaRepository
from core.camera.camera_manager import CameraManager
from ui.widgets.color_list_widget import ColorListWidget
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
from ui.widgets.split_view_widget import SplitView, CameraPlaceholder, ViewWithOverlay
from core.camera.states import CameraState, NoCamerasState, ReadyToStreamState, StreamingState, DetectingState
class MainController:
def __init__(self, view):
self.view = view
self.db = DatabaseManager()
self.media_repo = MediaRepository(self.db)
self.db = db_manager
self.media_repo = MediaRepository()
self.camera_manager = CameraManager()
# --- State ---
self.selected_color_name: str | None = None
self._latest_pixmap: QPixmap | None = None
# --- UI Widgets ---
self.color_list: ColorListWidget = view.color_list_widget
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
@@ -31,7 +36,19 @@ class MainController:
self.db.connect()
self.media_repo.sync_media()
self.camera_manager.detect_cameras()
# Disable button by default
self.photo_button.setEnabled(False)
# Initialize state machine
self.state: CameraState = NoCamerasState()
self.state.enter_state(self)
def transition_to(self, new_state: CameraState):
"""Transitions the controller to a new state."""
print(f"Transitioning to state: {new_state.__class__.__name__}")
self.state = new_state
self.state.enter_state(self)
def _connect_signals(self):
"""Connects all signals to slots."""
@@ -49,7 +66,7 @@ class MainController:
# UI control signals
self.photo_button.clicked.connect(self.take_photo)
self.welcome_view.camera_start_btn.clicked.connect(self.camera_detect)
self.welcome_view.camera_start_btn.clicked.connect(self._on_start_button_clicked)
self.live_view.rotateCW.connect(self.camera_manager.rotate_right)
self.live_view.rotateCCW.connect(self.camera_manager.rotate_left)
@@ -67,6 +84,9 @@ class MainController:
@Slot(str)
def on_color_selected(self, color_name: str):
self.selected_color_name = color_name
self.photo_button.setEnabled(True)
color_id = self.db.get_color_id(color_name)
if color_id is not None:
media_items = self.db.get_media_for_color(color_id)
@@ -90,57 +110,49 @@ class MainController:
@Slot(list)
def on_cameras_detected(self, cameras: list[dict]):
"""Handles the list of detected cameras."""
"""Handles the list of detected cameras and transitions state."""
print("Detected cameras:", cameras)
self.welcome_view.set_info_text(f"Detected {len(cameras)} cameras.")
self.welcome_view.camera_start_btn.clicked.disconnect()
if len(cameras) == 0:
self.welcome_view.set_button_text("Wykryj kamery")
self.welcome_view.camera_start_btn.clicked.connect(self.camera_detect)
if cameras:
self.transition_to(ReadyToStreamState())
else:
self.welcome_view.set_button_text("Uruchom kamere")
self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview)
# Populate a combobox in the UI here
# self.view.camera_combobox.clear()
# for camera in cameras:
# self.view.camera_combobox.addItem(camera['name'], userData=camera['id'])
self.transition_to(NoCamerasState())
@Slot(QPixmap)
def on_frame_ready(self, pixmap: QPixmap):
"""Displays a new frame from the camera."""
"""Displays a new frame from the camera and stores it."""
self._latest_pixmap = pixmap
self.split_view.set_live_image(pixmap)
@Slot(str)
def on_camera_error(self, error_message: str):
"""Shows an error message from the camera."""
"""Shows an error message from the camera and transitions to a safe state."""
print(f"Camera Error: {error_message}")
self.welcome_view.set_error_text(error_message)
self.transition_to(NoCamerasState())
@Slot()
def on_camera_started(self):
"""Updates UI when the camera stream starts."""
self.split_view.toggle_live_view()
self.welcome_view.set_button_text("Stop Camera")
# Re-route button click to stop the camera
self.welcome_view.camera_start_btn.clicked.disconnect()
self.welcome_view.camera_start_btn.clicked.connect(self.stop_liveview)
"""Transitions to StreamingState when the camera starts."""
self.transition_to(StreamingState())
@Slot()
def on_camera_stopped(self):
"""Updates UI when the camera stream stops."""
# self.split_view.show_placeholder()
self.welcome_view.set_button_text("Start Camera")
# Re-route button click to start the camera
self.welcome_view.camera_start_btn.clicked.disconnect()
self.welcome_view.camera_start_btn.clicked.connect(self.start_liveview)
"""Transitions to a post-streaming state."""
self.split_view.toggle_live_view()
if self.camera_manager.get_detected_cameras():
self.transition_to(ReadyToStreamState())
else:
self.transition_to(NoCamerasState())
# --- UI Actions ---
def _on_start_button_clicked(self):
"""Delegates the button click to the current state."""
self.state.handle_start_button(self)
def camera_detect(self):
"""Initiates camera detection and transitions to DetectingState."""
self.transition_to(DetectingState())
self.camera_manager.detect_cameras()
def start_liveview(self):
@@ -150,8 +162,6 @@ class MainController:
self.on_camera_error("No cameras detected.")
return
# For now, just start the first detected camera.
# In a real app, you'd get the selected camera ID from the UI.
camera_id = detected_cameras[0]['id']
self.camera_manager.start_camera(camera_id)
@@ -161,7 +171,17 @@ class MainController:
def take_photo(self):
"""Takes a photo with the active camera."""
print("Taking photo...") # Placeholder
# This needs to be implemented in CameraManager and called here.
# e.g., self.camera_manager.take_photo()
self.split_view.toggle_live_view() # This seems like a UI toggle, maybe rename?
if self.selected_color_name is None:
print("Cannot take photo: No color selected.")
# Optionally: show a message to the user in the UI
return
if self._latest_pixmap is None or self._latest_pixmap.isNull():
print("Cannot take photo: No frame available.")
return
print(f"Taking photo for color: {self.selected_color_name}")
self.media_repo.save_photo(self._latest_pixmap, self.selected_color_name)
# Refresh thumbnail list
self.on_color_selected(self.selected_color_name)

View File

@@ -1,148 +0,0 @@
import cv2
import numpy as np
GP_WIDGET_WINDOW = 0
GP_WIDGET_SECTION = 0
GP_WIDGET_TEXT = 0
GP_WIDGET_RANGE = 0
GP_WIDGET_TOGGLE = 0
GP_WIDGET_RADIO = 0
GP_WIDGET_MENU = 0
GP_WIDGET_BUTTON = 0
GP_WIDGET_DATE = 0
class GPhoto2Error(Exception):
pass
class CameraFileMock:
"""Mock obiektu zwracanego przez gphoto2.Camera.capture_preview()"""
def __init__(self, frame: np.ndarray):
# Kodowanie do JPEG, żeby symulować prawdziwe dane z kamery
success, buf = cv2.imencode(".jpg", frame)
if not success:
raise GPhoto2Error("Nie udało się zakodować ramki testowej.")
self._data = buf.tobytes()
def get_data_and_size(self):
return self._data
return self._data, len(self._data)
class CameraListMock:
def count(self):
return 1
def get_name(self, idx):
return f"mock_name {idx}"
def get_value(self, idx):
return f"mock_value {idx}"
class MockPortInfo:
def __init__(self, address):
self.address = address
class PortInfoList:
def __init__(self):
self._ports = []
def load(self):
# Dodaj przykładowe porty
self._ports = [MockPortInfo("usb:001,002"), MockPortInfo("usb:001,003")]
def lookup_path(self, port_address):
for idx, port in enumerate(self._ports):
if port.address == port_address:
return idx
raise ValueError("Port not found")
def __getitem__(self, idx):
return self._ports[idx]
class ConfigMock:
def get_id(self):
return 0
def get_name(self):
return "name"
def get_label(self):
return "label"
def get_type(self):
return 0
def get_value(self):
return "value"
def get_choices(self):
return []
def count_children(self):
return 0
def get_child(self):
return ConfigMock()
class CameraAbilitiesList:
def __init__(self) -> None:
self.abilities = []
def load(self):
return
def lookup_model(self, name):
return 1
def get_abilities(self, abilities_index):
return 0
class Camera:
def __init__(self):
self._frame_counter = 0
self._running = False
def init(self):
self._running = True
print("[my_gphoto] Kamera MOCK zainicjalizowana")
def exit(self):
self._running = False
print("[my_gphoto] Kamera MOCK wyłączona")
def capture_preview(self):
if not self._running:
raise GPhoto2Error("Kamera MOCK nie jest uruchomiona")
# przykład 1: wczytaj stały obrazek z pliku
# frame = cv2.imread("test_frame.jpg")
# if frame is None:
# raise GPhoto2Error("Nie znaleziono test_frame.jpg")
# przykład 2: wygeneruj kolorową planszę
h, w = 480, 640
color = (self._frame_counter % 255, 100, 200)
frame = np.full((h, w, 3), color, dtype=np.uint8)
# dodanie napisu
text = "OBRAZ TESTOWY"
font = cv2.FONT_HERSHEY_SIMPLEX
scale = 1.5
thickness = 3
color_text = (255, 255, 255)
(text_w, text_h), _ = cv2.getTextSize(text, font, scale, thickness)
x = (w - text_w) // 2
y = (h + text_h) // 2
cv2.putText(frame, text, (x, y), font, scale, color_text, thickness, cv2.LINE_AA)
self._frame_counter += 1
return CameraFileMock(frame)
def set_port_info(self, obj):
return False
def get_config(self):
return ConfigMock()
def set_single_config(self, name, widget):
return True
def gp_camera_autodetect():
return CameraListMock()
def check_result(obj):
return obj

57
core/camera/states.py Normal file
View File

@@ -0,0 +1,57 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from controllers.main_controller import MainController
class CameraState(ABC):
"""Abstract base class for all camera states."""
def enter_state(self, controller: MainController):
"""Called upon entering the state, e.g., to update the UI."""
pass
@abstractmethod
def handle_start_button(self, controller: MainController):
"""Handles the main camera button click."""
pass
class NoCamerasState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykryj kamery")
controller.welcome_view.set_info_text("Nie wykryto kamer. Kliknij, aby rozpocząć.")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.camera_detect()
class DetectingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Wykrywanie...")
controller.welcome_view.set_info_text("Trwa wykrywanie kamer...")
controller.welcome_view.camera_start_btn.setEnabled(False)
def handle_start_button(self, controller: MainController):
# Do nothing while detecting
pass
class ReadyToStreamState(CameraState):
def enter_state(self, controller: MainController):
cameras = controller.camera_manager.get_detected_cameras()
controller.welcome_view.set_button_text("Uruchom kamerę")
controller.welcome_view.set_info_text(f"Wykryto {len(cameras)} kamer(y).")
controller.welcome_view.camera_start_btn.setEnabled(True)
def handle_start_button(self, controller: MainController):
controller.start_liveview()
class StreamingState(CameraState):
def enter_state(self, controller: MainController):
controller.welcome_view.set_button_text("Zatrzymaj kamerę")
controller.welcome_view.camera_start_btn.setEnabled(True)
if controller.split_view.stack.currentWidget() != controller.live_view:
controller.split_view.toggle_live_view()
def handle_start_button(self, controller: MainController):
controller.stop_liveview()

View File

@@ -154,3 +154,6 @@ class DatabaseManager:
cur = self.conn.cursor()
cur.execute("DELETE FROM media WHERE color_id = ?", (color_id,))
self.conn.commit()
db_manager = DatabaseManager()

View File

@@ -1,13 +1,40 @@
from pathlib import Path
import shutil
from core.database import DatabaseManager
from datetime import datetime
from PySide6.QtGui import QPixmap
from core.database import db_manager
MEDIA_DIR = Path("media")
DEFAULT_ICON = Path("media/default_icon.jpg")
class MediaRepository:
def __init__(self, db: DatabaseManager):
self.db = db
def __init__(self):
self.db = db_manager
def save_photo(self, pixmap: QPixmap, color_name: str):
"""Saves a pixmap to the media directory for the given color."""
color_id = self.db.get_color_id(color_name)
if color_id is None:
print(f"Error: Could not find color ID for {color_name}")
return
# Create directory if it doesn't exist
color_dir = MEDIA_DIR / color_name
color_dir.mkdir(parents=True, exist_ok=True)
# Generate unique filename
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
filename = f"photo_{timestamp}.jpg"
file_path = color_dir / filename
# Save the pixmap
if not pixmap.save(str(file_path), "JPG", 95):
print(f"Error: Failed to save pixmap to {file_path}")
return
# Add to database
self.db.add_media(color_id, str(file_path), 'photo')
print(f"Saved photo to {file_path}")
def sync_media(self):
disk_colors = {d.name for d in MEDIA_DIR.iterdir() if d.is_dir()}

72
review.md Normal file
View File

@@ -0,0 +1,72 @@
Przejrzałem kod Twojej aplikacji. Gratuluję, to bardzo dobrze zorganizowany projekt, zwłaszcza jak na aplikację, która jest jeszcze w trakcie rozwoju. Widać dużą dbałość o architekturę i separację poszczególnych warstw.
Poniżej znajdziesz moje uwagi podzielone na kategorie, o które prosiłeś.
### 1. Architektura Aplikacji
Twoja architektura jest największym plusem projektu.
* **Wzorzec MVC/Controller:** Podział na foldery `core` (Model), `ui` (View) i `controllers` (Controller) jest świetnym zastosowaniem tego wzorca. `MainController` dobrze pełni rolę pośrednika, oddzielając logikę biznesową od interfejsu użytkownika.
* **Podsystem kamery (`core/camera`):** To jest wzorowo zaprojektowana część aplikacji.
* **Fasada (`CameraManager`):** Użycie `CameraManager` jako fasady, która ukrywa skomplikowaną logikę wyboru i zarządzania kamerą, jest doskonałym pomysłem. Upraszcza to reszcie aplikacji interakcję z kamerami.
* **Strategia (`BaseCamera`):** Abstrakcyjna klasa `BaseCamera` i jej konkretne implementacje (`GPhotoCamera`, `OpenCvCamera`) to świetne użycie wzorca strategii. Pozwala to na łatwe dodawanie nowych typów kamer w przyszłości.
* **Wątkowość:** Przeniesienie obsługi kamery do osobnego wątku (`CameraWorker` i `QThread`) jest kluczowe dla aplikacji Qt i zostało zrobione poprawnie. Dzięki temu interfejs użytkownika nie zawiesza się podczas przechwytywania obrazu.
* **Problem do rozwiązania:**
* **Zduplikowane pliki:** Zauważyłem, że w projekcie istnieją dwa pliki o nazwie `camera_controller.py` (w `controllers/` i `core/camera/`) oraz dwa pliki `mock_gphoto.py`. Wygląda na to, że te w folderze `controllers/` są starszymi, nieużywanymi wersjami. **Sugeruję ich usunięcie**, aby uniknąć pomyłek w przyszłości. Nowsze wersje w `core/camera/` są znacznie bardziej rozbudowane i bezpieczniejsze (np. używają `QMutex`).
### 2. Co można napisać lepiej? (Sugestie i Ulepszenia)
* **Zarządzanie ścieżkami i zasobami:**
* W kodzie UI (np. w `ui/widgets/split_view_widget.py`) ścieżki do ikon są wpisane na stałe (np. `"ui/icons/rotate-cw-svgrepo-com.svg"`). To może powodować problemy, jeśli uruchomisz aplikację z innego katalogu.
* **Sugestia:** Użyj biblioteki `pathlib` w połączeniu z globalną stałą, aby tworzyć absolutne ścieżki do zasobów. Możesz stworzyć plik `settings.py` lub `config.py`, w którym zdefiniujesz główny katalog aplikacji i podkatalogi z zasobami.
* **Logika rotacji obrazu referencyjnego:**
* W `SplitView.rotate_left()` i `rotate_right()`, transformujesz pixmapę, która mogła być już wcześniej obrócona (`self.ref_pixmap = self.ref_pixmap.transformed(...)`). Każda taka operacja może prowadzić do utraty jakości obrazu.
* **Sugestia:** Przechowuj oryginalny, niezmodyfikowany `QPixmap` w osobnej zmiennej. Przy każdej rotacji transformuj ten oryginalny obraz o łączny kąt obrotu, a nie wynik poprzedniej transformacji.
* **Upraszczanie logiki sygnałów w `MainController`:**
* W metodzie `on_cameras_detected` i innych, wielokrotnie rozłączasz i podłączasz sygnały do slotów przycisku (`camera_start_btn.clicked.disconnect()`). Jest to podejście podatne na błędy.
* **Sugestia:** Zamiast tego, użyj jednego slotu, który w środku sprawdza aktualny stan aplikacji (np. `if self.camera_manager.is_camera_active(): ...`). Możesz też po prostu zmieniać tekst i stan (`setEnabled`) przycisków w zależności od kontekstu.
* **Obsługa błędów:**
* W niektórych miejscach (np. `GPhotoCamera.detect`) używasz szerokiego `except Exception as e:`. Warto łapać bardziej specyficzne wyjątki (np. `gp.GPhoto2Error`), aby lepiej reagować na konkretne problemy.
### 3. Wzorce projektowe do rozważenia
Używasz już wielu dobrych wzorców (Fasada, Strategia, Obserwator przez sygnały/sloty, Worker Thread). Oto kilka dodatkowych, które mogłyby się przydać:
* **Singleton:** Klasa `DatabaseManager` jest idealnym kandydatem na Singletona. W całej aplikacji potrzebujesz tylko jednego połączenia z bazą danych. Można to zrealizować tworząc jedną, globalną instancję w module `database.py`, którą inne części aplikacji będą importować.
* **State (Stan):** Logika związana ze stanem kamery (brak kamery, wykryto, działa, zatrzymana) w `MainController` mogłaby zostać zamknięta we wzorcu Stan. Miałbyś obiekty reprezentujące każdy stan (np. `NoCameraState`, `StreamingState`), a każdy z nich inaczej obsługiwałby te same akcje (np. kliknięcie przycisku "Start"). To oczyściłoby kod z wielu instrukcji `if/else`.
### Podsumowanie i konkretne kroki
To bardzo obiecujący projekt z solidnymi fundamentami. Moje sugestie mają na celu głównie "doszlifowanie" istniejącego kodu.
**Zalecane działania (w kolejności od najważniejszych):**
1. **Wyczyść projekt:** Usuń zduplikowane pliki `controllers/camera_controller.py` i `controllers/mock_gphoto.py`, aby uniknąć nieporozumień.
2. **Zrefaktoryzuj ścieżki:** Popraw sposób zarządzania ścieżkami do ikon i innych zasobów, aby uniezależnić się od bieżącego katalogu roboczego.
3. **Popraw rotację obrazu:** Zmodyfikuj logikę w `SplitView`, aby uniknąć degradacji jakości obrazu przy wielokrotnym obracaniu.
4. **Uprość logikę UI:** Zastanów się nad refaktoryzacją obsługi stanu przycisków w `MainController`, aby kod był bardziej czytelny i mniej podatny na błędy.
Świetna robota! Jeśli masz więcej pytań lub chciałbyś, żebym przyjrzał się jakiemuś konkretnemu fragmentowi, daj znać.
---
### Postęp Prac (14.10.2025)
Na podstawie powyższej recenzji, wspólnie wprowadziliśmy następujące zmiany:
* **Zrefaktoryzowano ścieżki do zasobów (Zrealizowano):**
* Utworzono plik `settings.py` do centralnego zarządzania ścieżkami.
* Zaktualizowano komponenty UI (`split_view_widget.py`, `view_settings_dialog.py`), aby korzystały ze scentralizowanych ścieżek, co uniezależniło aplikację od katalogu roboczego.
* **Poprawiono logikę rotacji obrazu (Zrealizowano):**
* Zmieniono mechanizm obracania obrazu referencyjnego w `SplitView`, aby operacje były wykonywane na oryginalnym obrazie. Zapobiega to stopniowej utracie jakości przy wielokrotnych rotacjach.
* **Uproszczono logikę sygnałów w `MainController` (Zrealizowano):**
* Zastąpiono dynamiczne łączenie i rozłączanie sygnałów przycisku kamery jednym, stałym połączeniem i centralną metodą obsługi. Zwiększyło to czytelność i niezawodność kodu.
* **Wyczyszczono projekt (Zrealizowano):**
* Użytkownik potwierdził usunięcie zduplikowanych plików (`camera_controller.py` i `mock_gphoto.py`) z katalogu `controllers`.

13
settings.py Normal file
View File

@@ -0,0 +1,13 @@
from pathlib import Path
# Absolutna ścieżka do głównego katalogu projektu
ROOT_DIR = Path(__file__).parent.resolve()
# Ścieżka do katalogu UI
UI_DIR = ROOT_DIR / "ui"
# Ścieżka do katalogu z ikonami
ICONS_DIR = UI_DIR / "icons"
# Ścieżka do katalogu z mediami
MEDIA_DIR = ROOT_DIR / "media"

View File

@@ -2,6 +2,8 @@ from PySide6.QtWidgets import QDialog, QHBoxLayout ,QVBoxLayout, QPushButton, QG
from PySide6.QtGui import QIcon
from PySide6.QtCore import Qt, QSize, Signal
from settings import ICONS_DIR
ISO_ARR = ["AUTO","100", "200", "400", "800", "1600", "3200"]
SPEED_ARR = ["30", "25", "20", "15", "13", "10.3", "8", "6.3", "5", "4", "3.2", "2.5", "2", "1.6", "1.3", "1", "0.8", "0.6", "0.5", "0.4", "0.3", "1/4", "1/5", "1/6", "1/8", "1/10", "1/13", "1/15", "1/20", "1/25", "1/30", "1/40", "1/50", "1/60", "1/80", "1/100", "1/125", "1/160", "1/200", "1/250", "1/320", "1/400", "1/500", "1/640", "1/800", "1/1000", "1/1250", "1/1600", "1/2000", "1/2500", "1/3200", "1/4000"]
@@ -19,13 +21,13 @@ class LabeledSpinSelector(QWidget):
self.title_label = QLabel(title)
decrement_button = QToolButton()
decrement_button.setIcon(QIcon("ui/icons/arrow-left-335-svgrepo-com.svg"))
decrement_button.setIcon(QIcon(str(ICONS_DIR / "arrow-left-335-svgrepo-com.svg")))
decrement_button.setFixedSize(button_size, button_size)
decrement_button.setIconSize(QSize(icon_size, icon_size))
decrement_button.clicked.connect(self._decrement)
increment_button = QToolButton()
increment_button.setIcon(QIcon("ui/icons/arrow-right-336-svgrepo-com.svg"))
increment_button.setIcon(QIcon(str(ICONS_DIR / "arrow-right-336-svgrepo-com.svg")))
increment_button.setFixedSize(button_size, button_size)
increment_button.setIconSize(QSize(icon_size, icon_size))
increment_button.clicked.connect(self._increment)

View File

@@ -3,6 +3,7 @@ from PySide6.QtGui import QEnterEvent, QPixmap, QWheelEvent, QPainter, QBrush, Q
from PySide6.QtCore import Qt, QSize, Signal, QEvent
import sys
from ui.widgets.placeholder_widget import PlaceholderWidget
from settings import ICONS_DIR
class ZoomableImageView(QGraphicsView):
@@ -179,12 +180,12 @@ class ViewWithOverlay(QWidget):
def _create_top_right_buttons(self):
self.cw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-cw-svgrepo-com.svg",
icon_path=str(ICONS_DIR / "rotate-cw-svgrepo-com.svg"),
callback=self.rotateCW,
)
self.ccw_btn = self._create_tool_button(
icon_path="ui/icons/rotate-ccw-svgrepo-com.svg",
icon_path=str(ICONS_DIR / "rotate-ccw-svgrepo-com.svg"),
callback=self.rotateCCW,
)
@@ -200,12 +201,12 @@ class ViewWithOverlay(QWidget):
def _create_top_left_buttons(self):
self.camera_btn = self._create_tool_button(
icon_path="ui/icons/settings-svgrepo-com.svg",
icon_path=str(ICONS_DIR / "settings-svgrepo-com.svg"),
callback=self.cameraConnection
)
self.settings_btn = self._create_tool_button(
icon_path="ui/icons/error-16-svgrepo-com.svg",
icon_path=str(ICONS_DIR / "error-16-svgrepo-com.svg"),
callback=self.cameraSettings
)
@@ -232,12 +233,11 @@ class ViewWithOverlay(QWidget):
def toggle_orientation(self, orientation):
if orientation == Qt.Orientation.Vertical:
self.flip_btn.setIcon(QIcon("ui/icons/flip-vertical-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/horizontal-stacks-svgrepo-com.svg"))
self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-vertical-svgrepo-com.svg")))
self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "horizontal-stacks-svgrepo-com.svg")))
else:
self.flip_btn.setIcon(QIcon("ui/icons/flip-horizontal-svgrepo-com.svg"))
self.orient_btn.setIcon(QIcon("ui/icons/vertical-stacks-svgrepo-com.svg"))
self.flip_btn.setIcon(QIcon(str(ICONS_DIR / "flip-horizontal-svgrepo-com.svg")))
self.orient_btn.setIcon(QIcon(str(ICONS_DIR / "vertical-stacks-svgrepo-com.svg")))
def enterEvent(self, event: QEnterEvent) -> None:
if self.live:
self.camera_btn.show()
@@ -286,7 +286,7 @@ class SplitView(QSplitter):
# self.widget_live.set_image(pixmap)
self.ref_image_rotate = 0
self.ref_pixmap = None
self.original_ref_pixmap = None
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
@@ -324,10 +324,9 @@ class SplitView(QSplitter):
def set_reference_image(self, path_image: str):
"""Ustawienie obrazu referencyjnego"""
self.ref_pixmap = QPixmap(path_image)
if self.ref_image_rotate != 0:
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(self.ref_image_rotate))
self.widget_ref.set_image(self.ref_pixmap)
self.original_ref_pixmap = QPixmap(path_image)
self.ref_image_rotate = 0
self.widget_ref.set_image(self.original_ref_pixmap)
def toggle_live_view(self):
"""Przełączanie widoku na żywo"""
@@ -337,15 +336,17 @@ class SplitView(QSplitter):
self.stack.setCurrentWidget(self.widget_start)
def rotate_left(self):
if not self.ref_pixmap:
if not self.original_ref_pixmap:
return
self.ref_image_rotate = (self.ref_image_rotate - 90) % 360
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(-90))
self.widget_ref.set_image(self.ref_pixmap)
transform = QTransform().rotate(self.ref_image_rotate)
rotated_pixmap = self.original_ref_pixmap.transformed(transform)
self.widget_ref.set_image(rotated_pixmap)
def rotate_right(self):
if not self.ref_pixmap:
if not self.original_ref_pixmap:
return
self.ref_image_rotate = (self.ref_image_rotate + 90) % 360
self.ref_pixmap = self.ref_pixmap.transformed(QTransform().rotate(90))
self.widget_ref.set_image(self.ref_pixmap)
transform = QTransform().rotate(self.ref_image_rotate)
rotated_pixmap = self.original_ref_pixmap.transformed(transform)
self.widget_ref.set_image(rotated_pixmap)