This commit is contained in:
Vic Sergeev 2026-05-07 17:15:56 +03:00
parent f7e794774d
commit 09d181eba8
37 changed files with 1898 additions and 5 deletions

View file

@ -0,0 +1,4 @@
# UI package
from ui.main_window import MainWindow
__all__ = ['MainWindow']

Binary file not shown.

Binary file not shown.

View file

@ -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']

Binary file not shown.

View file

@ -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}'")

View file

@ -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}' удалён")

View file

@ -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)

View file

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