diff --git a/.idea/AstroSessionWatcher.iml b/.idea/AstroSessionWatcher.iml
index d0876a7..63ab128 100644
--- a/.idea/AstroSessionWatcher.iml
+++ b/.idea/AstroSessionWatcher.iml
@@ -1,8 +1,10 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 9d7e713..bec30ab 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -1,12 +1,41 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -14,16 +43,27 @@
"associatedIndex": 8
}]]>
+
+
+
@@ -42,11 +82,14 @@
1778143911036
-
+
+
+
+
\ No newline at end of file
diff --git a/astro_settings.json b/astro_settings.json
new file mode 100644
index 0000000..fd39922
--- /dev/null
+++ b/astro_settings.json
@@ -0,0 +1,15 @@
+{
+ "cameras": [
+ "Canon 40D",
+ "Canon 400D",
+ "Canon 500D"
+ ],
+ "lenses": [
+ "MTO-500A",
+ "Юпитер-21м",
+ "Tamron 18-200mm"
+ ],
+ "last_watch_folder": "C:/Users/Juliette/Documents/testwatcher",
+ "last_camera": "Canon 40D",
+ "last_lens": "MTO-500A"
+}
\ No newline at end of file
diff --git a/build_exe.spec b/build_exe.spec
index e69de29..ce5a8c7 100644
--- a/build_exe.spec
+++ b/build_exe.spec
@@ -0,0 +1,40 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+a = Analysis(
+ ['main.py'],
+ pathex=[],
+ binaries=[],
+ datas=[
+ ('resources/done.mp3', 'resources'),
+ ],
+ hiddenimports=['customtkinter', 'watchdog', 'pygame', 'ctypes', 'queue'],
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+)
+
+pyz = PYZ(a.pure)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ a.binaries,
+ a.datas,
+ [],
+ name='AstroSessionWatcher',
+ debug=False,
+ bootloader_ignore_signals=False,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ runtime_tmpdir=None,
+ console=False, # Без консоли
+ disable_windowed_traceback=False,
+ argv_emulation=False,
+ target_arch=None,
+ codesign_identity=None,
+ entitlements_file=None,
+ icon='resources/icon.ico', # Если есть иконка
+)
\ No newline at end of file
diff --git a/celestial_bodies.json b/celestial_bodies.json
new file mode 100644
index 0000000..c510679
--- /dev/null
+++ b/celestial_bodies.json
@@ -0,0 +1,14 @@
+[
+ "M31 (Andromeda Galaxy)",
+ "M42 (Orion Nebula)",
+ "M45 (Pleiades)",
+ "M57 (Ring Nebula)",
+ "Солнце",
+ "Moon",
+ "Jupiter",
+ "Сатурн",
+ "M89",
+ "Венера",
+ "Меркурий",
+ "Нептун"
+]
\ No newline at end of file
diff --git a/main.py b/main.py
index e69de29..b079cf5 100644
--- a/main.py
+++ b/main.py
@@ -0,0 +1,33 @@
+"""
+Astro Session Watcher - Главный входной файл
+Приложение для астрофотографов с отслеживанием файлов и сортировкой по объектам
+"""
+import sys
+import os
+from pathlib import Path
+
+# Добавляем корневую директорию в путь
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+
+from PySide6.QtWidgets import QApplication
+from ui.main_window import MainWindow
+
+
+def main():
+ """Точка входа в приложение"""
+ app = QApplication(sys.argv)
+
+ # Устанавливаем стиль Fusion для современного вида
+ app.setStyle("Fusion")
+
+ # Тёмная палитра
+ app.setPalette(app.style().standardPalette())
+
+ window = MainWindow()
+ window.show()
+
+ sys.exit(app.exec())
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/models/__init__.py b/models/__init__.py
index e69de29..cde0065 100644
--- a/models/__init__.py
+++ b/models/__init__.py
@@ -0,0 +1,4 @@
+from models.astro_object import AstroObject
+from models.session import Session
+
+__all__ = ['AstroObject', 'Session']
\ No newline at end of file
diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b164826
Binary files /dev/null and b/models/__pycache__/__init__.cpython-313.pyc differ
diff --git a/models/__pycache__/astro_object.cpython-313.pyc b/models/__pycache__/astro_object.cpython-313.pyc
new file mode 100644
index 0000000..7d6fbd6
Binary files /dev/null and b/models/__pycache__/astro_object.cpython-313.pyc differ
diff --git a/models/__pycache__/session.cpython-313.pyc b/models/__pycache__/session.cpython-313.pyc
new file mode 100644
index 0000000..7f6a944
Binary files /dev/null and b/models/__pycache__/session.cpython-313.pyc differ
diff --git a/models/astro_object.py b/models/astro_object.py
index e69de29..c513182 100644
--- a/models/astro_object.py
+++ b/models/astro_object.py
@@ -0,0 +1,19 @@
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class AstroObject:
+ """Модель астрономического объекта"""
+ name: str
+ folder: Path
+ photo_count: int = 0
+
+ def increment_photo_count(self):
+ self.photo_count += 1
+
+ def get_object_log_path(self) -> Path:
+ return self.folder / "ObjectLog.txt"
+
+ def __str__(self):
+ return f"AstroObject(name='{self.name}', photos={self.photo_count})"
\ No newline at end of file
diff --git a/models/session.py b/models/session.py
index e69de29..8c5443e 100644
--- a/models/session.py
+++ b/models/session.py
@@ -0,0 +1,31 @@
+from dataclasses import dataclass, field
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional
+from models.astro_object import AstroObject
+
+
+@dataclass
+class Session:
+ """Модель сессии наблюдения"""
+ camera: str
+ optics: str
+ start_time: datetime
+ end_time: Optional[datetime] = None
+ objects: List[AstroObject] = field(default_factory=list)
+ session_folder: Optional[Path] = None
+
+ def add_object(self, astro_object: AstroObject):
+ self.objects.append(astro_object)
+
+ def get_current_object(self) -> Optional[AstroObject]:
+ return self.objects[-1] if self.objects else None
+
+ def get_session_name(self) -> str:
+ return f"AstroSession_{self.start_time.strftime('%Y-%m-%d')}"
+
+ def finish(self):
+ self.end_time = datetime.now()
+
+ def is_active(self) -> bool:
+ return self.end_time is None
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index e69de29..5a4c3e3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+PySide6>=6.5.0
+watchdog>=3.0.0
\ No newline at end of file
diff --git a/services/__init__.py b/services/__init__.py
index e69de29..91d3dc3 100644
--- a/services/__init__.py
+++ b/services/__init__.py
@@ -0,0 +1,6 @@
+from services.config_service import ConfigService
+from services.file_service import FileService
+from services.session_service import SessionService
+from services.watch_service import WatchService
+
+__all__ = ['ConfigService', 'FileService', 'SessionService', 'WatchService']
\ No newline at end of file
diff --git a/services/__pycache__/__init__.cpython-313.pyc b/services/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..d667db5
Binary files /dev/null and b/services/__pycache__/__init__.cpython-313.pyc differ
diff --git a/services/__pycache__/config_service.cpython-313.pyc b/services/__pycache__/config_service.cpython-313.pyc
new file mode 100644
index 0000000..035f70f
Binary files /dev/null and b/services/__pycache__/config_service.cpython-313.pyc differ
diff --git a/services/__pycache__/file_service.cpython-313.pyc b/services/__pycache__/file_service.cpython-313.pyc
new file mode 100644
index 0000000..2a68435
Binary files /dev/null and b/services/__pycache__/file_service.cpython-313.pyc differ
diff --git a/services/__pycache__/session_service.cpython-313.pyc b/services/__pycache__/session_service.cpython-313.pyc
new file mode 100644
index 0000000..327b811
Binary files /dev/null and b/services/__pycache__/session_service.cpython-313.pyc differ
diff --git a/services/__pycache__/watch_service.cpython-313.pyc b/services/__pycache__/watch_service.cpython-313.pyc
new file mode 100644
index 0000000..d00565b
Binary files /dev/null and b/services/__pycache__/watch_service.cpython-313.pyc differ
diff --git a/services/config_service.py b/services/config_service.py
index e69de29..5ada103 100644
--- a/services/config_service.py
+++ b/services/config_service.py
@@ -0,0 +1,161 @@
+"""
+ConfigService - управление настройками приложения
+"""
+import json
+import os
+from typing import List, Optional
+from dataclasses import dataclass, asdict
+
+
+@dataclass
+class AppConfig:
+ """Конфигурация приложения"""
+ cameras: List[str]
+ lenses: List[str]
+ celestial_bodies: List[str]
+ last_watch_folder: str
+ last_camera: str
+ last_lens: str
+
+
+class ConfigService:
+ """Сервис для работы с конфигурацией"""
+
+ SETTINGS_FILE = "astro_settings.json"
+ CELESTIAL_BODIES_FILE = "celestial_bodies.json"
+
+ def __init__(self):
+ self.config = AppConfig(
+ cameras=[],
+ lenses=[],
+ celestial_bodies=[],
+ last_watch_folder="",
+ last_camera="",
+ last_lens=""
+ )
+ self.load_all()
+
+ def load_all(self):
+ """Загружает все настройки"""
+ self._load_settings()
+ self._load_celestial_bodies()
+
+ if not self.config.celestial_bodies:
+ self.config.celestial_bodies = [
+ "M31 (Andromeda Galaxy)",
+ "M42 (Orion Nebula)",
+ "M45 (Pleiades)",
+ "M57 (Ring Nebula)",
+ "Sun",
+ "Moon",
+ "Jupiter",
+ "Saturn"
+ ]
+ self._save_celestial_bodies()
+
+ def _load_settings(self):
+ if os.path.exists(self.SETTINGS_FILE):
+ try:
+ with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ self.config.cameras = data.get('cameras', [])
+ self.config.lenses = data.get('lenses', [])
+ self.config.last_watch_folder = data.get('last_watch_folder', '')
+ self.config.last_camera = data.get('last_camera', '')
+ self.config.last_lens = data.get('last_lens', '')
+ except Exception as e:
+ print(f"Ошибка загрузки настроек: {e}")
+
+ def save_settings(self):
+ try:
+ with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f:
+ json.dump({
+ 'cameras': self.config.cameras,
+ 'lenses': self.config.lenses,
+ 'last_watch_folder': self.config.last_watch_folder,
+ 'last_camera': self.config.last_camera,
+ 'last_lens': self.config.last_lens
+ }, f, ensure_ascii=False, indent=2)
+ except Exception as e:
+ print(f"Ошибка сохранения настроек: {e}")
+
+ def _load_celestial_bodies(self):
+ if os.path.exists(self.CELESTIAL_BODIES_FILE):
+ try:
+ with open(self.CELESTIAL_BODIES_FILE, 'r', encoding='utf-8') as f:
+ self.config.celestial_bodies = json.load(f)
+ except Exception as e:
+ print(f"Ошибка загрузки небесных тел: {e}")
+
+ def _save_celestial_bodies(self):
+ try:
+ with open(self.CELESTIAL_BODIES_FILE, 'w', encoding='utf-8') as f:
+ json.dump(self.config.celestial_bodies, f, ensure_ascii=False, indent=2)
+ except Exception as e:
+ print(f"Ошибка сохранения небесных тел: {e}")
+
+ def get_cameras(self) -> List[str]:
+ return self.config.cameras.copy()
+
+ def add_camera(self, camera: str):
+ if camera and camera not in self.config.cameras:
+ self.config.cameras.append(camera)
+ self.save_settings()
+
+ def remove_camera(self, camera: str):
+ if camera in self.config.cameras:
+ self.config.cameras.remove(camera)
+ self.save_settings()
+
+ def get_lenses(self) -> List[str]:
+ return self.config.lenses.copy()
+
+ def add_lens(self, lens: str):
+ if lens and lens not in self.config.lenses:
+ self.config.lenses.append(lens)
+ self.save_settings()
+
+ def remove_lens(self, lens: str):
+ if lens in self.config.lenses:
+ self.config.lenses.remove(lens)
+ self.save_settings()
+
+ def get_celestial_bodies(self) -> List[str]:
+ return self.config.celestial_bodies.copy()
+
+ def add_celestial_body(self, name: str):
+ if name and name not in self.config.celestial_bodies:
+ self.config.celestial_bodies.append(name)
+ self._save_celestial_bodies()
+
+ def remove_celestial_body(self, name: str):
+ if name in self.config.celestial_bodies:
+ self.config.celestial_bodies.remove(name)
+ self._save_celestial_bodies()
+
+ def update_celestial_body(self, old_name: str, new_name: str):
+ if old_name in self.config.celestial_bodies:
+ idx = self.config.celestial_bodies.index(old_name)
+ self.config.celestial_bodies[idx] = new_name
+ self._save_celestial_bodies()
+
+ def get_last_watch_folder(self) -> str:
+ return self.config.last_watch_folder
+
+ def set_last_watch_folder(self, folder: str):
+ self.config.last_watch_folder = folder
+ self.save_settings()
+
+ def get_last_camera(self) -> str:
+ return self.config.last_camera
+
+ def set_last_camera(self, camera: str):
+ self.config.last_camera = camera
+ self.save_settings()
+
+ def get_last_lens(self) -> str:
+ return self.config.last_lens
+
+ def set_last_lens(self, lens: str):
+ self.config.last_lens = lens
+ self.save_settings()
\ No newline at end of file
diff --git a/services/file_service.py b/services/file_service.py
index e69de29..68d7990 100644
--- a/services/file_service.py
+++ b/services/file_service.py
@@ -0,0 +1,88 @@
+"""
+FileService - сервис для работы с файлами
+"""
+import shutil
+from datetime import datetime
+from pathlib import Path
+from typing import Optional
+
+
+class FileService:
+ """Сервис для перемещения файлов и ведения логов"""
+
+ SUPPORTED_EXTENSIONS = {'.cr2', '.dng', '.arw', '.jpg', '.jpeg', '.png', '.raw', '.tiff'}
+
+ @classmethod
+ def is_photo(cls, file_path: Path) -> bool:
+ """Проверяет, является ли файл фотографией"""
+ return file_path.suffix.lower() in cls.SUPPORTED_EXTENSIONS
+
+ @classmethod
+ def resolve_conflict(cls, target_path: Path) -> Path:
+ """Разрешает конфликт имён файлов"""
+ if not target_path.exists():
+ return target_path
+
+ counter = 1
+ stem = target_path.stem
+ suffix = target_path.suffix
+ parent = target_path.parent
+
+ while True:
+ new_name = f"{stem}_{counter}{suffix}"
+ new_path = parent / new_name
+ if not new_path.exists():
+ return new_path
+ counter += 1
+
+ @classmethod
+ def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str,
+ timestamp: Optional[datetime] = None) -> None:
+ """Записывает запись в лог объекта"""
+ if timestamp is None:
+ timestamp = datetime.now()
+
+ log_file = folder / "ObjectLog.txt"
+ line = f"{timestamp} {camera if camera else 'Unknown'} {optics if optics else 'Unknown'} - {filename}\n"
+
+ try:
+ with open(log_file, 'a', encoding='utf-8') as f:
+ f.write(line)
+ except Exception as e:
+ print(f"Ошибка записи лога: {e}")
+
+ @classmethod
+ def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str) -> bool:
+ """Перемещает файл в целевую папку"""
+ if not source.exists():
+ return False
+
+ if not cls.is_photo(source):
+ return False
+
+ try:
+ target_folder.mkdir(parents=True, exist_ok=True)
+ creation_time = datetime.fromtimestamp(source.stat().st_ctime)
+ cls.write_object_log(target_folder, source.name, camera, optics, creation_time)
+ target_path = cls.resolve_conflict(target_folder / source.name)
+ shutil.move(str(source), str(target_path))
+ return True
+ except Exception as e:
+ print(f"Ошибка перемещения {source.name}: {e}")
+ return False
+
+ @classmethod
+ def clear_watch_folder(cls, folder: Path) -> int:
+ """Очищает папку наблюдения"""
+ if not folder.exists():
+ return 0
+
+ count = 0
+ for file_path in folder.iterdir():
+ if file_path.is_file() and cls.is_photo(file_path):
+ try:
+ file_path.unlink()
+ count += 1
+ except Exception as e:
+ print(f"Ошибка удаления {file_path.name}: {e}")
+ return count
\ No newline at end of file
diff --git a/services/session_service.py b/services/session_service.py
index e69de29..9469d2c 100644
--- a/services/session_service.py
+++ b/services/session_service.py
@@ -0,0 +1,155 @@
+"""
+SessionService - сервис управления сессией
+"""
+from datetime import datetime
+from pathlib import Path
+from typing import Optional, Callable
+from models.astro_object import AstroObject
+from models.session import Session
+from services.file_service import FileService
+
+
+class SessionService:
+ """Сервис для управления сессией наблюдения"""
+
+ def __init__(self):
+ self._current_session: Optional[Session] = None
+ self._file_service = FileService()
+
+ def start_session(self, watch_folder: Path, object_name: str, camera: str, optics: str) -> Session:
+ """Начинает новую сессию"""
+ start_time = datetime.now()
+ self._current_session = Session(
+ camera=camera,
+ optics=optics,
+ start_time=start_time
+ )
+
+ session_folder = watch_folder / self._current_session.get_session_name()
+ session_folder.mkdir(parents=True, exist_ok=True)
+ self._current_session.session_folder = session_folder
+
+ self._log_session_event(f"Session started at {start_time}")
+ self._log_session_event(f"Camera: {camera}")
+ self._log_session_event(f"Optics: {optics}")
+
+ self.create_new_object(object_name)
+ return self._current_session
+
+ def create_new_object(self, object_name: str) -> AstroObject:
+ """Создаёт новый объект съёмки"""
+ if not self._current_session:
+ raise ValueError("Session not started")
+
+ if self._current_session.get_current_object():
+ current_obj = self._current_session.get_current_object()
+ self._log_session_event(f"Object finished: {current_obj.name}, photos: {current_obj.photo_count}")
+
+ object_folder = self._current_session.session_folder / object_name
+ object_folder.mkdir(parents=True, exist_ok=True)
+
+ astro_object = AstroObject(
+ name=object_name,
+ folder=object_folder,
+ photo_count=0
+ )
+
+ self._current_session.add_object(astro_object)
+ self._log_session_event(f"New object: {object_name}")
+ return astro_object
+
+ def handle_file(self, file_path: Path) -> bool:
+ """Обрабатывает новый файл"""
+ if not self._current_session:
+ return False
+
+ current_object = self._current_session.get_current_object()
+ if not current_object:
+ return False
+
+ success = self._file_service.move_file(
+ file_path,
+ current_object.folder,
+ self._current_session.camera,
+ self._current_session.optics
+ )
+
+ if success:
+ current_object.increment_photo_count()
+
+ return success
+
+ def move_remaining_files(self, watch_folder: Path, on_file_moved: Optional[Callable] = None) -> int:
+ """Перемещает все существующие файлы из папки наблюдения"""
+ if not self._current_session:
+ return 0
+
+ current_object = self._current_session.get_current_object()
+ if not current_object:
+ return 0
+
+ count = 0
+ if watch_folder.exists():
+ for file_path in watch_folder.iterdir():
+ if file_path.is_file() and self._file_service.is_photo(file_path):
+ if self._file_service.move_file(
+ file_path,
+ current_object.folder,
+ self._current_session.camera,
+ self._current_session.optics
+ ):
+ current_object.increment_photo_count()
+ count += 1
+ if on_file_moved:
+ on_file_moved(file_path)
+ return count
+
+ def finish_session(self) -> Session:
+ """Завершает сессию"""
+ if not self._current_session:
+ raise ValueError("No active session")
+
+ self._current_session.finish()
+
+ self._log_session_event(f"Session finished at {self._current_session.end_time}")
+ self._log_session_event("=== SESSION SUMMARY ===")
+ for obj in self._current_session.objects:
+ self._log_session_event(f"Object: {obj.name}, Photos: {obj.photo_count}")
+
+ return self._current_session
+
+ def get_current_session(self) -> Optional[Session]:
+ return self._current_session
+
+ def get_current_object(self) -> Optional[AstroObject]:
+ if self._current_session:
+ return self._current_session.get_current_object()
+ return None
+
+ def get_current_object_folder(self) -> Optional[Path]:
+ current_obj = self.get_current_object()
+ return current_obj.folder if current_obj else None
+
+ def change_camera(self, camera: str):
+ if self._current_session:
+ self._current_session.camera = camera
+ self._log_session_event(f"Camera changed to: {camera}")
+
+ def change_optics(self, optics: str):
+ if self._current_session:
+ self._current_session.optics = optics
+ self._log_session_event(f"Optics changed to: {optics}")
+
+ def _log_session_event(self, message: str):
+ if self._current_session and self._current_session.session_folder:
+ log_file = self._current_session.session_folder / "SessionLog.txt"
+ timestamp = datetime.now()
+ line = f"{timestamp} - {message}\n"
+ try:
+ with open(log_file, 'a', encoding='utf-8') as f:
+ f.write(line)
+ except Exception as e:
+ print(f"Ошибка записи лога: {e}")
+
+ def is_active(self) -> bool:
+ return self._current_session is not None and self._current_session.is_active()
\ No newline at end of file
diff --git a/services/watch_service.py b/services/watch_service.py
index e69de29..9ef8119 100644
--- a/services/watch_service.py
+++ b/services/watch_service.py
@@ -0,0 +1,99 @@
+"""
+WatchService - сервис отслеживания файлов с очередью
+"""
+import time
+import threading
+import queue
+from pathlib import Path
+from typing import Callable, Optional
+from watchdog.observers import Observer
+from watchdog.events import FileSystemEventHandler
+from services.file_service import FileService
+
+
+class PhotoHandler(FileSystemEventHandler):
+ """Обработчик событий файловой системы"""
+
+ def __init__(self, callback: Callable[[Path], None]):
+ self.callback = callback
+ self._pending_files = queue.Queue()
+ self._processing = True
+ self._processor_thread = threading.Thread(target=self._process_queue, daemon=True)
+ self._processor_thread.start()
+
+ def on_created(self, event):
+ if not event.is_directory:
+ src_path = Path(event.src_path)
+ if FileService.is_photo(src_path):
+ time.sleep(0.1) # Даём время на запись файла
+ self._pending_files.put(src_path)
+
+ def _process_queue(self):
+ while self._processing:
+ try:
+ file_path = self._pending_files.get(timeout=1)
+ if self.callback and file_path.exists():
+ self.callback(file_path)
+ except queue.Empty:
+ continue
+ except Exception as e:
+ print(f"Ошибка обработки файла: {e}")
+
+ def stop(self):
+ self._processing = False
+
+
+class WatchService:
+ """Сервис для отслеживания папки на новые файлы"""
+
+ def __init__(self):
+ self._observer: Optional[Observer] = None
+ self._event_handler: Optional[PhotoHandler] = None
+ self._is_running = False
+
+ def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool:
+ if self._is_running:
+ return False
+
+ if not watch_folder.exists():
+ return False
+
+ try:
+ self._event_handler = PhotoHandler(on_new_file)
+ self._observer = Observer()
+ self._observer.schedule(self._event_handler, str(watch_folder), recursive=False)
+ self._observer.start()
+ self._is_running = True
+ return True
+ except Exception as e:
+ print(f"Ошибка запуска отслеживания: {e}")
+ return False
+
+ def stop(self):
+ if self._observer:
+ self._observer.stop()
+ self._observer.join()
+ self._observer = None
+
+ if self._event_handler:
+ self._event_handler.stop()
+ self._event_handler = None
+
+ self._is_running = False
+
+ def is_running(self) -> bool:
+ return self._is_running
+
+ def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int:
+ """Перемещает все существующие файлы"""
+ count = 0
+ if watch_folder.exists():
+ for file_path in watch_folder.iterdir():
+ if file_path.is_file() and FileService.is_photo(file_path):
+ try:
+ on_file_moved(file_path)
+ count += 1
+ time.sleep(0.05)
+ except Exception as e:
+ print(f"Ошибка перемещения {file_path.name}: {e}")
+ return count
\ No newline at end of file
diff --git a/ui/__init__.py b/ui/__init__.py
index e69de29..17f31d2 100644
--- a/ui/__init__.py
+++ b/ui/__init__.py
@@ -0,0 +1,4 @@
+# UI package
+from ui.main_window import MainWindow
+
+__all__ = ['MainWindow']
\ No newline at end of file
diff --git a/ui/__pycache__/__init__.cpython-313.pyc b/ui/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..ac8a3f4
Binary files /dev/null and b/ui/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ui/__pycache__/main_window.cpython-313.pyc b/ui/__pycache__/main_window.cpython-313.pyc
new file mode 100644
index 0000000..223c5b5
Binary files /dev/null and b/ui/__pycache__/main_window.cpython-313.pyc differ
diff --git a/ui/dialogs/__init__.py b/ui/dialogs/__init__.py
index e69de29..0db4844 100644
--- a/ui/dialogs/__init__.py
+++ b/ui/dialogs/__init__.py
@@ -0,0 +1,5 @@
+from ui.dialogs.equipment_dialog import EquipmentDialog
+from ui.dialogs.celestial_dialog import CelestialDialog
+from ui.dialogs.instructions_dialog import InstructionsDialog
+
+__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog']
\ No newline at end of file
diff --git a/ui/dialogs/__pycache__/__init__.cpython-313.pyc b/ui/dialogs/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..b57a0cc
Binary files /dev/null and b/ui/dialogs/__pycache__/__init__.cpython-313.pyc differ
diff --git a/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc
new file mode 100644
index 0000000..d7018e0
Binary files /dev/null and b/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc differ
diff --git a/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc
new file mode 100644
index 0000000..a4d67bf
Binary files /dev/null and b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc differ
diff --git a/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc
new file mode 100644
index 0000000..cc50530
Binary files /dev/null and b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc differ
diff --git a/ui/dialogs/celestial_dialog.py b/ui/dialogs/celestial_dialog.py
index e69de29..4798cf5 100644
--- a/ui/dialogs/celestial_dialog.py
+++ b/ui/dialogs/celestial_dialog.py
@@ -0,0 +1,160 @@
+"""
+CelestialDialog - диалог управления небесными телами
+Аналог CelestialBodiesDialogController из JavaFX версии
+"""
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
+ QPushButton, QLineEdit, QInputDialog, QMessageBox
+)
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QFont
+
+from services.config_service import ConfigService
+
+
+class CelestialDialog(QDialog):
+ """Диалог для управления списком небесных тел"""
+
+ def __init__(self, parent, config_service: ConfigService):
+ super().__init__(parent)
+
+ self.config_service = config_service
+ self.setWindowTitle("Небесные тела")
+ self.setMinimumSize(400, 500)
+ self.resize(450, 550)
+
+ # Загружаем текущий список
+ self.celestial_bodies = self.config_service.get_celestial_bodies()
+
+ self._create_ui()
+ self._update_list()
+
+ def _create_ui(self):
+ """Создаёт интерфейс диалога"""
+ layout = QVBoxLayout(self)
+ layout.setSpacing(15)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # Заголовок
+ title_label = QLabel("Управление небесными телами")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ title_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title_label)
+
+ # Подпись
+ subtitle_label = QLabel("Список объектов для наблюдения")
+ subtitle_font = QFont()
+ subtitle_font.setPointSize(11)
+ subtitle_font.setBold(True)
+ subtitle_label.setFont(subtitle_font)
+ layout.addWidget(subtitle_label)
+
+ # Список небесных тел
+ self.bodies_list = QListWidget()
+ self.bodies_list.itemClicked.connect(lambda item: self._select_body(item.text()))
+ layout.addWidget(self.bodies_list)
+
+ # Поле для добавления нового
+ add_layout = QHBoxLayout()
+
+ self.new_body_entry = QLineEdit()
+ self.new_body_entry.setPlaceholderText("Название объекта (например: M31, NGC 224)")
+ self.new_body_entry.returnPressed.connect(self._add_celestial_body)
+ add_layout.addWidget(self.new_body_entry)
+
+ add_btn = QPushButton("➕ Добавить")
+ add_btn.clicked.connect(self._add_celestial_body)
+ add_layout.addWidget(add_btn)
+
+ layout.addLayout(add_layout)
+
+ # Кнопки удаления и редактирования
+ buttons_layout = QHBoxLayout()
+
+ self.remove_btn = QPushButton("❌ Удалить выбранный")
+ self.remove_btn.setEnabled(False)
+ self.remove_btn.clicked.connect(self._remove_celestial_body)
+ buttons_layout.addWidget(self.remove_btn)
+
+ self.edit_btn = QPushButton("✏ Редактировать")
+ self.edit_btn.setEnabled(False)
+ self.edit_btn.clicked.connect(self._edit_celestial_body)
+ buttons_layout.addWidget(self.edit_btn)
+
+ layout.addLayout(buttons_layout)
+
+ # Кнопка закрытия
+ close_btn = QPushButton("Закрыть")
+ close_btn.clicked.connect(self.accept)
+ close_layout = QHBoxLayout()
+ close_layout.addStretch()
+ close_layout.addWidget(close_btn)
+ layout.addLayout(close_layout)
+
+ def _update_list(self):
+ """Обновляет отображение списка небесных тел"""
+ self.bodies_list.clear()
+ for body in self.celestial_bodies:
+ self.bodies_list.addItem(body)
+ self._selected_body = None
+ self.remove_btn.setEnabled(False)
+ self.edit_btn.setEnabled(False)
+
+ def _select_body(self, body: str):
+ """Выделяет объект в списке"""
+ self._selected_body = body
+ self.remove_btn.setEnabled(True)
+ self.edit_btn.setEnabled(True)
+
+ def _add_celestial_body(self):
+ """Добавляет новое небесное тело"""
+ new_body = self.new_body_entry.text()
+ if not new_body or not new_body.strip():
+ QMessageBox.warning(self, "Ошибка", "Введите название объекта")
+ return
+
+ new_name = new_body.strip()
+ if new_name in self.celestial_bodies:
+ QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!")
+ return
+
+ self.celestial_bodies.append(new_name)
+ self.config_service.add_celestial_body(new_name)
+ self._update_list()
+ self.new_body_entry.clear()
+ QMessageBox.information(self, "Успех", f"Объект '{new_name}' добавлен")
+
+ def _remove_celestial_body(self):
+ """Удаляет выбранное небесное тело"""
+ if hasattr(self, '_selected_body') and self._selected_body:
+ reply = QMessageBox.question(self, "Подтверждение",
+ f"Удалить объект '{self._selected_body}'?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ self.celestial_bodies.remove(self._selected_body)
+ self.config_service.remove_celestial_body(self._selected_body)
+ self._update_list()
+ QMessageBox.information(self, "Успех", f"Объект '{self._selected_body}' удалён")
+
+ def _edit_celestial_body(self):
+ """Редактирует выбранное небесное тело"""
+ if hasattr(self, '_selected_body') and self._selected_body:
+ new_name, ok = QInputDialog.getText(self, "Редактировать",
+ f"Изменить '{self._selected_body}' на:",
+ text=self._selected_body)
+ if ok and new_name and new_name.strip():
+ new_name = new_name.strip()
+ if new_name != self._selected_body:
+ if new_name in self.celestial_bodies:
+ QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!")
+ return
+
+ idx = self.celestial_bodies.index(self._selected_body)
+ old_name = self.celestial_bodies[idx]
+ self.celestial_bodies[idx] = new_name
+ self.config_service.update_celestial_body(old_name, new_name)
+ self._update_list()
+ QMessageBox.information(self, "Успех", f"Объект переименован в '{new_name}'")
\ No newline at end of file
diff --git a/ui/dialogs/equipment_dialog.py b/ui/dialogs/equipment_dialog.py
index e69de29..b0d8784 100644
--- a/ui/dialogs/equipment_dialog.py
+++ b/ui/dialogs/equipment_dialog.py
@@ -0,0 +1,202 @@
+"""
+EquipmentDialog - диалог управления оборудованием (камеры и объективы)
+Аналог EquipmentDialogController из JavaFX версии
+"""
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget,
+ QPushButton, QInputDialog, QMessageBox, QListWidgetItem
+)
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QFont
+
+from services.config_service import ConfigService
+
+
+class EquipmentDialog(QDialog):
+ """Диалог для управления списками камер и объективов"""
+
+ def __init__(self, parent, config_service: ConfigService):
+ super().__init__(parent)
+
+ self.config_service = config_service
+ self.setWindowTitle("Управление оборудованием")
+ self.setMinimumSize(600, 400)
+ self.resize(650, 450)
+
+ # Загружаем текущие списки
+ self.cameras = self.config_service.get_cameras()
+ self.lenses = self.config_service.get_lenses()
+
+ self._create_ui()
+ self._update_cameras_list()
+ self._update_lenses_list()
+
+ def _create_ui(self):
+ """Создаёт интерфейс диалога"""
+ layout = QVBoxLayout(self)
+ layout.setSpacing(15)
+ layout.setContentsMargins(20, 20, 20, 20)
+
+ # Заголовок
+ title_label = QLabel("Управление оборудованием")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ title_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title_label)
+
+ # Контейнер для двух колонок
+ columns_layout = QHBoxLayout()
+ columns_layout.setSpacing(20)
+
+ # Левая колонка - Камеры
+ left_layout = QVBoxLayout()
+
+ cameras_label = QLabel("Камеры")
+ cameras_font = QFont()
+ cameras_font.setPointSize(12)
+ cameras_font.setBold(True)
+ cameras_label.setFont(cameras_font)
+ left_layout.addWidget(cameras_label)
+
+ self.cameras_list = QListWidget()
+ self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text()))
+ left_layout.addWidget(self.cameras_list)
+
+ cameras_buttons_layout = QHBoxLayout()
+
+ add_camera_btn = QPushButton("➕ Добавить")
+ add_camera_btn.clicked.connect(self._add_camera)
+ cameras_buttons_layout.addWidget(add_camera_btn)
+
+ self.remove_camera_btn = QPushButton("❌ Удалить")
+ self.remove_camera_btn.setEnabled(False)
+ self.remove_camera_btn.clicked.connect(self._remove_camera)
+ cameras_buttons_layout.addWidget(self.remove_camera_btn)
+
+ left_layout.addLayout(cameras_buttons_layout)
+
+ # Правая колонка - Объективы
+ right_layout = QVBoxLayout()
+
+ lenses_label = QLabel("Объективы")
+ lenses_label.setFont(cameras_font)
+ right_layout.addWidget(lenses_label)
+
+ self.lenses_list = QListWidget()
+ self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text()))
+ right_layout.addWidget(self.lenses_list)
+
+ lenses_buttons_layout = QHBoxLayout()
+
+ add_lens_btn = QPushButton("➕ Добавить")
+ add_lens_btn.clicked.connect(self._add_lens)
+ lenses_buttons_layout.addWidget(add_lens_btn)
+
+ self.remove_lens_btn = QPushButton("❌ Удалить")
+ self.remove_lens_btn.setEnabled(False)
+ self.remove_lens_btn.clicked.connect(self._remove_lens)
+ lenses_buttons_layout.addWidget(self.remove_lens_btn)
+
+ right_layout.addLayout(lenses_buttons_layout)
+
+ columns_layout.addLayout(left_layout)
+ columns_layout.addLayout(right_layout)
+ layout.addLayout(columns_layout)
+
+ # Кнопка закрытия
+ close_btn = QPushButton("Закрыть")
+ close_btn.clicked.connect(self.accept)
+ close_layout = QHBoxLayout()
+ close_layout.addStretch()
+ close_layout.addWidget(close_btn)
+ layout.addLayout(close_layout)
+
+ def _update_cameras_list(self):
+ """Обновляет отображение списка камер"""
+ self.cameras_list.clear()
+ for camera in self.cameras:
+ self.cameras_list.addItem(camera)
+ self._selected_camera = None
+ self.remove_camera_btn.setEnabled(False)
+
+ def _update_lenses_list(self):
+ """Обновляет отображение списка объективов"""
+ self.lenses_list.clear()
+ for lens in self.lenses:
+ self.lenses_list.addItem(lens)
+ self._selected_lens = None
+ self.remove_lens_btn.setEnabled(False)
+
+ def _select_camera(self, camera: str):
+ """Выделяет камеру в списке"""
+ self._selected_camera = camera
+ self.remove_camera_btn.setEnabled(True)
+
+ # Снимаем выделение с объективов
+ self.lenses_list.clearSelection()
+ self._selected_lens = None
+ self.remove_lens_btn.setEnabled(False)
+
+ def _select_lens(self, lens: str):
+ """Выделяет объектив в списке"""
+ self._selected_lens = lens
+ self.remove_lens_btn.setEnabled(True)
+
+ # Снимаем выделение с камер
+ self.cameras_list.clearSelection()
+ self._selected_camera = None
+ self.remove_camera_btn.setEnabled(False)
+
+ def _add_camera(self):
+ """Добавляет новую камеру"""
+ new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:")
+ if ok and new_camera and new_camera.strip():
+ new_name = new_camera.strip()
+ if new_name in self.cameras:
+ QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!")
+ return
+
+ self.cameras.append(new_name)
+ self.config_service.add_camera(new_name)
+ self._update_cameras_list()
+ QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена")
+
+ def _remove_camera(self):
+ """Удаляет выбранную камеру"""
+ if hasattr(self, '_selected_camera') and self._selected_camera:
+ reply = QMessageBox.question(self, "Подтверждение",
+ f"Удалить камеру '{self._selected_camera}'?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ self.cameras.remove(self._selected_camera)
+ self.config_service.remove_camera(self._selected_camera)
+ self._update_cameras_list()
+ QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена")
+
+ def _add_lens(self):
+ """Добавляет новый объектив"""
+ new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:")
+ if ok and new_lens and new_lens.strip():
+ new_name = new_lens.strip()
+ if new_name in self.lenses:
+ QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!")
+ return
+
+ self.lenses.append(new_name)
+ self.config_service.add_lens(new_name)
+ self._update_lenses_list()
+ QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен")
+
+ def _remove_lens(self):
+ """Удаляет выбранный объектив"""
+ if hasattr(self, '_selected_lens') and self._selected_lens:
+ reply = QMessageBox.question(self, "Подтверждение",
+ f"Удалить объектив '{self._selected_lens}'?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ self.lenses.remove(self._selected_lens)
+ self.config_service.remove_lens(self._selected_lens)
+ self._update_lenses_list()
+ QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён")
\ No newline at end of file
diff --git a/ui/dialogs/instructions_dialog.py b/ui/dialogs/instructions_dialog.py
index e69de29..64b8b28 100644
--- a/ui/dialogs/instructions_dialog.py
+++ b/ui/dialogs/instructions_dialog.py
@@ -0,0 +1,164 @@
+"""
+InstructionsDialog - диалог с инструкцией по использованию
+"""
+from PySide6.QtWidgets import (
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit,
+ QPushButton, QScrollArea
+)
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QFont
+
+
+class InstructionsDialog(QDialog):
+ """Диалог с подробной инструкцией пользователя"""
+
+ def __init__(self, parent):
+ super().__init__(parent)
+
+ self.setWindowTitle("Инструкция по использованию")
+ self.setMinimumSize(700, 500)
+ self.resize(750, 550)
+
+ self._create_ui()
+
+ def _create_ui(self):
+ """Создаёт интерфейс диалога"""
+ layout = QVBoxLayout(self)
+ layout.setSpacing(10)
+ layout.setContentsMargins(15, 15, 15, 15)
+
+ # Заголовок
+ title_label = QLabel("Astro Session Watcher - Руководство пользователя")
+ title_font = QFont()
+ title_font.setPointSize(16)
+ title_font.setBold(True)
+ title_label.setFont(title_font)
+ title_label.setAlignment(Qt.AlignCenter)
+ layout.addWidget(title_label)
+
+ # Текст инструкции
+ text_edit = QTextEdit()
+ text_edit.setReadOnly(True)
+ text_edit.setFont(QFont("Consolas", 10))
+
+ instructions = """
+======================= ASTRO SESSION WATCHER =======================
+
+Приложение автоматически отслеживает появление новых фотографий в указанной папке,
+сортирует их по объектам съемки и ведет подробный лог всего процесса.
+
+📸 ДЛЯ ЧЕГО ЭТО НУЖНО?
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Когда вы снимаете астрономические объекты через EOS Utility или аналогичное ПО,
+все фотографии сохраняются в одну папку. Astro Session Watcher помогает:
+
+• Автоматически распределять снимки по папкам объектов
+• Вести лог каждой сессии
+• Не пропустить ни одного кадра при смене объекта
+• Хранить историю оборудования и небесных тел
+
+🚀 КАК ЭТО РАБОТАЕТ?
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+1. Вы выбираете папку, куда камера сохраняет снимки
+2. Приложение создает папку сессии: "AstroSession_ГГГГ-ММ-ДД"
+3. Каждый объект съемки получает свою подпапку внутри папки сессии
+4. Когда вы меняете объект (нажимаете "Новая цель"), все накопленные
+ в папке наблюдения файлы автоматически ПЕРЕМЕЩАЮТСЯ в папку предыдущего объекта
+5. При завершении сессии оставшиеся файлы также перемещаются в папку последнего объекта
+
+📝 ПОШАГОВАЯ ИНСТРУКЦИЯ
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+█ 1. ПЕРВЫЙ ЗАПУСК (НАСТРОЙКА)
+───────────────────────────────────────────────────────────────────────────────
+
+• Откройте меню "Файл" → "Оборудование" и добавьте ваши камеры и объективы
+• Откройте меню "Файл" → "Небесные тела" и добавьте объекты для наблюдения
+• Все данные сохраняются автоматически в файлах настроек
+
+█ 2. ЗАПУСК СЕССИИ
+───────────────────────────────────────────────────────────────────────────────
+
+1. Нажмите "Обзор" и выберите папку, куда камера сохраняет снимки
+2. Выберите камеру и объектив из выпадающих списков
+3. Введите название цели (или выберите из списка небесных тел)
+4. Нажмите ▶ "Начать отслеживание"
+
+✅ После запуска:
+ • Статус изменится на "● ON AIR" с мигающим красным текстом
+ • Кнопка "Новая цель" начнет мигать красным контуром
+ • В папке наблюдения создастся папка "AstroSession_дата"
+ • Внутри - папка с вашей первой целью
+
+█ 3. СМЕНА ОБЪЕКТА ВО ВРЕМЯ СЕССИИ
+───────────────────────────────────────────────────────────────────────────────
+
+Когда вы заканчиваете снимать один объект и переходите к другому:
+
+1. Нажмите кнопку "Новая цель" (или Ctrl+Shift+N)
+2. Введите название нового объекта
+3. Приложение автоматически:
+ • Переместит все накопленные файлы в папку предыдущего объекта
+ • Создаст новую папку для следующего объекта
+ • Сбросит счетчик файлов
+ • Продолжит отслеживание
+
+💡 ВАЖНО: Если перед сменой объекта в папке наблюдения уже есть файлы,
+ они НЕ ПОТЕРЯЮТСЯ - все будут перемещены в папку текущего объекта!
+
+█ 4. ЗАВЕРШЕНИЕ СЕССИИ
+───────────────────────────────────────────────────────────────────────────────
+
+1. Нажмите ■ "Остановить" (или Ctrl+X)
+2. Приложение:
+ • Переместит все оставшиеся файлы в папку последнего объекта
+ • Запишет итоговый лог сессии
+ • Покажет диалог с предложением открыть папку сессии
+ • Восстановит интерфейс для новой сессии
+
+⌨️ ГОРЯЧИЕ КЛАВИШИ
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Ctrl + O → Выбрать папку наблюдения
+Ctrl + E → Управление оборудованием
+Ctrl + B → Управление небесными телами
+Ctrl + S → Начать сессию
+Ctrl + X → Остановить сессию
+Ctrl + F → Открыть папку текущей сессии
+Ctrl + Shift+N → Создать новый объект
+F1 → О программе
+F2 → Эта инструкция
+
+🔧 ФАЙЛЫ НАСТРОЕК
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+📄 astro_settings.json ← камеры, объективы, последняя папка
+📄 celestial_bodies.json ← список небесных тел
+
+Все файлы хранятся в папке с программой. Вы можете редактировать их вручную.
+
+📧 ТЕХНИЧЕСКАЯ ПОДДЕРЖКА
+━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+Разработчик: Vic Sergeev
+Версия: 0.3.0-alpha
+
+При обнаружении ошибок или для предложений по улучшению:
+• Сообщите разработчику
+• Приложите файлы логов (SessionLog.txt, ObjectLog.txt)
+"""
+
+ text_edit.setText(instructions)
+ layout.addWidget(text_edit)
+
+ # Кнопка закрытия
+ close_layout = QHBoxLayout()
+ close_layout.addStretch()
+
+ close_btn = QPushButton("Закрыть")
+ close_btn.clicked.connect(self.accept)
+ close_layout.addWidget(close_btn)
+
+ layout.addLayout(close_layout)
\ No newline at end of file
diff --git a/ui/main_window.py b/ui/main_window.py
index e69de29..eff6d48 100644
--- a/ui/main_window.py
+++ b/ui/main_window.py
@@ -0,0 +1,642 @@
+"""
+MainWindow - главное окно приложения на PySide6
+"""
+import sys
+import subprocess
+import platform
+from pathlib import Path
+from datetime import datetime
+from typing import Optional
+
+from PySide6.QtWidgets import (
+ QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
+ QLabel, QLineEdit, QComboBox, QPushButton, QMenuBar, QMenu,
+ QMessageBox, QFileDialog, QInputDialog, QFrame, QApplication
+)
+from PySide6.QtCore import Qt, QTimer, Signal
+from PySide6.QtGui import QFont, QIcon, QAction
+
+from services.config_service import ConfigService
+from services.session_service import SessionService
+from services.watch_service import WatchService
+from services.file_service import FileService
+
+
+class MainWindow(QMainWindow):
+ """Главное окно приложения"""
+
+ def __init__(self):
+ super().__init__()
+
+ # Сервисы
+ self.config_service = ConfigService()
+ self.session_service = SessionService()
+ self.watch_service = WatchService()
+
+ # Переменные состояния
+ self.running = False
+ self.file_count = 0
+ self._blink_timer = None
+ self._new_object_blink_timer = None
+
+ # Настройка окна
+ self.setWindowTitle("Astro Session Watcher v0.3.0")
+ self.setMinimumSize(700, 500)
+ self.resize(800, 550)
+
+ self.center_window()
+ self._create_menu_bar()
+ self._create_main_content()
+ self._load_saved_settings()
+ self._setup_hotkeys()
+ self._update_file_count_display()
+
+ self.setAttribute(Qt.WA_DeleteOnClose)
+
+ def center_window(self):
+ screen = QApplication.primaryScreen().availableGeometry()
+ self.setGeometry(
+ (screen.width() - self.width()) // 2,
+ (screen.height() - self.height()) // 2,
+ self.width(),
+ self.height()
+ )
+
+ def _create_menu_bar(self):
+ menubar = self.menuBar()
+
+ # Меню Файл
+ file_menu = menubar.addMenu("Файл")
+
+ select_folder_action = QAction("Выбрать папку...", self)
+ select_folder_action.setShortcut("Ctrl+O")
+ select_folder_action.triggered.connect(self.select_folder)
+ file_menu.addAction(select_folder_action)
+
+ file_menu.addSeparator()
+
+ equipment_action = QAction("Оборудование...", self)
+ equipment_action.setShortcut("Ctrl+E")
+ equipment_action.triggered.connect(self.open_equipment_dialog)
+ file_menu.addAction(equipment_action)
+
+ celestial_action = QAction("Небесные тела...", self)
+ celestial_action.setShortcut("Ctrl+B")
+ celestial_action.triggered.connect(self.open_celestial_dialog)
+ file_menu.addAction(celestial_action)
+
+ file_menu.addSeparator()
+
+ exit_action = QAction("Выход", self)
+ exit_action.setShortcut("Ctrl+Q")
+ exit_action.triggered.connect(self.close)
+ file_menu.addAction(exit_action)
+
+ # Меню Сессия
+ session_menu = menubar.addMenu("Сессия")
+
+ start_action = QAction("Начать наблюдение", self)
+ start_action.setShortcut("Ctrl+S")
+ start_action.triggered.connect(self.start)
+ session_menu.addAction(start_action)
+
+ stop_action = QAction("Остановить наблюдение", self)
+ stop_action.setShortcut("Ctrl+X")
+ stop_action.triggered.connect(self.stop)
+ session_menu.addAction(stop_action)
+
+ session_menu.addSeparator()
+
+ open_folder_action = QAction("Открыть папку сессии", self)
+ open_folder_action.setShortcut("Ctrl+F")
+ open_folder_action.triggered.connect(self.open_session_folder)
+ session_menu.addAction(open_folder_action)
+
+ session_menu.addSeparator()
+
+ new_object_action = QAction("Новая цель...", self)
+ new_object_action.setShortcut("Ctrl+Shift+N")
+ new_object_action.triggered.connect(self.set_new_object)
+ session_menu.addAction(new_object_action)
+
+ # Меню Помощь
+ help_menu = menubar.addMenu("Помощь")
+
+ instructions_action = QAction("Инструкция", self)
+ instructions_action.setShortcut("F2")
+ instructions_action.triggered.connect(self.show_instructions)
+ help_menu.addAction(instructions_action)
+
+ help_menu.addSeparator()
+
+ about_action = QAction("О программе", self)
+ about_action.setShortcut("F1")
+ about_action.triggered.connect(self.show_info)
+ help_menu.addAction(about_action)
+
+ def _create_main_content(self):
+ central_widget = QWidget()
+ self.setCentralWidget(central_widget)
+
+ main_layout = QVBoxLayout(central_widget)
+ main_layout.setContentsMargins(20, 20, 20, 20)
+ main_layout.setSpacing(15)
+
+ grid_layout = QGridLayout()
+ grid_layout.setVerticalSpacing(12)
+ grid_layout.setHorizontalSpacing(15)
+
+ # Row 0: Папка
+ folder_label = QLabel("Папка:")
+ folder_label.setFont(QFont("", 10, QFont.Bold))
+ grid_layout.addWidget(folder_label, 0, 0, Qt.AlignRight | Qt.AlignVCenter)
+
+ folder_widget = QWidget()
+ folder_layout = QHBoxLayout(folder_widget)
+ folder_layout.setContentsMargins(0, 0, 0, 0)
+ folder_layout.setSpacing(10)
+
+ self.folder_entry = QLineEdit()
+ self.folder_entry.setPlaceholderText("Выберите папку для отслеживания")
+ folder_layout.addWidget(self.folder_entry)
+
+ self.folder_button = QPushButton("Обзор...")
+ self.folder_button.setFixedWidth(80)
+ self.folder_button.clicked.connect(self.select_folder)
+ folder_layout.addWidget(self.folder_button)
+
+ grid_layout.addWidget(folder_widget, 0, 1)
+
+ # Row 1: Оборудование
+ equipment_label = QLabel("Оборудование:")
+ equipment_label.setFont(QFont("", 10, QFont.Bold))
+ grid_layout.addWidget(equipment_label, 1, 0, Qt.AlignRight | Qt.AlignVCenter)
+
+ equipment_widget = QWidget()
+ equipment_layout = QHBoxLayout(equipment_widget)
+ equipment_layout.setContentsMargins(0, 0, 0, 0)
+ equipment_layout.setSpacing(10)
+
+ self.camera_combo = QComboBox()
+ self.camera_combo.setEditable(False)
+ equipment_layout.addWidget(self.camera_combo)
+
+ self.lens_combo = QComboBox()
+ self.lens_combo.setEditable(False)
+ equipment_layout.addWidget(self.lens_combo)
+
+ grid_layout.addWidget(equipment_widget, 1, 1)
+
+ # Row 2: Цель
+ target_label = QLabel("Цель:")
+ target_label.setFont(QFont("", 10, QFont.Bold))
+ grid_layout.addWidget(target_label, 2, 0, Qt.AlignRight | Qt.AlignVCenter)
+
+ target_widget = QWidget()
+ target_layout = QHBoxLayout(target_widget)
+ target_layout.setContentsMargins(0, 0, 0, 0)
+ target_layout.setSpacing(10)
+
+ self.object_combo = QComboBox()
+ self.object_combo.setEditable(True)
+ self.object_combo.setInsertPolicy(QComboBox.NoInsert)
+ # Настройка плейсхолдера
+ self.object_combo.lineEdit().setPlaceholderText("Введите название цели")
+ # Автодополнение при вводе
+ self.object_combo.lineEdit().textChanged.connect(self._on_object_text_changed)
+ target_layout.addWidget(self.object_combo)
+
+ self.new_object_button = QPushButton("Новая цель")
+ self.new_object_button.setFixedWidth(100)
+ self.new_object_button.setEnabled(False)
+ self.new_object_button.clicked.connect(self.set_new_object)
+ target_layout.addWidget(self.new_object_button)
+
+ grid_layout.addWidget(target_widget, 2, 1)
+
+ # Row 3: Статистика
+ stats_label = QLabel("Статистика:")
+ stats_label.setFont(QFont("", 10, QFont.Bold))
+ grid_layout.addWidget(stats_label, 3, 0, Qt.AlignRight | Qt.AlignVCenter)
+
+ self.file_count_label = QLabel("Файлов получено: 0")
+ self.file_count_label.setFont(QFont("", 11))
+ grid_layout.addWidget(self.file_count_label, 3, 1, Qt.AlignLeft)
+
+ # Row 4: Статус
+ status_label = QLabel("Статус:")
+ status_label.setFont(QFont("", 10, QFont.Bold))
+ grid_layout.addWidget(status_label, 4, 0, Qt.AlignRight | Qt.AlignVCenter)
+
+ self.status_label = QLabel("IDLE")
+ self.status_label.setFont(QFont("", 12, QFont.Bold))
+ self.status_label.setStyleSheet("color: #666666;")
+ grid_layout.addWidget(self.status_label, 4, 1, Qt.AlignLeft)
+
+ main_layout.addLayout(grid_layout)
+
+ separator = QFrame()
+ separator.setFrameShape(QFrame.HLine)
+ separator.setStyleSheet("background-color: #333333; max-height: 1px;")
+ main_layout.addWidget(separator)
+
+ buttons_layout = QHBoxLayout()
+ buttons_layout.setSpacing(15)
+ buttons_layout.setAlignment(Qt.AlignCenter)
+
+ self.start_button = QPushButton("▶ Начать отслеживание")
+ self.start_button.setFixedSize(180, 35)
+ self.start_button.setStyleSheet("""
+ QPushButton {
+ background-color: #4CAF50;
+ color: white;
+ font-weight: bold;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #45a049;
+ }
+ """)
+ self.start_button.clicked.connect(self.start)
+ buttons_layout.addWidget(self.start_button)
+
+ self.stop_button = QPushButton("■ Остановить")
+ self.stop_button.setFixedSize(180, 35)
+ self.stop_button.setEnabled(False)
+ self.stop_button.setStyleSheet("""
+ QPushButton {
+ background-color: #f44336;
+ color: white;
+ font-weight: bold;
+ border-radius: 4px;
+ }
+ QPushButton:hover {
+ background-color: #d32f2f;
+ }
+ """)
+ self.stop_button.clicked.connect(self.stop)
+ buttons_layout.addWidget(self.stop_button)
+
+ main_layout.addLayout(buttons_layout)
+
+ footer_layout = QHBoxLayout()
+
+ version_label = QLabel("v0.3.0-alpha")
+ version_label.setStyleSheet("color: #666666; font-size: 11px;")
+ footer_layout.addWidget(version_label)
+
+ footer_layout.addStretch()
+
+ copyright_label = QLabel("Made by Vic Sergeev 2026")
+ copyright_label.setStyleSheet("color: #666666; font-size: 11px;")
+ footer_layout.addWidget(copyright_label)
+
+ main_layout.addLayout(footer_layout)
+
+ def _on_object_text_changed(self, text):
+ """Автодополнение при вводе названия цели"""
+ if not text:
+ return
+
+ # Поиск совпадений в списке небесных тел
+ celestial_bodies = self.config_service.get_celestial_bodies()
+ matches = [body for body in celestial_bodies if body.lower().startswith(text.lower())]
+
+ if matches and matches[0] != text:
+ # Временно отключаем сигнал, чтобы избежать рекурсии
+ self.object_combo.lineEdit().blockSignals(True)
+ self.object_combo.lineEdit().setText(matches[0])
+ self.object_combo.lineEdit().setSelection(len(text), len(matches[0]))
+ self.object_combo.lineEdit().blockSignals(False)
+
+ def _load_saved_settings(self):
+ cameras = self.config_service.get_cameras()
+ lenses = self.config_service.get_lenses()
+ celestial_bodies = self.config_service.get_celestial_bodies()
+
+ if cameras:
+ self.camera_combo.addItems(cameras)
+ last_camera = self.config_service.get_last_camera()
+ if last_camera and last_camera in cameras:
+ self.camera_combo.setCurrentText(last_camera)
+
+ if lenses:
+ self.lens_combo.addItems(lenses)
+ last_lens = self.config_service.get_last_lens()
+ if last_lens and last_lens in lenses:
+ self.lens_combo.setCurrentText(last_lens)
+
+ if celestial_bodies:
+ self.object_combo.addItems(celestial_bodies)
+
+ last_folder = self.config_service.get_last_watch_folder()
+ if last_folder:
+ self.folder_entry.setText(last_folder)
+
+ def _setup_hotkeys(self):
+ pass
+
+ def _set_running_state(self, state: bool):
+ self.running = state
+
+ if state:
+ self.start_button.setEnabled(False)
+ self.stop_button.setEnabled(True)
+ self.new_object_button.setEnabled(True)
+ self.status_label.setText("● ON AIR")
+ self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;")
+ self._start_blinking()
+ self._start_new_object_blinking()
+ else:
+ self.start_button.setEnabled(True)
+ self.stop_button.setEnabled(False)
+ self.new_object_button.setEnabled(False)
+ self.status_label.setText("IDLE")
+ self.status_label.setStyleSheet("color: #666666; font-weight: bold;")
+ self._stop_blinking()
+ self._stop_new_object_blinking()
+
+ def _start_blinking(self):
+ self._blink_timer = QTimer()
+ self._blink_timer.timeout.connect(self._do_blink)
+ self._blink_timer.start(500)
+
+ def _do_blink(self):
+ if not self.running:
+ return
+ current_style = self.status_label.styleSheet()
+ if "color: #ff0000" in current_style:
+ self.status_label.setStyleSheet("color: #ffffff; font-weight: bold;")
+ else:
+ self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;")
+
+ def _stop_blinking(self):
+ if self._blink_timer:
+ self._blink_timer.stop()
+ self._blink_timer = None
+ self.status_label.setStyleSheet("color: #666666; font-weight: bold;")
+
+ def _start_new_object_blinking(self):
+ self._new_object_blink_timer = QTimer()
+ self._new_object_blink_timer.timeout.connect(self._do_new_object_blink)
+ self._new_object_blink_timer.start(500)
+
+ def _do_new_object_blink(self):
+ if not self.running:
+ return
+ current_style = self.new_object_button.styleSheet()
+ if "border: 2px solid red" in current_style:
+ self.new_object_button.setStyleSheet("")
+ else:
+ self.new_object_button.setStyleSheet("border: 2px solid red; border-radius: 4px;")
+
+ def _stop_new_object_blinking(self):
+ if self._new_object_blink_timer:
+ self._new_object_blink_timer.stop()
+ self._new_object_blink_timer = None
+ self.new_object_button.setStyleSheet("")
+
+ def _update_file_count_display(self):
+ if self.running and self.session_service.get_current_object():
+ current_obj = self.session_service.get_current_object()
+ self.file_count = current_obj.photo_count
+ self.file_count_label.setText(f"Файлов получено: {self.file_count}")
+ QTimer.singleShot(1000, self._update_file_count_display)
+
+ def _on_file_received(self, file_path: Path):
+ """Обработчик получения нового файла"""
+ print(f"Обнаружен файл: {file_path}")
+ if self.session_service.handle_file(file_path):
+ self.file_count += 1
+ self.file_count_label.setText(f"Файлов получено: {self.file_count}")
+ print(f"Файл обработан: {file_path.name}")
+ else:
+ print(f"Не удалось обработать файл: {file_path}")
+
+ def select_folder(self):
+ folder = QFileDialog.getExistingDirectory(self, "Выберите папку для отслеживания")
+ if folder:
+ self.folder_entry.setText(folder)
+ self.config_service.set_last_watch_folder(folder)
+
+ def start(self):
+ watch_folder = self.folder_entry.text()
+ object_name = self.object_combo.currentText()
+
+ if not watch_folder:
+ QMessageBox.critical(self, "Ошибка", "Папка для отслеживания не выбрана")
+ return
+
+ if not object_name:
+ QMessageBox.critical(self, "Ошибка", "Цель не указана")
+ return
+
+ # Проверка, существует ли объект в списке небесных тел
+ celestial_bodies = self.config_service.get_celestial_bodies()
+ if object_name not in celestial_bodies:
+ reply = QMessageBox.question(self, "Новый объект",
+ f"Объект '{object_name}' не найден в списке.\nДобавить его в список?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ self.config_service.add_celestial_body(object_name)
+ self.object_combo.addItem(object_name)
+ else:
+ return
+
+ camera = self.camera_combo.currentText()
+ lens = self.lens_combo.currentText()
+
+ if not camera or not lens:
+ reply = QMessageBox.question(self, "Предупреждение",
+ "Камера или объектив не выбраны. Продолжить?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.No:
+ return
+
+ try:
+ watch_path = Path(watch_folder)
+
+ # Очищаем папку наблюдения от старых файлов
+ FileService.clear_watch_folder(watch_path)
+
+ camera_val = camera if camera else "Unknown"
+ lens_val = lens if lens else "Unknown"
+
+ self.session_service.start_session(watch_path, object_name, camera_val, lens_val)
+
+ self.config_service.set_last_camera(camera_val)
+ self.config_service.set_last_lens(lens_val)
+
+ # Запускаем отслеживание
+ success = self.watch_service.start(watch_path, self._on_file_received)
+ if not success:
+ QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки")
+ return
+
+ self._set_running_state(True)
+
+ print(f"Отслеживание начато! Папка наблюдения: {watch_path}")
+ print(f"Папка сессии: {self.session_service.get_current_session().session_folder}")
+
+ except Exception as e:
+ QMessageBox.critical(self, "Ошибка", f"Не удалось начать сессию: {e}")
+ import traceback
+ traceback.print_exc()
+
+ def stop(self):
+ if not self.running:
+ return
+
+ try:
+ watch_folder = Path(self.folder_entry.text())
+
+ print(f"Остановка сессии. Перемещаем файлы из {watch_folder}")
+
+ # Перемещаем все оставшиеся файлы
+ moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received)
+ print(f"Перемещено файлов: {moved_count}")
+
+ # Останавливаем отслеживание
+ self.watch_service.stop()
+
+ # Завершаем сессию
+ session = self.session_service.finish_session()
+
+ self._set_running_state(False)
+
+ # Показываем диалог завершения
+ self._show_session_end_dialog(session)
+
+ except Exception as e:
+ QMessageBox.critical(self, "Ошибка", f"Ошибка при завершении сессии: {e}")
+ import traceback
+ traceback.print_exc()
+
+ def set_new_object(self):
+ if not self.running:
+ QMessageBox.critical(self, "Ошибка", "Сессия не активна")
+ return
+
+ # Перемещаем все накопленные файлы в папку текущего объекта
+ watch_folder = Path(self.folder_entry.text())
+ moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received)
+
+ if moved_count > 0:
+ print(f"Перемещено файлов перед сменой объекта: {moved_count}")
+
+ new_object, ok = QInputDialog.getText(self, "Новый объект", "Введите название объекта:")
+
+ if ok and new_object and new_object.strip():
+ new_name = new_object.strip()
+
+ # Проверка, существует ли объект в списке
+ celestial_bodies = self.config_service.get_celestial_bodies()
+ if new_name not in celestial_bodies:
+ reply = QMessageBox.question(self, "Новый объект",
+ f"Объект '{new_name}' не найден в списке.\nДобавить его в список?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ self.config_service.add_celestial_body(new_name)
+ self.object_combo.addItem(new_name)
+ else:
+ return
+
+ self.session_service.create_new_object(new_name)
+ self.object_combo.setCurrentText(new_name)
+ QMessageBox.information(self, "Успех", f"Объект изменён на: {new_name}")
+
+ def open_equipment_dialog(self):
+ from ui.dialogs.equipment_dialog import EquipmentDialog
+ dialog = EquipmentDialog(self, self.config_service)
+ dialog.exec()
+
+ self.camera_combo.clear()
+ self.lens_combo.clear()
+ self.camera_combo.addItems(self.config_service.get_cameras())
+ self.lens_combo.addItems(self.config_service.get_lenses())
+
+ def open_celestial_dialog(self):
+ from ui.dialogs.celestial_dialog import CelestialDialog
+ dialog = CelestialDialog(self, self.config_service)
+ dialog.exec()
+
+ self.object_combo.clear()
+ self.object_combo.addItems(self.config_service.get_celestial_bodies())
+
+ def open_session_folder(self):
+ if self.running and self.session_service.get_current_session():
+ folder = self.session_service.get_current_session().session_folder
+ if folder and folder.exists():
+ try:
+ if platform.system() == "Windows":
+ subprocess.Popen(['explorer', str(folder)])
+ elif platform.system() == "Darwin":
+ subprocess.Popen(['open', str(folder)])
+ else:
+ subprocess.Popen(['xdg-open', str(folder)])
+ except Exception as e:
+ QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}")
+ else:
+ QMessageBox.critical(self, "Ошибка", "Папка сессии не найдена")
+ else:
+ QMessageBox.information(self, "Информация", "Нет активной сессии")
+
+ def show_instructions(self):
+ from ui.dialogs.instructions_dialog import InstructionsDialog
+ dialog = InstructionsDialog(self)
+ dialog.exec()
+
+ def show_info(self):
+ QMessageBox.about(self, "О программе",
+ "Astro Session Watcher\nВерсия: 0.3.0-alpha\n\n"
+ "Приложение для автоматической сортировки астрофотографий\n\n"
+ "Особенности:\n"
+ "• Автоматическое отслеживание новых файлов\n"
+ "• Сортировка по объектам съёмки\n"
+ "• Ведение детальных логов\n"
+ "• Сохранение истории оборудования\n\n"
+ "Разработчик: Vic Sergeev\n2026")
+
+ def _show_session_end_dialog(self, session):
+ current_object = session.get_current_object()
+ object_name = current_object.name if current_object else "Unknown"
+ photo_count = current_object.photo_count if current_object else 0
+ session_folder = session.session_folder
+
+ msg_box = QMessageBox(self)
+ msg_box.setWindowTitle("Сессия завершена")
+ msg_box.setIcon(QMessageBox.Information)
+ msg_box.setText(f"Наблюдение остановлено\n\nСессия для объекта '{object_name}' завершена.\nПолучено файлов: {photo_count}")
+ msg_box.setInformativeText(f"Папка с данными:\n{session_folder}")
+
+ open_folder_btn = msg_box.addButton("📁 Открыть папку", QMessageBox.AcceptRole)
+ close_btn = msg_box.addButton("Закрыть", QMessageBox.RejectRole)
+
+ msg_box.exec()
+ if msg_box.clickedButton() == open_folder_btn:
+ if session_folder and session_folder.exists():
+ try:
+ if platform.system() == "Windows":
+ subprocess.Popen(['explorer', str(session_folder)])
+ elif platform.system() == "Darwin":
+ subprocess.Popen(['open', str(session_folder)])
+ else:
+ subprocess.Popen(['xdg-open', str(session_folder)])
+ except Exception as e:
+ QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}")
+
+ def closeEvent(self, event):
+ if self.running:
+ reply = QMessageBox.question(self, "Выход",
+ "Сессия активна. Остановить сессию и выйти?",
+ QMessageBox.Yes | QMessageBox.No)
+ if reply == QMessageBox.Yes:
+ try:
+ self.stop()
+ except:
+ pass
+ event.accept()
+ else:
+ event.ignore()
+ else:
+ event.accept()
\ No newline at end of file
diff --git a/utils/__init__.py b/utils/__init__.py
index e69de29..92ecf6f 100644
--- a/utils/__init__.py
+++ b/utils/__init__.py
@@ -0,0 +1,4 @@
+# Utils package
+from utils.sound_manager import SoundManager
+
+__all__ = ['SoundManager']
\ No newline at end of file
diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc
new file mode 100644
index 0000000..6404c8f
Binary files /dev/null and b/utils/__pycache__/__init__.cpython-313.pyc differ