""" 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()