Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cc496a2b2 | |||
| 0bc6c01e7e | |||
| 4d00f83083 | |||
| 2c1233f304 | |||
| feebc5153a | |||
| 49a1405340 | |||
| c6345c569d | |||
| 73b51c696e | |||
| 511e668cb3 | |||
| ce1b864b17 | |||
| 46734208e7 | |||
| bbdc2af605 | |||
| 2a5f570e5e | |||
| d63d616675 | |||
| eb72b25fdb | |||
| 86b9cc70a6 | |||
| 5b345e6641 | |||
| cc37d7054c | |||
| 71a55843c1 | |||
| ca08e44358 | |||
| c815762f72 | |||
| ca25b06f99 | |||
| dea17b8b26 | |||
| 324ab2e016 | |||
| 196eff7fd8 | |||
| e2c8352c44 | |||
| c63821617a | |||
| f2a002a249 | |||
| d86a6429f7 | |||
| a1c608f279 | |||
| dcf4ef0f0a | |||
| 1ff5091250 | |||
| 373e01310e | |||
| abc07fd08d | |||
| 35576986c9 | |||
| 19e2c7977c | |||
| 508930ae39 |
@@ -1,73 +1,167 @@
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import Slot
|
||||
from PySide6.QtGui import QPixmap
|
||||
from PySide6.QtWidgets import QPushButton
|
||||
|
||||
from core.database import DatabaseManager
|
||||
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
|
||||
from .camera_controller import CameraController
|
||||
from ui.widgets.split_view_widget import SplitView, CameraPlaceholder, ViewWithOverlay
|
||||
|
||||
|
||||
class MainController:
|
||||
def __init__(self, view):
|
||||
self.db = DatabaseManager()
|
||||
self.db.connect()
|
||||
self.media_repo = MediaRepository(self.db)
|
||||
self.media_repo.sync_media()
|
||||
|
||||
self.camera_controller = CameraController()
|
||||
|
||||
self.view = view
|
||||
self.db = DatabaseManager()
|
||||
self.media_repo = MediaRepository(self.db)
|
||||
self.camera_manager = CameraManager()
|
||||
|
||||
# --- UI Widgets ---
|
||||
self.color_list: ColorListWidget = view.color_list_widget
|
||||
self.thumbnail_list: ThumbnailListWidget = view.thumbnail_widget
|
||||
self.split_view: SplitView = view.preview_widget
|
||||
|
||||
self.welcome_view: CameraPlaceholder = self.split_view.widget_start
|
||||
self.live_view: ViewWithOverlay = self.split_view.widget_live
|
||||
self.photo_button: QPushButton = view.photo_button
|
||||
self.photo_button.clicked.connect(self.take_photo)
|
||||
self.record_button: QPushButton = view.record_button
|
||||
|
||||
self._connect_signals()
|
||||
|
||||
self.db.connect()
|
||||
self.media_repo.sync_media()
|
||||
self.camera_manager.detect_cameras()
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connects all signals to slots."""
|
||||
# Database and media signals
|
||||
self.color_list.colorSelected.connect(self.on_color_selected)
|
||||
self.color_list.editColor.connect(self.on_edit_color)
|
||||
self.thumbnail_list.selectedThumbnail.connect(self.on_thumbnail_selected)
|
||||
|
||||
self.camera_controller.errorOccurred.connect(self.split_view.widget_start.set_info_text)
|
||||
self.camera_controller.frameReady.connect(self.split_view.set_live_image)
|
||||
self.split_view.widget_start.camera_start_btn.clicked.connect(self.camera_controller.start)
|
||||
# Camera signals
|
||||
self.camera_manager.cameras_detected.connect(self.on_cameras_detected)
|
||||
self.camera_manager.frame_ready.connect(self.on_frame_ready)
|
||||
self.camera_manager.error_occurred.connect(self.on_camera_error)
|
||||
self.camera_manager.camera_started.connect(self.on_camera_started)
|
||||
self.camera_manager.camera_stopped.connect(self.on_camera_stopped)
|
||||
|
||||
def start_camera(self):
|
||||
pass
|
||||
# UI control signals
|
||||
self.photo_button.clicked.connect(self.take_photo)
|
||||
self.welcome_view.camera_start_btn.clicked.connect(self.camera_detect)
|
||||
self.live_view.rotateCW.connect(self.camera_manager.rotate_right)
|
||||
self.live_view.rotateCCW.connect(self.camera_manager.rotate_left)
|
||||
|
||||
def load_colors(self) -> None:
|
||||
"""Loads colors from the database and populates the list."""
|
||||
colors = self.db.get_all_colors()
|
||||
print("Loaded colors:", colors)
|
||||
self.color_list.set_colors(colors)
|
||||
|
||||
def shutdown(self):
|
||||
"""Cleans up resources before application exit."""
|
||||
self.camera_manager.shutdown()
|
||||
self.db.disconnect()
|
||||
|
||||
# --- Slots for Database/Media ---
|
||||
|
||||
@Slot(str)
|
||||
def on_color_selected(self, color_name: str):
|
||||
print(f"Wybrano kolor: {color_name}")
|
||||
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)
|
||||
print(f"Media dla koloru {color_name} (ID: {color_id}):", media_items)
|
||||
|
||||
self.thumbnail_list.list_widget.clear()
|
||||
for media in media_items:
|
||||
if media['file_type'] == 'photo':
|
||||
file_name = Path(media['media_path']).name
|
||||
self.thumbnail_list.add_thumbnail(media['media_path'], file_name, media['id'])
|
||||
else:
|
||||
print(f"Nie znaleziono koloru o nazwie: {color_name}")
|
||||
|
||||
@Slot(str)
|
||||
def on_edit_color(self, color_name: str):
|
||||
print(f"Edycja koloru: {color_name}")
|
||||
print(f"Edycja koloru: {color_name}") # Placeholder
|
||||
|
||||
@Slot(int)
|
||||
def on_thumbnail_selected(self, media_id: int):
|
||||
media = self.db.get_media(media_id)
|
||||
if media:
|
||||
print(f"Wybrano miniaturę o ID: {media_id}, ścieżka: {media['media_path']}")
|
||||
self.split_view.set_reference_image(media['media_path'])
|
||||
|
||||
# --- Slots for CameraManager ---
|
||||
|
||||
@Slot(list)
|
||||
def on_cameras_detected(self, cameras: list[dict]):
|
||||
"""Handles the list of detected cameras."""
|
||||
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)
|
||||
else:
|
||||
print(f"Nie znaleziono mediów o ID: {media_id}")
|
||||
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'])
|
||||
|
||||
@Slot(QPixmap)
|
||||
def on_frame_ready(self, pixmap: QPixmap):
|
||||
"""Displays a new frame from the camera."""
|
||||
self.split_view.set_live_image(pixmap)
|
||||
|
||||
@Slot(str)
|
||||
def on_camera_error(self, error_message: str):
|
||||
"""Shows an error message from the camera."""
|
||||
print(f"Camera Error: {error_message}")
|
||||
self.welcome_view.set_error_text(error_message)
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
# --- UI Actions ---
|
||||
|
||||
def camera_detect(self):
|
||||
self.camera_manager.detect_cameras()
|
||||
|
||||
def start_liveview(self):
|
||||
"""Starts the camera feed."""
|
||||
detected_cameras = self.camera_manager.get_detected_cameras()
|
||||
if not detected_cameras:
|
||||
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)
|
||||
|
||||
def stop_liveview(self):
|
||||
"""Stops the camera feed."""
|
||||
self.camera_manager.stop_camera()
|
||||
|
||||
def take_photo(self):
|
||||
print("Robienie zdjęcia...")
|
||||
self.split_view.toglle_live_view()
|
||||
"""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?
|
||||
@@ -1,6 +1,16 @@
|
||||
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
|
||||
|
||||
@@ -19,6 +29,65 @@ class CameraFileMock:
|
||||
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):
|
||||
@@ -62,3 +131,18 @@ class Camera:
|
||||
|
||||
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
|
||||
42
core/camera/base_camera.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseCamera(ABC):
|
||||
def __init__(self) -> None:
|
||||
self.error_msg = None
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def detect() -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def disconnect(self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_frame(self):
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_config_by_id(self, id: int) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def get_config_by_name(self, name: str) -> dict:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set_config_by_id(self, id: int, value) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def set_config_by_name(self, name: str, value) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
def get_error_msg(self):
|
||||
return str(self.error_msg)
|
||||
194
core/camera/camera_controller.py
Normal file
@@ -0,0 +1,194 @@
|
||||
from PySide6.QtCore import QObject, QTimer, Signal, Slot, QMutex, QMutexLocker, QThread
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
import cv2
|
||||
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
|
||||
class CameraWorker(QObject):
|
||||
frame_ready = Signal(QPixmap)
|
||||
photo_ready = Signal(QPixmap)
|
||||
error_occurred = Signal(str)
|
||||
camera_ready = Signal(bool)
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.camera: BaseCamera | None = None
|
||||
self.timer: QTimer | None = None
|
||||
self.fps = 15
|
||||
self.is_streaming = False
|
||||
self.is_connected = False
|
||||
self._rotation_index = 0
|
||||
self._camera_mutex = QMutex()
|
||||
|
||||
@Slot()
|
||||
def initialize_worker(self):
|
||||
"""Initializes the timer in the worker's thread."""
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self._update_frame)
|
||||
|
||||
@Slot(BaseCamera, int)
|
||||
def set_camera(self, camera: BaseCamera, fps: int = 15) -> None:
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
if self.is_streaming:
|
||||
self.stop_stream()
|
||||
if self.is_connected:
|
||||
self.stop_camera()
|
||||
|
||||
self.camera = camera
|
||||
self.fps = fps
|
||||
|
||||
@Slot()
|
||||
def start_camera(self) -> None:
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
if self.camera is None or self.is_connected:
|
||||
return
|
||||
|
||||
if self.camera.connect():
|
||||
self.is_connected = True
|
||||
self.camera_ready.emit(True)
|
||||
else:
|
||||
self.is_connected = False
|
||||
self.camera_ready.emit(False)
|
||||
self.error_occurred.emit(self.camera.get_error_msg())
|
||||
|
||||
@Slot()
|
||||
def stop_camera(self) -> None:
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
if self.is_streaming:
|
||||
self.stop_stream()
|
||||
|
||||
if self.camera is not None and self.is_connected:
|
||||
self.camera.disconnect()
|
||||
|
||||
self.is_connected = False
|
||||
|
||||
@Slot()
|
||||
def start_stream(self):
|
||||
if not self.is_connected or self.is_streaming or self.timer is None:
|
||||
return
|
||||
|
||||
self.is_streaming = True
|
||||
self.timer.setInterval(int(1000 / self.fps))
|
||||
self.timer.start()
|
||||
|
||||
@Slot()
|
||||
def stop_stream(self) -> None:
|
||||
if self.is_streaming and self.timer is not None:
|
||||
self.is_streaming = False
|
||||
self.timer.stop()
|
||||
|
||||
@Slot()
|
||||
def _update_frame(self) -> None:
|
||||
# This method is called by the timer, which is in the same thread.
|
||||
# A mutex is still good practice for accessing the shared camera object.
|
||||
with QMutexLocker(self._camera_mutex):
|
||||
if self.camera is None or not self.is_connected or not self.is_streaming:
|
||||
return
|
||||
|
||||
ret, frame = self.camera.get_frame()
|
||||
|
||||
if not ret:
|
||||
error_msg = self.camera.get_error_msg()
|
||||
if error_msg:
|
||||
self.error_occurred.emit(error_msg)
|
||||
return
|
||||
|
||||
if frame is None:
|
||||
return
|
||||
|
||||
if self._rotation_index == 1:
|
||||
frame = cv2.rotate(frame, cv2.ROTATE_90_CLOCKWISE)
|
||||
elif self._rotation_index == 2:
|
||||
frame = cv2.rotate(frame, cv2.ROTATE_180)
|
||||
elif self._rotation_index == 3:
|
||||
frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
|
||||
|
||||
# Process the frame and emit it.
|
||||
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.frame_ready.emit(pixmap)
|
||||
|
||||
@Slot()
|
||||
def rotate_left(self):
|
||||
self._rotation_index = (self._rotation_index - 1) % 4
|
||||
|
||||
@Slot()
|
||||
def rotate_right(self):
|
||||
self._rotation_index = (self._rotation_index + 1) % 4
|
||||
|
||||
|
||||
|
||||
class CameraController(QObject):
|
||||
frame_ready = Signal(QPixmap)
|
||||
photo_ready = Signal(QPixmap)
|
||||
error_occurred = Signal(str)
|
||||
camera_ready = Signal(bool)
|
||||
|
||||
# Signals to command the worker
|
||||
_set_camera_requested = Signal(BaseCamera, int)
|
||||
_start_camera_requested = Signal()
|
||||
_stop_camera_requested = Signal()
|
||||
_start_stream_requested = Signal()
|
||||
_stop_stream_requested = Signal()
|
||||
_rotate_left_requested = Signal()
|
||||
_rotate_right_requested = Signal()
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._thread = QThread()
|
||||
self._worker = CameraWorker()
|
||||
|
||||
self._worker.moveToThread(self._thread)
|
||||
|
||||
# Connect worker signals to controller signals
|
||||
self._worker.frame_ready.connect(self.frame_ready)
|
||||
self._worker.photo_ready.connect(self.photo_ready)
|
||||
self._worker.error_occurred.connect(self.error_occurred)
|
||||
self._worker.camera_ready.connect(self.camera_ready)
|
||||
|
||||
# Connect controller's command signals to worker's slots
|
||||
self._set_camera_requested.connect(self._worker.set_camera)
|
||||
self._start_camera_requested.connect(self._worker.start_camera)
|
||||
self._stop_camera_requested.connect(self._worker.stop_camera)
|
||||
self._start_stream_requested.connect(self._worker.start_stream)
|
||||
self._stop_stream_requested.connect(self._worker.stop_stream)
|
||||
self._rotate_left_requested.connect(self._worker.rotate_left)
|
||||
self._rotate_right_requested.connect(self._worker.rotate_right)
|
||||
|
||||
# Initialize worker when thread starts
|
||||
self._thread.started.connect(self._worker.initialize_worker)
|
||||
self._thread.finished.connect(self._worker.stop_camera)
|
||||
|
||||
self._thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self._thread.isRunning():
|
||||
self._thread.quit()
|
||||
self._thread.wait()
|
||||
self._thread.deleteLater()
|
||||
|
||||
def set_camera(self, camera: BaseCamera, fps: int = 15) -> None:
|
||||
self._set_camera_requested.emit(camera, fps)
|
||||
|
||||
def start_camera(self) -> None:
|
||||
self._start_camera_requested.emit()
|
||||
|
||||
def stop_camera(self) -> None:
|
||||
self._stop_camera_requested.emit()
|
||||
|
||||
def start_stream(self):
|
||||
self._start_stream_requested.emit()
|
||||
|
||||
def stop_stream(self) -> None:
|
||||
self._stop_stream_requested.emit()
|
||||
|
||||
def rotate_left(self):
|
||||
self._rotate_left_requested.emit()
|
||||
|
||||
def rotate_right(self):
|
||||
self._rotate_right_requested.emit()
|
||||
|
||||
|
||||
127
core/camera/camera_manager.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
from PySide6.QtGui import QPixmap
|
||||
|
||||
from .camera_controller import CameraController
|
||||
from .gphoto_camera import GPhotoCamera
|
||||
from .opencv_camera import OpenCvCamera
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
|
||||
class CameraManager(QObject):
|
||||
"""
|
||||
Zarządza wszystkimi operacjami związanymi z kamerami,
|
||||
stanowiąc fasadę dla reszty aplikacji.
|
||||
"""
|
||||
frame_ready = Signal(QPixmap)
|
||||
error_occurred = Signal(str)
|
||||
cameras_detected = Signal(list)
|
||||
camera_started = Signal()
|
||||
camera_stopped = Signal()
|
||||
|
||||
def __init__(self, parent: QObject | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._camera_controller = CameraController()
|
||||
self._detected_cameras: list[dict] = []
|
||||
self._active_camera: BaseCamera | None = None
|
||||
self._active_camera_info: dict | None = None
|
||||
|
||||
# Przekazywanie sygnałów z kontrolera kamery na zewnątrz
|
||||
self._camera_controller.frame_ready.connect(self.frame_ready)
|
||||
self._camera_controller.error_occurred.connect(self.error_occurred)
|
||||
self._camera_controller.camera_ready.connect(self.start_liveview)
|
||||
|
||||
def detect_cameras(self) -> None:
|
||||
"""Wykrywa wszystkie dostępne kamery (GPhoto i OpenCV)."""
|
||||
self._detected_cameras.clear()
|
||||
|
||||
# Wykryj kamery GPhoto
|
||||
try:
|
||||
gphoto_cameras = GPhotoCamera.detect()
|
||||
for index, info in gphoto_cameras.items():
|
||||
self._detected_cameras.append({
|
||||
"id": f"gphoto_{index}",
|
||||
"name": f"{info['name']} ({info['port']})",
|
||||
"type": "gphoto",
|
||||
"index": index
|
||||
})
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(
|
||||
f"Błąd podczas wykrywania kamer GPhoto: {e}")
|
||||
|
||||
# Wykryj kamery OpenCV
|
||||
try:
|
||||
opencv_cameras = OpenCvCamera.detect()
|
||||
for index, info in opencv_cameras.items():
|
||||
self._detected_cameras.append({
|
||||
"id": f"opencv_{index}",
|
||||
"name": f"OpenCV: {info['name']}",
|
||||
"type": "opencv",
|
||||
"index": index
|
||||
})
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(
|
||||
f"Błąd podczas wykrywania kamer OpenCV: {e}")
|
||||
|
||||
self.cameras_detected.emit(self._detected_cameras)
|
||||
|
||||
def get_detected_cameras(self) -> list[dict]:
|
||||
return self._detected_cameras
|
||||
|
||||
def start_camera(self, camera_id: str, fps: int = 15) -> None:
|
||||
"""Uruchamia wybraną kamerę."""
|
||||
if self._active_camera:
|
||||
self.stop_camera()
|
||||
|
||||
camera_info = next(
|
||||
(c for c in self._detected_cameras if c['id'] == camera_id), None)
|
||||
|
||||
if not camera_info:
|
||||
self.error_occurred.emit(
|
||||
f"Nie znaleziono kamery o ID: {camera_id}")
|
||||
return
|
||||
|
||||
camera_type = camera_info['type']
|
||||
camera_index = camera_info['index']
|
||||
|
||||
if camera_type == "gphoto":
|
||||
self._active_camera = GPhotoCamera()
|
||||
elif camera_type == "opencv":
|
||||
self._active_camera = OpenCvCamera()
|
||||
else:
|
||||
self.error_occurred.emit(f"Nieznany typ kamery: {camera_type}")
|
||||
return
|
||||
|
||||
self._active_camera_info = camera_info
|
||||
|
||||
self._camera_controller.set_camera(self._active_camera, fps)
|
||||
self._camera_controller.start_camera()
|
||||
|
||||
def start_liveview(self, connected):
|
||||
if connected:
|
||||
self._camera_controller.start_stream()
|
||||
self.camera_started.emit()
|
||||
else:
|
||||
self._active_camera = None
|
||||
self._active_camera_info = None
|
||||
|
||||
def stop_camera(self) -> None:
|
||||
"""Zatrzymuje aktywną kamerę."""
|
||||
if self._active_camera:
|
||||
self._camera_controller.stop_camera()
|
||||
self._active_camera = None
|
||||
self._active_camera_info = None
|
||||
self.camera_stopped.emit()
|
||||
|
||||
def get_active_camera_info(self) -> dict | None:
|
||||
return self._active_camera_info
|
||||
|
||||
def rotate_left(self):
|
||||
self._camera_controller.rotate_left()
|
||||
|
||||
def rotate_right(self):
|
||||
self._camera_controller.rotate_right()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
"""Zamyka kontroler kamery i jego wątek."""
|
||||
self.stop_camera()
|
||||
self._camera_controller.stop()
|
||||
173
core/camera/gphoto_camera.py
Normal file
@@ -0,0 +1,173 @@
|
||||
from typing import Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
try:
|
||||
import gphoto2 as gp # type: ignore
|
||||
except:
|
||||
import core.camera.mock_gphoto as gp
|
||||
|
||||
camera_widget_types = {
|
||||
gp.GP_WIDGET_WINDOW: "GP_WIDGET_WINDOW", # type: ignore
|
||||
gp.GP_WIDGET_SECTION: "GP_WIDGET_SECTION", # type: ignore
|
||||
gp.GP_WIDGET_TEXT: "GP_WIDGET_TEXT", # type: ignore
|
||||
gp.GP_WIDGET_RANGE: "GP_WIDGET_RANGE", # type: ignore
|
||||
gp.GP_WIDGET_TOGGLE: "GP_WIDGET_TOGGLE", # type: ignore
|
||||
gp.GP_WIDGET_RADIO: "GP_WIDGET_RADIO", # type: ignore
|
||||
gp.GP_WIDGET_MENU: "GP_WIDGET_MENU", # type: ignore
|
||||
gp.GP_WIDGET_BUTTON: "GP_WIDGET_BUTTON", # type: ignore
|
||||
gp.GP_WIDGET_DATE: "GP_WIDGET_DATE", # type: ignore
|
||||
}
|
||||
|
||||
operations = [
|
||||
("GP_OPERATION_NONE", gp.GP_OPERATION_NONE), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_IMAGE", gp.GP_OPERATION_CAPTURE_IMAGE), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_VIDEO", gp.GP_OPERATION_CAPTURE_VIDEO), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_AUDIO", gp.GP_OPERATION_CAPTURE_AUDIO), # type: ignore
|
||||
("GP_OPERATION_CAPTURE_PREVIEW", gp.GP_OPERATION_CAPTURE_PREVIEW), # type: ignore
|
||||
("GP_OPERATION_CONFIG", gp.GP_OPERATION_CONFIG), # type: ignore
|
||||
("GP_OPERATION_TRIGGER_CAPTURE", gp.GP_OPERATION_TRIGGER_CAPTURE), # type: ignore
|
||||
]
|
||||
|
||||
|
||||
class GPhotoCamera(BaseCamera):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.camera = None
|
||||
self.configs: List[dict] = []
|
||||
self.camera_index = 0
|
||||
|
||||
@staticmethod
|
||||
def detect() -> dict:
|
||||
cameras = gp.check_result(gp.gp_camera_autodetect()) # type: ignore
|
||||
# cameras = gp.Camera().autodetect()
|
||||
if not cameras or cameras.count() == 0: # type: ignore
|
||||
return {}
|
||||
|
||||
abilities_list = gp.CameraAbilitiesList() # type: ignore
|
||||
abilities_list.load()
|
||||
camera_list = {}
|
||||
for i in range(cameras.count()): # type: ignore
|
||||
name = cameras.get_name(i) # type: ignore
|
||||
port = cameras.get_value(i) # type: ignore
|
||||
|
||||
abilities_index = abilities_list.lookup_model(name)
|
||||
abilities = abilities_list.get_abilities(abilities_index)
|
||||
abilities_name = []
|
||||
for operation, bit in operations:
|
||||
if abilities.operations & bit: # type: ignore
|
||||
abilities_name.append(operation)
|
||||
|
||||
camera_list[i] = {"name": name, "port": port, "abilities": abilities_name}
|
||||
return camera_list
|
||||
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
self.error_msg = None
|
||||
self.camera = gp.Camera() # type: ignore
|
||||
|
||||
try:
|
||||
if index:
|
||||
self.camera_index = index
|
||||
camera_list = GPhotoCamera.detect()
|
||||
port_info_list = gp.PortInfoList()
|
||||
port_info_list.load()
|
||||
|
||||
port_address = camera_list[index]["port"]
|
||||
port_index = port_info_list.lookup_path(port_address)
|
||||
|
||||
self.camera.set_port_info(port_info_list[port_index])
|
||||
|
||||
self.camera.init()
|
||||
config = self.camera.get_config()
|
||||
self.read_config(config)
|
||||
return True
|
||||
except Exception as e:
|
||||
self.error_msg = f"[GPHOTO2] {e}"
|
||||
self.camera = None
|
||||
return False
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self.camera:
|
||||
self.camera.exit()
|
||||
self.camera = None
|
||||
self.configs.clear()
|
||||
|
||||
def get_frame(self):
|
||||
self.error_msg = None
|
||||
|
||||
if self.camera is None:
|
||||
self.error_msg = "[GPHOTO2] Camera is not initialized."
|
||||
return (False, None)
|
||||
|
||||
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)
|
||||
|
||||
return (True, frame)
|
||||
except Exception as e:
|
||||
self.error_msg = f"[GPHOTO2] {e}"
|
||||
return (False, None)
|
||||
|
||||
def get_config_by_id(self, id: int):
|
||||
return next(w for w in self.configs if w['id'] == id)
|
||||
|
||||
def get_config_by_name(self, name: str):
|
||||
return next(w for w in self.configs if w['name'] == name)
|
||||
|
||||
def set_config(self, config, value):
|
||||
if value not in config['choices']:
|
||||
return
|
||||
|
||||
config['widget'].set_value(value) # type: ignore
|
||||
if self._save_config(config):
|
||||
config['value'] = value
|
||||
|
||||
def set_config_by_id(self, id: int, value: str):
|
||||
config = self.get_config_by_id(id)
|
||||
|
||||
self.set_config(config, value)
|
||||
|
||||
def set_config_by_name(self, name: str, value: str):
|
||||
config = self.get_config_by_name(name)
|
||||
|
||||
self.set_config(config, value)
|
||||
|
||||
def _save_config(self, config):
|
||||
if not self.camera:
|
||||
return False
|
||||
|
||||
self.camera.set_single_config(config['name'], config['widget'])
|
||||
return True
|
||||
|
||||
def parse_config(self, config):
|
||||
new_config = {
|
||||
"id": config.get_id(),
|
||||
"name": config.get_name(),
|
||||
"label": config.get_label(),
|
||||
"type": camera_widget_types[config.get_type()],
|
||||
"widget": config
|
||||
}
|
||||
|
||||
try:
|
||||
new_config["value"] = config.get_value()
|
||||
except gp.GPhoto2Error:
|
||||
pass
|
||||
|
||||
try:
|
||||
new_config["choices"] = list(config.get_choices())
|
||||
except gp.GPhoto2Error:
|
||||
pass
|
||||
|
||||
return new_config
|
||||
|
||||
def read_config(self, config):
|
||||
self.configs.append(self.parse_config(config))
|
||||
|
||||
for i in range(config.count_children()):
|
||||
child = config.get_child(i)
|
||||
self.read_config(child)
|
||||
204
core/camera/mock_gphoto.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
Mock gphoto2 module for Windows testing and development.
|
||||
Simulates gphoto2 API behavior to allow the app to run without a camera.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
# --- Constants (simulate gphoto2 enums) ---
|
||||
|
||||
GP_WIDGET_WINDOW = 0
|
||||
GP_WIDGET_SECTION = 1
|
||||
GP_WIDGET_TEXT = 2
|
||||
GP_WIDGET_RANGE = 3
|
||||
GP_WIDGET_TOGGLE = 4
|
||||
GP_WIDGET_RADIO = 5
|
||||
GP_WIDGET_MENU = 6
|
||||
GP_WIDGET_BUTTON = 7
|
||||
GP_WIDGET_DATE = 8
|
||||
|
||||
GP_OPERATION_NONE = 0x00
|
||||
GP_OPERATION_CAPTURE_IMAGE = 0x01
|
||||
GP_OPERATION_CAPTURE_VIDEO = 0x02
|
||||
GP_OPERATION_CAPTURE_AUDIO = 0x04
|
||||
GP_OPERATION_CAPTURE_PREVIEW = 0x08
|
||||
GP_OPERATION_CONFIG = 0x10
|
||||
GP_OPERATION_TRIGGER_CAPTURE = 0x20
|
||||
|
||||
|
||||
# --- Error class ---
|
||||
class GPhoto2Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# --- Mock camera configuration widget ---
|
||||
|
||||
class MockWidget:
|
||||
def __init__(self, name, label, wtype, value=None, choices=None):
|
||||
self._name = name
|
||||
self._label = label
|
||||
self._type = wtype
|
||||
self._value = value
|
||||
self._choices = choices or []
|
||||
self._children = []
|
||||
|
||||
def get_id(self):
|
||||
return id(self)
|
||||
|
||||
def get_name(self):
|
||||
return self._name
|
||||
|
||||
def get_label(self):
|
||||
return self._label
|
||||
|
||||
def get_type(self):
|
||||
return self._type
|
||||
|
||||
def get_value(self):
|
||||
return self._value
|
||||
|
||||
def set_value(self, value):
|
||||
if self._choices and value not in self._choices:
|
||||
raise GPhoto2Error(f"Invalid value '{value}' for widget '{self._name}'")
|
||||
self._value = value
|
||||
|
||||
def get_choices(self):
|
||||
return self._choices
|
||||
|
||||
def count_children(self):
|
||||
return len(self._children)
|
||||
|
||||
def get_child(self, i):
|
||||
return self._children[i]
|
||||
|
||||
def add_child(self, child):
|
||||
self._children.append(child)
|
||||
|
||||
|
||||
# --- Mock classes for detection / abilities ---
|
||||
|
||||
@dataclass
|
||||
class MockCameraInfo:
|
||||
name: str
|
||||
port: str
|
||||
|
||||
|
||||
class MockCameraList:
|
||||
def __init__(self):
|
||||
self._cameras = [
|
||||
MockCameraInfo("Mock Camera 1", "usb:001,002"),
|
||||
MockCameraInfo("Mock Camera 2", "usb:001,003")
|
||||
]
|
||||
|
||||
def count(self):
|
||||
return len(self._cameras)
|
||||
|
||||
def get_name(self, i):
|
||||
return self._cameras[i].name
|
||||
|
||||
def get_value(self, i):
|
||||
return self._cameras[i].port
|
||||
|
||||
|
||||
class CameraAbilities:
|
||||
def __init__(self, operations):
|
||||
self.operations = operations
|
||||
|
||||
|
||||
class CameraAbilitiesList:
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def lookup_model(self, name):
|
||||
return 0
|
||||
|
||||
def get_abilities(self, index):
|
||||
return CameraAbilities(
|
||||
GP_OPERATION_CAPTURE_IMAGE
|
||||
| GP_OPERATION_CAPTURE_PREVIEW
|
||||
| GP_OPERATION_CONFIG
|
||||
)
|
||||
|
||||
|
||||
class PortInfoList:
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def lookup_path(self, path):
|
||||
return 0
|
||||
|
||||
def __getitem__(self, index):
|
||||
return f"MockPortInfo({index})"
|
||||
|
||||
|
||||
# --- Mock Camera class ---
|
||||
|
||||
class Camera:
|
||||
def __init__(self):
|
||||
self.initialized = False
|
||||
self.port_info = None
|
||||
|
||||
def init(self):
|
||||
self.initialized = True
|
||||
|
||||
def exit(self):
|
||||
self.initialized = False
|
||||
|
||||
def set_port_info(self, info):
|
||||
self.port_info = info
|
||||
|
||||
def get_config(self):
|
||||
# Simulate config tree
|
||||
root = MockWidget("root", "Root", GP_WIDGET_WINDOW)
|
||||
|
||||
iso = MockWidget("iso", "ISO", GP_WIDGET_MENU, "100", ["100", "200", "400", "800"])
|
||||
shutter = MockWidget("shutter", "Shutter Speed", GP_WIDGET_MENU, "1/60", ["1/30", "1/60", "1/125"])
|
||||
wb = MockWidget("whitebalance", "White Balance", GP_WIDGET_RADIO, "Auto", ["Auto", "Daylight", "Tungsten"])
|
||||
|
||||
root.add_child(iso)
|
||||
root.add_child(shutter)
|
||||
root.add_child(wb)
|
||||
|
||||
return root
|
||||
|
||||
def set_single_config(self, name, widget):
|
||||
# Simulate saving a setting
|
||||
print(f"[mock_gphoto] Setting '{name}' = '{widget.get_value()}'")
|
||||
|
||||
def capture_preview(self):
|
||||
# Generate a fake image (OpenCV compatible)
|
||||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
cv2.putText(frame, "Mock Preview", (150, 240), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||
_, buf = cv2.imencode(".jpg", frame)
|
||||
|
||||
class MockFile:
|
||||
def get_data_and_size(self_inner):
|
||||
return buf.tobytes()
|
||||
|
||||
return MockFile()
|
||||
|
||||
|
||||
# --- Mock detection functions ---
|
||||
|
||||
def gp_camera_autodetect():
|
||||
return MockCameraList()
|
||||
|
||||
|
||||
def check_result(value):
|
||||
# gphoto2.check_result usually raises error if return < 0
|
||||
return value
|
||||
|
||||
|
||||
# --- API aliases to match gphoto2 ---
|
||||
|
||||
GP_ERROR = -1
|
||||
|
||||
gp_camera_autodetect = gp_camera_autodetect
|
||||
check_result = check_result
|
||||
Camera = Camera
|
||||
CameraAbilitiesList = CameraAbilitiesList
|
||||
PortInfoList = PortInfoList
|
||||
GPhoto2Error = GPhoto2Error
|
||||
122
core/camera/opencv_camera.py
Normal file
@@ -0,0 +1,122 @@
|
||||
import cv2
|
||||
from cv2_enumerate_cameras import enumerate_cameras
|
||||
from typing import List
|
||||
|
||||
from .base_camera import BaseCamera
|
||||
|
||||
|
||||
class OpenCvCamera(BaseCamera):
|
||||
"""Kamera oparta na cv2.VideoCapture"""
|
||||
|
||||
config_map = {
|
||||
0: {"name": "frame_width", "cv_prop": cv2.CAP_PROP_FRAME_WIDTH, "default": 640},
|
||||
1: {"name": "frame_height", "cv_prop": cv2.CAP_PROP_FRAME_HEIGHT, "default": 480},
|
||||
2: {"name": "fps", "cv_prop": cv2.CAP_PROP_FPS, "default": 30},
|
||||
3: {"name": "brightness", "cv_prop": cv2.CAP_PROP_BRIGHTNESS, "default": 0.5},
|
||||
4: {"name": "contrast", "cv_prop": cv2.CAP_PROP_CONTRAST, "default": 0.5},
|
||||
5: {"name": "saturation", "cv_prop": cv2.CAP_PROP_SATURATION, "default": 0.5},
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.cap = None
|
||||
self.configs: List[dict] = []
|
||||
self.camera_list = []
|
||||
self.camera_index = 0
|
||||
|
||||
@staticmethod
|
||||
def detect():
|
||||
camera_list = enumerate_cameras(cv2.CAP_ANY)
|
||||
result = {}
|
||||
|
||||
for camera in camera_list:
|
||||
cap = cv2.VideoCapture(camera.index, camera.backend)
|
||||
# ret, frame = cap.read()
|
||||
cap.release()
|
||||
|
||||
# if ret and frame is not None and frame.size > 0:
|
||||
result[camera.index] = {
|
||||
"name": camera.name,
|
||||
"port": camera.path,
|
||||
"backend": camera.backend,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def connect(self, index: int | None = None) -> bool:
|
||||
self.error_msg = None
|
||||
try:
|
||||
if index:
|
||||
self.camera_index = index
|
||||
|
||||
self.cap = cv2.VideoCapture(self.camera_index)
|
||||
|
||||
if not self.cap.isOpened():
|
||||
self.error_msg = f"[CV2] Could not open camera {self.camera_index}"
|
||||
return False
|
||||
|
||||
self.configs.clear()
|
||||
for id, conf in self.config_map.items():
|
||||
value = self.cap.get(conf["cv_prop"])
|
||||
self.configs.append(
|
||||
{
|
||||
"id": id,
|
||||
"name": conf["name"],
|
||||
"label": conf["name"].capitalize(),
|
||||
"value": value,
|
||||
"choices": None, # brak predefiniowanych wyborów
|
||||
"cv_prop": conf["cv_prop"],
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
self.cap = None
|
||||
return False
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
self.configs.clear()
|
||||
|
||||
def get_frame(self):
|
||||
self.error_msg = None
|
||||
if self.cap is None or not self.cap.isOpened():
|
||||
self.error_msg = "[CV2] Camera is not initialized."
|
||||
return (False, None)
|
||||
|
||||
try:
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
self.error_msg = "[CV2] Failed to read frame."
|
||||
return (False, None)
|
||||
return (True, frame)
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
return (False, None)
|
||||
|
||||
def get_config_by_id(self, id: int):
|
||||
return next(w for w in self.configs if w["id"] == id)
|
||||
|
||||
def get_config_by_name(self, name: str):
|
||||
return next(w for w in self.configs if w["name"] == name)
|
||||
|
||||
def set_config(self, config, value: float):
|
||||
if not self.cap:
|
||||
return
|
||||
try:
|
||||
self.cap.set(config["cv_prop"], value)
|
||||
config["value"] = self.cap.get(
|
||||
config["cv_prop"]) # sprawdz co ustawiło
|
||||
except Exception as e:
|
||||
self.error_msg = f"[CV2] {e}"
|
||||
|
||||
def set_config_by_id(self, id: int, value: float):
|
||||
config = self.get_config_by_id(id)
|
||||
self.set_config(config, value)
|
||||
|
||||
def set_config_by_name(self, name: str, value: float):
|
||||
config = self.get_config_by_name(name)
|
||||
self.set_config(config, value)
|
||||
6
main.py
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from ui.main_palette import set_dark_theme
|
||||
from ui.main_window import MainWindow
|
||||
from controllers.main_controller import MainController
|
||||
|
||||
@@ -8,10 +9,13 @@ from controllers.main_controller import MainController
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyle("Fusion")
|
||||
set_dark_theme(app)
|
||||
# app.setStyle("Fusion")
|
||||
window = MainWindow()
|
||||
controller = MainController(window)
|
||||
controller.load_colors()
|
||||
|
||||
app.aboutToQuit.connect(controller.shutdown)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
cv2_enumerate_cameras==1.3.0
|
||||
gphoto2==2.6.2
|
||||
mypy==1.18.2
|
||||
mypy_extensions==1.1.0
|
||||
numpy==2.2.6
|
||||
opencv-python==4.12.0.88
|
||||
pathspec==0.12.1
|
||||
PySide6==6.9.2
|
||||
PySide6_Addons==6.9.2
|
||||
PySide6_Essentials==6.9.2
|
||||
shiboken6==6.9.2
|
||||
typing_extensions==4.15.0
|
||||
19
ui/icons/arrow-left-335-svgrepo-com.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-4.5 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>arrow_left [#335]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-345.000000, -6679.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M299.633777,6519.29231 L299.633777,6519.29231 C299.228878,6518.90256 298.573377,6518.90256 298.169513,6519.29231 L289.606572,6527.55587 C288.797809,6528.33636 288.797809,6529.60253 289.606572,6530.38301 L298.231646,6538.70754 C298.632403,6539.09329 299.27962,6539.09828 299.685554,6538.71753 L299.685554,6538.71753 C300.100809,6538.32879 300.104951,6537.68821 299.696945,6537.29347 L291.802968,6529.67648 C291.398069,6529.28574 291.398069,6528.65315 291.802968,6528.26241 L299.633777,6520.70538 C300.038676,6520.31563 300.038676,6519.68305 299.633777,6519.29231" id="arrow_left-[#335]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
19
ui/icons/arrow-right-336-svgrepo-com.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="-4.5 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
|
||||
<title>arrow_right [#336]</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Dribbble-Light-Preview" transform="translate(-305.000000, -6679.000000)" fill="#000000">
|
||||
<g id="icons" transform="translate(56.000000, 160.000000)">
|
||||
<path d="M249.365851,6538.70769 L249.365851,6538.70769 C249.770764,6539.09744 250.426289,6539.09744 250.830166,6538.70769 L259.393407,6530.44413 C260.202198,6529.66364 260.202198,6528.39747 259.393407,6527.61699 L250.768031,6519.29246 C250.367261,6518.90671 249.720021,6518.90172 249.314072,6519.28247 L249.314072,6519.28247 C248.899839,6519.67121 248.894661,6520.31179 249.302681,6520.70653 L257.196934,6528.32352 C257.601847,6528.71426 257.601847,6529.34685 257.196934,6529.73759 L249.365851,6537.29462 C248.960938,6537.68437 248.960938,6538.31795 249.365851,6538.70769" id="arrow_right-[#336]">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
64
ui/icons/camera_hdmi.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 800 800"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
sodipodi:docname="camera_hdmi.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1.0805556"
|
||||
inkscape:cx="-35.167095"
|
||||
inkscape:cy="455.78406"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<rect
|
||||
x="88.843188"
|
||||
y="123.08483"
|
||||
width="808.84319"
|
||||
height="255.42416"
|
||||
id="rect1" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Warstwa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text1"
|
||||
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
x="-24.242525"
|
||||
y="518.46466"
|
||||
id="text4"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4"
|
||||
x="-24.242525"
|
||||
y="518.46466"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
64
ui/icons/camera_usb.svg
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 800 800"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
sodipodi:docname="camera_usb.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1.0805556"
|
||||
inkscape:cx="-35.167095"
|
||||
inkscape:cy="455.78406"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<rect
|
||||
x="88.843188"
|
||||
y="123.08483"
|
||||
width="808.84319"
|
||||
height="255.42416"
|
||||
id="rect1" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Warstwa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text1"
|
||||
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
x="46.868595"
|
||||
y="518.38385"
|
||||
id="text4"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4"
|
||||
x="46.868595"
|
||||
y="518.38385"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
75
ui/icons/camera_usb_hdmi.svg
Normal file
@@ -0,0 +1,75 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="800"
|
||||
height="800"
|
||||
viewBox="0 0 800 800"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
sodipodi:docname="camera_usb_hdmi.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="px"
|
||||
inkscape:zoom="1.0805556"
|
||||
inkscape:cx="-35.167095"
|
||||
inkscape:cy="455.78406"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<rect
|
||||
x="88.843188"
|
||||
y="123.08483"
|
||||
width="808.84319"
|
||||
height="255.42416"
|
||||
id="rect1" />
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Warstwa 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
id="text1"
|
||||
style="text-align:start;writing-mode:lr-tb;direction:ltr;white-space:pre;shape-inside:url(#rect1);display:inline;fill:#000000;stroke:#979797;stroke-width:12;stroke-linecap:round" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
x="-24.243189"
|
||||
y="721.06696"
|
||||
id="text3"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan3"
|
||||
x="-24.243189"
|
||||
y="721.06696"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">HDMI</tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
x="46.868595"
|
||||
y="298.13632"
|
||||
id="text4"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan4"
|
||||
x="46.868595"
|
||||
y="298.13632"
|
||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:330.99px;font-family:Arial;-inkscape-font-specification:'Arial Bold';fill:#000000;fill-opacity:1;stroke:none;stroke-width:22.6283;stroke-dasharray:none;stroke-opacity:1">USB</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
2
ui/icons/error-16-svgrepo-com.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.493 0.015 C 7.442 0.021,7.268 0.039,7.107 0.055 C 5.234 0.242,3.347 1.208,2.071 2.634 C 0.660 4.211,-0.057 6.168,0.009 8.253 C 0.124 11.854,2.599 14.903,6.110 15.771 C 8.169 16.280,10.433 15.917,12.227 14.791 C 14.017 13.666,15.270 11.933,15.771 9.887 C 15.943 9.186,15.983 8.829,15.983 8.000 C 15.983 7.171,15.943 6.814,15.771 6.113 C 14.979 2.878,12.315 0.498,9.000 0.064 C 8.716 0.027,7.683 -0.006,7.493 0.015 M8.853 1.563 C 9.967 1.707,11.010 2.136,11.944 2.834 C 12.273 3.080,12.920 3.727,13.166 4.056 C 13.727 4.807,14.142 5.690,14.330 6.535 C 14.544 7.500,14.544 8.500,14.330 9.465 C 13.916 11.326,12.605 12.978,10.867 13.828 C 10.239 14.135,9.591 14.336,8.880 14.444 C 8.456 14.509,7.544 14.509,7.120 14.444 C 5.172 14.148,3.528 13.085,2.493 11.451 C 2.279 11.114,1.999 10.526,1.859 10.119 C 1.618 9.422,1.514 8.781,1.514 8.000 C 1.514 6.961,1.715 6.075,2.160 5.160 C 2.500 4.462,2.846 3.980,3.413 3.413 C 3.980 2.846,4.462 2.500,5.160 2.160 C 6.313 1.599,7.567 1.397,8.853 1.563 M7.706 4.290 C 7.482 4.363,7.355 4.491,7.293 4.705 C 7.257 4.827,7.253 5.106,7.259 6.816 C 7.267 8.786,7.267 8.787,7.325 8.896 C 7.398 9.033,7.538 9.157,7.671 9.204 C 7.803 9.250,8.197 9.250,8.329 9.204 C 8.462 9.157,8.602 9.033,8.675 8.896 C 8.733 8.787,8.733 8.786,8.741 6.816 C 8.749 4.664,8.749 4.662,8.596 4.481 C 8.472 4.333,8.339 4.284,8.040 4.276 C 7.893 4.272,7.743 4.278,7.706 4.290 M7.786 10.530 C 7.597 10.592,7.410 10.753,7.319 10.932 C 7.249 11.072,7.237 11.325,7.294 11.495 C 7.388 11.780,7.697 12.000,8.000 12.000 C 8.303 12.000,8.612 11.780,8.706 11.495 C 8.763 11.325,8.751 11.072,8.681 10.932 C 8.616 10.804,8.460 10.646,8.333 10.580 C 8.217 10.520,7.904 10.491,7.786 10.530 " stroke="none" fill-rule="evenodd" fill="#000000"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
4
ui/icons/flip-horizontal-svgrepo-com.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.079 3.46209C15.3762 3.17355 15.851 3.18054 16.1396 3.47771L19.538 6.9777C19.8205 7.26871 19.8205 7.73162 19.538 8.02263L16.1396 11.5226C15.851 11.8198 15.3762 11.8268 15.079 11.5382C14.7819 11.2497 14.7749 10.7749 15.0634 10.4777L17.2263 8.25015L4.99989 8.25015C4.58567 8.25015 4.24989 7.91437 4.24989 7.50015C4.24989 7.08594 4.58567 6.75015 4.99989 6.75015L17.2263 6.75015L15.0634 4.52264C14.7749 4.22546 14.7819 3.75064 15.079 3.46209ZM8.92071 12.4618C9.21788 12.7504 9.22488 13.2252 8.93633 13.5224L6.77327 15.7501L18.9999 15.7501C19.4141 15.7501 19.7499 16.0859 19.7499 16.5001C19.7499 16.9143 19.4141 17.2501 18.9999 17.2501L6.77366 17.2501L8.93633 19.4774C9.22488 19.7746 9.21788 20.2494 8.92071 20.538C8.62353 20.8265 8.14871 20.8195 7.86016 20.5224L4.46177 17.0224C4.17922 16.7314 4.17922 16.2685 4.46177 15.9774L7.86016 12.4775C8.14871 12.1803 8.62353 12.1733 8.92071 12.4618Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
5
ui/icons/flip-vertical-svgrepo-com.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.49976 4.25001C7.91398 4.25001 8.24976 4.5858 8.24976 5.00001L8.24976 17.2266L10.4775 15.0636C10.7747 14.775 11.2495 14.782 11.538 15.0792C11.8266 15.3763 11.8196 15.8512 11.5224 16.1397L8.02243 19.5381C7.73142 19.8207 7.26851 19.8207 6.9775 19.5381L3.47751 16.1397C3.18034 15.8512 3.17335 15.3763 3.4619 15.0792C3.75044 14.782 4.22527 14.775 4.52244 15.0636L6.74976 17.2262L6.74976 5.00001C6.74976 4.5858 7.08555 4.25001 7.49976 4.25001Z" fill="#000000"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.9773 4.4619C16.2683 4.17934 16.7312 4.17934 17.0222 4.4619L20.5222 7.86029C20.8193 8.14884 20.8263 8.62366 20.5378 8.92083C20.2492 9.21801 19.7744 9.225 19.4772 8.93645L17.2497 6.7736L17.2497 19C17.2497 19.4142 16.9139 19.75 16.4997 19.75C16.0855 19.75 15.7497 19.4142 15.7497 19L15.7497 6.77358L13.5222 8.93645C13.225 9.225 12.7502 9.21801 12.4616 8.92083C12.1731 8.62366 12.1801 8.14884 12.4773 7.86029L15.9773 4.4619Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
40
ui/icons/horizontal-stacks-svgrepo-com.svg
Normal file
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width="800px"
|
||||
height="800px"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
sodipodi:docname="horizontal-stacks-svgrepo-com.svg"
|
||||
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.98875"
|
||||
inkscape:cx="579.5196"
|
||||
inkscape:cy="342.35145"
|
||||
inkscape:window-width="3440"
|
||||
inkscape:window-height="1369"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg1" />
|
||||
<path
|
||||
d="M 13.5,14.82 V 1.18 C 13.5,0.53 12.97,0 12.32,0 H 9.8 C 9.15,0 8.62,0.53 8.62,1.18 V 14.82 C 8.62,15.47 9.15,16 9.8,16 h 2.52 c 0.65,0 1.18,-0.53 1.18,-1.18 z M 9.88,14.75 V 1.25 h 2.37 v 13.5 z m -2.5,0.07 V 1.18 C 7.38,0.53 6.85,0 6.2,0 H 3.68 C 3.03,0 2.5,0.53 2.5,1.18 V 14.82 C 2.5,15.47 3.03,16 3.68,16 H 6.2 c 0.65,0 1.18,-0.53 1.18,-1.18 z M 3.75,14.75 V 1.25 h 2.38 v 13.5 z"
|
||||
id="path1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
4
ui/icons/rotate-ccw-svgrepo-com.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 20.5C17.1944 20.5 21 16.6944 21 12C21 7.30558 17.1944 3.5 12.5 3.5C7.80558 3.5 4 7.30558 4 12C4 13.5433 4.41128 14.9905 5.13022 16.238M1.5 15L5.13022 16.238M6.82531 12.3832L5.47107 16.3542L5.13022 16.238" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 533 B |
4
ui/icons/rotate-cw-svgrepo-com.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 20.5C6.80558 20.5 3 16.6944 3 12C3 7.30558 6.80558 3.5 11.5 3.5C16.1944 3.5 20 7.30558 20 12C20 13.5433 19.5887 14.9905 18.8698 16.238M22.5 15L18.8698 16.238M17.1747 12.3832L18.5289 16.3542L18.8698 16.238" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 534 B |
17
ui/icons/settings-svgrepo-com.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
|
||||
<title>settings</title>
|
||||
<desc>Created with Sketch Beta.</desc>
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<g id="Icon-Set" sketch:type="MSLayerGroup" transform="translate(-101.000000, -360.000000)" fill="#000000">
|
||||
<path d="M128.52,381.134 L127.528,382.866 C127.254,383.345 126.648,383.508 126.173,383.232 L123.418,381.628 C122.02,383.219 120.129,384.359 117.983,384.799 L117.983,387 C117.983,387.553 117.54,388 116.992,388 L115.008,388 C114.46,388 114.017,387.553 114.017,387 L114.017,384.799 C111.871,384.359 109.98,383.219 108.582,381.628 L105.827,383.232 C105.352,383.508 104.746,383.345 104.472,382.866 L103.48,381.134 C103.206,380.656 103.369,380.044 103.843,379.769 L106.609,378.157 C106.28,377.163 106.083,376.106 106.083,375 C106.083,373.894 106.28,372.838 106.609,371.843 L103.843,370.232 C103.369,369.956 103.206,369.345 103.48,368.866 L104.472,367.134 C104.746,366.656 105.352,366.492 105.827,366.768 L108.582,368.372 C109.98,366.781 111.871,365.641 114.017,365.201 L114.017,363 C114.017,362.447 114.46,362 115.008,362 L116.992,362 C117.54,362 117.983,362.447 117.983,363 L117.983,365.201 C120.129,365.641 122.02,366.781 123.418,368.372 L126.173,366.768 C126.648,366.492 127.254,366.656 127.528,367.134 L128.52,368.866 C128.794,369.345 128.631,369.956 128.157,370.232 L125.391,371.843 C125.72,372.838 125.917,373.894 125.917,375 C125.917,376.106 125.72,377.163 125.391,378.157 L128.157,379.769 C128.631,380.044 128.794,380.656 128.52,381.134 L128.52,381.134 Z M130.008,378.536 L127.685,377.184 C127.815,376.474 127.901,375.749 127.901,375 C127.901,374.252 127.815,373.526 127.685,372.816 L130.008,371.464 C130.957,370.912 131.281,369.688 130.733,368.732 L128.75,365.268 C128.203,364.312 126.989,363.983 126.041,364.536 L123.694,365.901 C122.598,364.961 121.352,364.192 119.967,363.697 L119.967,362 C119.967,360.896 119.079,360 117.983,360 L114.017,360 C112.921,360 112.033,360.896 112.033,362 L112.033,363.697 C110.648,364.192 109.402,364.961 108.306,365.901 L105.959,364.536 C105.011,363.983 103.797,364.312 103.25,365.268 L101.267,368.732 C100.719,369.688 101.044,370.912 101.992,371.464 L104.315,372.816 C104.185,373.526 104.099,374.252 104.099,375 C104.099,375.749 104.185,376.474 104.315,377.184 L101.992,378.536 C101.044,379.088 100.719,380.312 101.267,381.268 L103.25,384.732 C103.797,385.688 105.011,386.017 105.959,385.464 L108.306,384.099 C109.402,385.039 110.648,385.809 112.033,386.303 L112.033,388 C112.033,389.104 112.921,390 114.017,390 L117.983,390 C119.079,390 119.967,389.104 119.967,388 L119.967,386.303 C121.352,385.809 122.598,385.039 123.694,384.099 L126.041,385.464 C126.989,386.017 128.203,385.688 128.75,384.732 L130.733,381.268 C131.281,380.312 130.957,379.088 130.008,378.536 L130.008,378.536 Z M116,378 C114.357,378 113.025,376.657 113.025,375 C113.025,373.344 114.357,372 116,372 C117.643,372 118.975,373.344 118.975,375 C118.975,376.657 117.643,378 116,378 L116,378 Z M116,370 C113.261,370 111.042,372.238 111.042,375 C111.042,377.762 113.261,380 116,380 C118.739,380 120.959,377.762 120.959,375 C120.959,372.238 118.739,370 116,370 L116,370 Z" id="settings" sketch:type="MSShapeGroup">
|
||||
|
||||
</path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
2
ui/icons/vertical-stacks-svgrepo-com.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.82 2.5H1.18C.53 2.5 0 3.03 0 3.68V6.2c0 .65.53 1.18 1.18 1.18h13.64c.65 0 1.18-.53 1.18-1.18V3.68c0-.65-.53-1.18-1.18-1.18zm-.07 3.62H1.25V3.75h13.5v2.37zm.07 2.5H1.18C.53 8.62 0 9.15 0 9.8v2.52c0 .65.53 1.18 1.18 1.18h13.64c.65 0 1.18-.53 1.18-1.18V9.8c0-.65-.53-1.18-1.18-1.18zm-.07 3.63H1.25V9.87h13.5v2.38z"/></svg>
|
||||
|
After Width: | Height: | Size: 556 B |
48
ui/main_palette.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QPalette, QColor
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
def set_dark_theme(app: QApplication):
|
||||
"""Definiuje i stosuje ciemną paletę kolorów do aplikacji."""
|
||||
|
||||
# 1. Upewnij się, że styl jest ustawiony na "Fusion"
|
||||
app.setStyle('Fusion')
|
||||
|
||||
# 2. Definicja kolorów dla ciemnego motywu
|
||||
palette = QPalette()
|
||||
|
||||
# Kolory tła
|
||||
DARK_GRAY = QColor(45, 45, 45) # Ogólne tło okien i widżetów (Base, Window)
|
||||
LIGHT_GRAY = QColor(53, 53, 53) # Tło elementów, np. toolbara, menu (Window)
|
||||
VERY_DARK_GRAY = QColor(32, 32, 32) # Kolor tła dla kontrolek (Button)
|
||||
|
||||
# Kolory tekstu i obramowań
|
||||
WHITE = QColor(200, 200, 200) # Główny kolor tekstu (Text, WindowText)
|
||||
HIGHLIGHT = QColor(66, 135, 245) # Kolor podświetlenia (Highlight)
|
||||
|
||||
# Ustawienie głównej palety
|
||||
# palette.setColor(QPalette.ColorRole.Window, LIGHT_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.Window, VERY_DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.WindowText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Base, DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.AlternateBase, LIGHT_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.ToolTipBase, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.ToolTipText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Text, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.Button, VERY_DARK_GRAY)
|
||||
palette.setColor(QPalette.ColorRole.ButtonText, WHITE)
|
||||
palette.setColor(QPalette.ColorRole.BrightText, Qt.GlobalColor.red)
|
||||
palette.setColor(QPalette.ColorRole.Link, QColor(42, 130, 218))
|
||||
palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(150, 150, 150))
|
||||
|
||||
# Kolory zaznaczenia/interakcji
|
||||
palette.setColor(QPalette.ColorRole.Highlight, HIGHLIGHT)
|
||||
palette.setColor(QPalette.ColorRole.HighlightedText, Qt.GlobalColor.black)
|
||||
|
||||
# Kontrolki wyłączone (Disabled)
|
||||
# palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.Text, QColor(127, 127, 127))
|
||||
# palette.setColor(QPalette.ColorRole.Disabled, QPalette.ColorGroup.Active, QPalette.ColorRole.ButtonText, QColor(127, 127, 127))
|
||||
|
||||
# 3. Zastosowanie palety do aplikacji
|
||||
app.setPalette(palette)
|
||||
@@ -10,6 +10,7 @@ from ui.widgets.placeholder_widget import PlaceholderWidget
|
||||
from ui.widgets.color_list_widget import ColorListWidget
|
||||
from ui.widgets.thumbnail_list_widget import ThumbnailListWidget
|
||||
from ui.widgets.split_view_widget import SplitView
|
||||
from ui.view_settings_dialog import ViewSettingsDialog
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
@@ -42,6 +43,14 @@ class MainWindow(QMainWindow):
|
||||
histogram_view = PlaceholderWidget("Histogram View", "#FF5733")
|
||||
histogram_view.setFixedHeight(200)
|
||||
|
||||
self.view_settings_button = QPushButton("Ustawienia widoku")
|
||||
self.view_settings_button.setMinimumHeight(40)
|
||||
self.view_settings_button.setStyleSheet("font-size: 12pt;")
|
||||
|
||||
self.view_settings_dialog = ViewSettingsDialog(self)
|
||||
self.view_settings_button.clicked.connect(self.view_settings_dialog.show)
|
||||
|
||||
|
||||
self.color_list_widget = ColorListWidget(self.control_widget)
|
||||
|
||||
self.record_button = QPushButton("Nagraj Wideo")
|
||||
@@ -53,6 +62,7 @@ class MainWindow(QMainWindow):
|
||||
self.photo_button.setStyleSheet("font-size: 12pt;")
|
||||
|
||||
control_layout.addWidget(histogram_view)
|
||||
control_layout.addWidget(self.view_settings_button)
|
||||
control_layout.addWidget(self.color_list_widget)
|
||||
control_layout.addWidget(self.record_button)
|
||||
control_layout.addWidget(self.photo_button)
|
||||
|
||||
209
ui/view_settings_dialog.py
Normal file
@@ -0,0 +1,209 @@
|
||||
from PySide6.QtWidgets import QDialog, QHBoxLayout ,QVBoxLayout, QPushButton, QGroupBox, QLabel, QRadioButton, QWidget, QToolButton, QSlider, QButtonGroup
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import Qt, QSize, Signal
|
||||
|
||||
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"]
|
||||
|
||||
class LabeledSpinSelector(QWidget):
|
||||
indexChanged = Signal(int)
|
||||
|
||||
def __init__(self, title: str, values: list[str], show_slider: bool = False, parent=None):
|
||||
super().__init__(parent)
|
||||
self.values = values
|
||||
self.current_index = 0
|
||||
self.show_slider = show_slider
|
||||
self._init_ui(title)
|
||||
|
||||
def _init_ui(self, title: str, button_size: int = 24, icon_size: int = 16):
|
||||
self.title_label = QLabel(title)
|
||||
|
||||
decrement_button = QToolButton()
|
||||
decrement_button.setIcon(QIcon("ui/icons/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.setFixedSize(button_size, button_size)
|
||||
increment_button.setIconSize(QSize(icon_size, icon_size))
|
||||
increment_button.clicked.connect(self._increment)
|
||||
|
||||
self.value_label = QLabel(self.values[self.current_index] if self.values else "N/A")
|
||||
self.value_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.value_label.setStyleSheet("background-color: rgb(48, 48, 48);")
|
||||
self.value_label.setFixedHeight(button_size - 2)
|
||||
|
||||
spin_layout = QHBoxLayout()
|
||||
spin_layout.addWidget(decrement_button)
|
||||
spin_layout.addWidget(self.value_label)
|
||||
spin_layout.addWidget(increment_button)
|
||||
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.addWidget(self.title_label)
|
||||
top_layout.addLayout(spin_layout)
|
||||
|
||||
self.slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.slider.setRange(0, max(0, len(self.values) - 1))
|
||||
self.slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
||||
self.slider.valueChanged.connect(self._slider_changed)
|
||||
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.addLayout(top_layout)
|
||||
if self.show_slider:
|
||||
main_layout.addWidget(self.slider)
|
||||
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
def _increment(self):
|
||||
if not self.values:
|
||||
return
|
||||
new_index = min(self.current_index + 1, len(self.values) - 1)
|
||||
self.set_index(new_index)
|
||||
|
||||
def _decrement(self):
|
||||
if not self.values:
|
||||
return
|
||||
new_index = max(self.current_index - 1, 0)
|
||||
self.set_index(new_index)
|
||||
|
||||
def _slider_changed(self, index):
|
||||
if not self.values:
|
||||
return
|
||||
self.set_index(index)
|
||||
|
||||
def set_label(self, label: str):
|
||||
self.title_label.setText(label)
|
||||
|
||||
def set_index(self, index: int):
|
||||
if not self.values or not (0 <= index < len(self.values)):
|
||||
return
|
||||
if self.current_index != index:
|
||||
self.current_index = index
|
||||
self.value_label.setText(self.values[index])
|
||||
if self.show_slider:
|
||||
self.slider.setValue(index)
|
||||
self.indexChanged.emit(index)
|
||||
else:
|
||||
# Always update UI even if index is the same (for initial set)
|
||||
self.value_label.setText(self.values[index])
|
||||
if self.show_slider:
|
||||
self.slider.setValue(index)
|
||||
|
||||
class ViewSettingsDialog(QDialog):
|
||||
detectDevice = Signal(str)
|
||||
connectionChanged = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Ustawienia widoku")
|
||||
self.setFixedSize(640, 480)
|
||||
|
||||
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
|
||||
|
||||
# self.setup_ui()
|
||||
camera_frame = self._create_devices_frame("camera")
|
||||
hdmi_frame = self._create_devices_frame("hdmi")
|
||||
conn_frame = self._create_connection_frame()
|
||||
camera_settings = self._create_settings_frame()
|
||||
hdmi_settings = self._create_settings_frame()
|
||||
dialog_buttons = self._create_dialog_buttons()
|
||||
|
||||
hdmi_settings.setEnabled(False)
|
||||
|
||||
settings_layout = QHBoxLayout()
|
||||
settings_layout.addWidget(camera_settings, 1)
|
||||
settings_layout.addWidget(hdmi_settings, 1)
|
||||
# main layout
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.addWidget(camera_frame)
|
||||
main_layout.addWidget(hdmi_frame)
|
||||
main_layout.addWidget(conn_frame)
|
||||
main_layout.addLayout(settings_layout)
|
||||
main_layout.addLayout(dialog_buttons)
|
||||
|
||||
main_layout.setStretch(3, 1)
|
||||
|
||||
self.last_choice = None
|
||||
|
||||
|
||||
def _create_dialog_buttons(self):
|
||||
ok_btn = QPushButton("OK")
|
||||
ok_btn.clicked.connect(self.accept)
|
||||
|
||||
cancel_btn = QPushButton("Anuluj")
|
||||
cancel_btn.setDefault(True)
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addStretch()
|
||||
layout.addWidget(ok_btn)
|
||||
layout.addWidget(cancel_btn)
|
||||
|
||||
return layout
|
||||
|
||||
def _create_devices_frame(self, name):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Wykryte aparaty")
|
||||
frame.setContentsMargins(6, 20, 6, 10)
|
||||
|
||||
device_label = QLabel("Nie wykryto podłączonych urządzeń.")
|
||||
detect_button = QPushButton("Wykryj...")
|
||||
detect_button.clicked.connect(lambda: self.detectDevice.emit(name))
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(device_label)
|
||||
layout.addStretch()
|
||||
layout.addWidget(detect_button)
|
||||
|
||||
frame.setLayout(layout)
|
||||
|
||||
return frame
|
||||
|
||||
def _create_connection_frame(self):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Wybór połączenia")
|
||||
frame.setContentsMargins(6, 20, 6, 10)
|
||||
|
||||
radio_usb = QRadioButton("USB")
|
||||
radio_hybrid = QRadioButton("USB + HDMI")
|
||||
radio_hdmi = QRadioButton("HDMI")
|
||||
|
||||
radio_hdmi.setEnabled(False)
|
||||
|
||||
radio_usb.clicked.connect(lambda: self.radio_toggle("usb"))
|
||||
radio_hybrid.clicked.connect(lambda: self.radio_toggle("hybrid"))
|
||||
radio_hdmi.clicked.connect(lambda: self.radio_toggle("hdmi"))
|
||||
|
||||
radio_layout = QHBoxLayout()
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_usb)
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_hybrid)
|
||||
radio_layout.addStretch()
|
||||
radio_layout.addWidget(radio_hdmi)
|
||||
radio_layout.addStretch()
|
||||
|
||||
frame.setLayout(radio_layout)
|
||||
|
||||
return frame
|
||||
|
||||
# def _create_settings_frame(self, settings: dict[str, list[str]]):
|
||||
def _create_settings_frame(self):
|
||||
frame = QGroupBox()
|
||||
frame.setTitle("Ustawienia aparatu")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
# for key, value in settings.items():
|
||||
# layout.addWidget(LabeledSpinSelector(key, value))
|
||||
layout.addStretch()
|
||||
|
||||
frame.setLayout(layout)
|
||||
return frame
|
||||
|
||||
def radio_toggle(self, value):
|
||||
if self.last_choice != value:
|
||||
self.last_choice = value
|
||||
self.connectionChanged.emit(value)
|
||||
@@ -1,164 +1,351 @@
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QStackedWidget, QPushButton, QLabel
|
||||
from PySide6.QtGui import QPixmap, QWheelEvent, QPainter, QBrush, QColor
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QApplication, QMainWindow, QWidget, QVBoxLayout, QSplitter, QStackedWidget, QPushButton, QLabel, QToolButton
|
||||
from PySide6.QtGui import QEnterEvent, QPixmap, QWheelEvent, QPainter, QBrush, QColor, QIcon, QImage, QTransform
|
||||
from PySide6.QtCore import Qt, QSize, Signal, QEvent
|
||||
import sys
|
||||
from ui.widgets.placeholder_widget import PlaceholderWidget
|
||||
|
||||
|
||||
class ZoomableImageView(QGraphicsView):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Scena i element obrazu
|
||||
self._scene = QGraphicsScene(self)
|
||||
self.setScene(self._scene)
|
||||
self._scene.setBackgroundBrush(QBrush(QColor(20, 20, 20))) # ciemne tło
|
||||
# Scena i element obrazu
|
||||
self._scene = QGraphicsScene(self)
|
||||
self.setScene(self._scene)
|
||||
self._scene.setBackgroundBrush(
|
||||
QBrush(QColor(20, 20, 20))) # ciemne tło
|
||||
|
||||
self._pixmap_item = QGraphicsPixmapItem()
|
||||
self._scene.addItem(self._pixmap_item)
|
||||
self._pixmap_item = QGraphicsPixmapItem()
|
||||
self._scene.addItem(self._pixmap_item)
|
||||
|
||||
# Ustawienia widoku
|
||||
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag) # przesuwanie myszą
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
# Ustawienia widoku
|
||||
# przesuwanie myszą
|
||||
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
|
||||
self.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
|
||||
|
||||
# Wyłączenie suwaków
|
||||
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# Parametry zoomu
|
||||
self._zoom_factor = 1.25
|
||||
self._current_scale = 1.0
|
||||
# Wyłączenie suwaków
|
||||
self.setHorizontalScrollBarPolicy(
|
||||
Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
# Parametry zoomu
|
||||
self._zoom_factor = 1.25
|
||||
self._current_scale = 1.0
|
||||
|
||||
def set_image(self, pixmap: QPixmap):
|
||||
# pixmap = QPixmap(image_path)
|
||||
self._pixmap_item.setPixmap(pixmap)
|
||||
self._scene.setSceneRect(pixmap.rect())
|
||||
# self.reset_transform()
|
||||
def set_image(self, pixmap: QPixmap):
|
||||
if pixmap.isNull():
|
||||
return
|
||||
|
||||
self._pixmap_item.setPixmap(pixmap)
|
||||
self._scene.setSceneRect(pixmap.rect())
|
||||
if self._current_scale == 1.0:
|
||||
self.fitInView(self._pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
|
||||
self._first_image = False
|
||||
|
||||
def reset_transform(self):
|
||||
"""Resetuje skalowanie i ustawia 1:1"""
|
||||
self._current_scale = 1.0
|
||||
self.setTransform(self.transform().fromScale(1, 1))
|
||||
def reset_transform(self):
|
||||
"""Resetuje skalowanie i ustawia 1:1"""
|
||||
self._current_scale = 1.0
|
||||
self.setTransform(self.transform().fromScale(1, 1))
|
||||
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
"""Zoom kółkiem myszy"""
|
||||
if event.modifiers() & Qt.KeyboardModifier.ControlModifier: # zoom tylko z CTRL
|
||||
if event.angleDelta().y() > 0:
|
||||
zoom = self._zoom_factor
|
||||
else:
|
||||
zoom = 1 / self._zoom_factor
|
||||
def wheelEvent(self, event: QWheelEvent):
|
||||
"""Zoom kółkiem myszy"""
|
||||
if event.modifiers() & Qt.KeyboardModifier.ControlModifier: # zoom tylko z CTRL
|
||||
if event.angleDelta().y() > 0:
|
||||
zoom = self._zoom_factor
|
||||
else:
|
||||
zoom = 1 / self._zoom_factor
|
||||
|
||||
self._current_scale *= zoom
|
||||
self.scale(zoom, zoom)
|
||||
else:
|
||||
return
|
||||
super().wheelEvent(event) # normalne przewijanie
|
||||
|
||||
self._current_scale *= zoom
|
||||
self.scale(zoom, zoom)
|
||||
else:
|
||||
return
|
||||
super().wheelEvent(event) # normalne przewijanie
|
||||
|
||||
class CameraPlaceholder(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setAutoFillBackground(True)
|
||||
self.setStyleSheet("background-color: #141414;")
|
||||
self.setAutoFillBackground(True)
|
||||
self.setStyleSheet("background-color: #141414;")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(20)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(20)
|
||||
|
||||
self.camera_start_btn = QPushButton("Start Camera")
|
||||
self.camera_start_btn.setFixedSize(200, 50)
|
||||
style_sheet = """
|
||||
QPushButton {
|
||||
/* --- Styl podstawowy --- */
|
||||
background-color: transparent;
|
||||
border: 2px solid #CECECE; /* Grubość, styl i kolor obramowania */
|
||||
border-radius: 25px; /* Kluczowa właściwość do zaokrąglenia rogów! */
|
||||
color: #CECECE;
|
||||
padding: 10px 20px; /* Wewnętrzny margines */
|
||||
font-size: 16px;
|
||||
}
|
||||
self.camera_start_btn = QPushButton("Start Camera")
|
||||
self.camera_start_btn.setFixedSize(200, 50)
|
||||
style_sheet = """
|
||||
QPushButton {
|
||||
/* --- Styl podstawowy --- */
|
||||
background-color: transparent;
|
||||
border: 2px solid #CECECE; /* Grubość, styl i kolor obramowania */
|
||||
border-radius: 25px; /* Kluczowa właściwość do zaokrąglenia rogów! */
|
||||
color: #CECECE;
|
||||
padding: 10px 20px; /* Wewnętrzny margines */
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
/* --- Styl po najechaniu myszką --- */
|
||||
color: #F0F0F0;
|
||||
border: 2px solid #F0F0F0;
|
||||
}
|
||||
QPushButton:hover {
|
||||
/* --- Styl po najechaniu myszką --- */
|
||||
color: #F0F0F0;
|
||||
border: 2px solid #F0F0F0;
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
/* --- Styl po naciśnięciu --- */
|
||||
background-color: #e0e0e0; /* Ciemniejsze tło w momencie kliknięcia */
|
||||
border: 2px solid #e0e0e0; /* Zmiana koloru ramki dla sygnalizacji akcji */
|
||||
}
|
||||
"""
|
||||
self.camera_start_btn.setStyleSheet(style_sheet)
|
||||
QPushButton:pressed {
|
||||
/* --- Styl po naciśnięciu --- */
|
||||
background-color: #e0e0e0; /* Ciemniejsze tło w momencie kliknięcia */
|
||||
border: 2px solid #e0e0e0; /* Zmiana koloru ramki dla sygnalizacji akcji */
|
||||
}
|
||||
"""
|
||||
self.camera_start_btn.setStyleSheet(style_sheet)
|
||||
|
||||
self.info_label = QLabel("Kliknij, aby uruchomić kamerę")
|
||||
self.info_label.setStyleSheet("background-color: transparent; color: #CECECE; font-size: 18px;")
|
||||
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.info_label = QLabel("Kliknij, aby uruchomić kamerę")
|
||||
self.info_label.setStyleSheet(
|
||||
"background-color: transparent; color: #CECECE; font-size: 18px;")
|
||||
self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.camera_start_btn, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.info_label)
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
self.error_label = QLabel()
|
||||
self.error_label.setStyleSheet(
|
||||
"background-color: transparent; color: #CECECE; font-size: 18px; font-style: italic;")
|
||||
self.error_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
def set_info_text(self, text: str):
|
||||
self.info_label.setText(text)
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.camera_start_btn,
|
||||
alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(self.info_label)
|
||||
layout.addWidget(self.error_label)
|
||||
layout.addStretch()
|
||||
self.setLayout(layout)
|
||||
|
||||
def set_info_text(self, text: str):
|
||||
self.info_label.setText(text)
|
||||
|
||||
def set_error_text(self, text: str):
|
||||
self.error_label.setText(text)
|
||||
|
||||
def set_button_text(self, text:str):
|
||||
self.camera_start_btn.setText(text)
|
||||
|
||||
|
||||
class ViewWithOverlay(QWidget):
|
||||
cameraConnection = Signal()
|
||||
cameraSettings = Signal()
|
||||
toggleOrientation = Signal()
|
||||
swapViews = Signal()
|
||||
rotateCW = Signal()
|
||||
rotateCCW = Signal()
|
||||
|
||||
def __init__(self, live: bool = False):
|
||||
super().__init__()
|
||||
self.live = live
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.viewer = ZoomableImageView()
|
||||
layout.addWidget(self.viewer)
|
||||
|
||||
self._create_top_right_buttons()
|
||||
if self.live:
|
||||
self._create_top_left_buttons()
|
||||
|
||||
self.resize(self.size())
|
||||
|
||||
# self.cw_btn.raise_()
|
||||
# self.ccw_btn.raise_()
|
||||
# self.flip_btn.raise_()
|
||||
# self.orient_btn.raise_()
|
||||
|
||||
self.toggle_orientation(Qt.Orientation.Vertical)
|
||||
|
||||
def _create_tool_button(self, callback, icon_path: str | None):
|
||||
icon_size = QSize(32, 32)
|
||||
btn_size = (48, 48)
|
||||
btn_style = """
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 8px;
|
||||
border: 2px solid #1f1f1f;
|
||||
"""
|
||||
|
||||
btn = QToolButton(self)
|
||||
if icon_path:
|
||||
btn.setIcon(QIcon(icon_path))
|
||||
btn.setIconSize(icon_size)
|
||||
btn.setStyleSheet(btn_style)
|
||||
btn.setFixedSize(*btn_size)
|
||||
btn.clicked.connect(callback)
|
||||
|
||||
return btn
|
||||
|
||||
def _create_top_right_buttons(self):
|
||||
self.cw_btn = self._create_tool_button(
|
||||
icon_path="ui/icons/rotate-cw-svgrepo-com.svg",
|
||||
callback=self.rotateCW,
|
||||
)
|
||||
|
||||
self.ccw_btn = self._create_tool_button(
|
||||
icon_path="ui/icons/rotate-ccw-svgrepo-com.svg",
|
||||
callback=self.rotateCCW,
|
||||
)
|
||||
|
||||
self.flip_btn = self._create_tool_button(
|
||||
icon_path=None,
|
||||
callback=self.swapViews,
|
||||
)
|
||||
|
||||
self.orient_btn = self._create_tool_button(
|
||||
icon_path=None,
|
||||
callback=self.toggleOrientation,
|
||||
)
|
||||
|
||||
def _create_top_left_buttons(self):
|
||||
self.camera_btn = self._create_tool_button(
|
||||
icon_path="ui/icons/settings-svgrepo-com.svg",
|
||||
callback=self.cameraConnection
|
||||
)
|
||||
|
||||
self.settings_btn = self._create_tool_button(
|
||||
icon_path="ui/icons/error-16-svgrepo-com.svg",
|
||||
callback=self.cameraSettings
|
||||
)
|
||||
|
||||
def set_image(self, pixmap: QPixmap):
|
||||
self.viewer.set_image(pixmap)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
# Aktualizacja pozycji przycisku przy zmianie rozmiaru
|
||||
if self.live:
|
||||
left_corner = 10
|
||||
self.camera_btn.move(left_corner, 10)
|
||||
left_corner += self.camera_btn.width() + 10
|
||||
self.settings_btn.move(left_corner, 10)
|
||||
|
||||
right_corner = self.cw_btn.width() + 10
|
||||
self.cw_btn.move(self.width() - right_corner, 10)
|
||||
right_corner += self.ccw_btn.width() + 10
|
||||
self.ccw_btn.move(self.width() - right_corner, 10)
|
||||
right_corner += self.flip_btn.width() + 10
|
||||
self.flip_btn.move(self.width() - right_corner, 10)
|
||||
right_corner += self.orient_btn.width() + 10
|
||||
self.orient_btn.move(self.width() - right_corner, 10)
|
||||
|
||||
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"))
|
||||
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"))
|
||||
|
||||
def enterEvent(self, event: QEnterEvent) -> None:
|
||||
if self.live:
|
||||
self.camera_btn.show()
|
||||
self.settings_btn.show()
|
||||
|
||||
self.orient_btn.show()
|
||||
self.flip_btn.show()
|
||||
self.ccw_btn.show()
|
||||
self.cw_btn.show()
|
||||
return super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None:
|
||||
if self.live:
|
||||
self.camera_btn.hide()
|
||||
self.settings_btn.hide()
|
||||
|
||||
self.orient_btn.hide()
|
||||
self.flip_btn.hide()
|
||||
self.ccw_btn.hide()
|
||||
self.cw_btn.hide()
|
||||
return super().leaveEvent(event)
|
||||
|
||||
|
||||
class SplitView(QSplitter):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
print("Inicjalizacja SplitView2")
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
print("Inicjalizacja SplitView2")
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
|
||||
self.widget_start = CameraPlaceholder()
|
||||
self.widget_live = ZoomableImageView()
|
||||
# self.widget_live = PlaceholderWidget("Camera View", "#750466")
|
||||
self.widget_ref = ZoomableImageView()
|
||||
# self.widget_ref = PlaceholderWidget("Image View", "#007981")
|
||||
self.widget_start = CameraPlaceholder()
|
||||
self.widget_live = ViewWithOverlay(live=True)
|
||||
self.widget_ref = ViewWithOverlay()
|
||||
|
||||
self.stack = QStackedWidget()
|
||||
self.stack.addWidget(self.widget_start)
|
||||
self.stack.addWidget(self.widget_live)
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
self.stack = QStackedWidget()
|
||||
self.stack.addWidget(self.widget_start)
|
||||
self.stack.addWidget(self.widget_live)
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
|
||||
self.addWidget(self.stack)
|
||||
self.addWidget(self.widget_ref)
|
||||
self.addWidget(self.stack)
|
||||
self.addWidget(self.widget_ref)
|
||||
|
||||
self.setSizes([self.height(), 0])
|
||||
self.setSizes([self.height(), 0])
|
||||
|
||||
pixmap = QPixmap("media/empty_guitar_h.jpg")
|
||||
# pixmap.fill(Qt.GlobalColor.lightGray)
|
||||
self.widget_live.set_image(pixmap)
|
||||
# pixmap = QPixmap("media/empty_guitar_h.jpg")
|
||||
# pixmap.fill(Qt.GlobalColor.lightGray)
|
||||
# self.widget_live.set_image(pixmap)
|
||||
|
||||
def toggle_orientation(self):
|
||||
if self.orientation() == Qt.Orientation.Vertical:
|
||||
self.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.setSizes([self.width()//2, self.width()//2])
|
||||
else:
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
self.setSizes([self.height()//2, self.height()//2])
|
||||
|
||||
# def set_live_image(self, path_image: str):
|
||||
# """Ustawienie obrazu na żywo"""
|
||||
# pixmap = QPixmap(path_image)
|
||||
# self.widget_live.set_image(pixmap)
|
||||
self.ref_image_rotate = 0
|
||||
self.ref_pixmap = None
|
||||
|
||||
def set_live_image(self, pixmap: QPixmap):
|
||||
"""Ustawienie obrazu na żywo"""
|
||||
self.widget_live.set_image(pixmap)
|
||||
if self.stack.currentWidget() != self.widget_live:
|
||||
self.stack.setCurrentWidget(self.widget_live)
|
||||
self.widget_live.toggleOrientation.connect(self.toggle_orientation)
|
||||
self.widget_ref.toggleOrientation.connect(self.toggle_orientation)
|
||||
self.widget_live.swapViews.connect(self.swap_views)
|
||||
self.widget_ref.swapViews.connect(self.swap_views)
|
||||
self.widget_ref.rotateCCW.connect(self.rotate_left)
|
||||
self.widget_ref.rotateCW.connect(self.rotate_right)
|
||||
|
||||
def set_reference_image(self, path_image: str):
|
||||
"""Ustawienie obrazu referencyjnego"""
|
||||
pixmap = QPixmap(path_image)
|
||||
self.widget_ref.set_image(pixmap)
|
||||
def toggle_orientation(self):
|
||||
if self.orientation() == Qt.Orientation.Vertical:
|
||||
self.setOrientation(Qt.Orientation.Horizontal)
|
||||
self.setSizes([self.width()//2, self.width()//2])
|
||||
self.widget_live.toggle_orientation(Qt.Orientation.Horizontal)
|
||||
self.widget_ref.toggle_orientation(Qt.Orientation.Horizontal)
|
||||
else:
|
||||
self.setOrientation(Qt.Orientation.Vertical)
|
||||
self.setSizes([self.height()//2, self.height()//2])
|
||||
self.widget_live.toggle_orientation(Qt.Orientation.Vertical)
|
||||
self.widget_ref.toggle_orientation(Qt.Orientation.Vertical)
|
||||
|
||||
def toglle_live_view(self):
|
||||
"""Przełączanie widoku na żywo"""
|
||||
if self.stack.currentWidget() == self.widget_start:
|
||||
self.stack.setCurrentWidget(self.widget_live)
|
||||
else:
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
def swap_views(self):
|
||||
"""Zamiana widoków miejscami"""
|
||||
index_live = self.indexOf(self.stack)
|
||||
index_ref = self.indexOf(self.widget_ref)
|
||||
sizes = self.sizes()
|
||||
self.insertWidget(index_live, self.widget_ref)
|
||||
self.insertWidget(index_ref, self.stack)
|
||||
self.setSizes(sizes)
|
||||
|
||||
|
||||
def set_live_image(self, pixmap: QPixmap):
|
||||
"""Ustawienie obrazu na żywo"""
|
||||
self.widget_live.set_image(pixmap)
|
||||
# if self.stack.currentWidget() != self.widget_live:
|
||||
# self.stack.setCurrentWidget(self.widget_live)
|
||||
|
||||
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)
|
||||
|
||||
def toggle_live_view(self):
|
||||
"""Przełączanie widoku na żywo"""
|
||||
if self.stack.currentWidget() == self.widget_start:
|
||||
self.stack.setCurrentWidget(self.widget_live)
|
||||
else:
|
||||
self.stack.setCurrentWidget(self.widget_start)
|
||||
|
||||
def rotate_left(self):
|
||||
if not self.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)
|
||||
|
||||
def rotate_right(self):
|
||||
if not self.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)
|
||||