From 09d181eba8332452f0b996cbf2188d9f9ebe885d Mon Sep 17 00:00:00 2001 From: Vic Sergeev Date: Thu, 7 May 2026 17:15:56 +0300 Subject: [PATCH] working --- .idea/AstroSessionWatcher.iml | 6 +- .idea/workspace.xml | 49 +- astro_settings.json | 15 + build_exe.spec | 40 ++ celestial_bodies.json | 14 + main.py | 33 + models/__init__.py | 4 + models/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 311 bytes .../__pycache__/astro_object.cpython-313.pyc | Bin 0 -> 1406 bytes models/__pycache__/session.cpython-313.pyc | Bin 0 -> 2200 bytes models/astro_object.py | 19 + models/session.py | 31 + requirements.txt | 2 + services/__init__.py | 6 + services/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 461 bytes .../config_service.cpython-313.pyc | Bin 0 -> 11091 bytes .../__pycache__/file_service.cpython-313.pyc | Bin 0 -> 5136 bytes .../session_service.cpython-313.pyc | Bin 0 -> 8823 bytes .../__pycache__/watch_service.cpython-313.pyc | Bin 0 -> 6100 bytes services/config_service.py | 161 +++++ services/file_service.py | 88 +++ services/session_service.py | 155 +++++ services/watch_service.py | 99 +++ ui/__init__.py | 4 + ui/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 244 bytes ui/__pycache__/main_window.cpython-313.pyc | Bin 0 -> 37831 bytes ui/dialogs/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 424 bytes .../celestial_dialog.cpython-313.pyc | Bin 0 -> 10441 bytes .../equipment_dialog.cpython-313.pyc | Bin 0 -> 12877 bytes .../instructions_dialog.cpython-313.pyc | Bin 0 -> 11153 bytes ui/dialogs/celestial_dialog.py | 160 +++++ ui/dialogs/equipment_dialog.py | 202 ++++++ ui/dialogs/instructions_dialog.py | 164 +++++ ui/main_window.py | 642 ++++++++++++++++++ utils/__init__.py | 4 + utils/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 255 bytes 37 files changed, 1898 insertions(+), 5 deletions(-) create mode 100644 astro_settings.json create mode 100644 celestial_bodies.json create mode 100644 models/__pycache__/__init__.cpython-313.pyc create mode 100644 models/__pycache__/astro_object.cpython-313.pyc create mode 100644 models/__pycache__/session.cpython-313.pyc create mode 100644 services/__pycache__/__init__.cpython-313.pyc create mode 100644 services/__pycache__/config_service.cpython-313.pyc create mode 100644 services/__pycache__/file_service.cpython-313.pyc create mode 100644 services/__pycache__/session_service.cpython-313.pyc create mode 100644 services/__pycache__/watch_service.cpython-313.pyc create mode 100644 ui/__pycache__/__init__.cpython-313.pyc create mode 100644 ui/__pycache__/main_window.cpython-313.pyc create mode 100644 ui/dialogs/__pycache__/__init__.cpython-313.pyc create mode 100644 ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc create mode 100644 ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc create mode 100644 ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc create mode 100644 utils/__pycache__/__init__.cpython-313.pyc diff --git a/.idea/AstroSessionWatcher.iml b/.idea/AstroSessionWatcher.iml index d0876a7..63ab128 100644 --- a/.idea/AstroSessionWatcher.iml +++ b/.idea/AstroSessionWatcher.iml @@ -1,8 +1,10 @@ - - + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 9d7e713..bec30ab 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,12 +1,41 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -14,16 +43,27 @@ "associatedIndex": 8 }]]> + + + @@ -42,11 +82,14 @@ + + + \ No newline at end of file diff --git a/astro_settings.json b/astro_settings.json new file mode 100644 index 0000000..fd39922 --- /dev/null +++ b/astro_settings.json @@ -0,0 +1,15 @@ +{ + "cameras": [ + "Canon 40D", + "Canon 400D", + "Canon 500D" + ], + "lenses": [ + "MTO-500A", + "Юпитер-21м", + "Tamron 18-200mm" + ], + "last_watch_folder": "C:/Users/Juliette/Documents/testwatcher", + "last_camera": "Canon 40D", + "last_lens": "MTO-500A" +} \ No newline at end of file diff --git a/build_exe.spec b/build_exe.spec index e69de29..ce5a8c7 100644 --- a/build_exe.spec +++ b/build_exe.spec @@ -0,0 +1,40 @@ +# -*- mode: python ; coding: utf-8 -*- + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[ + ('resources/done.mp3', 'resources'), + ], + hiddenimports=['customtkinter', 'watchdog', 'pygame', 'ctypes', 'queue'], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='AstroSessionWatcher', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # Без консоли + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon='resources/icon.ico', # Если есть иконка +) \ No newline at end of file diff --git a/celestial_bodies.json b/celestial_bodies.json new file mode 100644 index 0000000..c510679 --- /dev/null +++ b/celestial_bodies.json @@ -0,0 +1,14 @@ +[ + "M31 (Andromeda Galaxy)", + "M42 (Orion Nebula)", + "M45 (Pleiades)", + "M57 (Ring Nebula)", + "Солнце", + "Moon", + "Jupiter", + "Сатурн", + "M89", + "Венера", + "Меркурий", + "Нептун" +] \ No newline at end of file diff --git a/main.py b/main.py index e69de29..b079cf5 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,33 @@ +""" +Astro Session Watcher - Главный входной файл +Приложение для астрофотографов с отслеживанием файлов и сортировкой по объектам +""" +import sys +import os +from pathlib import Path + +# Добавляем корневую директорию в путь +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from PySide6.QtWidgets import QApplication +from ui.main_window import MainWindow + + +def main(): + """Точка входа в приложение""" + app = QApplication(sys.argv) + + # Устанавливаем стиль Fusion для современного вида + app.setStyle("Fusion") + + # Тёмная палитра + app.setPalette(app.style().standardPalette()) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py index e69de29..cde0065 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -0,0 +1,4 @@ +from models.astro_object import AstroObject +from models.session import Session + +__all__ = ['AstroObject', 'Session'] \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-313.pyc b/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b164826f061a9d815498f7ce56b96b9c5f24093c GIT binary patch literal 311 zcmey&%ge<81RwJLWR?Tz#~=<2FhLogRe+4C48aV+jNS}hj75wJAU2aXlNWOlGmy<3 z%%aa!#FEac$@UVYMU(Lsw_|ZhQNDjtR%&tykjoyNT3no&pI5{TlrCZc5`LPjw}f-^ zQ&Mw^^%6m9ej`1qZ z$xJONNsS4pOwLFw$_*&W2U%Pk19k<}?(oEtLm^@$s2?nI-Y@dIgoYIBbA| sr8%i~MchDx8G*Q16i9qvW@Kc%%b~Tq=$1MVvqx0Jct6?EnA( literal 0 HcmV?d00001 diff --git a/models/__pycache__/astro_object.cpython-313.pyc b/models/__pycache__/astro_object.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d6fbd6403b09be6e56a86f754d921afd6d2788d GIT binary patch literal 1406 zcmZ`(PiP!f7=LeOc6YLWx^36s8c{c`B@U#EpaokSp(w&8+ZY_Hmx0G*GTBU+%*6NJ zf=w@Jdnk&~65;z@g}XrWZ?+2f3Y4G(%KJ>?)oSMkz=-#5G6ZfJZk-+cexoA3Ag zzBj!}rAS~sh#m!hP(mJ}@p$D1O79FPTf`OCK1$4?)=?4&l@$T(c?HPr&!M z;pCjWtxrMJDS&36X6Lx+jDeNUtilB0McZ@=uWR<$1X(qB$u5GgY?pYYQ?@FqbWTVf zzqh#RdvX^ccF%%HzDn*UKP0!3AJfkp$<6d~dL`Wi*U#|$l-y3QCAZSc$sMr1PwqD0 z^f zR;LqlnjZ!4zPfRKvbu4G;EK?LD?bF|3z5O)fYjlFi2hm*Fd6e3M@I^K1xIRTYNI$c z-N+gsW)!t7jhnDFYyVSV3yeV%8B?{9bD9GuRxXZ#08mGcZ@=?R>+9C`o8PY9e&*ik zR(iUXyfORBWKwM>W;;W9gUnlpl*t&@0cRE&gGuRzsFxZ0xa)?4NR_eWfD0K0ksrkn zhB}sF4-_5BQ4n^cD3-2-p%aX8<1qlG2a)85(Huwd90)az^+K-4Tq$|5*p)tRdL9!H zL)KTGM-2t?FJ1+4jXW^k-6>YCwsy*gueNudt#8hKS^LeX|7FnJi+g#RJGuSA-vnO& zfPS9#55ptot(+^e!1EA_Sc}yvwcF|;`rmiJ19`u(e=f Z-^&xN^siQ-=jk3n+0()CXXePh{~ynTX|@0W literal 0 HcmV?d00001 diff --git a/models/__pycache__/session.cpython-313.pyc b/models/__pycache__/session.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f6a944402a8e2156a5a7540d6709d9c0c5a306f GIT binary patch literal 2200 zcma)7-ES0C6u+}OyF2^c55To(yM-=gA@oHE&@>oDpxKgzblMohB$MgxY&&GSQ}3Ns zs}PDlSQ54P(4>AO^bL7~M3N>3`c4#bI3e&LA!((zO zLL)+#$D*Yejg=It2s<(sFC}OqPc$h<_7FANL)6%jkpKQ8!7(&BtrX*r&?<&_ylS{c z#Won@k(y=N)uO}|z)aVgH9?op8}1D3lh0br<;uA^*K+EH4W8s{%%#q`sf%XCtpx8D ze{x{7egI{eXoN~Cp&?bG;R0}>vKG;zYIqR5LX(9S11y4?8l{RB*J79!z+%Fi*h93W zsvMVSN=s{*z!wkpXJLOwfF%T$)G~)dYO04^lxe4$2ETX)u#CXE0E>W?6;`*}(L?AC zz;*)GDX?8?7kyzmTI}XA)np7Ol4?j3=0DP*=wcm$_ort3rj8W~*k+XxOe^Gb*k_ z=XuN#>18~_{226_V^>YeQwAnq_uZAC!VnoD&|Tu!8U|&VOp!~#<9g%z} zPo~K&say=vE&zDcn45zju?d)I*XRxa86KI>^D`%hwMk}Drj;7DWxB4Zou9AF7r_pfX#uIBxp|&2s?~rF?+SJgJ;5*#h6gW@4JEadx|X`H z>~1Q%SI+*Z6kC8pnACI|v|b5FAVx4sq}n zhYC=23TYu+fqtuD$I?8Q+v#>7rJ#~}9Xg%owv*lN5t-_P=YeWx?}~fl$`@De_Z@Ba z9bHST_nq{5-|(`hJmu80Y_|$|44z&l7eM-Je0%Q)*dEfJb(6&2@=p`G3pNzP6pP8j zb?05$0opT!qBW~-u^FCe)9~%xu$N&j7Rb-J{EgI?sjo7N@~7$l2(}k?Kd&)v16IhN`36sMj6Wz|qfiUF6S? z7$+jdPUNx;-v@CcXbizPif$B96g?>VQS3u;00ft(9LJ_OPx($=hnWb&Q8!$dT2l?z zgoADop92$apwAa%}jn>9{j0)-)*9@!`q7s}U1S0+9$ugk?vS&AH3{ooG*f16P$GQ2wa&4uQ`aRi1D zI4!O2MdP&eJ6MAV6;=*E2BhGJhmn Path: + return self.folder / "ObjectLog.txt" + + def __str__(self): + return f"AstroObject(name='{self.name}', photos={self.photo_count})" \ No newline at end of file diff --git a/models/session.py b/models/session.py index e69de29..8c5443e 100644 --- a/models/session.py +++ b/models/session.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import List, Optional +from models.astro_object import AstroObject + + +@dataclass +class Session: + """Модель сессии наблюдения""" + camera: str + optics: str + start_time: datetime + end_time: Optional[datetime] = None + objects: List[AstroObject] = field(default_factory=list) + session_folder: Optional[Path] = None + + def add_object(self, astro_object: AstroObject): + self.objects.append(astro_object) + + def get_current_object(self) -> Optional[AstroObject]: + return self.objects[-1] if self.objects else None + + def get_session_name(self) -> str: + return f"AstroSession_{self.start_time.strftime('%Y-%m-%d')}" + + def finish(self): + self.end_time = datetime.now() + + def is_active(self) -> bool: + return self.end_time is None \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e69de29..5a4c3e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,2 @@ +PySide6>=6.5.0 +watchdog>=3.0.0 \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py index e69de29..91d3dc3 100644 --- a/services/__init__.py +++ b/services/__init__.py @@ -0,0 +1,6 @@ +from services.config_service import ConfigService +from services.file_service import FileService +from services.session_service import SessionService +from services.watch_service import WatchService + +__all__ = ['ConfigService', 'FileService', 'SessionService', 'WatchService'] \ No newline at end of file diff --git a/services/__pycache__/__init__.cpython-313.pyc b/services/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d667db5272d02644dca5c9071082be6e5aed1333 GIT binary patch literal 461 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 literal 0 HcmV?d00001 diff --git a/services/__pycache__/session_service.cpython-313.pyc b/services/__pycache__/session_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..327b811af8815144b9dd81aa109732a30fc3eb42 GIT binary patch literal 8823 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
    ^v0F}`I!+J z>!U`afWil6`L;q!JC4!_rs~@{uYZAl7 z&%IrJtH{hMSKl_vOuMIVlVxTT2l?gs!1Mo{kZsJ#fw7l{dz~4E#ibpfyqsI6pAx`g z1v4#=f5UNVW%JT`z3f}}o)`OuaaJpDnmXTc3{slGvOb$hrYQp(10c?}oZ*;Nt6qYd z%mji=8i9y`=IoBMI}Gi%D16H--}-}cQ!66n7DB8Uejv~D*f!4x6+Sr2cNW?@8LDoT zTk#6bf<86wL-+n}+!q8_58gGj^R#3+s;fliYJJ73N`eEpHZ;z#HgJ)=EsD2AcC;|d zt5yL)Gf!?jb5cxHFtbCS-SZ505mVX%%!_kg5_Ciuy3T;E@5#|MWDjh1+B$5#7sB-M z66kcp7TxzgXw`3a6+JhzYlj)`i-sKV4(~PVfW_06``#BV`ltc2_s4{|3HA;WU|!r* z7Jzv>VJxo9rN^}y%&fN~#6u$7FbAAmHWzUAZE3ET+j|DyQdq8xmF>Bm&|AJ(eC+xV z(pP|luPQGEz>R>nGkEg>LVg8m@ZIMf$iE7u=?edXqK`mB5EZYimv`oZIBdnG;ggDQ zTrIx5{nWfQ5QYa7q;l1s%!(AQxsZ_ka|1)GOqw`@LhKpx% zaK(KX8#8K0UO;Nsue$XJM*$wM1a>DWz0m|KKz^52_{=E%38ELsEnoE{S8&(E303f% z1{8Oo&=krybt_HXUo>sNn?+;3CaBZ|^EI6}YC8Y2Ex&f>^|d>%IrD1=Zmb=cH*aINz$AGc~MI8PwCa%eg`J$fxM zol-UqvKo2+fkl+u3A&)E-^2wj^e+-f=50Jzccp)xK=PSijvSPS4#`K3%0ow$Ck0k3 zC&s=(sWuP%dZ=1k5Cc^$RA{$SJ3gnRlE>H@;g#>y1iGo& A1poj5 literal 0 HcmV?d00001 diff --git a/services/config_service.py b/services/config_service.py index e69de29..5ada103 100644 --- a/services/config_service.py +++ b/services/config_service.py @@ -0,0 +1,161 @@ +""" +ConfigService - управление настройками приложения +""" +import json +import os +from typing import List, Optional +from dataclasses import dataclass, asdict + + +@dataclass +class AppConfig: + """Конфигурация приложения""" + cameras: List[str] + lenses: List[str] + celestial_bodies: List[str] + last_watch_folder: str + last_camera: str + last_lens: str + + +class ConfigService: + """Сервис для работы с конфигурацией""" + + SETTINGS_FILE = "astro_settings.json" + CELESTIAL_BODIES_FILE = "celestial_bodies.json" + + def __init__(self): + self.config = AppConfig( + cameras=[], + lenses=[], + celestial_bodies=[], + last_watch_folder="", + last_camera="", + last_lens="" + ) + self.load_all() + + def load_all(self): + """Загружает все настройки""" + self._load_settings() + self._load_celestial_bodies() + + if not self.config.celestial_bodies: + self.config.celestial_bodies = [ + "M31 (Andromeda Galaxy)", + "M42 (Orion Nebula)", + "M45 (Pleiades)", + "M57 (Ring Nebula)", + "Sun", + "Moon", + "Jupiter", + "Saturn" + ] + self._save_celestial_bodies() + + def _load_settings(self): + if os.path.exists(self.SETTINGS_FILE): + try: + with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + self.config.cameras = data.get('cameras', []) + self.config.lenses = data.get('lenses', []) + self.config.last_watch_folder = data.get('last_watch_folder', '') + self.config.last_camera = data.get('last_camera', '') + self.config.last_lens = data.get('last_lens', '') + except Exception as e: + print(f"Ошибка загрузки настроек: {e}") + + def save_settings(self): + try: + with open(self.SETTINGS_FILE, 'w', encoding='utf-8') as f: + json.dump({ + 'cameras': self.config.cameras, + 'lenses': self.config.lenses, + 'last_watch_folder': self.config.last_watch_folder, + 'last_camera': self.config.last_camera, + 'last_lens': self.config.last_lens + }, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"Ошибка сохранения настроек: {e}") + + def _load_celestial_bodies(self): + if os.path.exists(self.CELESTIAL_BODIES_FILE): + try: + with open(self.CELESTIAL_BODIES_FILE, 'r', encoding='utf-8') as f: + self.config.celestial_bodies = json.load(f) + except Exception as e: + print(f"Ошибка загрузки небесных тел: {e}") + + def _save_celestial_bodies(self): + try: + with open(self.CELESTIAL_BODIES_FILE, 'w', encoding='utf-8') as f: + json.dump(self.config.celestial_bodies, f, ensure_ascii=False, indent=2) + except Exception as e: + print(f"Ошибка сохранения небесных тел: {e}") + + def get_cameras(self) -> List[str]: + return self.config.cameras.copy() + + def add_camera(self, camera: str): + if camera and camera not in self.config.cameras: + self.config.cameras.append(camera) + self.save_settings() + + def remove_camera(self, camera: str): + if camera in self.config.cameras: + self.config.cameras.remove(camera) + self.save_settings() + + def get_lenses(self) -> List[str]: + return self.config.lenses.copy() + + def add_lens(self, lens: str): + if lens and lens not in self.config.lenses: + self.config.lenses.append(lens) + self.save_settings() + + def remove_lens(self, lens: str): + if lens in self.config.lenses: + self.config.lenses.remove(lens) + self.save_settings() + + def get_celestial_bodies(self) -> List[str]: + return self.config.celestial_bodies.copy() + + def add_celestial_body(self, name: str): + if name and name not in self.config.celestial_bodies: + self.config.celestial_bodies.append(name) + self._save_celestial_bodies() + + def remove_celestial_body(self, name: str): + if name in self.config.celestial_bodies: + self.config.celestial_bodies.remove(name) + self._save_celestial_bodies() + + def update_celestial_body(self, old_name: str, new_name: str): + if old_name in self.config.celestial_bodies: + idx = self.config.celestial_bodies.index(old_name) + self.config.celestial_bodies[idx] = new_name + self._save_celestial_bodies() + + def get_last_watch_folder(self) -> str: + return self.config.last_watch_folder + + def set_last_watch_folder(self, folder: str): + self.config.last_watch_folder = folder + self.save_settings() + + def get_last_camera(self) -> str: + return self.config.last_camera + + def set_last_camera(self, camera: str): + self.config.last_camera = camera + self.save_settings() + + def get_last_lens(self) -> str: + return self.config.last_lens + + def set_last_lens(self, lens: str): + self.config.last_lens = lens + self.save_settings() \ No newline at end of file diff --git a/services/file_service.py b/services/file_service.py index e69de29..68d7990 100644 --- a/services/file_service.py +++ b/services/file_service.py @@ -0,0 +1,88 @@ +""" +FileService - сервис для работы с файлами +""" +import shutil +from datetime import datetime +from pathlib import Path +from typing import Optional + + +class FileService: + """Сервис для перемещения файлов и ведения логов""" + + SUPPORTED_EXTENSIONS = {'.cr2', '.dng', '.arw', '.jpg', '.jpeg', '.png', '.raw', '.tiff'} + + @classmethod + def is_photo(cls, file_path: Path) -> bool: + """Проверяет, является ли файл фотографией""" + return file_path.suffix.lower() in cls.SUPPORTED_EXTENSIONS + + @classmethod + def resolve_conflict(cls, target_path: Path) -> Path: + """Разрешает конфликт имён файлов""" + if not target_path.exists(): + return target_path + + counter = 1 + stem = target_path.stem + suffix = target_path.suffix + parent = target_path.parent + + while True: + new_name = f"{stem}_{counter}{suffix}" + new_path = parent / new_name + if not new_path.exists(): + return new_path + counter += 1 + + @classmethod + def write_object_log(cls, folder: Path, filename: str, camera: str, optics: str, + timestamp: Optional[datetime] = None) -> None: + """Записывает запись в лог объекта""" + if timestamp is None: + timestamp = datetime.now() + + log_file = folder / "ObjectLog.txt" + line = f"{timestamp} {camera if camera else 'Unknown'} {optics if optics else 'Unknown'} - {filename}\n" + + try: + with open(log_file, 'a', encoding='utf-8') as f: + f.write(line) + except Exception as e: + print(f"Ошибка записи лога: {e}") + + @classmethod + def move_file(cls, source: Path, target_folder: Path, camera: str, optics: str) -> bool: + """Перемещает файл в целевую папку""" + if not source.exists(): + return False + + if not cls.is_photo(source): + return False + + try: + target_folder.mkdir(parents=True, exist_ok=True) + creation_time = datetime.fromtimestamp(source.stat().st_ctime) + cls.write_object_log(target_folder, source.name, camera, optics, creation_time) + target_path = cls.resolve_conflict(target_folder / source.name) + shutil.move(str(source), str(target_path)) + return True + except Exception as e: + print(f"Ошибка перемещения {source.name}: {e}") + return False + + @classmethod + def clear_watch_folder(cls, folder: Path) -> int: + """Очищает папку наблюдения""" + if not folder.exists(): + return 0 + + count = 0 + for file_path in folder.iterdir(): + if file_path.is_file() and cls.is_photo(file_path): + try: + file_path.unlink() + count += 1 + except Exception as e: + print(f"Ошибка удаления {file_path.name}: {e}") + return count \ No newline at end of file diff --git a/services/session_service.py b/services/session_service.py index e69de29..9469d2c 100644 --- a/services/session_service.py +++ b/services/session_service.py @@ -0,0 +1,155 @@ +""" +SessionService - сервис управления сессией +""" +from datetime import datetime +from pathlib import Path +from typing import Optional, Callable +from models.astro_object import AstroObject +from models.session import Session +from services.file_service import FileService + + +class SessionService: + """Сервис для управления сессией наблюдения""" + + def __init__(self): + self._current_session: Optional[Session] = None + self._file_service = FileService() + + def start_session(self, watch_folder: Path, object_name: str, camera: str, optics: str) -> Session: + """Начинает новую сессию""" + start_time = datetime.now() + self._current_session = Session( + camera=camera, + optics=optics, + start_time=start_time + ) + + session_folder = watch_folder / self._current_session.get_session_name() + session_folder.mkdir(parents=True, exist_ok=True) + self._current_session.session_folder = session_folder + + self._log_session_event(f"Session started at {start_time}") + self._log_session_event(f"Camera: {camera}") + self._log_session_event(f"Optics: {optics}") + + self.create_new_object(object_name) + return self._current_session + + def create_new_object(self, object_name: str) -> AstroObject: + """Создаёт новый объект съёмки""" + if not self._current_session: + raise ValueError("Session not started") + + if self._current_session.get_current_object(): + current_obj = self._current_session.get_current_object() + self._log_session_event(f"Object finished: {current_obj.name}, photos: {current_obj.photo_count}") + + object_folder = self._current_session.session_folder / object_name + object_folder.mkdir(parents=True, exist_ok=True) + + astro_object = AstroObject( + name=object_name, + folder=object_folder, + photo_count=0 + ) + + self._current_session.add_object(astro_object) + self._log_session_event(f"New object: {object_name}") + return astro_object + + def handle_file(self, file_path: Path) -> bool: + """Обрабатывает новый файл""" + if not self._current_session: + return False + + current_object = self._current_session.get_current_object() + if not current_object: + return False + + success = self._file_service.move_file( + file_path, + current_object.folder, + self._current_session.camera, + self._current_session.optics + ) + + if success: + current_object.increment_photo_count() + + return success + + def move_remaining_files(self, watch_folder: Path, on_file_moved: Optional[Callable] = None) -> int: + """Перемещает все существующие файлы из папки наблюдения""" + if not self._current_session: + return 0 + + current_object = self._current_session.get_current_object() + if not current_object: + return 0 + + count = 0 + if watch_folder.exists(): + for file_path in watch_folder.iterdir(): + if file_path.is_file() and self._file_service.is_photo(file_path): + if self._file_service.move_file( + file_path, + current_object.folder, + self._current_session.camera, + self._current_session.optics + ): + current_object.increment_photo_count() + count += 1 + if on_file_moved: + on_file_moved(file_path) + return count + + def finish_session(self) -> Session: + """Завершает сессию""" + if not self._current_session: + raise ValueError("No active session") + + self._current_session.finish() + + self._log_session_event(f"Session finished at {self._current_session.end_time}") + self._log_session_event("=== SESSION SUMMARY ===") + for obj in self._current_session.objects: + self._log_session_event(f"Object: {obj.name}, Photos: {obj.photo_count}") + + return self._current_session + + def get_current_session(self) -> Optional[Session]: + return self._current_session + + def get_current_object(self) -> Optional[AstroObject]: + if self._current_session: + return self._current_session.get_current_object() + return None + + def get_current_object_folder(self) -> Optional[Path]: + current_obj = self.get_current_object() + return current_obj.folder if current_obj else None + + def change_camera(self, camera: str): + if self._current_session: + self._current_session.camera = camera + self._log_session_event(f"Camera changed to: {camera}") + + def change_optics(self, optics: str): + if self._current_session: + self._current_session.optics = optics + self._log_session_event(f"Optics changed to: {optics}") + + def _log_session_event(self, message: str): + if self._current_session and self._current_session.session_folder: + log_file = self._current_session.session_folder / "SessionLog.txt" + timestamp = datetime.now() + line = f"{timestamp} - {message}\n" + try: + with open(log_file, 'a', encoding='utf-8') as f: + f.write(line) + except Exception as e: + print(f"Ошибка записи лога: {e}") + + def is_active(self) -> bool: + return self._current_session is not None and self._current_session.is_active() \ No newline at end of file diff --git a/services/watch_service.py b/services/watch_service.py index e69de29..9ef8119 100644 --- a/services/watch_service.py +++ b/services/watch_service.py @@ -0,0 +1,99 @@ +""" +WatchService - сервис отслеживания файлов с очередью +""" +import time +import threading +import queue +from pathlib import Path +from typing import Callable, Optional +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler +from services.file_service import FileService + + +class PhotoHandler(FileSystemEventHandler): + """Обработчик событий файловой системы""" + + def __init__(self, callback: Callable[[Path], None]): + self.callback = callback + self._pending_files = queue.Queue() + self._processing = True + self._processor_thread = threading.Thread(target=self._process_queue, daemon=True) + self._processor_thread.start() + + def on_created(self, event): + if not event.is_directory: + src_path = Path(event.src_path) + if FileService.is_photo(src_path): + time.sleep(0.1) # Даём время на запись файла + self._pending_files.put(src_path) + + def _process_queue(self): + while self._processing: + try: + file_path = self._pending_files.get(timeout=1) + if self.callback and file_path.exists(): + self.callback(file_path) + except queue.Empty: + continue + except Exception as e: + print(f"Ошибка обработки файла: {e}") + + def stop(self): + self._processing = False + + +class WatchService: + """Сервис для отслеживания папки на новые файлы""" + + def __init__(self): + self._observer: Optional[Observer] = None + self._event_handler: Optional[PhotoHandler] = None + self._is_running = False + + def start(self, watch_folder: Path, on_new_file: Callable[[Path], None]) -> bool: + if self._is_running: + return False + + if not watch_folder.exists(): + return False + + try: + self._event_handler = PhotoHandler(on_new_file) + self._observer = Observer() + self._observer.schedule(self._event_handler, str(watch_folder), recursive=False) + self._observer.start() + self._is_running = True + return True + except Exception as e: + print(f"Ошибка запуска отслеживания: {e}") + return False + + def stop(self): + if self._observer: + self._observer.stop() + self._observer.join() + self._observer = None + + if self._event_handler: + self._event_handler.stop() + self._event_handler = None + + self._is_running = False + + def is_running(self) -> bool: + return self._is_running + + def move_all_existing_files(self, watch_folder: Path, on_file_moved: Callable[[Path], None]) -> int: + """Перемещает все существующие файлы""" + count = 0 + if watch_folder.exists(): + for file_path in watch_folder.iterdir(): + if file_path.is_file() and FileService.is_photo(file_path): + try: + on_file_moved(file_path) + count += 1 + time.sleep(0.05) + except Exception as e: + print(f"Ошибка перемещения {file_path.name}: {e}") + return count \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py index e69de29..17f31d2 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -0,0 +1,4 @@ +# UI package +from ui.main_window import MainWindow + +__all__ = ['MainWindow'] \ No newline at end of file diff --git a/ui/__pycache__/__init__.cpython-313.pyc b/ui/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac8a3f41652144c911813f01d452c1d0a320a65e GIT binary patch literal 244 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 KPdNHz4fub2JK;zG literal 0 HcmV?d00001 diff --git a/ui/dialogs/__init__.py b/ui/dialogs/__init__.py index e69de29..0db4844 100644 --- a/ui/dialogs/__init__.py +++ b/ui/dialogs/__init__.py @@ -0,0 +1,5 @@ +from ui.dialogs.equipment_dialog import EquipmentDialog +from ui.dialogs.celestial_dialog import CelestialDialog +from ui.dialogs.instructions_dialog import InstructionsDialog + +__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog'] \ No newline at end of file diff --git a/ui/dialogs/__pycache__/__init__.cpython-313.pyc b/ui/dialogs/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b57a0cc44c3857d284e5437676fb314122c0d1bd GIT binary patch literal 424 zcmY*UO-sZu5KXr2R#u9jM?v=5OZ6s*3bHELimVjz5(uRmb|dMBNqX4h{s@1BcYjU6 zi=Mm*JqaG1)K=CZnY?+td6Q{8Z-gLT%5VM!@4G4b7<8aH0Puuj6ypemSmLq4gJ=+1 z(rU34*`XtyG1^D5eT(IA?)bwG)9&MX>*BHEWyW%S&J$704K-ncDGlWxwSAQ|C(P4W|-bR-zS8f9T1N1k2-v9sr literal 0 HcmV?d00001 diff --git a/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7018e0c81f5b5fcc08d10ca8016ad08cc5265d9 GIT binary patch literal 10441 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 OFPJV(-{n|>QT<=_f4XJ> literal 0 HcmV?d00001 diff --git a/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a4d67bf5803111708397a2698a5b5132f96aebf4 GIT binary patch literal 12877 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 X>EAfG%l(nvcFcBq>L$k$4C;RY{6(Wn literal 0 HcmV?d00001 diff --git a/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc b/ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc50530b17118ccf445d102dd58d4f0552b58038 GIT binary patch literal 11153 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 literal 0 HcmV?d00001 diff --git a/ui/dialogs/celestial_dialog.py b/ui/dialogs/celestial_dialog.py index e69de29..4798cf5 100644 --- a/ui/dialogs/celestial_dialog.py +++ b/ui/dialogs/celestial_dialog.py @@ -0,0 +1,160 @@ +""" +CelestialDialog - диалог управления небесными телами +Аналог CelestialBodiesDialogController из JavaFX версии +""" +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, + QPushButton, QLineEdit, QInputDialog, QMessageBox +) +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont + +from services.config_service import ConfigService + + +class CelestialDialog(QDialog): + """Диалог для управления списком небесных тел""" + + def __init__(self, parent, config_service: ConfigService): + super().__init__(parent) + + self.config_service = config_service + self.setWindowTitle("Небесные тела") + self.setMinimumSize(400, 500) + self.resize(450, 550) + + # Загружаем текущий список + self.celestial_bodies = self.config_service.get_celestial_bodies() + + self._create_ui() + self._update_list() + + def _create_ui(self): + """Создаёт интерфейс диалога""" + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # Заголовок + title_label = QLabel("Управление небесными телами") + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # Подпись + subtitle_label = QLabel("Список объектов для наблюдения") + subtitle_font = QFont() + subtitle_font.setPointSize(11) + subtitle_font.setBold(True) + subtitle_label.setFont(subtitle_font) + layout.addWidget(subtitle_label) + + # Список небесных тел + self.bodies_list = QListWidget() + self.bodies_list.itemClicked.connect(lambda item: self._select_body(item.text())) + layout.addWidget(self.bodies_list) + + # Поле для добавления нового + add_layout = QHBoxLayout() + + self.new_body_entry = QLineEdit() + self.new_body_entry.setPlaceholderText("Название объекта (например: M31, NGC 224)") + self.new_body_entry.returnPressed.connect(self._add_celestial_body) + add_layout.addWidget(self.new_body_entry) + + add_btn = QPushButton("➕ Добавить") + add_btn.clicked.connect(self._add_celestial_body) + add_layout.addWidget(add_btn) + + layout.addLayout(add_layout) + + # Кнопки удаления и редактирования + buttons_layout = QHBoxLayout() + + self.remove_btn = QPushButton("❌ Удалить выбранный") + self.remove_btn.setEnabled(False) + self.remove_btn.clicked.connect(self._remove_celestial_body) + buttons_layout.addWidget(self.remove_btn) + + self.edit_btn = QPushButton("✏ Редактировать") + self.edit_btn.setEnabled(False) + self.edit_btn.clicked.connect(self._edit_celestial_body) + buttons_layout.addWidget(self.edit_btn) + + layout.addLayout(buttons_layout) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.accept) + close_layout = QHBoxLayout() + close_layout.addStretch() + close_layout.addWidget(close_btn) + layout.addLayout(close_layout) + + def _update_list(self): + """Обновляет отображение списка небесных тел""" + self.bodies_list.clear() + for body in self.celestial_bodies: + self.bodies_list.addItem(body) + self._selected_body = None + self.remove_btn.setEnabled(False) + self.edit_btn.setEnabled(False) + + def _select_body(self, body: str): + """Выделяет объект в списке""" + self._selected_body = body + self.remove_btn.setEnabled(True) + self.edit_btn.setEnabled(True) + + def _add_celestial_body(self): + """Добавляет новое небесное тело""" + new_body = self.new_body_entry.text() + if not new_body or not new_body.strip(): + QMessageBox.warning(self, "Ошибка", "Введите название объекта") + return + + new_name = new_body.strip() + if new_name in self.celestial_bodies: + QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + return + + self.celestial_bodies.append(new_name) + self.config_service.add_celestial_body(new_name) + self._update_list() + self.new_body_entry.clear() + QMessageBox.information(self, "Успех", f"Объект '{new_name}' добавлен") + + def _remove_celestial_body(self): + """Удаляет выбранное небесное тело""" + if hasattr(self, '_selected_body') and self._selected_body: + reply = QMessageBox.question(self, "Подтверждение", + f"Удалить объект '{self._selected_body}'?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.celestial_bodies.remove(self._selected_body) + self.config_service.remove_celestial_body(self._selected_body) + self._update_list() + QMessageBox.information(self, "Успех", f"Объект '{self._selected_body}' удалён") + + def _edit_celestial_body(self): + """Редактирует выбранное небесное тело""" + if hasattr(self, '_selected_body') and self._selected_body: + new_name, ok = QInputDialog.getText(self, "Редактировать", + f"Изменить '{self._selected_body}' на:", + text=self._selected_body) + if ok and new_name and new_name.strip(): + new_name = new_name.strip() + if new_name != self._selected_body: + if new_name in self.celestial_bodies: + QMessageBox.warning(self, "Ошибка", f"Объект '{new_name}' уже существует!") + return + + idx = self.celestial_bodies.index(self._selected_body) + old_name = self.celestial_bodies[idx] + self.celestial_bodies[idx] = new_name + self.config_service.update_celestial_body(old_name, new_name) + self._update_list() + QMessageBox.information(self, "Успех", f"Объект переименован в '{new_name}'") \ No newline at end of file diff --git a/ui/dialogs/equipment_dialog.py b/ui/dialogs/equipment_dialog.py index e69de29..b0d8784 100644 --- a/ui/dialogs/equipment_dialog.py +++ b/ui/dialogs/equipment_dialog.py @@ -0,0 +1,202 @@ +""" +EquipmentDialog - диалог управления оборудованием (камеры и объективы) +Аналог EquipmentDialogController из JavaFX версии +""" +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QListWidget, + QPushButton, QInputDialog, QMessageBox, QListWidgetItem +) +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont + +from services.config_service import ConfigService + + +class EquipmentDialog(QDialog): + """Диалог для управления списками камер и объективов""" + + def __init__(self, parent, config_service: ConfigService): + super().__init__(parent) + + self.config_service = config_service + self.setWindowTitle("Управление оборудованием") + self.setMinimumSize(600, 400) + self.resize(650, 450) + + # Загружаем текущие списки + self.cameras = self.config_service.get_cameras() + self.lenses = self.config_service.get_lenses() + + self._create_ui() + self._update_cameras_list() + self._update_lenses_list() + + def _create_ui(self): + """Создаёт интерфейс диалога""" + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # Заголовок + title_label = QLabel("Управление оборудованием") + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # Контейнер для двух колонок + columns_layout = QHBoxLayout() + columns_layout.setSpacing(20) + + # Левая колонка - Камеры + left_layout = QVBoxLayout() + + cameras_label = QLabel("Камеры") + cameras_font = QFont() + cameras_font.setPointSize(12) + cameras_font.setBold(True) + cameras_label.setFont(cameras_font) + left_layout.addWidget(cameras_label) + + self.cameras_list = QListWidget() + self.cameras_list.itemClicked.connect(lambda item: self._select_camera(item.text())) + left_layout.addWidget(self.cameras_list) + + cameras_buttons_layout = QHBoxLayout() + + add_camera_btn = QPushButton("➕ Добавить") + add_camera_btn.clicked.connect(self._add_camera) + cameras_buttons_layout.addWidget(add_camera_btn) + + self.remove_camera_btn = QPushButton("❌ Удалить") + self.remove_camera_btn.setEnabled(False) + self.remove_camera_btn.clicked.connect(self._remove_camera) + cameras_buttons_layout.addWidget(self.remove_camera_btn) + + left_layout.addLayout(cameras_buttons_layout) + + # Правая колонка - Объективы + right_layout = QVBoxLayout() + + lenses_label = QLabel("Объективы") + lenses_label.setFont(cameras_font) + right_layout.addWidget(lenses_label) + + self.lenses_list = QListWidget() + self.lenses_list.itemClicked.connect(lambda item: self._select_lens(item.text())) + right_layout.addWidget(self.lenses_list) + + lenses_buttons_layout = QHBoxLayout() + + add_lens_btn = QPushButton("➕ Добавить") + add_lens_btn.clicked.connect(self._add_lens) + lenses_buttons_layout.addWidget(add_lens_btn) + + self.remove_lens_btn = QPushButton("❌ Удалить") + self.remove_lens_btn.setEnabled(False) + self.remove_lens_btn.clicked.connect(self._remove_lens) + lenses_buttons_layout.addWidget(self.remove_lens_btn) + + right_layout.addLayout(lenses_buttons_layout) + + columns_layout.addLayout(left_layout) + columns_layout.addLayout(right_layout) + layout.addLayout(columns_layout) + + # Кнопка закрытия + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.accept) + close_layout = QHBoxLayout() + close_layout.addStretch() + close_layout.addWidget(close_btn) + layout.addLayout(close_layout) + + def _update_cameras_list(self): + """Обновляет отображение списка камер""" + self.cameras_list.clear() + for camera in self.cameras: + self.cameras_list.addItem(camera) + self._selected_camera = None + self.remove_camera_btn.setEnabled(False) + + def _update_lenses_list(self): + """Обновляет отображение списка объективов""" + self.lenses_list.clear() + for lens in self.lenses: + self.lenses_list.addItem(lens) + self._selected_lens = None + self.remove_lens_btn.setEnabled(False) + + def _select_camera(self, camera: str): + """Выделяет камеру в списке""" + self._selected_camera = camera + self.remove_camera_btn.setEnabled(True) + + # Снимаем выделение с объективов + self.lenses_list.clearSelection() + self._selected_lens = None + self.remove_lens_btn.setEnabled(False) + + def _select_lens(self, lens: str): + """Выделяет объектив в списке""" + self._selected_lens = lens + self.remove_lens_btn.setEnabled(True) + + # Снимаем выделение с камер + self.cameras_list.clearSelection() + self._selected_camera = None + self.remove_camera_btn.setEnabled(False) + + def _add_camera(self): + """Добавляет новую камеру""" + new_camera, ok = QInputDialog.getText(self, "Добавить камеру", "Введите название камеры:") + if ok and new_camera and new_camera.strip(): + new_name = new_camera.strip() + if new_name in self.cameras: + QMessageBox.warning(self, "Ошибка", "Такая камера уже существует!") + return + + self.cameras.append(new_name) + self.config_service.add_camera(new_name) + self._update_cameras_list() + QMessageBox.information(self, "Успех", f"Камера '{new_name}' добавлена") + + def _remove_camera(self): + """Удаляет выбранную камеру""" + if hasattr(self, '_selected_camera') and self._selected_camera: + reply = QMessageBox.question(self, "Подтверждение", + f"Удалить камеру '{self._selected_camera}'?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.cameras.remove(self._selected_camera) + self.config_service.remove_camera(self._selected_camera) + self._update_cameras_list() + QMessageBox.information(self, "Успех", f"Камера '{self._selected_camera}' удалена") + + def _add_lens(self): + """Добавляет новый объектив""" + new_lens, ok = QInputDialog.getText(self, "Добавить объектив", "Введите название объектива:") + if ok and new_lens and new_lens.strip(): + new_name = new_lens.strip() + if new_name in self.lenses: + QMessageBox.warning(self, "Ошибка", "Такой объектив уже существует!") + return + + self.lenses.append(new_name) + self.config_service.add_lens(new_name) + self._update_lenses_list() + QMessageBox.information(self, "Успех", f"Объектив '{new_name}' добавлен") + + def _remove_lens(self): + """Удаляет выбранный объектив""" + if hasattr(self, '_selected_lens') and self._selected_lens: + reply = QMessageBox.question(self, "Подтверждение", + f"Удалить объектив '{self._selected_lens}'?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.lenses.remove(self._selected_lens) + self.config_service.remove_lens(self._selected_lens) + self._update_lenses_list() + QMessageBox.information(self, "Успех", f"Объектив '{self._selected_lens}' удалён") \ No newline at end of file diff --git a/ui/dialogs/instructions_dialog.py b/ui/dialogs/instructions_dialog.py index e69de29..64b8b28 100644 --- a/ui/dialogs/instructions_dialog.py +++ b/ui/dialogs/instructions_dialog.py @@ -0,0 +1,164 @@ +""" +InstructionsDialog - диалог с инструкцией по использованию +""" +from PySide6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, + QPushButton, QScrollArea +) +from PySide6.QtCore import Qt +from PySide6.QtGui import QFont + + +class InstructionsDialog(QDialog): + """Диалог с подробной инструкцией пользователя""" + + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Инструкция по использованию") + self.setMinimumSize(700, 500) + self.resize(750, 550) + + self._create_ui() + + def _create_ui(self): + """Создаёт интерфейс диалога""" + layout = QVBoxLayout(self) + layout.setSpacing(10) + layout.setContentsMargins(15, 15, 15, 15) + + # Заголовок + title_label = QLabel("Astro Session Watcher - Руководство пользователя") + title_font = QFont() + title_font.setPointSize(16) + title_font.setBold(True) + title_label.setFont(title_font) + title_label.setAlignment(Qt.AlignCenter) + layout.addWidget(title_label) + + # Текст инструкции + text_edit = QTextEdit() + text_edit.setReadOnly(True) + text_edit.setFont(QFont("Consolas", 10)) + + instructions = """ +======================= ASTRO SESSION WATCHER ======================= + +Приложение автоматически отслеживает появление новых фотографий в указанной папке, +сортирует их по объектам съемки и ведет подробный лог всего процесса. + +📸 ДЛЯ ЧЕГО ЭТО НУЖНО? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Когда вы снимаете астрономические объекты через EOS Utility или аналогичное ПО, +все фотографии сохраняются в одну папку. Astro Session Watcher помогает: + +• Автоматически распределять снимки по папкам объектов +• Вести лог каждой сессии +• Не пропустить ни одного кадра при смене объекта +• Хранить историю оборудования и небесных тел + +🚀 КАК ЭТО РАБОТАЕТ? +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +1. Вы выбираете папку, куда камера сохраняет снимки +2. Приложение создает папку сессии: "AstroSession_ГГГГ-ММ-ДД" +3. Каждый объект съемки получает свою подпапку внутри папки сессии +4. Когда вы меняете объект (нажимаете "Новая цель"), все накопленные + в папке наблюдения файлы автоматически ПЕРЕМЕЩАЮТСЯ в папку предыдущего объекта +5. При завершении сессии оставшиеся файлы также перемещаются в папку последнего объекта + +📝 ПОШАГОВАЯ ИНСТРУКЦИЯ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +█ 1. ПЕРВЫЙ ЗАПУСК (НАСТРОЙКА) +─────────────────────────────────────────────────────────────────────────────── + +• Откройте меню "Файл" → "Оборудование" и добавьте ваши камеры и объективы +• Откройте меню "Файл" → "Небесные тела" и добавьте объекты для наблюдения +• Все данные сохраняются автоматически в файлах настроек + +█ 2. ЗАПУСК СЕССИИ +─────────────────────────────────────────────────────────────────────────────── + +1. Нажмите "Обзор" и выберите папку, куда камера сохраняет снимки +2. Выберите камеру и объектив из выпадающих списков +3. Введите название цели (или выберите из списка небесных тел) +4. Нажмите ▶ "Начать отслеживание" + +✅ После запуска: + • Статус изменится на "● ON AIR" с мигающим красным текстом + • Кнопка "Новая цель" начнет мигать красным контуром + • В папке наблюдения создастся папка "AstroSession_дата" + • Внутри - папка с вашей первой целью + +█ 3. СМЕНА ОБЪЕКТА ВО ВРЕМЯ СЕССИИ +─────────────────────────────────────────────────────────────────────────────── + +Когда вы заканчиваете снимать один объект и переходите к другому: + +1. Нажмите кнопку "Новая цель" (или Ctrl+Shift+N) +2. Введите название нового объекта +3. Приложение автоматически: + • Переместит все накопленные файлы в папку предыдущего объекта + • Создаст новую папку для следующего объекта + • Сбросит счетчик файлов + • Продолжит отслеживание + +💡 ВАЖНО: Если перед сменой объекта в папке наблюдения уже есть файлы, + они НЕ ПОТЕРЯЮТСЯ - все будут перемещены в папку текущего объекта! + +█ 4. ЗАВЕРШЕНИЕ СЕССИИ +─────────────────────────────────────────────────────────────────────────────── + +1. Нажмите ■ "Остановить" (или Ctrl+X) +2. Приложение: + • Переместит все оставшиеся файлы в папку последнего объекта + • Запишет итоговый лог сессии + • Покажет диалог с предложением открыть папку сессии + • Восстановит интерфейс для новой сессии + +⌨️ ГОРЯЧИЕ КЛАВИШИ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Ctrl + O → Выбрать папку наблюдения +Ctrl + E → Управление оборудованием +Ctrl + B → Управление небесными телами +Ctrl + S → Начать сессию +Ctrl + X → Остановить сессию +Ctrl + F → Открыть папку текущей сессии +Ctrl + Shift+N → Создать новый объект +F1 → О программе +F2 → Эта инструкция + +🔧 ФАЙЛЫ НАСТРОЕК +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📄 astro_settings.json ← камеры, объективы, последняя папка +📄 celestial_bodies.json ← список небесных тел + +Все файлы хранятся в папке с программой. Вы можете редактировать их вручную. + +📧 ТЕХНИЧЕСКАЯ ПОДДЕРЖКА +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Разработчик: Vic Sergeev +Версия: 0.3.0-alpha + +При обнаружении ошибок или для предложений по улучшению: +• Сообщите разработчику +• Приложите файлы логов (SessionLog.txt, ObjectLog.txt) +""" + + text_edit.setText(instructions) + layout.addWidget(text_edit) + + # Кнопка закрытия + close_layout = QHBoxLayout() + close_layout.addStretch() + + close_btn = QPushButton("Закрыть") + close_btn.clicked.connect(self.accept) + close_layout.addWidget(close_btn) + + layout.addLayout(close_layout) \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index e69de29..eff6d48 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -0,0 +1,642 @@ +""" +MainWindow - главное окно приложения на PySide6 +""" +import sys +import subprocess +import platform +from pathlib import Path +from datetime import datetime +from typing import Optional + +from PySide6.QtWidgets import ( + QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, + QLabel, QLineEdit, QComboBox, QPushButton, QMenuBar, QMenu, + QMessageBox, QFileDialog, QInputDialog, QFrame, QApplication +) +from PySide6.QtCore import Qt, QTimer, Signal +from PySide6.QtGui import QFont, QIcon, QAction + +from services.config_service import ConfigService +from services.session_service import SessionService +from services.watch_service import WatchService +from services.file_service import FileService + + +class MainWindow(QMainWindow): + """Главное окно приложения""" + + def __init__(self): + super().__init__() + + # Сервисы + self.config_service = ConfigService() + self.session_service = SessionService() + self.watch_service = WatchService() + + # Переменные состояния + self.running = False + self.file_count = 0 + self._blink_timer = None + self._new_object_blink_timer = None + + # Настройка окна + self.setWindowTitle("Astro Session Watcher v0.3.0") + self.setMinimumSize(700, 500) + self.resize(800, 550) + + self.center_window() + self._create_menu_bar() + self._create_main_content() + self._load_saved_settings() + self._setup_hotkeys() + self._update_file_count_display() + + self.setAttribute(Qt.WA_DeleteOnClose) + + def center_window(self): + screen = QApplication.primaryScreen().availableGeometry() + self.setGeometry( + (screen.width() - self.width()) // 2, + (screen.height() - self.height()) // 2, + self.width(), + self.height() + ) + + def _create_menu_bar(self): + menubar = self.menuBar() + + # Меню Файл + file_menu = menubar.addMenu("Файл") + + select_folder_action = QAction("Выбрать папку...", self) + select_folder_action.setShortcut("Ctrl+O") + select_folder_action.triggered.connect(self.select_folder) + file_menu.addAction(select_folder_action) + + file_menu.addSeparator() + + equipment_action = QAction("Оборудование...", self) + equipment_action.setShortcut("Ctrl+E") + equipment_action.triggered.connect(self.open_equipment_dialog) + file_menu.addAction(equipment_action) + + celestial_action = QAction("Небесные тела...", self) + celestial_action.setShortcut("Ctrl+B") + celestial_action.triggered.connect(self.open_celestial_dialog) + file_menu.addAction(celestial_action) + + file_menu.addSeparator() + + exit_action = QAction("Выход", self) + exit_action.setShortcut("Ctrl+Q") + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + # Меню Сессия + session_menu = menubar.addMenu("Сессия") + + start_action = QAction("Начать наблюдение", self) + start_action.setShortcut("Ctrl+S") + start_action.triggered.connect(self.start) + session_menu.addAction(start_action) + + stop_action = QAction("Остановить наблюдение", self) + stop_action.setShortcut("Ctrl+X") + stop_action.triggered.connect(self.stop) + session_menu.addAction(stop_action) + + session_menu.addSeparator() + + open_folder_action = QAction("Открыть папку сессии", self) + open_folder_action.setShortcut("Ctrl+F") + open_folder_action.triggered.connect(self.open_session_folder) + session_menu.addAction(open_folder_action) + + session_menu.addSeparator() + + new_object_action = QAction("Новая цель...", self) + new_object_action.setShortcut("Ctrl+Shift+N") + new_object_action.triggered.connect(self.set_new_object) + session_menu.addAction(new_object_action) + + # Меню Помощь + help_menu = menubar.addMenu("Помощь") + + instructions_action = QAction("Инструкция", self) + instructions_action.setShortcut("F2") + instructions_action.triggered.connect(self.show_instructions) + help_menu.addAction(instructions_action) + + help_menu.addSeparator() + + about_action = QAction("О программе", self) + about_action.setShortcut("F1") + about_action.triggered.connect(self.show_info) + help_menu.addAction(about_action) + + def _create_main_content(self): + central_widget = QWidget() + self.setCentralWidget(central_widget) + + main_layout = QVBoxLayout(central_widget) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(15) + + grid_layout = QGridLayout() + grid_layout.setVerticalSpacing(12) + grid_layout.setHorizontalSpacing(15) + + # Row 0: Папка + folder_label = QLabel("Папка:") + folder_label.setFont(QFont("", 10, QFont.Bold)) + grid_layout.addWidget(folder_label, 0, 0, Qt.AlignRight | Qt.AlignVCenter) + + folder_widget = QWidget() + folder_layout = QHBoxLayout(folder_widget) + folder_layout.setContentsMargins(0, 0, 0, 0) + folder_layout.setSpacing(10) + + self.folder_entry = QLineEdit() + self.folder_entry.setPlaceholderText("Выберите папку для отслеживания") + folder_layout.addWidget(self.folder_entry) + + self.folder_button = QPushButton("Обзор...") + self.folder_button.setFixedWidth(80) + self.folder_button.clicked.connect(self.select_folder) + folder_layout.addWidget(self.folder_button) + + grid_layout.addWidget(folder_widget, 0, 1) + + # Row 1: Оборудование + equipment_label = QLabel("Оборудование:") + equipment_label.setFont(QFont("", 10, QFont.Bold)) + grid_layout.addWidget(equipment_label, 1, 0, Qt.AlignRight | Qt.AlignVCenter) + + equipment_widget = QWidget() + equipment_layout = QHBoxLayout(equipment_widget) + equipment_layout.setContentsMargins(0, 0, 0, 0) + equipment_layout.setSpacing(10) + + self.camera_combo = QComboBox() + self.camera_combo.setEditable(False) + equipment_layout.addWidget(self.camera_combo) + + self.lens_combo = QComboBox() + self.lens_combo.setEditable(False) + equipment_layout.addWidget(self.lens_combo) + + grid_layout.addWidget(equipment_widget, 1, 1) + + # Row 2: Цель + target_label = QLabel("Цель:") + target_label.setFont(QFont("", 10, QFont.Bold)) + grid_layout.addWidget(target_label, 2, 0, Qt.AlignRight | Qt.AlignVCenter) + + target_widget = QWidget() + target_layout = QHBoxLayout(target_widget) + target_layout.setContentsMargins(0, 0, 0, 0) + target_layout.setSpacing(10) + + self.object_combo = QComboBox() + self.object_combo.setEditable(True) + self.object_combo.setInsertPolicy(QComboBox.NoInsert) + # Настройка плейсхолдера + self.object_combo.lineEdit().setPlaceholderText("Введите название цели") + # Автодополнение при вводе + self.object_combo.lineEdit().textChanged.connect(self._on_object_text_changed) + target_layout.addWidget(self.object_combo) + + self.new_object_button = QPushButton("Новая цель") + self.new_object_button.setFixedWidth(100) + self.new_object_button.setEnabled(False) + self.new_object_button.clicked.connect(self.set_new_object) + target_layout.addWidget(self.new_object_button) + + grid_layout.addWidget(target_widget, 2, 1) + + # Row 3: Статистика + stats_label = QLabel("Статистика:") + stats_label.setFont(QFont("", 10, QFont.Bold)) + grid_layout.addWidget(stats_label, 3, 0, Qt.AlignRight | Qt.AlignVCenter) + + self.file_count_label = QLabel("Файлов получено: 0") + self.file_count_label.setFont(QFont("", 11)) + grid_layout.addWidget(self.file_count_label, 3, 1, Qt.AlignLeft) + + # Row 4: Статус + status_label = QLabel("Статус:") + status_label.setFont(QFont("", 10, QFont.Bold)) + grid_layout.addWidget(status_label, 4, 0, Qt.AlignRight | Qt.AlignVCenter) + + self.status_label = QLabel("IDLE") + self.status_label.setFont(QFont("", 12, QFont.Bold)) + self.status_label.setStyleSheet("color: #666666;") + grid_layout.addWidget(self.status_label, 4, 1, Qt.AlignLeft) + + main_layout.addLayout(grid_layout) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setStyleSheet("background-color: #333333; max-height: 1px;") + main_layout.addWidget(separator) + + buttons_layout = QHBoxLayout() + buttons_layout.setSpacing(15) + buttons_layout.setAlignment(Qt.AlignCenter) + + self.start_button = QPushButton("▶ Начать отслеживание") + self.start_button.setFixedSize(180, 35) + self.start_button.setStyleSheet(""" + QPushButton { + background-color: #4CAF50; + color: white; + font-weight: bold; + border-radius: 4px; + } + QPushButton:hover { + background-color: #45a049; + } + """) + self.start_button.clicked.connect(self.start) + buttons_layout.addWidget(self.start_button) + + self.stop_button = QPushButton("■ Остановить") + self.stop_button.setFixedSize(180, 35) + self.stop_button.setEnabled(False) + self.stop_button.setStyleSheet(""" + QPushButton { + background-color: #f44336; + color: white; + font-weight: bold; + border-radius: 4px; + } + QPushButton:hover { + background-color: #d32f2f; + } + """) + self.stop_button.clicked.connect(self.stop) + buttons_layout.addWidget(self.stop_button) + + main_layout.addLayout(buttons_layout) + + footer_layout = QHBoxLayout() + + version_label = QLabel("v0.3.0-alpha") + version_label.setStyleSheet("color: #666666; font-size: 11px;") + footer_layout.addWidget(version_label) + + footer_layout.addStretch() + + copyright_label = QLabel("Made by Vic Sergeev 2026") + copyright_label.setStyleSheet("color: #666666; font-size: 11px;") + footer_layout.addWidget(copyright_label) + + main_layout.addLayout(footer_layout) + + def _on_object_text_changed(self, text): + """Автодополнение при вводе названия цели""" + if not text: + return + + # Поиск совпадений в списке небесных тел + celestial_bodies = self.config_service.get_celestial_bodies() + matches = [body for body in celestial_bodies if body.lower().startswith(text.lower())] + + if matches and matches[0] != text: + # Временно отключаем сигнал, чтобы избежать рекурсии + self.object_combo.lineEdit().blockSignals(True) + self.object_combo.lineEdit().setText(matches[0]) + self.object_combo.lineEdit().setSelection(len(text), len(matches[0])) + self.object_combo.lineEdit().blockSignals(False) + + def _load_saved_settings(self): + cameras = self.config_service.get_cameras() + lenses = self.config_service.get_lenses() + celestial_bodies = self.config_service.get_celestial_bodies() + + if cameras: + self.camera_combo.addItems(cameras) + last_camera = self.config_service.get_last_camera() + if last_camera and last_camera in cameras: + self.camera_combo.setCurrentText(last_camera) + + if lenses: + self.lens_combo.addItems(lenses) + last_lens = self.config_service.get_last_lens() + if last_lens and last_lens in lenses: + self.lens_combo.setCurrentText(last_lens) + + if celestial_bodies: + self.object_combo.addItems(celestial_bodies) + + last_folder = self.config_service.get_last_watch_folder() + if last_folder: + self.folder_entry.setText(last_folder) + + def _setup_hotkeys(self): + pass + + def _set_running_state(self, state: bool): + self.running = state + + if state: + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + self.new_object_button.setEnabled(True) + self.status_label.setText("● ON AIR") + self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + self._start_blinking() + self._start_new_object_blinking() + else: + self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) + self.new_object_button.setEnabled(False) + self.status_label.setText("IDLE") + self.status_label.setStyleSheet("color: #666666; font-weight: bold;") + self._stop_blinking() + self._stop_new_object_blinking() + + def _start_blinking(self): + self._blink_timer = QTimer() + self._blink_timer.timeout.connect(self._do_blink) + self._blink_timer.start(500) + + def _do_blink(self): + if not self.running: + return + current_style = self.status_label.styleSheet() + if "color: #ff0000" in current_style: + self.status_label.setStyleSheet("color: #ffffff; font-weight: bold;") + else: + self.status_label.setStyleSheet("color: #ff0000; font-weight: bold;") + + def _stop_blinking(self): + if self._blink_timer: + self._blink_timer.stop() + self._blink_timer = None + self.status_label.setStyleSheet("color: #666666; font-weight: bold;") + + def _start_new_object_blinking(self): + self._new_object_blink_timer = QTimer() + self._new_object_blink_timer.timeout.connect(self._do_new_object_blink) + self._new_object_blink_timer.start(500) + + def _do_new_object_blink(self): + if not self.running: + return + current_style = self.new_object_button.styleSheet() + if "border: 2px solid red" in current_style: + self.new_object_button.setStyleSheet("") + else: + self.new_object_button.setStyleSheet("border: 2px solid red; border-radius: 4px;") + + def _stop_new_object_blinking(self): + if self._new_object_blink_timer: + self._new_object_blink_timer.stop() + self._new_object_blink_timer = None + self.new_object_button.setStyleSheet("") + + def _update_file_count_display(self): + if self.running and self.session_service.get_current_object(): + current_obj = self.session_service.get_current_object() + self.file_count = current_obj.photo_count + self.file_count_label.setText(f"Файлов получено: {self.file_count}") + QTimer.singleShot(1000, self._update_file_count_display) + + def _on_file_received(self, file_path: Path): + """Обработчик получения нового файла""" + print(f"Обнаружен файл: {file_path}") + if self.session_service.handle_file(file_path): + self.file_count += 1 + self.file_count_label.setText(f"Файлов получено: {self.file_count}") + print(f"Файл обработан: {file_path.name}") + else: + print(f"Не удалось обработать файл: {file_path}") + + def select_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Выберите папку для отслеживания") + if folder: + self.folder_entry.setText(folder) + self.config_service.set_last_watch_folder(folder) + + def start(self): + watch_folder = self.folder_entry.text() + object_name = self.object_combo.currentText() + + if not watch_folder: + QMessageBox.critical(self, "Ошибка", "Папка для отслеживания не выбрана") + return + + if not object_name: + QMessageBox.critical(self, "Ошибка", "Цель не указана") + return + + # Проверка, существует ли объект в списке небесных тел + celestial_bodies = self.config_service.get_celestial_bodies() + if object_name not in celestial_bodies: + reply = QMessageBox.question(self, "Новый объект", + f"Объект '{object_name}' не найден в списке.\nДобавить его в список?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.config_service.add_celestial_body(object_name) + self.object_combo.addItem(object_name) + else: + return + + camera = self.camera_combo.currentText() + lens = self.lens_combo.currentText() + + if not camera or not lens: + reply = QMessageBox.question(self, "Предупреждение", + "Камера или объектив не выбраны. Продолжить?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.No: + return + + try: + watch_path = Path(watch_folder) + + # Очищаем папку наблюдения от старых файлов + FileService.clear_watch_folder(watch_path) + + camera_val = camera if camera else "Unknown" + lens_val = lens if lens else "Unknown" + + self.session_service.start_session(watch_path, object_name, camera_val, lens_val) + + self.config_service.set_last_camera(camera_val) + self.config_service.set_last_lens(lens_val) + + # Запускаем отслеживание + success = self.watch_service.start(watch_path, self._on_file_received) + if not success: + QMessageBox.critical(self, "Ошибка", "Не удалось запустить отслеживание папки") + return + + self._set_running_state(True) + + print(f"Отслеживание начато! Папка наблюдения: {watch_path}") + print(f"Папка сессии: {self.session_service.get_current_session().session_folder}") + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось начать сессию: {e}") + import traceback + traceback.print_exc() + + def stop(self): + if not self.running: + return + + try: + watch_folder = Path(self.folder_entry.text()) + + print(f"Остановка сессии. Перемещаем файлы из {watch_folder}") + + # Перемещаем все оставшиеся файлы + moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) + print(f"Перемещено файлов: {moved_count}") + + # Останавливаем отслеживание + self.watch_service.stop() + + # Завершаем сессию + session = self.session_service.finish_session() + + self._set_running_state(False) + + # Показываем диалог завершения + self._show_session_end_dialog(session) + + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Ошибка при завершении сессии: {e}") + import traceback + traceback.print_exc() + + def set_new_object(self): + if not self.running: + QMessageBox.critical(self, "Ошибка", "Сессия не активна") + return + + # Перемещаем все накопленные файлы в папку текущего объекта + watch_folder = Path(self.folder_entry.text()) + moved_count = self.watch_service.move_all_existing_files(watch_folder, self._on_file_received) + + if moved_count > 0: + print(f"Перемещено файлов перед сменой объекта: {moved_count}") + + new_object, ok = QInputDialog.getText(self, "Новый объект", "Введите название объекта:") + + if ok and new_object and new_object.strip(): + new_name = new_object.strip() + + # Проверка, существует ли объект в списке + celestial_bodies = self.config_service.get_celestial_bodies() + if new_name not in celestial_bodies: + reply = QMessageBox.question(self, "Новый объект", + f"Объект '{new_name}' не найден в списке.\nДобавить его в список?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.config_service.add_celestial_body(new_name) + self.object_combo.addItem(new_name) + else: + return + + self.session_service.create_new_object(new_name) + self.object_combo.setCurrentText(new_name) + QMessageBox.information(self, "Успех", f"Объект изменён на: {new_name}") + + def open_equipment_dialog(self): + from ui.dialogs.equipment_dialog import EquipmentDialog + dialog = EquipmentDialog(self, self.config_service) + dialog.exec() + + self.camera_combo.clear() + self.lens_combo.clear() + self.camera_combo.addItems(self.config_service.get_cameras()) + self.lens_combo.addItems(self.config_service.get_lenses()) + + def open_celestial_dialog(self): + from ui.dialogs.celestial_dialog import CelestialDialog + dialog = CelestialDialog(self, self.config_service) + dialog.exec() + + self.object_combo.clear() + self.object_combo.addItems(self.config_service.get_celestial_bodies()) + + def open_session_folder(self): + if self.running and self.session_service.get_current_session(): + folder = self.session_service.get_current_session().session_folder + if folder and folder.exists(): + try: + if platform.system() == "Windows": + subprocess.Popen(['explorer', str(folder)]) + elif platform.system() == "Darwin": + subprocess.Popen(['open', str(folder)]) + else: + subprocess.Popen(['xdg-open', str(folder)]) + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") + else: + QMessageBox.critical(self, "Ошибка", "Папка сессии не найдена") + else: + QMessageBox.information(self, "Информация", "Нет активной сессии") + + def show_instructions(self): + from ui.dialogs.instructions_dialog import InstructionsDialog + dialog = InstructionsDialog(self) + dialog.exec() + + def show_info(self): + QMessageBox.about(self, "О программе", + "Astro Session Watcher\nВерсия: 0.3.0-alpha\n\n" + "Приложение для автоматической сортировки астрофотографий\n\n" + "Особенности:\n" + "• Автоматическое отслеживание новых файлов\n" + "• Сортировка по объектам съёмки\n" + "• Ведение детальных логов\n" + "• Сохранение истории оборудования\n\n" + "Разработчик: Vic Sergeev\n2026") + + def _show_session_end_dialog(self, session): + current_object = session.get_current_object() + object_name = current_object.name if current_object else "Unknown" + photo_count = current_object.photo_count if current_object else 0 + session_folder = session.session_folder + + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Сессия завершена") + msg_box.setIcon(QMessageBox.Information) + msg_box.setText(f"Наблюдение остановлено\n\nСессия для объекта '{object_name}' завершена.\nПолучено файлов: {photo_count}") + msg_box.setInformativeText(f"Папка с данными:\n{session_folder}") + + open_folder_btn = msg_box.addButton("📁 Открыть папку", QMessageBox.AcceptRole) + close_btn = msg_box.addButton("Закрыть", QMessageBox.RejectRole) + + msg_box.exec() + if msg_box.clickedButton() == open_folder_btn: + if session_folder and session_folder.exists(): + try: + if platform.system() == "Windows": + subprocess.Popen(['explorer', str(session_folder)]) + elif platform.system() == "Darwin": + subprocess.Popen(['open', str(session_folder)]) + else: + subprocess.Popen(['xdg-open', str(session_folder)]) + except Exception as e: + QMessageBox.critical(self, "Ошибка", f"Не удалось открыть папку: {e}") + + def closeEvent(self, event): + if self.running: + reply = QMessageBox.question(self, "Выход", + "Сессия активна. Остановить сессию и выйти?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + try: + self.stop() + except: + pass + event.accept() + else: + event.ignore() + else: + event.accept() \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py index e69de29..92ecf6f 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -0,0 +1,4 @@ +# Utils package +from utils.sound_manager import SoundManager + +__all__ = ['SoundManager'] \ No newline at end of file diff --git a/utils/__pycache__/__init__.cpython-313.pyc b/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6404c8f55d03e5c998ab1b8350756c39b2d070ef GIT binary patch literal 255 zcmey&%ge<81UV6ZGNXa?V-N=hn4pZ$VnD`JhG2$ZMsEf$#v(=qhF~Ur#v-P4W=)ot zAVr#tw|Ii{OY>5E6Y~<&Q;Uk2fr5UT%(sL~OEPnc^@>4q@wrHnx7g$36LWIn<5x0# z2AOcn-PtN8v^ce>IL50qCo{FABsC_WGC3o$C^w)eKPxr4q&UX0xTGjQIJLMqGe0jp zu_QSowI~K`eoTCPW?p7Ve7s&k