working
This commit is contained in:
parent
f7e794774d
commit
09d181eba8
37 changed files with 1898 additions and 5 deletions
6
.idea/AstroSessionWatcher.iml
generated
6
.idea/AstroSessionWatcher.iml
generated
|
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$">
|
||||||
<orderEntry type="inheritedJdk" />
|
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Python 3.13 (AstroSessionWatcher)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
49
.idea/workspace.xml
generated
49
.idea/workspace.xml
generated
|
|
@ -1,12 +1,41 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="" />
|
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="">
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/build_exe.spec" beforeDir="false" afterPath="$PROJECT_DIR$/build_exe.spec" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/models/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/models/astro_object.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/astro_object.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/models/session.py" beforeDir="false" afterPath="$PROJECT_DIR$/models/session.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" afterPath="$PROJECT_DIR$/requirements.txt" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/services/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/services/config_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/config_service.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/services/file_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/file_service.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/services/session_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/session_service.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/services/watch_service.py" beforeDir="false" afterPath="$PROJECT_DIR$/services/watch_service.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/dialogs/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/celestial_dialog.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/equipment_dialog.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/dialogs/instructions_dialog.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/ui/main_window.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/utils/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/utils/__init__.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/utils/sound_manager.py" beforeDir="false" />
|
||||||
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="FileTemplateManagerImpl">
|
||||||
|
<option name="RECENT_TEMPLATES">
|
||||||
|
<list>
|
||||||
|
<option value="Python Script" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
|
|
@ -14,16 +43,27 @@
|
||||||
"associatedIndex": 8
|
"associatedIndex": 8
|
||||||
}]]></component>
|
}]]></component>
|
||||||
<component name="ProjectId" id="3DOCpPEAGgO0as5zvcSEwPDpHTU" />
|
<component name="ProjectId" id="3DOCpPEAGgO0as5zvcSEwPDpHTU" />
|
||||||
|
<component name="ProjectLevelVcsManager">
|
||||||
|
<ConfirmationsSetting value="2" id="Add" />
|
||||||
|
</component>
|
||||||
<component name="ProjectViewState">
|
<component name="ProjectViewState">
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
|
"Python.main.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||||
"git-widget-placeholder": "master",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
|
"ai.playground.ignore.import.keys.banner.in.settings": "true",
|
||||||
|
"git-widget-placeholder": "dev-pyside",
|
||||||
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
|
"settings.editor.selected.configurable": "preferences.lookFeel",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></component>
|
}]]></component>
|
||||||
|
|
@ -42,11 +82,14 @@
|
||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1778143911036</updated>
|
<updated>1778143911036</updated>
|
||||||
<workItem from="1778143912090" duration="3000" />
|
<workItem from="1778143912090" duration="6772000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
|
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778151943591" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
15
astro_settings.json
Normal file
15
astro_settings.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
|
@ -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', # Если есть иконка
|
||||||
|
)
|
||||||
14
celestial_bodies.json
Normal file
14
celestial_bodies.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[
|
||||||
|
"M31 (Andromeda Galaxy)",
|
||||||
|
"M42 (Orion Nebula)",
|
||||||
|
"M45 (Pleiades)",
|
||||||
|
"M57 (Ring Nebula)",
|
||||||
|
"Солнце",
|
||||||
|
"Moon",
|
||||||
|
"Jupiter",
|
||||||
|
"Сатурн",
|
||||||
|
"M89",
|
||||||
|
"Венера",
|
||||||
|
"Меркурий",
|
||||||
|
"Нептун"
|
||||||
|
]
|
||||||
33
main.py
33
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()
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
from models.astro_object import AstroObject
|
||||||
|
from models.session import Session
|
||||||
|
|
||||||
|
__all__ = ['AstroObject', 'Session']
|
||||||
BIN
models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/astro_object.cpython-313.pyc
Normal file
BIN
models/__pycache__/astro_object.cpython-313.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/session.cpython-313.pyc
Normal file
BIN
models/__pycache__/session.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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})"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
PySide6>=6.5.0
|
||||||
|
watchdog>=3.0.0
|
||||||
|
|
@ -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']
|
||||||
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/config_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/config_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/file_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/file_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/session_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/session_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/watch_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/watch_service.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# UI package
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
|
||||||
|
__all__ = ['MainWindow']
|
||||||
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/main_window.cpython-313.pyc
Normal file
BIN
ui/__pycache__/main_window.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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']
|
||||||
BIN
ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc
Normal file
Binary file not shown.
|
|
@ -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}'")
|
||||||
|
|
@ -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}' удалён")
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Utils package
|
||||||
|
from utils.sound_manager import SoundManager
|
||||||
|
|
||||||
|
__all__ = ['SoundManager']
|
||||||
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue