Compare commits

..

No commits in common. "b2331818ce0026a337329c060be5a9ceee62a3cd" and "77e94b2188d7dcc4aa53e39dadf5bbbe93f319de" have entirely different histories.

77 changed files with 12 additions and 25951 deletions

19
.gitignore vendored
View file

@ -1,19 +0,0 @@
# Виртуальное окружение
.venv/
venv/
env/
ENV/
# Скомпилированные файлы
*.exe
*.pyc
*.pyd
__pycache__/
# PyCharm (опционально, но полезно)
.idea/
*.iml
# Системные файлы
.DS_Store
Thumbs.db

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (AstroSessionWatcher)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated
View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.13 (AstroSessionWatcher)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (AstroSessionWatcher)" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated
View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" filepath="$PROJECT_DIR$/.idea/AstroSessionWatcher.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

78
.idea/workspace.xml generated
View file

@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 8
}</component>
<component name="ProjectId" id="3DOCpPEAGgO0as5zvcSEwPDpHTU" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"Python.main.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ai.playground.ignore.import.keys.banner.in.settings": "true",
"git-widget-placeholder": "dev-camera-control",
"ignore.virus.scanning.warn.message": "true",
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-4762d8aabb82-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.173" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="40c00e43-cac1-46a2-8996-291e69775bbb" name="Changes" comment="" />
<created>1778143911036</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1778143911036</updated>
<workItem from="1778143912090" duration="15655000" />
<workItem from="1778484865590" duration="2044000" />
<workItem from="1778787253526" duration="126000" />
<workItem from="1778854594819" duration="1683000" />
<workItem from="1778931411219" duration="4228000" />
<workItem from="1778958956101" duration="23000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/AstroSessionWatcher$main.coverage" NAME="main Coverage Results" MODIFIED="1778486479271" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component>
</project>

View file

@ -1,38 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['main.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
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,
)

9
LICENSE Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2026 Vics_Lab
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# Astro-Session-Watcher
Автоматизированная сортировка фотографий с астросессии

View file

@ -1,14 +0,0 @@
Что можно улучшить в следующих версиях:
Увеличение шрифтов для лучшей читаемости
Ночной режим (красная тема)
Автодополнение в комбобоксах
Предпросмотр изображений
Экспорт отчётов (PDF/HTML)
FITS поддержка
Dark/Flat/Bias калькулятор (рекомендации по количеству кадров)

View file

@ -1,17 +0,0 @@
{
"cameras": [
"Canon 40D",
"Canon 400D"
],
"lenses": [
"MTO-500A",
"Tamron 18-200mm",
"Юпитер-21м 200мм"
],
"telescopes": [
"Celestron Astromaster 130 (f/5.0, F=650mm, D=130mm)"
],
"last_watch_folder": "C:/Users/Juliette/Documents/testwatcher",
"last_camera": "Canon 40D",
"last_lens": "MTO-500A"
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -1,647 +0,0 @@
('C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\build\\AstroSessionWatcher\\PYZ-00.pyz',
[('__future__',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\__future__.py',
'PYMODULE'),
('_aix_support',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_aix_support.py',
'PYMODULE'),
('_colorize',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_colorize.py',
'PYMODULE'),
('_compat_pickle',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_compat_pickle.py',
'PYMODULE'),
('_compression',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_compression.py',
'PYMODULE'),
('_opcode_metadata',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_opcode_metadata.py',
'PYMODULE'),
('_py_abc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_py_abc.py',
'PYMODULE'),
('_pydatetime',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_pydatetime.py',
'PYMODULE'),
('_pydecimal',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_pydecimal.py',
'PYMODULE'),
('_strptime',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_strptime.py',
'PYMODULE'),
('_threading_local',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\_threading_local.py',
'PYMODULE'),
('argparse',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\argparse.py',
'PYMODULE'),
('ast',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ast.py',
'PYMODULE'),
('base64',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\base64.py',
'PYMODULE'),
('bisect',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\bisect.py',
'PYMODULE'),
('bz2',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\bz2.py',
'PYMODULE'),
('calendar',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\calendar.py',
'PYMODULE'),
('contextlib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\contextlib.py',
'PYMODULE'),
('contextvars',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\contextvars.py',
'PYMODULE'),
('copy',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\copy.py',
'PYMODULE'),
('csv',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\csv.py',
'PYMODULE'),
('ctypes',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\__init__.py',
'PYMODULE'),
('ctypes._aix',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\_aix.py',
'PYMODULE'),
('ctypes._endian',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\_endian.py',
'PYMODULE'),
('ctypes.macholib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\macholib\\__init__.py',
'PYMODULE'),
('ctypes.macholib.dyld',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\macholib\\dyld.py',
'PYMODULE'),
('ctypes.macholib.dylib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\macholib\\dylib.py',
'PYMODULE'),
('ctypes.macholib.framework',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\macholib\\framework.py',
'PYMODULE'),
('ctypes.util',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\util.py',
'PYMODULE'),
('ctypes.wintypes',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ctypes\\wintypes.py',
'PYMODULE'),
('dataclasses',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\dataclasses.py',
'PYMODULE'),
('datetime',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\datetime.py',
'PYMODULE'),
('decimal',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\decimal.py',
'PYMODULE'),
('dis',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\dis.py',
'PYMODULE'),
('email',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\__init__.py',
'PYMODULE'),
('email._encoded_words',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\_encoded_words.py',
'PYMODULE'),
('email._header_value_parser',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\_header_value_parser.py',
'PYMODULE'),
('email._parseaddr',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\_parseaddr.py',
'PYMODULE'),
('email._policybase',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\_policybase.py',
'PYMODULE'),
('email.base64mime',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\base64mime.py',
'PYMODULE'),
('email.charset',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\charset.py',
'PYMODULE'),
('email.contentmanager',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\contentmanager.py',
'PYMODULE'),
('email.encoders',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\encoders.py',
'PYMODULE'),
('email.errors',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\errors.py',
'PYMODULE'),
('email.feedparser',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\feedparser.py',
'PYMODULE'),
('email.generator',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\generator.py',
'PYMODULE'),
('email.header',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\header.py',
'PYMODULE'),
('email.headerregistry',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\headerregistry.py',
'PYMODULE'),
('email.iterators',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\iterators.py',
'PYMODULE'),
('email.message',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\message.py',
'PYMODULE'),
('email.parser',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\parser.py',
'PYMODULE'),
('email.policy',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\policy.py',
'PYMODULE'),
('email.quoprimime',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\quoprimime.py',
'PYMODULE'),
('email.utils',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\email\\utils.py',
'PYMODULE'),
('fnmatch',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\fnmatch.py',
'PYMODULE'),
('fractions',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\fractions.py',
'PYMODULE'),
('getopt',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\getopt.py',
'PYMODULE'),
('gettext',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\gettext.py',
'PYMODULE'),
('glob',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\glob.py',
'PYMODULE'),
('gzip',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\gzip.py',
'PYMODULE'),
('hashlib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\hashlib.py',
'PYMODULE'),
('importlib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\__init__.py',
'PYMODULE'),
('importlib._abc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\_abc.py',
'PYMODULE'),
('importlib._bootstrap',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\_bootstrap.py',
'PYMODULE'),
('importlib._bootstrap_external',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\_bootstrap_external.py',
'PYMODULE'),
('importlib.abc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\abc.py',
'PYMODULE'),
('importlib.machinery',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\machinery.py',
'PYMODULE'),
('importlib.metadata',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\__init__.py',
'PYMODULE'),
('importlib.metadata._adapters',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_adapters.py',
'PYMODULE'),
('importlib.metadata._collections',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_collections.py',
'PYMODULE'),
('importlib.metadata._functools',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_functools.py',
'PYMODULE'),
('importlib.metadata._itertools',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_itertools.py',
'PYMODULE'),
('importlib.metadata._meta',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_meta.py',
'PYMODULE'),
('importlib.metadata._text',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\metadata\\_text.py',
'PYMODULE'),
('importlib.readers',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\readers.py',
'PYMODULE'),
('importlib.resources',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\__init__.py',
'PYMODULE'),
('importlib.resources._adapters',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\_adapters.py',
'PYMODULE'),
('importlib.resources._common',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\_common.py',
'PYMODULE'),
('importlib.resources._functional',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\_functional.py',
'PYMODULE'),
('importlib.resources._itertools',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\_itertools.py',
'PYMODULE'),
('importlib.resources.abc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\abc.py',
'PYMODULE'),
('importlib.resources.readers',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\resources\\readers.py',
'PYMODULE'),
('importlib.util',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\importlib\\util.py',
'PYMODULE'),
('inspect',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\inspect.py',
'PYMODULE'),
('ipaddress',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\ipaddress.py',
'PYMODULE'),
('json',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\json\\__init__.py',
'PYMODULE'),
('json.decoder',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\json\\decoder.py',
'PYMODULE'),
('json.encoder',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\json\\encoder.py',
'PYMODULE'),
('json.scanner',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\json\\scanner.py',
'PYMODULE'),
('logging',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\logging\\__init__.py',
'PYMODULE'),
('lzma',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\lzma.py',
'PYMODULE'),
('models',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\models\\__init__.py',
'PYMODULE'),
('models.astro_object',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\models\\astro_object.py',
'PYMODULE'),
('models.session',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\models\\session.py',
'PYMODULE'),
('numbers',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\numbers.py',
'PYMODULE'),
('opcode',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\opcode.py',
'PYMODULE'),
('pathlib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\pathlib\\__init__.py',
'PYMODULE'),
('pathlib._abc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\pathlib\\_abc.py',
'PYMODULE'),
('pathlib._local',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\pathlib\\_local.py',
'PYMODULE'),
('pickle',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\pickle.py',
'PYMODULE'),
('platform',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\platform.py',
'PYMODULE'),
('pprint',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\pprint.py',
'PYMODULE'),
('py_compile',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\py_compile.py',
'PYMODULE'),
('queue',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\queue.py',
'PYMODULE'),
('quopri',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\quopri.py',
'PYMODULE'),
('random',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\random.py',
'PYMODULE'),
('selectors',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\selectors.py',
'PYMODULE'),
('services',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\services\\__init__.py',
'PYMODULE'),
('services.config_service',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\services\\config_service.py',
'PYMODULE'),
('services.file_service',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\services\\file_service.py',
'PYMODULE'),
('services.session_service',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\services\\session_service.py',
'PYMODULE'),
('services.watch_service',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\services\\watch_service.py',
'PYMODULE'),
('shutil',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\shutil.py',
'PYMODULE'),
('signal',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\signal.py',
'PYMODULE'),
('socket',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\socket.py',
'PYMODULE'),
('statistics',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\statistics.py',
'PYMODULE'),
('string',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\string.py',
'PYMODULE'),
('stringprep',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\stringprep.py',
'PYMODULE'),
('subprocess',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\subprocess.py',
'PYMODULE'),
('sysconfig',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\sysconfig\\__init__.py',
'PYMODULE'),
('tarfile',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tarfile.py',
'PYMODULE'),
('tempfile',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tempfile.py',
'PYMODULE'),
('textwrap',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\textwrap.py',
'PYMODULE'),
('threading',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\threading.py',
'PYMODULE'),
('tkinter',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\__init__.py',
'PYMODULE'),
('tkinter.commondialog',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\commondialog.py',
'PYMODULE'),
('tkinter.constants',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\constants.py',
'PYMODULE'),
('tkinter.dialog',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\dialog.py',
'PYMODULE'),
('tkinter.filedialog',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\filedialog.py',
'PYMODULE'),
('tkinter.messagebox',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\messagebox.py',
'PYMODULE'),
('tkinter.simpledialog',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\simpledialog.py',
'PYMODULE'),
('tkinter.ttk',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tkinter\\ttk.py',
'PYMODULE'),
('token',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\token.py',
'PYMODULE'),
('tokenize',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tokenize.py',
'PYMODULE'),
('tracemalloc',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\tracemalloc.py',
'PYMODULE'),
('typing',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\typing.py',
'PYMODULE'),
('ui',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\__init__.py',
'PYMODULE'),
('ui.dialogs',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\__init__.py',
'PYMODULE'),
('ui.dialogs.calibration_dialog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\calibration_dialog.py',
'PYMODULE'),
('ui.dialogs.calibration_type_dialog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\calibration_type_dialog.py',
'PYMODULE'),
('ui.dialogs.celestial_dialog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\celestial_dialog.py',
'PYMODULE'),
('ui.dialogs.equipment_dialog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\equipment_dialog.py',
'PYMODULE'),
('ui.dialogs.instructions_dialog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\dialogs\\instructions_dialog.py',
'PYMODULE'),
('ui.main_window',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\ui\\main_window.py',
'PYMODULE'),
('urllib',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\urllib\\__init__.py',
'PYMODULE'),
('urllib.parse',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\urllib\\parse.py',
'PYMODULE'),
('watchdog',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\__init__.py',
'PYMODULE'),
('watchdog.events',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\events.py',
'PYMODULE'),
('watchdog.observers',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\__init__.py',
'PYMODULE'),
('watchdog.observers.api',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\api.py',
'PYMODULE'),
('watchdog.observers.fsevents',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\fsevents.py',
'PYMODULE'),
('watchdog.observers.inotify',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\inotify.py',
'PYMODULE'),
('watchdog.observers.inotify_buffer',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\inotify_buffer.py',
'PYMODULE'),
('watchdog.observers.inotify_c',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\inotify_c.py',
'PYMODULE'),
('watchdog.observers.kqueue',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\kqueue.py',
'PYMODULE'),
('watchdog.observers.polling',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\polling.py',
'PYMODULE'),
('watchdog.observers.read_directory_changes',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\read_directory_changes.py',
'PYMODULE'),
('watchdog.observers.winapi',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\observers\\winapi.py',
'PYMODULE'),
('watchdog.tricks',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\tricks\\__init__.py',
'PYMODULE'),
('watchdog.utils',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\__init__.py',
'PYMODULE'),
('watchdog.utils.bricks',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\bricks.py',
'PYMODULE'),
('watchdog.utils.delayed_queue',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\delayed_queue.py',
'PYMODULE'),
('watchdog.utils.dirsnapshot',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\dirsnapshot.py',
'PYMODULE'),
('watchdog.utils.echo',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\echo.py',
'PYMODULE'),
('watchdog.utils.event_debouncer',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\event_debouncer.py',
'PYMODULE'),
('watchdog.utils.patterns',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\patterns.py',
'PYMODULE'),
('watchdog.utils.platform',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\platform.py',
'PYMODULE'),
('watchdog.utils.process_watcher',
'C:\\Users\\Juliette\\PycharmProjects\\AstroSessionWatcher\\.venv\\Lib\\site-packages\\watchdog\\utils\\process_watcher.py',
'PYMODULE'),
('zipfile',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\zipfile\\__init__.py',
'PYMODULE'),
('zipfile._path',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\zipfile\\_path\\__init__.py',
'PYMODULE'),
('zipfile._path.glob',
'C:\\Program '
'Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3568.0_x64__qbz5n2kfra8p0\\Lib\\zipfile\\_path\\glob.py',
'PYMODULE')])

View file

@ -1,29 +0,0 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean these modules are required for running your program. Both
Python's standard library and 3rd-party Python packages often conditionally
import optional modules, some of which may be available only on certain
platforms.
Types of import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional)
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional)
missing module named 'collections.abc' - imported by traceback (top-level), typing (top-level), inspect (top-level), logging (top-level), importlib.resources.readers (top-level), selectors (top-level), tracemalloc (top-level), watchdog.utils.patterns (conditional), watchdog.events (conditional), watchdog.observers.inotify_c (conditional), watchdog.utils.dirsnapshot (conditional), watchdog.observers.kqueue (conditional), watchdog.observers.polling (conditional)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional)
missing module named resource - imported by posix (top-level)
missing module named _watchdog_fsevents - imported by watchdog.observers.fsevents (top-level)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _posixsubprocess - imported by subprocess (conditional)
missing module named fcntl - imported by subprocess (optional)

File diff suppressed because it is too large Load diff

View file

@ -1,40 +0,0 @@
# -*- 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', # Если есть иконка
)

View file

@ -1,16 +0,0 @@
[
"M31 (Andromeda Galaxy)",
"M42 (Orion Nebula)",
"M45 (Pleiades)",
"M57 (Ring Nebula)",
"Солнце",
"Moon",
"Jupiter",
"Сатурн",
"M89",
"Венера",
"Меркурий",
"Нептун",
"Saturn",
"NGC"
]

Binary file not shown.

View file

@ -1,12 +0,0 @@
{
"cameras": [
"ыфв"
],
"lenses": [
"фы"
],
"telescopes": [],
"last_watch_folder": "C:/Users/Juliette/Documents/prodTest",
"last_camera": "ыфв",
"last_lens": "фы"
}

View file

@ -1,11 +0,0 @@
[
"M31 (Andromeda Galaxy)",
"M42 (Orion Nebula)",
"M45 (Pleiades)",
"M57 (Ring Nebula)",
"Sun",
"Moon",
"Jupiter",
"Saturn",
"МКС"
]

22
main.py
View file

@ -1,22 +0,0 @@
"""
Astro Session Watcher v0.4.0 - tkinter версия
"""
import tkinter as tk
import sys
import os
# Добавляем корневую директорию в путь
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ui.main_window import MainWindow
def main():
root = tk.Tk()
app = MainWindow(root)
root.mainloop()
if __name__ == "__main__":
main()

View file

@ -1,4 +0,0 @@
from models.astro_object import AstroObject
from models.session import Session
__all__ = ['AstroObject', 'Session']

View file

@ -1,19 +0,0 @@
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})"

View file

@ -1,106 +0,0 @@
from dataclasses import dataclass, field
from typing import List, Dict
import json
from pathlib import Path
@dataclass
class ExposureProfile:
"""Профиль выдержки для определённого ISO"""
iso: int
exposure_seconds: int
dark_count: int = 20
flat_count: int = 30
bias_count: int = 50
@dataclass
class LensProfile:
"""Профиль объектива"""
name: str
aperture: str # например "f/2.8"
flat_duration_minutes: int = 10 # когда снимать Flat (рассвет/закат)
@dataclass
class CameraProfile:
"""Профиль камеры"""
name: str # "Canon EOS 600D"
sensor_type: str = "APS-C" # APS-C, Full Frame
pixel_size_um: float = 4.3
read_noise_e: float = 2.5
# Настройки по умолчанию
default_iso: int = 800
default_exposure: int = 120
# Профили выдержек
exposures: List[ExposureProfile] = field(default_factory=list)
# Объективы
lenses: List[LensProfile] = field(default_factory=list)
def save(self, config_service):
"""Сохраняет профиль в конфиг"""
config_service.save_camera_profile(self)
def to_dict(self) -> dict:
return {
'name': self.name,
'sensor_type': self.sensor_type,
'pixel_size_um': self.pixel_size_um,
'read_noise_e': self.read_noise_e,
'default_iso': self.default_iso,
'default_exposure': self.default_exposure,
'exposures': [
{
'iso': e.iso,
'exposure_seconds': e.exposure_seconds,
'dark_count': e.dark_count,
'flat_count': e.flat_count,
'bias_count': e.bias_count
}
for e in self.exposures
],
'lenses': [
{
'name': l.name,
'aperture': l.aperture,
'flat_duration_minutes': l.flat_duration_minutes
}
for l in self.lenses
]
}
@classmethod
def from_dict(cls, data: dict) -> 'CameraProfile':
exposures = [
ExposureProfile(
iso=e['iso'],
exposure_seconds=e['exposure_seconds'],
dark_count=e.get('dark_count', 20),
flat_count=e.get('flat_count', 30),
bias_count=e.get('bias_count', 50)
)
for e in data.get('exposures', [])
]
lenses = [
LensProfile(
name=l['name'],
aperture=l['aperture'],
flat_duration_minutes=l.get('flat_duration_minutes', 10)
)
for l in data.get('lenses', [])
]
return cls(
name=data['name'],
sensor_type=data.get('sensor_type', 'APS-C'),
pixel_size_um=data.get('pixel_size_um', 4.3),
read_noise_e=data.get('read_noise_e', 2.5),
default_iso=data.get('default_iso', 800),
default_exposure=data.get('default_exposure', 120),
exposures=exposures,
lenses=lenses
)

View file

@ -1,35 +0,0 @@
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
class EquipmentType(Enum):
LENS = "lens"
TELESCOPE = "telescope"
@dataclass
class Lens:
"""Объектив"""
name: str
min_aperture: float # например 1.8
max_aperture: float # например 22
focal_length: int # например 50
@dataclass
class Telescope:
"""Телескоп"""
name: str
aperture_ratio: float # f/5, f/7, f/10
focal_length: int # в мм
diameter: int # в мм
@dataclass
class Camera:
"""Камера"""
name: str
sensor_size: str # "APS-C", "Full Frame", "4/3"
pixel_size_um: float = 4.3
default_iso: int = 800

View file

@ -1,31 +0,0 @@
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

View file

@ -1,2 +0,0 @@
PySide6>=6.5.0
watchdog>=3.0.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1,6 +0,0 @@
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']

View file

@ -1,113 +0,0 @@
"""
CalibrationService - управление съёмкой калибровочных кадров
"""
from pathlib import Path
from datetime import datetime
from typing import Optional, Callable
from PySide6.QtCore import QObject, Signal
from services.file_service import FileService
from services.watch_service import WatchService
class CalibrationService(QObject):
"""Сервис для съёмки калибровочных кадров с авто-остановкой"""
# Сигналы для UI
progress_updated = Signal(int, int) # (current, target)
capture_completed = Signal(str) # тип кадра
capture_error = Signal(str)
def __init__(self):
super().__init__()
self._watch_service = WatchService()
self._target_count = 0
self._current_count = 0
self._calibration_type = None
self._target_folder = None
self._stop_requested = False
def start_calibration(self, calibration_type: str, target_folder: Path,
camera_name: str, target_count: int,
on_file_received: Callable) -> bool:
"""
Начинает съёмку калибровочных кадров
calibration_type: 'bias', 'dark', 'flat'
"""
self._calibration_type = calibration_type
self._target_count = target_count
self._current_count = 0
self._stop_requested = False
# Создаём папку назначения
self._target_folder = target_folder
self._target_folder.mkdir(parents=True, exist_ok=True)
# Очищаем папку наблюдения от старых файлов
watch_folder = self._get_watch_folder()
if watch_folder:
FileService.clear_watch_folder(watch_folder)
# Запускаем отслеживание
def on_file(file_path: Path):
if self._stop_requested:
return
# Обрабатываем файл
if self._process_calibration_file(file_path, camera_name):
self._current_count += 1
self.progress_updated.emit(self._current_count, self._target_count)
if self._current_count >= self._target_count:
self.stop_calibration()
self.capture_completed.emit(calibration_type)
if on_file_received:
on_file_received(file_path)
watch_path = Path(".") # Нужно получить реальный путь
return self._watch_service.start(watch_path, on_file)
def _process_calibration_file(self, file_path: Path, camera_name: str) -> bool:
"""Обрабатывает калибровочный файл"""
if not FileService.is_photo(file_path):
return False
# Генерируем имя файла с типом кадра
timestamp = datetime.now()
suffix = file_path.suffix
type_prefix = {
'bias': 'Bias',
'dark': 'Dark',
'flat': 'Flat'
}.get(self._calibration_type, 'Calib')
new_filename = f"{type_prefix}_{camera_name}_{timestamp.strftime('%Y%m%d_%H%M%S')}{suffix}"
target_path = self._target_folder / new_filename
target_path = FileService.resolve_conflict(target_path)
try:
import shutil
shutil.move(str(file_path), str(target_path))
print(f"Калибровочный кадр сохранён: {target_path.name}")
return True
except Exception as e:
print(f"Ошибка сохранения кадра: {e}")
return False
def stop_calibration(self):
"""Останавливает съёмку калибровочных кадров"""
self._stop_requested = True
self._watch_service.stop()
def _get_watch_folder(self) -> Optional[Path]:
"""Возвращает папку наблюдения (нужно из main_window)"""
# TODO: Получить из главного окна
return None
def get_progress(self) -> tuple:
"""Возвращает прогресс (current, target)"""
return (self._current_count, self._target_count)

View file

@ -1,194 +0,0 @@
"""
ConfigService - управление настройками приложения
"""
import json
import os
from typing import List, Optional
class ConfigService:
"""Сервис для работы с конфигурацией"""
SETTINGS_FILE = "astro_settings.json"
CELESTIAL_BODIES_FILE = "celestial_bodies.json"
def __init__(self):
self.config = {
'cameras': [],
'lenses': [],
'telescopes': [],
'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['telescopes'] = data.get('telescopes', [])
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'],
'telescopes': self.config['telescopes'],
'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 update_lens(self, old_name: str, new_name: str):
if old_name in self.config['lenses']:
idx = self.config['lenses'].index(old_name)
self.config['lenses'][idx] = new_name
self.save_settings()
# ===== Методы для работы с телескопами =====
def get_telescopes(self) -> List[str]:
return self.config.get('telescopes', []).copy()
def add_telescope(self, telescope: str):
if 'telescopes' not in self.config:
self.config['telescopes'] = []
if telescope and telescope not in self.config['telescopes']:
self.config['telescopes'].append(telescope)
self.save_settings()
def remove_telescope(self, telescope: str):
if 'telescopes' in self.config and telescope in self.config['telescopes']:
self.config['telescopes'].remove(telescope)
self.save_settings()
def update_telescope(self, old_name: str, new_name: str):
if 'telescopes' in self.config and old_name in self.config['telescopes']:
idx = self.config['telescopes'].index(old_name)
self.config['telescopes'][idx] = new_name
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.get('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.get('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.get('last_lens', '')
def set_last_lens(self, lens: str):
self.config['last_lens'] = lens
self.save_settings()

View file

@ -1,126 +0,0 @@
"""
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 generate_new_filename(cls, object_name: str, timestamp: datetime, original_suffix: str) -> str:
"""
Генерирует новое имя файла в формате:
ИмяОбъекта_ГГГГ-ММ-ДД_ЧЧ-ММ-СС.расширение
"""
# Очищаем имя объекта от недопустимых символов
safe_object_name = "".join(c for c in object_name if c.isalnum() or c in (' ', '-', '_')).strip()
safe_object_name = safe_object_name.replace(' ', '_')
# Форматируем дату и время
date_str = timestamp.strftime("%Y-%m-%d")
time_str = timestamp.strftime("%H-%M-%S")
return f"{safe_object_name}_{date_str}_{time_str}{original_suffix}"
@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,
object_name: str = None) -> bool:
"""
Перемещает файл в целевую папку с переименованием
Если object_name указан, файл переименовывается в формат: ИмяОбъектаатаремя.расширение
"""
if not source.exists():
print(f"Файл не существует: {source}")
return False
if not cls.is_photo(source):
print(f"Неподдерживаемый формат: {source}")
return False
try:
target_folder.mkdir(parents=True, exist_ok=True)
# Получаем время создания файла
creation_time = datetime.fromtimestamp(source.stat().st_ctime)
# Генерируем новое имя файла, если указано имя объекта
if object_name:
new_filename = cls.generate_new_filename(object_name, creation_time, source.suffix)
else:
new_filename = source.name
# Записываем в лог (сохраняем оригинальное имя в логе для отслеживания)
cls.write_object_log(target_folder, f"{source.name} -> {new_filename}", camera, optics, creation_time)
# Формируем целевой путь
target_path = cls.resolve_conflict(target_folder / new_filename)
# Перемещаем файл
shutil.move(str(source), str(target_path))
print(f"Файл перемещён и переименован: {source.name} -> {target_path.name}")
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

View file

@ -1,159 +0,0 @@
"""
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,
current_object.name # Добавляем имя объекта для переименования
)
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):
success = self._file_service.move_file(
file_path,
current_object.folder,
self._current_session.camera,
self._current_session.optics,
current_object.name # Добавляем имя объекта для переименования
)
if success:
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()

View file

@ -1,119 +0,0 @@
"""
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):
print(f"[Watchdog] Обнаружен файл: {src_path}")
time.sleep(0.1)
self._pending_files.put(src_path)
def on_modified(self, event):
"""Также обрабатываем modified, так как некоторые программы сначала создают временный файл"""
if not event.is_directory:
src_path = Path(event.src_path)
if FileService.is_photo(src_path):
print(f"[Watchdog] Изменён файл: {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:
print("Watcher already running")
return False
if not watch_folder.exists():
print(f"Папка не существует: {watch_folder}")
return False
try:
print(f"Запуск отслеживания папки: {watch_folder}")
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
print(f"Отслеживание успешно запущено для: {watch_folder}")
return True
except Exception as e:
print(f"Ошибка запуска отслеживания: {e}")
import traceback
traceback.print_exc()
return False
def stop(self):
"""Останавливает отслеживание"""
print("Остановка отслеживания...")
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
print("Отслеживание остановлено")
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

View file

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

View file

@ -1,8 +0,0 @@
from ui.dialogs.equipment_dialog import EquipmentDialog
from ui.dialogs.celestial_dialog import CelestialDialog
from ui.dialogs.instructions_dialog import InstructionsDialog
from ui.dialogs.calibration_dialog import CalibrationDialog
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
__all__ = ['EquipmentDialog', 'CelestialDialog', 'InstructionsDialog',
'CalibrationDialog', 'CalibrationTypeDialog']

View file

@ -1,180 +0,0 @@
"""
CalibrationDialog - главный диалог калибровки (tkinter)
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from pathlib import Path
class CalibrationDialog(tk.Toplevel):
"""Главное окно калибровки"""
def __init__(self, parent, config_service):
super().__init__(parent)
self.parent = parent
self.config_service = config_service
self._blink_active = False
self._blink_after_id = None
self.title("Calibration Frames")
self.geometry("650x500")
self.minsize(600, 450)
self.transient(parent)
self.grab_set()
self._create_ui()
self._load_saved_settings()
self._check_folder_path()
self._center_window()
def _create_ui(self):
# Main frame
main_frame = ttk.Frame(self, padding="25")
main_frame.pack(fill='both', expand=True)
# Title
title_label = ttk.Label(main_frame, text="Calibration Frames", font=('Segoe UI', 18, 'bold'))
title_label.pack(pady=(0, 15))
# Separator
ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=(0, 15))
# Camera selection
camera_frame = ttk.Frame(main_frame)
camera_frame.pack(fill='x', pady=5)
ttk.Label(camera_frame, text="Camera:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10))
self.camera_combo = ttk.Combobox(camera_frame, width=30)
self.camera_combo.pack(side='left', fill='x', expand=True)
# Folder selection
folder_frame = ttk.Frame(main_frame)
folder_frame.pack(fill='x', pady=5)
ttk.Label(folder_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).pack(side='left', padx=(0, 10))
folder_input_frame = ttk.Frame(folder_frame)
folder_input_frame.pack(side='left', fill='x', expand=True)
self.folder_entry = ttk.Entry(folder_input_frame)
self.folder_entry.pack(side='left', fill='x', expand=True, padx=(0, 10))
self.browse_btn = tk.Button(folder_input_frame, text="Browse...", width=10,
bg='#3c3c3c', fg='#e0e0e0', activebackground='#4c4c4c',
relief='raised', borderwidth=1)
self.browse_btn.config(command=self._browse_folder)
self.browse_btn.pack(side='right')
# Separator
ttk.Separator(main_frame, orient='horizontal').pack(fill='x', pady=15)
# Type buttons
types_frame = ttk.Frame(main_frame)
types_frame.pack(pady=10)
self.bias_btn = tk.Button(types_frame, text="BIAS", font=('Segoe UI', 12, 'bold'),
bg='#2196F3', fg='white', activebackground='#1976D2',
width=12, height=2,
command=lambda: self._open_calibration_type('bias'))
self.bias_btn.pack(side='left', padx=10)
self.dark_btn = tk.Button(types_frame, text="DARK", font=('Segoe UI', 12, 'bold'),
bg='#9C27B0', fg='white', activebackground='#7B1FA2',
width=12, height=2,
command=lambda: self._open_calibration_type('dark'))
self.dark_btn.pack(side='left', padx=10)
self.flat_btn = tk.Button(types_frame, text="FLAT", font=('Segoe UI', 12, 'bold'),
bg='#4CAF50', fg='white', activebackground='#388E3C',
width=12, height=2,
command=lambda: self._open_calibration_type('flat'))
self.flat_btn.pack(side='left', padx=10)
# Tips frame
tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1)
tips_frame.pack(fill='x', pady=15, padx=10)
tk.Label(tips_frame, text="Tips:", font=('Segoe UI', 10, 'bold'),
bg='#2d2d2d', fg='#FFD700').pack(anchor='w', padx=10, pady=(10, 5))
self.tips_label = tk.Label(tips_frame,
text="• BIAS can be taken once a month (at home)\n• DARK must be taken on site at the same temperature\n• FLAT must be taken after session without changing focus",
bg='#2d2d2d', fg='#e0e0e0', justify='left', font=('Segoe UI', 9))
self.tips_label.pack(anchor='w', padx=10, pady=(0, 10))
# Cancel button
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Cancel", command=self.destroy).pack()
def _load_saved_settings(self):
cameras = self.config_service.get_cameras()
if cameras:
self.camera_combo['values'] = cameras
last_camera = self.config_service.get_last_camera()
if last_camera and last_camera in cameras:
self.camera_combo.set(last_camera)
def _browse_folder(self):
folder = filedialog.askdirectory(title="Select folder for calibration frames")
if folder:
self.folder_entry.delete(0, tk.END)
self.folder_entry.insert(0, folder)
self._stop_blinking()
self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0')
def _check_folder_path(self):
if not self.folder_entry.get():
self._start_blinking()
else:
self._stop_blinking()
def _start_blinking(self):
self._blink_active = True
def blink():
if not self._blink_active:
return
if self.browse_btn.cget('bg') == '#3c3c3c':
self.browse_btn.config(bg='#f44336', fg='white')
else:
self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0')
self._blink_after_id = self.after(1500, blink)
blink()
def _stop_blinking(self):
self._blink_active = False
if self._blink_after_id:
self.after_cancel(self._blink_after_id)
self._blink_after_id = None
self.browse_btn.config(bg='#3c3c3c', fg='#e0e0e0')
def _center_window(self):
self.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f'+{x}+{y}')
def _open_calibration_type(self, cal_type):
if not self.folder_entry.get():
messagebox.showwarning("Warning", "Please select a folder to save calibration frames!", parent=self)
self._start_blinking()
return
camera_name = self.camera_combo.get()
if not camera_name:
messagebox.showwarning("Warning", "Please enter or select a camera name!", parent=self)
return
from ui.dialogs.calibration_type_dialog import CalibrationTypeDialog
dialog = CalibrationTypeDialog(
self,
cal_type,
self.folder_entry.get(),
camera_name,
self.config_service
)
self.wait_window(dialog)

View file

@ -1,574 +0,0 @@
"""
CalibrationTypeDialog - диалог для конкретного типа калибровки (tkinter)
"""
import tkinter as tk
from tkinter import ttk, messagebox
import shutil
import re
from pathlib import Path
from datetime import datetime
from services.file_service import FileService
from services.watch_service import WatchService
class CalibrationTypeDialog(tk.Toplevel):
"""Диалог для съёмки калибровочных кадров определённого типа"""
def __init__(self, parent, cal_type, base_folder, camera_name, config_service):
super().__init__(parent)
self.parent = parent
self.cal_type = cal_type
self.base_folder = Path(base_folder)
self.camera_name = camera_name
self.config_service = config_service
self.is_capturing = False
self.current_count = 0
self.target_count = 0
self._watch_service = None
self.settings = self._get_default_settings()
self.title(self._get_title())
self.geometry("600x650")
self.minsize(550, 600)
self.transient(parent)
self.grab_set()
self._create_ui()
self._load_optics()
self._update_recommendations()
self._center_window()
def _get_title(self):
titles = {
'bias': 'BIAS (Bias Frames)',
'dark': 'DARK (Dark Frames)',
'flat': 'FLAT (Flat Fields)'
}
return titles.get(self.cal_type, 'Calibration Frames')
def _get_default_settings(self):
base = {
'bias': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'count': 50,
'min_count': 30,
'max_count': 100,
'recommended_count': 50,
},
'dark': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'exposure_values': [30, 60, 120, 180, 300],
'default_exposure': 120,
'count': 20,
'min_count': 10,
'max_count': 50,
'recommended_count': 20,
},
'flat': {
'iso_values': [800, 1600, 3200],
'default_iso': 800,
'aperture_values': ['f/2.8', 'f/4', 'f/5.6', 'f/8'],
'count': 30,
'min_count': 20,
'max_count': 60,
'recommended_count': 30,
}
}
return base.get(self.cal_type, {})
def _create_ui(self):
# Main frame
main_frame = ttk.Frame(self, padding="20")
main_frame.pack(fill='both', expand=True)
# Title with help button
title_frame = ttk.Frame(main_frame)
title_frame.pack(fill='x', pady=(0, 10))
ttk.Label(title_frame, text=self._get_title(), font=('Segoe UI', 16, 'bold')).pack(side='left')
help_btn = ttk.Button(title_frame, text="?", width=3, command=self._show_help)
help_btn.pack(side='right')
# Parameters frame
params_frame = ttk.LabelFrame(main_frame, text="Camera Settings", padding="10")
params_frame.pack(fill='x', pady=10)
# ISO
iso_row = ttk.Frame(params_frame)
iso_row.pack(fill='x', pady=5)
ttk.Label(iso_row, text="ISO:", width=15).pack(side='left')
self.iso_combo = ttk.Combobox(iso_row, values=self.settings['iso_values'], width=10)
self.iso_combo.set(str(self.settings['default_iso']))
self.iso_combo.pack(side='left', padx=5)
self.iso_combo.bind('<<ComboboxSelected>>', lambda e: self._update_recommendations())
ttk.Button(iso_row, text="Custom", width=8, command=self._add_custom_iso).pack(side='left', padx=5)
# Exposure (only for DARK)
if self.cal_type == 'dark':
exp_row = ttk.Frame(params_frame)
exp_row.pack(fill='x', pady=5)
ttk.Label(exp_row, text="Exposure (sec):", width=15).pack(side='left')
self.exposure_combo = ttk.Combobox(exp_row, values=self.settings['exposure_values'], width=10)
self.exposure_combo.set(str(self.settings['default_exposure']))
self.exposure_combo.pack(side='left', padx=5)
self.exposure_combo.bind('<<ComboboxSelected>>', lambda e: self._update_recommendations())
ttk.Button(exp_row, text="Custom", width=8, command=self._add_custom_exposure).pack(side='left', padx=5)
# Optics (only for FLAT)
if self.cal_type == 'flat':
optics_row = ttk.Frame(params_frame)
optics_row.pack(fill='x', pady=5)
ttk.Label(optics_row, text="Optics:", width=15).pack(side='left')
self.optics_combo = ttk.Combobox(optics_row, width=30)
self.optics_combo.pack(side='left', fill='x', expand=True, padx=5)
aperture_row = ttk.Frame(params_frame)
aperture_row.pack(fill='x', pady=5)
ttk.Label(aperture_row, text="Aperture:", width=15).pack(side='left')
self.aperture_combo = ttk.Combobox(aperture_row, values=self.settings['aperture_values'], width=10)
self.aperture_combo.set('f/5.6')
self.aperture_combo.pack(side='left', padx=5)
ttk.Label(aperture_row, text="(Fixed for telescopes)", foreground='#888888').pack(side='left', padx=10)
# Count
count_row = ttk.Frame(params_frame)
count_row.pack(fill='x', pady=5)
ttk.Label(count_row, text="Number of frames:", width=15).pack(side='left')
self.count_spin = tk.Spinbox(count_row, from_=self.settings['min_count'], to=self.settings['max_count'],
width=8, font=('Segoe UI', 10))
self.count_spin.delete(0, 'end')
self.count_spin.insert(0, str(self.settings['count']))
self.count_spin.pack(side='left', padx=5)
ttk.Label(count_row, text=f"(recommended: {self.settings['recommended_count']})", foreground='#888888').pack(side='left', padx=5)
# Recommendations frame
tips_frame = tk.Frame(main_frame, bg='#2d2d2d', relief='groove', bd=1)
tips_frame.pack(fill='x', pady=10)
self.tips_text = tk.Text(tips_frame, height=12, wrap='word', bg='#2d2d2d', fg='#FFD700',
font=('Segoe UI', 9), relief='flat', padx=10, pady=10)
self.tips_text.pack(fill='both', expand=True)
# Progress frame
self.progress_frame = ttk.LabelFrame(main_frame, text="Progress", padding="10")
self.progress_bar = ttk.Progressbar(self.progress_frame, orient='horizontal', length=400, mode='determinate')
self.progress_bar.pack(pady=5)
self.progress_label = ttk.Label(self.progress_frame, text="Ready to shoot")
self.progress_label.pack()
# Save path
save_frame = ttk.Frame(main_frame)
save_frame.pack(fill='x', pady=10)
ttk.Label(save_frame, text="Save to:", font=('Segoe UI', 9, 'bold')).pack(anchor='w')
self.save_path_label = ttk.Label(save_frame, foreground='#4CAF50')
self.save_path_label.pack(anchor='w')
# Buttons
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=10)
self.back_btn = ttk.Button(btn_frame, text="Back", command=self._on_back)
self.back_btn.pack(side='left', padx=5)
self.start_btn = ttk.Button(btn_frame, text="Start Shooting", command=self._start_capture, style='Green.TButton')
self.start_btn.pack(side='left', padx=5)
self.stop_btn = ttk.Button(btn_frame, text="Stop", command=self._on_stop, style='Red.TButton')
self.stop_btn.pack(side='left', padx=5)
self.stop_btn.config(state='disabled')
self._update_save_path()
def _load_optics(self):
if self.cal_type != 'flat':
return
lenses = self.config_service.get_lenses()
telescopes = self.config_service.get_telescopes()
all_optics = lenses + telescopes
self.optics_combo['values'] = all_optics
if all_optics:
self.optics_combo.set(all_optics[0])
def on_optics_change(*args):
current = self.optics_combo.get()
if 'f/' in current:
self.aperture_combo.config(state='disabled')
match = re.search(r'f/(\d+\.?\d*)', current)
if match:
self.aperture_combo.set(f"f/{match.group(1)}")
else:
self.aperture_combo.config(state='normal')
self.optics_combo.bind('<<ComboboxSelected>>', on_optics_change)
def _update_save_path(self):
iso = int(self.iso_combo.get())
if self.cal_type == 'bias':
path = self.base_folder / "Calibration" / self.camera_name / "Bias" / f"ISO{iso}"
elif self.cal_type == 'dark':
exposure = self.exposure_combo.get()
path = self.base_folder / "Calibration" / self.camera_name / "Dark" / f"ISO{iso}_{exposure}s"
elif self.cal_type == 'flat':
optics = self.optics_combo.get()
optics_name = optics
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
optics_name = optics_name.replace(char, '_')
date_str = datetime.now().strftime("%Y-%m-%d")
path = self.base_folder / "Calibration" / self.camera_name / "Flat" / optics_name / date_str
else:
path = self.base_folder
self.save_path_label.config(text=str(path))
return path
def _update_recommendations(self):
self.tips_text.config(state='normal')
self.tips_text.delete('1.0', 'end')
if self.cal_type == 'bias':
self.tips_text.insert('1.0',
"BIAS (Bias Frames)\n\n"
"HOW TO SHOOT:\n"
"• Close lens cap\n"
"• Shutter speed: FASTEST (1/4000 or 1/8000)\n"
"• ISO: same as light frames\n\n"
"TIPS:\n"
"• Can be taken at home anytime\n"
"• Works for all lenses/telescopes\n"
"• 50 frames recommended")
elif self.cal_type == 'dark':
self.tips_text.insert('1.0',
"DARK (Dark Frames)\n\n"
"IMPORTANT: Shoot AFTER session on site!\n\n"
"HOW TO SHOOT:\n"
"• Close lens cap\n"
"• SAME ISO and exposure as light frames\n"
"• Wait for camera to reach night temperature\n\n"
"TEMPERATURE:\n"
"• Shoot at the SAME temperature as light frames\n"
"• Difference >5C makes darks useless!\n"
"• Best taken immediately after session")
elif self.cal_type == 'flat':
self.tips_text.insert('1.0',
"FLAT (Flat Fields)\n\n"
"IMPORTANT: DON'T change focus or zoom!\n\n"
"HOW TO SHOOT:\n"
"• Method 1: LED panel (recommended)\n"
"• Method 2: Dawn/dusk sky, point at zenith\n"
"• Method 3: White T-shirt over lens\n\n"
"GOAL:\n"
"• Remove vignetting and dust\n"
"• Histogram at 50-70%\n"
"• 30 frames recommended")
self.tips_text.config(state='disabled')
self._update_save_path()
def _start_capture(self):
self.target_count = int(self.count_spin.get())
self.current_count = 0
target_folder = self._update_save_path()
try:
target_folder.mkdir(parents=True, exist_ok=True)
except Exception as e:
messagebox.showerror("Error", f"Failed to create folder:\n{target_folder}\n\n{str(e)}", parent=self)
return
# Show progress frame
self.progress_frame.pack(fill='x', pady=10)
self.progress_bar['maximum'] = self.target_count
self.progress_bar['value'] = 0
self.progress_label.config(text=f"0 / {self.target_count} frames")
# Disable controls
self.start_btn.config(state='disabled')
self.stop_btn.config(state='normal')
self.back_btn.config(state='disabled')
self.iso_combo.config(state='disabled')
self.count_spin.config(state='disabled')
if hasattr(self, 'exposure_combo'):
self.exposure_combo.config(state='disabled')
if hasattr(self, 'optics_combo'):
self.optics_combo.config(state='disabled')
self.aperture_combo.config(state='disabled')
self.is_capturing = True
# Get watch folder from main window
watch_folder = self._get_watch_folder()
if not watch_folder:
messagebox.showerror("Error", "Could not determine watch folder!\nPlease select a folder in the main window.", parent=self)
self._stop_capture()
return
if not watch_folder.exists():
messagebox.showerror("Error", f"Watch folder does not exist:\n{watch_folder}", parent=self)
self._stop_capture()
return
FileService.clear_watch_folder(watch_folder)
self._watch_service = WatchService()
def on_file_received(file_path):
if not self.is_capturing:
return
if self._process_calibration_file(file_path, target_folder):
self.current_count += 1
self.after(0, self._update_progress)
if self.current_count >= self.target_count:
self.after(0, self._stop_capture)
self.after(0, lambda: messagebox.showinfo("Success", f"Capture completed!\nSaved {self.current_count} frames to:\n{target_folder}", parent=self))
self.after(100, lambda: self.progress_frame.pack_forget())
success = self._watch_service.start(watch_folder, on_file_received)
if not success:
messagebox.showerror("Error", "Failed to start watching folder!", parent=self)
self._stop_capture()
return
self.progress_label.config(text=f"Watching: {watch_folder}\nWaiting for files...")
def _update_progress(self):
self.progress_bar['value'] = self.current_count
self.progress_label.config(text=f"{self.current_count} / {self.target_count} frames")
def _process_calibration_file(self, file_path, target_folder):
if not FileService.is_photo(file_path):
return False
try:
target_folder.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now()
date_str = timestamp.strftime("%Y-%m-%d")
time_str = timestamp.strftime("%H-%M-%S")
suffix = file_path.suffix
if self.cal_type == 'bias':
iso = self.iso_combo.get()
prefix = f"Bias_{self.camera_name}_ISO{iso}"
elif self.cal_type == 'dark':
iso = self.iso_combo.get()
exposure = self.exposure_combo.get()
prefix = f"Dark_{self.camera_name}_ISO{iso}_{exposure}s"
elif self.cal_type == 'flat':
optics = self.optics_combo.get()
optics_name = optics
invalid_chars = '<>:"/\\|?*'
for char in invalid_chars:
optics_name = optics_name.replace(char, '_')
aperture = self.aperture_combo.get()
prefix = f"Flat_{optics_name}_{aperture}"
else:
prefix = "Calibration"
for char in '<>:"/\\|?*':
prefix = prefix.replace(char, '_')
new_filename = f"{prefix}_{date_str}_{time_str}{suffix}"
target_path = target_folder / new_filename
target_path = FileService.resolve_conflict(target_path)
shutil.move(str(file_path), str(target_path))
return True
except Exception as e:
print(f"Error saving {file_path.name}: {e}")
return False
def _stop_capture(self):
self.is_capturing = False
if self._watch_service:
self._watch_service.stop()
self._watch_service = None
self.start_btn.config(state='normal')
self.stop_btn.config(state='disabled')
self.back_btn.config(state='normal')
self.iso_combo.config(state='normal')
self.count_spin.config(state='normal')
if hasattr(self, 'exposure_combo'):
self.exposure_combo.config(state='normal')
if hasattr(self, 'optics_combo'):
self.optics_combo.config(state='normal')
self.aperture_combo.config(state='normal')
self.progress_label.config(text="Capture stopped")
def _on_back(self):
if self.is_capturing:
messagebox.showwarning("Warning", "Please stop the capture first!", parent=self)
return
self.destroy()
def _on_stop(self):
if self.current_count < self.target_count and self.current_count > 0:
reply = messagebox.askyesno("Stop Capture",
f"You haven't finished capture ({self.current_count} of {self.target_count} frames).\n"
f"Are you sure you want to stop?",
parent=self)
if reply:
self._stop_capture()
elif self.current_count == 0:
reply = messagebox.askyesno("Stop Capture",
"Capture hasn't started yet. Are you sure?",
parent=self)
if reply:
self._stop_capture()
else:
self._stop_capture()
def _get_watch_folder(self):
# Traverse to find main window's folder_entry
parent = self.parent
while parent:
if hasattr(parent, 'folder_entry'):
watch_folder = parent.folder_entry.get()
if watch_folder and watch_folder != "Select watch folder...":
return Path(watch_folder)
parent = parent.master if hasattr(parent, 'master') else parent.parent if hasattr(parent, 'parent') else None
return None
def _add_custom_iso(self):
dialog = tk.Toplevel(self)
dialog.title("Custom ISO")
dialog.geometry("300x100")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Enter ISO value:").pack(pady=10)
entry = ttk.Entry(dialog)
entry.pack(pady=5)
entry.focus()
def save():
try:
value = int(entry.get())
if 100 <= value <= 12800:
iso_str = str(value)
values = list(self.iso_combo['values'])
if iso_str not in values:
values.append(iso_str)
values.sort(key=int)
self.iso_combo['values'] = values
self.iso_combo.set(iso_str)
dialog.destroy()
else:
messagebox.showerror("Error", "ISO must be between 100 and 12800", parent=dialog)
except ValueError:
messagebox.showerror("Error", "Please enter a valid number", parent=dialog)
ttk.Button(dialog, text="OK", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _add_custom_exposure(self):
dialog = tk.Toplevel(self)
dialog.title("Custom Exposure")
dialog.geometry("300x100")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Enter exposure (seconds):").pack(pady=10)
entry = ttk.Entry(dialog)
entry.pack(pady=5)
entry.focus()
def save():
try:
value = int(entry.get())
if 1 <= value <= 3600:
exp_str = str(value)
values = list(self.exposure_combo['values'])
if exp_str not in values:
values.append(exp_str)
values.sort(key=int)
self.exposure_combo['values'] = values
self.exposure_combo.set(exp_str)
dialog.destroy()
else:
messagebox.showerror("Error", "Exposure must be between 1 and 3600 seconds", parent=dialog)
except ValueError:
messagebox.showerror("Error", "Please enter a valid number", parent=dialog)
ttk.Button(dialog, text="OK", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _show_help(self):
if self.cal_type == 'bias':
help_text = (
"What are BIAS frames?\n\n"
"Bias frames are shots with the lens cap closed\n"
"at the shortest possible shutter speed.\n\n"
"Why they are needed:\n"
"• Remove read noise\n"
"• Correct black level offset\n\n"
"How many to take:\n"
"• 50 frames for good averaging\n"
"• Can be used for a whole month\n\n"
"When to take:\n"
"• At home anytime\n"
"• Temperature doesn't matter"
)
elif self.cal_type == 'dark':
help_text = (
"What are DARK frames?\n\n"
"Dark frames are shots with the lens cap closed\n"
"with the SAME ISO and exposure as light frames.\n\n"
"Why they are needed:\n"
"• Remove thermal noise\n"
"• Remove hot pixels\n\n"
"IMPORTANT about temperature:\n"
"• Take AFTER the session on site!\n"
"• Camera must be at the same temperature\n"
"• Difference >5C makes darks useless!"
)
else:
help_text = (
"What are FLAT frames?\n\n"
"Flat frames are shots of an evenly lit surface\n"
"with the SAME focus and zoom.\n\n"
"Why they are needed:\n"
"• Remove lens vignetting\n"
"• Remove dust on sensor/lens\n\n"
"How to shoot:\n"
"1. LED panel (best option)\n"
"2. Dawn/dusk sky, point at zenith\n"
"3. White T-shirt over lens\n\n"
"IMPORTANT:\n"
"• DON'T change focus!\n"
"• DON'T change zoom!\n"
"• Take at the end of the session"
)
messagebox.showinfo("Help", help_text, parent=self)
def _center_window(self):
self.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f'+{x}+{y}')

View file

@ -1,153 +0,0 @@
"""
CelestialDialog - диалог управления небесными телами (tkinter)
"""
import tkinter as tk
from tkinter import ttk, messagebox
class CelestialDialog(tk.Toplevel):
"""Диалог для управления списком небесных тел"""
def __init__(self, parent, config_service):
super().__init__(parent)
self.parent = parent
self.config_service = config_service
self.celestial_bodies = self.config_service.get_celestial_bodies()
self.selected_body = None
self.title("Celestial Bodies")
self.geometry("500x550")
self.minsize(450, 500)
self.transient(parent)
self.grab_set()
self._create_ui()
self._center_window()
def _create_ui(self):
# Main frame
main_frame = ttk.Frame(self, padding="20")
main_frame.pack(fill='both', expand=True)
# Title
ttk.Label(main_frame, text="Celestial Bodies", font=('Segoe UI', 14, 'bold')).pack(pady=(0, 10))
ttk.Label(main_frame, text="List of observation targets", font=('Segoe UI', 10)).pack(pady=(0, 15))
# Listbox with scrollbar
list_frame = ttk.Frame(main_frame)
list_frame.pack(fill='both', expand=True, pady=(0, 10))
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side='right', fill='y')
self.bodies_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set, height=15,
bg='#2d2d2d', fg='#e0e0e0',
selectbackground='#4CAF50', selectforeground='white',
font=('Segoe UI', 10))
self.bodies_listbox.pack(fill='both', expand=True)
scrollbar.config(command=self.bodies_listbox.yview)
for body in self.celestial_bodies:
self.bodies_listbox.insert('end', body)
self.bodies_listbox.bind('<<ListboxSelect>>', self._on_body_select)
# Add new body
add_frame = ttk.Frame(main_frame)
add_frame.pack(fill='x', pady=(0, 10))
self.new_body_entry = ttk.Entry(add_frame)
self.new_body_entry.pack(side='left', fill='x', expand=True, padx=(0, 10))
ttk.Button(add_frame, text="Add", command=self._add_celestial_body).pack(side='right')
# Buttons
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(pady=(0, 10))
self.remove_btn = ttk.Button(btn_frame, text="Remove Selected", command=self._remove_celestial_body, state='disabled')
self.remove_btn.pack(side='left', padx=5)
self.edit_btn = ttk.Button(btn_frame, text="Edit Selected", command=self._edit_celestial_body, state='disabled')
self.edit_btn.pack(side='left', padx=5)
# Close button
ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10)
def _center_window(self):
self.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f'+{x}+{y}')
def _on_body_select(self, event):
selection = self.bodies_listbox.curselection()
if selection:
self.selected_body = self.bodies_listbox.get(selection[0])
self.remove_btn.config(state='normal')
self.edit_btn.config(state='normal')
def _add_celestial_body(self):
new_body = self.new_body_entry.get().strip()
if not new_body:
messagebox.showwarning("Warning", "Please enter object name!", parent=self)
return
if new_body in self.celestial_bodies:
messagebox.showwarning("Warning", f"Object '{new_body}' already exists!", parent=self)
return
self.celestial_bodies.append(new_body)
self.config_service.add_celestial_body(new_body)
self.bodies_listbox.insert('end', new_body)
self.new_body_entry.delete(0, 'end')
messagebox.showinfo("Success", f"Object '{new_body}' added", parent=self)
def _remove_celestial_body(self):
if self.selected_body:
reply = messagebox.askyesno("Remove Object", f"Remove '{self.selected_body}'?", parent=self)
if reply:
self.celestial_bodies.remove(self.selected_body)
self.config_service.remove_celestial_body(self.selected_body)
self.bodies_listbox.delete(0, 'end')
for body in self.celestial_bodies:
self.bodies_listbox.insert('end', body)
self.selected_body = None
self.remove_btn.config(state='disabled')
self.edit_btn.config(state='disabled')
def _edit_celestial_body(self):
if self.selected_body:
dialog = tk.Toplevel(self)
dialog.title("Edit Celestial Body")
dialog.geometry("350x120")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="New name:", font=('Segoe UI', 10)).pack(pady=15)
entry = ttk.Entry(dialog, width=40)
entry.insert(0, self.selected_body)
entry.pack(pady=5)
entry.focus()
def save():
new_name = entry.get().strip()
if new_name and new_name != self.selected_body:
if new_name in self.celestial_bodies:
messagebox.showerror("Error", f"Object '{new_name}' already exists!", parent=dialog)
return
idx = self.celestial_bodies.index(self.selected_body)
self.celestial_bodies[idx] = new_name
self.config_service.update_celestial_body(self.selected_body, new_name)
self.bodies_listbox.delete(0, 'end')
for body in self.celestial_bodies:
self.bodies_listbox.insert('end', body)
dialog.destroy()
elif new_name == self.selected_body:
dialog.destroy()
else:
messagebox.showwarning("Warning", "Please enter a name!", parent=dialog)
ttk.Button(dialog, text="Save", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())

View file

@ -1,469 +0,0 @@
"""
EquipmentDialog - диалог управления оборудованием (tkinter)
"""
import tkinter as tk
from tkinter import ttk, messagebox
from threading import Thread
class EquipmentDialog(tk.Toplevel):
"""Диалог для управления оборудованием"""
def __init__(self, parent, config_service):
super().__init__(parent)
self.parent = parent
self.config_service = config_service
self.cameras = self.config_service.get_cameras()
self.lenses = self.config_service.get_lenses()
self.telescopes = self.config_service.get_telescopes()
self.selected_camera = None
self.selected_lens = None
self.selected_telescope = None
self.title("Manage Equipment")
self.geometry("750x500")
self.minsize(700, 450)
self.transient(parent)
self.grab_set()
self._create_ui()
self._center_window()
def _create_ui(self):
# Notebook for tabs
notebook = ttk.Notebook(self)
notebook.pack(fill='both', expand=True, padx=10, pady=10)
# Tab 1: Cameras
cameras_frame = ttk.Frame(notebook)
notebook.add(cameras_frame, text="Cameras")
self._create_cameras_tab(cameras_frame)
# Tab 2: Lenses
lenses_frame = ttk.Frame(notebook)
notebook.add(lenses_frame, text="Lenses")
self._create_lenses_tab(lenses_frame)
# Tab 3: Telescopes
telescopes_frame = ttk.Frame(notebook)
notebook.add(telescopes_frame, text="Telescopes")
self._create_telescopes_tab(telescopes_frame)
# Close button
btn_frame = ttk.Frame(self)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Close", command=self.destroy).pack()
def _center_window(self):
self.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f'+{x}+{y}')
def _create_cameras_tab(self, parent):
# Listbox
list_frame = ttk.Frame(parent)
list_frame.pack(fill='both', expand=True, padx=10, pady=10)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side='right', fill='y')
self.cameras_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set,
bg='#2d2d2d', fg='#e0e0e0',
selectbackground='#4CAF50', selectforeground='white',
font=('Segoe UI', 10))
self.cameras_listbox.pack(fill='both', expand=True)
scrollbar.config(command=self.cameras_listbox.yview)
for camera in self.cameras:
self.cameras_listbox.insert('end', camera)
self.cameras_listbox.bind('<<ListboxSelect>>', self._on_camera_select)
# Buttons
btn_frame = ttk.Frame(parent)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Add Camera", command=self._add_camera).pack(side='left', padx=5)
self.remove_camera_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_camera, state='disabled')
self.remove_camera_btn.pack(side='left', padx=5)
self.edit_camera_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_camera, state='disabled')
self.edit_camera_btn.pack(side='left', padx=5)
def _on_camera_select(self, event):
selection = self.cameras_listbox.curselection()
if selection:
self.selected_camera = self.cameras_listbox.get(selection[0])
self.remove_camera_btn.config(state='normal')
self.edit_camera_btn.config(state='normal')
def _add_camera(self):
dialog = tk.Toplevel(self)
dialog.title("Add Camera")
dialog.geometry("350x120")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Camera name:", font=('Segoe UI', 10)).pack(pady=15)
entry = ttk.Entry(dialog, width=40)
entry.pack(pady=5)
entry.focus()
def save():
new_camera = entry.get().strip()
if new_camera:
if new_camera in self.cameras:
messagebox.showerror("Error", "Camera already exists!", parent=dialog)
return
self.cameras.append(new_camera)
self.config_service.add_camera(new_camera)
self.cameras_listbox.insert('end', new_camera)
dialog.destroy()
else:
messagebox.showwarning("Warning", "Please enter camera name!", parent=dialog)
ttk.Button(dialog, text="OK", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _remove_camera(self):
if self.selected_camera:
reply = messagebox.askyesno("Remove Camera", f"Remove '{self.selected_camera}'?", parent=self)
if reply:
self.cameras.remove(self.selected_camera)
self.config_service.remove_camera(self.selected_camera)
self.cameras_listbox.delete(0, 'end')
for camera in self.cameras:
self.cameras_listbox.insert('end', camera)
self.selected_camera = None
self.remove_camera_btn.config(state='disabled')
self.edit_camera_btn.config(state='disabled')
def _edit_camera(self):
if self.selected_camera:
dialog = tk.Toplevel(self)
dialog.title("Edit Camera")
dialog.geometry("350x120")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="New camera name:", font=('Segoe UI', 10)).pack(pady=15)
entry = ttk.Entry(dialog, width=40)
entry.insert(0, self.selected_camera)
entry.pack(pady=5)
entry.focus()
def save():
new_name = entry.get().strip()
if new_name and new_name != self.selected_camera:
if new_name in self.cameras:
messagebox.showerror("Error", "Camera already exists!", parent=dialog)
return
idx = self.cameras.index(self.selected_camera)
self.cameras[idx] = new_name
self.config_service.remove_camera(self.selected_camera)
self.config_service.add_camera(new_name)
self.cameras_listbox.delete(0, 'end')
for camera in self.cameras:
self.cameras_listbox.insert('end', camera)
self.selected_camera = new_name
dialog.destroy()
elif new_name == self.selected_camera:
dialog.destroy()
else:
messagebox.showwarning("Warning", "Please enter a name!", parent=dialog)
ttk.Button(dialog, text="Save", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _create_lenses_tab(self, parent):
# Listbox
list_frame = ttk.Frame(parent)
list_frame.pack(fill='both', expand=True, padx=10, pady=10)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side='right', fill='y')
self.lenses_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set,
bg='#2d2d2d', fg='#e0e0e0',
selectbackground='#4CAF50', selectforeground='white',
font=('Segoe UI', 10))
self.lenses_listbox.pack(fill='both', expand=True)
scrollbar.config(command=self.lenses_listbox.yview)
for lens in self.lenses:
self.lenses_listbox.insert('end', lens)
self.lenses_listbox.bind('<<ListboxSelect>>', self._on_lens_select)
# Buttons
btn_frame = ttk.Frame(parent)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Add Lens", command=self._add_lens).pack(side='left', padx=5)
self.remove_lens_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_lens, state='disabled')
self.remove_lens_btn.pack(side='left', padx=5)
self.edit_lens_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_lens, state='disabled')
self.edit_lens_btn.pack(side='left', padx=5)
def _on_lens_select(self, event):
selection = self.lenses_listbox.curselection()
if selection:
self.selected_lens = self.lenses_listbox.get(selection[0])
self.remove_lens_btn.config(state='normal')
self.edit_lens_btn.config(state='normal')
def _add_lens(self):
dialog = tk.Toplevel(self)
dialog.title("Add Lens")
dialog.geometry("350x120")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Lens name:", font=('Segoe UI', 10)).pack(pady=15)
entry = ttk.Entry(dialog, width=40)
entry.pack(pady=5)
entry.focus()
def save():
new_lens = entry.get().strip()
if new_lens:
if new_lens in self.lenses:
messagebox.showerror("Error", "Lens already exists!", parent=dialog)
return
self.lenses.append(new_lens)
self.config_service.add_lens(new_lens)
self.lenses_listbox.insert('end', new_lens)
dialog.destroy()
else:
messagebox.showwarning("Warning", "Please enter lens name!", parent=dialog)
ttk.Button(dialog, text="OK", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _remove_lens(self):
if self.selected_lens:
reply = messagebox.askyesno("Remove Lens", f"Remove '{self.selected_lens}'?", parent=self)
if reply:
self.lenses.remove(self.selected_lens)
self.config_service.remove_lens(self.selected_lens)
self.lenses_listbox.delete(0, 'end')
for lens in self.lenses:
self.lenses_listbox.insert('end', lens)
self.selected_lens = None
self.remove_lens_btn.config(state='disabled')
self.edit_lens_btn.config(state='disabled')
def _edit_lens(self):
if self.selected_lens:
dialog = tk.Toplevel(self)
dialog.title("Edit Lens")
dialog.geometry("350x120")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="New lens name:", font=('Segoe UI', 10)).pack(pady=15)
entry = ttk.Entry(dialog, width=40)
entry.insert(0, self.selected_lens)
entry.pack(pady=5)
entry.focus()
def save():
new_name = entry.get().strip()
if new_name and new_name != self.selected_lens:
if new_name in self.lenses:
messagebox.showerror("Error", "Lens already exists!", parent=dialog)
return
idx = self.lenses.index(self.selected_lens)
self.lenses[idx] = new_name
self.config_service.remove_lens(self.selected_lens)
self.config_service.add_lens(new_name)
self.lenses_listbox.delete(0, 'end')
for lens in self.lenses:
self.lenses_listbox.insert('end', lens)
self.selected_lens = new_name
dialog.destroy()
elif new_name == self.selected_lens:
dialog.destroy()
else:
messagebox.showwarning("Warning", "Please enter a name!", parent=dialog)
ttk.Button(dialog, text="Save", command=save).pack(pady=10)
dialog.bind('<Return>', lambda e: save())
def _create_telescopes_tab(self, parent):
# Listbox
list_frame = ttk.Frame(parent)
list_frame.pack(fill='both', expand=True, padx=10, pady=10)
scrollbar = ttk.Scrollbar(list_frame)
scrollbar.pack(side='right', fill='y')
self.telescopes_listbox = tk.Listbox(list_frame, yscrollcommand=scrollbar.set,
bg='#2d2d2d', fg='#e0e0e0',
selectbackground='#4CAF50', selectforeground='white',
font=('Segoe UI', 10))
self.telescopes_listbox.pack(fill='both', expand=True)
scrollbar.config(command=self.telescopes_listbox.yview)
for telescope in self.telescopes:
self.telescopes_listbox.insert('end', telescope)
self.telescopes_listbox.bind('<<ListboxSelect>>', self._on_telescope_select)
# Buttons
btn_frame = ttk.Frame(parent)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="Add Telescope", command=self._add_telescope).pack(side='left', padx=5)
self.remove_telescope_btn = ttk.Button(btn_frame, text="Remove", command=self._remove_telescope, state='disabled')
self.remove_telescope_btn.pack(side='left', padx=5)
self.edit_telescope_btn = ttk.Button(btn_frame, text="Edit", command=self._edit_telescope, state='disabled')
self.edit_telescope_btn.pack(side='left', padx=5)
def _on_telescope_select(self, event):
selection = self.telescopes_listbox.curselection()
if selection:
self.selected_telescope = self.telescopes_listbox.get(selection[0])
self.remove_telescope_btn.config(state='normal')
self.edit_telescope_btn.config(state='normal')
def _add_telescope(self):
dialog = tk.Toplevel(self)
dialog.title("Add Telescope")
dialog.geometry("400x320")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5))
name_entry = ttk.Entry(dialog, width=40)
name_entry.pack()
ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5))
aperture_entry = ttk.Entry(dialog, width=20)
aperture_entry.insert(0, "5.0")
aperture_entry.pack()
ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5))
focal_entry = ttk.Entry(dialog, width=20)
focal_entry.insert(0, "1000")
focal_entry.pack()
ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5))
diameter_entry = ttk.Entry(dialog, width=20)
diameter_entry.insert(0, "200")
diameter_entry.pack()
def save():
name = name_entry.get().strip()
if not name:
messagebox.showerror("Error", "Please enter telescope name!", parent=dialog)
return
try:
aperture = float(aperture_entry.get())
focal = int(focal_entry.get())
diameter = int(diameter_entry.get())
except ValueError:
messagebox.showerror("Error", "Invalid numeric values!", parent=dialog)
return
telescope_info = f"{name} (f/{aperture}, F={focal}mm, D={diameter}mm)"
if telescope_info in self.telescopes:
messagebox.showerror("Error", "Telescope already exists!", parent=dialog)
return
self.telescopes.append(telescope_info)
self.config_service.add_telescope(telescope_info)
self.telescopes_listbox.insert('end', telescope_info)
dialog.destroy()
btn_frame = ttk.Frame(dialog)
btn_frame.pack(pady=20)
ttk.Button(btn_frame, text="OK", command=save).pack(side='left', padx=10)
ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10)
def _remove_telescope(self):
if self.selected_telescope:
reply = messagebox.askyesno("Remove Telescope", f"Remove '{self.selected_telescope}'?", parent=self)
if reply:
self.telescopes.remove(self.selected_telescope)
self.config_service.remove_telescope(self.selected_telescope)
self.telescopes_listbox.delete(0, 'end')
for telescope in self.telescopes:
self.telescopes_listbox.insert('end', telescope)
self.selected_telescope = None
self.remove_telescope_btn.config(state='disabled')
self.edit_telescope_btn.config(state='disabled')
def _edit_telescope(self):
if self.selected_telescope:
import re
match = re.search(r'(.+?) \(f/([\d\.]+), F=(\d+)mm, D=(\d+)mm\)', self.selected_telescope)
if match:
old_name = match.group(1)
old_aperture = match.group(2)
old_focal = match.group(3)
old_diameter = match.group(4)
else:
old_name = self.selected_telescope
old_aperture = "5.0"
old_focal = "1000"
old_diameter = "200"
dialog = tk.Toplevel(self)
dialog.title("Edit Telescope")
dialog.geometry("400x320")
dialog.transient(self)
dialog.grab_set()
ttk.Label(dialog, text="Telescope name:").pack(pady=(15, 5))
name_entry = ttk.Entry(dialog, width=40)
name_entry.insert(0, old_name)
name_entry.pack()
ttk.Label(dialog, text="Aperture (f/):").pack(pady=(10, 5))
aperture_entry = ttk.Entry(dialog, width=20)
aperture_entry.insert(0, old_aperture)
aperture_entry.pack()
ttk.Label(dialog, text="Focal length (mm):").pack(pady=(10, 5))
focal_entry = ttk.Entry(dialog, width=20)
focal_entry.insert(0, old_focal)
focal_entry.pack()
ttk.Label(dialog, text="Diameter (mm):").pack(pady=(10, 5))
diameter_entry = ttk.Entry(dialog, width=20)
diameter_entry.insert(0, old_diameter)
diameter_entry.pack()
def save():
new_name = name_entry.get().strip()
try:
aperture = float(aperture_entry.get())
focal = int(focal_entry.get())
diameter = int(diameter_entry.get())
except ValueError:
messagebox.showerror("Error", "Invalid numeric values!", parent=dialog)
return
new_info = f"{new_name} (f/{aperture}, F={focal}mm, D={diameter}mm)"
if new_info != self.selected_telescope and new_info in self.telescopes:
messagebox.showerror("Error", "Telescope already exists!", parent=dialog)
return
idx = self.telescopes.index(self.selected_telescope)
self.telescopes[idx] = new_info
self.config_service.remove_telescope(self.selected_telescope)
self.config_service.add_telescope(new_info)
self.telescopes_listbox.delete(0, 'end')
for telescope in self.telescopes:
self.telescopes_listbox.insert('end', telescope)
dialog.destroy()
btn_frame = ttk.Frame(dialog)
btn_frame.pack(pady=20)
ttk.Button(btn_frame, text="Save", command=save).pack(side='left', padx=10)
ttk.Button(btn_frame, text="Cancel", command=dialog.destroy).pack(side='left', padx=10)

View file

@ -1,152 +0,0 @@
"""
InstructionsDialog - диалог с инструкцией на английском (tkinter)
"""
import tkinter as tk
from tkinter import ttk
class InstructionsDialog(tk.Toplevel):
"""Диалог с подробной инструкцией пользователя на английском"""
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.title("Instructions")
self.geometry("750x600")
self.minsize(700, 500)
self.transient(parent)
self.grab_set()
self._create_ui()
self._center_window()
def _create_ui(self):
# Main frame
main_frame = ttk.Frame(self, padding="15")
main_frame.pack(fill='both', expand=True)
# Title
ttk.Label(main_frame, text="Astro Session Watcher - User Guide", font=('Segoe UI', 16, 'bold')).pack(pady=(0, 10))
# Text with scrollbar
text_frame = ttk.Frame(main_frame)
text_frame.pack(fill='both', expand=True)
scrollbar = ttk.Scrollbar(text_frame)
scrollbar.pack(side='right', fill='y')
self.text_widget = tk.Text(text_frame, yscrollcommand=scrollbar.set, wrap='word',
bg='#1e1e1e', fg='#e0e0e0', font=('Consolas', 10))
self.text_widget.pack(fill='both', expand=True)
scrollbar.config(command=self.text_widget.yview)
# Instructions text
instructions = """======================= ASTRO SESSION WATCHER =======================
The application automatically tracks new photos in the selected folder,
sorts them by observation targets and maintains detailed logs.
📸 WHAT IS IT FOR?
Automatically distribute shots into target folders
Keep a log of each session
Don't miss a single frame when changing targets
Store equipment and celestial bodies history
🚀 HOW IT WORKS?
1. Select the folder where your camera saves photos
2. The app creates a session folder: "AstroSession_YYYY-MM-DD"
3. Each target gets its own subfolder inside the session folder
4. When you change target (press "New Target"), all accumulated files are moved
5. When you end the session, remaining files are moved to the last target
📝 STEP-BY-STEP GUIDE
1. FIRST LAUNCH (SETUP)
Go to "File" "Equipment" and add your cameras and lenses/telescopes
Go to "File" "Celestial Bodies" and add your observation targets
All data is saved automatically in config files
2. STARTING A SESSION
1. Click "Browse" and select the folder where your camera saves photos
2. Select camera and lens/telescope from dropdowns
3. Enter target name (or select from celestial bodies list)
4. Click "▶ Start Tracking"
After launch:
Status changes to "● ON AIR" with blinking
"New Target" button becomes active
Session folder "AstroSession_date" is created
Inside - folder with your first target
3. CHANGING TARGET DURING SESSION
1. Click "New Target" button (or Ctrl+Shift+N)
2. Enter new target name
3. The app automatically moves all accumulated files to the previous target
4. Creates new folder for the next target
5. Resets file counter
6. Continues tracking
💡 IMPORTANT: If there are files in the watch folder before changing target,
they will NOT be lost - all will be moved!
4. ENDING A SESSION
1. Click "■ Stop" (or Ctrl+X)
2. The app moves all remaining files to the last target
3. Writes final session log
4. Shows dialog with option to open session folder
5. Restores interface for new session
HOTKEYS
Ctrl + O Select watch folder
Ctrl + E Equipment management
Ctrl + B Celestial bodies management
Ctrl + S Start session
Ctrl + X Stop session
Ctrl + F Open current session folder
Ctrl + Shift+N Create new target
F1 About
F2 This instruction
🔧 CONFIG FILES
📄 astro_settings.json cameras, lenses, last folder
📄 celestial_bodies.json list of celestial bodies
All files are stored in the program folder. You can edit them manually.
📧 TECHNICAL SUPPORT
Text me: norvicdev@gmail.com
Developer: Vic Sergeev
Version: 0.4.0-alpha
"""
self.text_widget.insert('1.0', instructions)
self.text_widget.config(state='disabled')
# Close button
ttk.Button(main_frame, text="Close", command=self.destroy).pack(pady=10)
def _center_window(self):
self.update_idletasks()
x = self.parent.winfo_x() + (self.parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = self.parent.winfo_y() + (self.parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f'+{x}+{y}')

View file

@ -1,579 +0,0 @@
"""
MainWindow - главное окно приложения на tkinter
"""
import tkinter as tk
from tkinter import ttk, messagebox, filedialog, simpledialog
from pathlib import Path
from threading import Thread
import subprocess
import platform
from datetime import datetime
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:
"""Главное окно приложения"""
def __init__(self, root):
self.root = root
self.root.title("Astro Session Watcher v0.4.0")
self.root.geometry("800x550")
self.root.minsize(700, 500)
self.center_window()
# Сервисы
self.config_service = ConfigService()
self.session_service = SessionService()
self.watch_service = WatchService()
# Переменные состояния
self.running = False
self.file_count = 0
self.current_target = ""
self.current_session_folder = ""
self._blink_active = False
# Стили
self._setup_styles()
# Создаём интерфейс
self._create_menu_bar()
self._create_main_content()
self._load_saved_settings()
self._setup_hotkeys()
# Обновление счётчика
self._update_file_count_display()
# Обработчик закрытия
self.root.protocol("WM_DELETE_WINDOW", self._on_closing)
def center_window(self):
self.root.update_idletasks()
x = (self.root.winfo_screenwidth() // 2) - (self.root.winfo_width() // 2)
y = (self.root.winfo_screenheight() // 2) - (self.root.winfo_height() // 2)
self.root.geometry(f'+{x}+{y}')
def _setup_styles(self):
style = ttk.Style()
style.theme_use('clam')
# Тёмная тема
style.configure('.', background='#1e1e1e', foreground='#e0e0e0')
style.configure('TLabel', background='#1e1e1e', foreground='#e0e0e0')
style.configure('TFrame', background='#1e1e1e')
style.configure('TLabelframe', background='#1e1e1e', foreground='#e0e0e0')
style.configure('TLabelframe.Label', background='#1e1e1e', foreground='#e0e0e0')
# Кнопки
style.configure('TButton', background='#3c3c3c', foreground='#e0e0e0', borderwidth=1)
style.map('TButton',
background=[('active', '#4c4c4c')],
foreground=[('active', '#ffffff')])
# Поля ввода
style.configure('TEntry', fieldbackground='#3c3c3c', foreground='#e0e0e0')
style.configure('TCombobox', fieldbackground='#3c3c3c', foreground='#e0e0e0')
# Специальные кнопки
style.configure('Green.TButton', background='#4CAF50', foreground='white')
style.map('Green.TButton', background=[('active', '#45a049')])
style.configure('Red.TButton', background='#f44336', foreground='black')
style.map('Red.TButton', background=[('active', '#d32f2f')])
self.root.configure(bg='#1e1e1e')
def _create_menu_bar(self):
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# File menu
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="File", menu=file_menu)
file_menu.add_command(label="Select Folder...", command=self.select_folder, accelerator="Ctrl+O")
file_menu.add_separator()
file_menu.add_command(label="Equipment...", command=self.open_equipment_dialog, accelerator="Ctrl+E")
file_menu.add_command(label="Celestial Bodies...", command=self.open_celestial_dialog, accelerator="Ctrl+B")
file_menu.add_separator()
file_menu.add_command(label="Exit", command=self._on_closing, accelerator="Ctrl+Q")
# Session menu
session_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Session", menu=session_menu)
session_menu.add_command(label="Start Tracking", command=self.start, accelerator="Ctrl+S")
session_menu.add_command(label="Stop", command=self.stop, accelerator="Ctrl+X")
session_menu.add_separator()
session_menu.add_command(label="Open Session Folder", command=self.open_session_folder, accelerator="Ctrl+F")
session_menu.add_separator()
session_menu.add_command(label="New Target...", command=self.set_new_object, accelerator="Ctrl+Shift+N")
session_menu.add_separator()
session_menu.add_command(label="Calibration Frames...", command=self.open_calibration_dialog, accelerator="Ctrl+K")
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="Instructions", command=self.show_instructions, accelerator="F2")
help_menu.add_separator()
help_menu.add_command(label="About", command=self.show_info, accelerator="F1")
def _create_main_content(self):
# Main frame
main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill="both", expand=True)
# Grid layout
main_frame.grid_columnconfigure(0, weight=0, minsize=100)
main_frame.grid_columnconfigure(1, weight=1)
# Row 0: Folder
ttk.Label(main_frame, text="Folder:", font=('Segoe UI', 10, 'bold')).grid(row=0, column=0, sticky='w', pady=5)
folder_frame = ttk.Frame(main_frame)
folder_frame.grid(row=0, column=1, sticky='ew', pady=5)
folder_frame.grid_columnconfigure(0, weight=1)
self.folder_entry = ttk.Entry(folder_frame)
self.folder_entry.grid(row=0, column=0, sticky='ew', padx=(0, 10))
self.folder_entry.insert(0, "Select watch folder...")
self.folder_entry.bind('<FocusIn>', lambda e: self._clear_placeholder(self.folder_entry, "Select watch folder..."))
self.folder_entry.bind('<FocusOut>', lambda e: self._restore_placeholder(self.folder_entry, "Select watch folder..."))
self.browse_btn = ttk.Button(folder_frame, text="Browse...", width=10, command=self.select_folder)
self.browse_btn.grid(row=0, column=1)
# Row 1: Equipment
ttk.Label(main_frame, text="Equipment:", font=('Segoe UI', 10, 'bold')).grid(row=1, column=0, sticky='w', pady=5)
equipment_frame = ttk.Frame(main_frame)
equipment_frame.grid(row=1, column=1, sticky='ew', pady=5)
equipment_frame.grid_columnconfigure(0, weight=1)
equipment_frame.grid_columnconfigure(1, weight=1)
self.camera_combo = ttk.Combobox(equipment_frame, values=[])
self.camera_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10))
self.camera_combo.set("Select or enter camera...")
self.camera_combo.bind('<FocusIn>', lambda e: self._clear_combo(self.camera_combo, "Select or enter camera..."))
self.camera_combo.bind('<FocusOut>', lambda e: self._restore_combo(self.camera_combo, "Select or enter camera..."))
self.lens_combo = ttk.Combobox(equipment_frame, values=[])
self.lens_combo.grid(row=0, column=1, sticky='ew')
self.lens_combo.set("Select or enter lens/telescope...")
self.lens_combo.bind('<FocusIn>', lambda e: self._clear_combo(self.lens_combo, "Select or enter lens/telescope..."))
self.lens_combo.bind('<FocusOut>', lambda e: self._restore_combo(self.lens_combo, "Select or enter lens/telescope..."))
# Row 2: Target
ttk.Label(main_frame, text="Target:", font=('Segoe UI', 10, 'bold')).grid(row=2, column=0, sticky='w', pady=5)
target_frame = ttk.Frame(main_frame)
target_frame.grid(row=2, column=1, sticky='ew', pady=5)
target_frame.grid_columnconfigure(0, weight=1)
self.target_combo = ttk.Combobox(target_frame, values=[])
self.target_combo.grid(row=0, column=0, sticky='ew', padx=(0, 10))
self.target_combo.set("Enter target name...")
self.target_combo.bind('<FocusIn>', lambda e: self._clear_combo(self.target_combo, "Enter target name..."))
self.target_combo.bind('<FocusOut>', lambda e: self._restore_combo(self.target_combo, "Enter target name..."))
self.new_target_btn = ttk.Button(target_frame, text="New Target", width=12, command=self.set_new_object)
self.new_target_btn.grid(row=0, column=1)
self.new_target_btn.configure(state='disabled')
# Row 3: Statistics
ttk.Label(main_frame, text="Statistics:", font=('Segoe UI', 10, 'bold')).grid(row=3, column=0, sticky='w', pady=5)
self.stats_label = ttk.Label(main_frame, text="Files received: 0", font=('Segoe UI', 11))
self.stats_label.grid(row=3, column=1, sticky='w', pady=5)
# Row 4: Status
ttk.Label(main_frame, text="Status:", font=('Segoe UI', 10, 'bold')).grid(row=4, column=0, sticky='w', pady=5)
self.status_label = ttk.Label(main_frame, text="IDLE", font=('Segoe UI', 12, 'bold'), foreground='#666666')
self.status_label.grid(row=4, column=1, sticky='w', pady=5)
# Separator
separator = ttk.Separator(main_frame, orient='horizontal')
separator.grid(row=5, column=0, columnspan=2, sticky='ew', pady=15)
# Buttons
buttons_frame = ttk.Frame(main_frame)
buttons_frame.grid(row=6, column=0, columnspan=2, pady=10)
self.start_btn = ttk.Button(buttons_frame, text="▶ Start Tracking", width=18, command=self.start, style='Green.TButton')
self.start_btn.pack(side='left', padx=10)
self.stop_btn = ttk.Button(buttons_frame, text="■ Stop", width=12, command=self.stop, style='Red.TButton')
self.stop_btn.pack(side='left', padx=10)
self.stop_btn.configure(state='disabled')
# Footer
footer_frame = ttk.Frame(self.root)
footer_frame.pack(side='bottom', fill='x', padx=20, pady=(0, 10))
ttk.Label(footer_frame, text="v0.4.0-alpha", foreground='#666666').pack(side='left')
ttk.Label(footer_frame, text="Made by Vic Sergeev 2026", foreground='#666666').pack(side='right')
def _clear_placeholder(self, entry, placeholder):
if entry.get() == placeholder:
entry.delete(0, 'end')
def _restore_placeholder(self, entry, placeholder):
if entry.get() == '':
entry.insert(0, placeholder)
def _clear_combo(self, combo, placeholder):
if combo.get() == placeholder:
combo.set('')
def _restore_combo(self, combo, placeholder):
if combo.get() == '':
combo.set(placeholder)
def _load_saved_settings(self):
cameras = self.config_service.get_cameras()
lenses = self.config_service.get_lenses()
telescopes = self.config_service.get_telescopes()
celestial_bodies = self.config_service.get_celestial_bodies()
all_optics = []
for lens in lenses:
all_optics.append(lens)
for telescope in telescopes:
all_optics.append(telescope)
if cameras:
self.camera_combo['values'] = cameras
last_camera = self.config_service.get_last_camera()
if last_camera and last_camera in cameras:
self.camera_combo.set(last_camera)
if all_optics:
self.lens_combo['values'] = all_optics
last_lens = self.config_service.get_last_lens()
if last_lens and last_lens in all_optics:
self.lens_combo.set(last_lens)
if celestial_bodies:
self.target_combo['values'] = celestial_bodies
last_folder = self.config_service.get_last_watch_folder()
if last_folder:
self.folder_entry.delete(0, 'end')
self.folder_entry.insert(0, last_folder)
def _setup_hotkeys(self):
def on_key(event):
if event.state & 0x4: # Ctrl
if event.keysym == 'o':
self.select_folder()
elif event.keysym == 'e':
self.open_equipment_dialog()
elif event.keysym == 'b':
self.open_celestial_dialog()
elif event.keysym == 's':
self.start()
elif event.keysym == 'x':
self.stop()
elif event.keysym == 'f':
self.open_session_folder()
elif event.keysym == 'k':
self.open_calibration_dialog()
elif event.state & 0x6: # Ctrl+Shift
if event.keysym == 'N':
self.set_new_object()
elif event.keysym == 'F1':
self.show_info()
elif event.keysym == 'F2':
self.show_instructions()
self.root.bind_all('<Key>', on_key)
def _set_running_state(self, state):
self.running = state
if state:
self.start_btn.configure(state='disabled')
self.stop_btn.configure(state='normal')
self.new_target_btn.configure(state='normal')
self.status_label.configure(text="● ON AIR", foreground='#ff0000')
self._start_blinking()
else:
self.start_btn.configure(state='normal')
self.stop_btn.configure(state='disabled')
self.new_target_btn.configure(state='disabled')
self.status_label.configure(text="IDLE", foreground='#666666')
self._stop_blinking()
def _start_blinking(self):
self._blink_active = True
self._do_blink()
def _do_blink(self):
if not self._blink_active or not self.running:
return
current = self.status_label.cget('foreground')
new_color = '#ffffff' if current == '#ff0000' else '#ff0000'
self.status_label.configure(foreground=new_color)
self.root.after(500, self._do_blink)
def _stop_blinking(self):
self._blink_active = False
self.status_label.configure(foreground='#666666')
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.stats_label.configure(text=f"Files received: {self.file_count}")
self.root.after(1000, self._update_file_count_display)
def _on_file_received(self, file_path: Path):
if self.session_service.handle_file(file_path):
self.file_count += 1
self.stats_label.configure(text=f"Files received: {self.file_count}")
print(f"File processed: {file_path.name}")
def select_folder(self):
folder = filedialog.askdirectory(title="Select watch folder")
if folder:
self.folder_entry.delete(0, 'end')
self.folder_entry.insert(0, folder)
self.config_service.set_last_watch_folder(folder)
def start(self):
watch_folder = self.folder_entry.get()
target_name = self.target_combo.get()
camera = self.camera_combo.get()
lens = self.lens_combo.get()
# Skip placeholders
if watch_folder == "Select watch folder...":
watch_folder = ""
if target_name == "Enter target name...":
target_name = ""
if camera == "Select or enter camera...":
camera = ""
if lens == "Select or enter lens/telescope...":
lens = ""
if not watch_folder:
messagebox.showerror("Error", "Please select a folder to watch!", parent=self.root)
return
if not target_name:
messagebox.showerror("Error", "Please enter a target name!", parent=self.root)
return
celestial_bodies = self.config_service.get_celestial_bodies()
if target_name not in celestial_bodies:
reply = messagebox.askyesno("New Target",
f"Target '{target_name}' not found in list.\nAdd it to the list?",
parent=self.root)
if reply:
self.config_service.add_celestial_body(target_name)
self.target_combo['values'] = self.config_service.get_celestial_bodies()
self.target_combo.set(target_name)
else:
return
if not camera or not lens:
reply = messagebox.askyesno("Warning", "Camera or lens not selected. Continue?",
parent=self.root)
if not reply:
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, target_name, camera_val, lens_val)
self.current_target = target_name
self.current_session_folder = str(self.session_service.get_current_session().session_folder)
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:
messagebox.showerror("Error", "Failed to start watching folder!", parent=self.root)
return
self._set_running_state(True)
except Exception as e:
messagebox.showerror("Error", f"Failed to start session: {e}", parent=self.root)
import traceback
traceback.print_exc()
def stop(self):
if not self.running:
return
try:
watch_folder = Path(self.folder_entry.get())
self.watch_service.move_all_existing_files(watch_folder, self._on_file_received)
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:
messagebox.showerror("Error", f"Error stopping session: {e}", parent=self.root)
def set_new_object(self):
if not self.running:
messagebox.showerror("Error", "Session is not active!", parent=self.root)
return
watch_folder = Path(self.folder_entry.get())
self.watch_service.move_all_existing_files(watch_folder, self._on_file_received)
dialog = tk.Toplevel(self.root)
dialog.title("New Target")
dialog.geometry("400x150")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="Enter new target name:", font=('Segoe UI', 11)).pack(pady=20)
entry = ttk.Entry(dialog, width=40)
entry.pack(pady=10)
entry.focus()
def confirm():
new_target = entry.get().strip()
if new_target:
dialog.destroy()
self._create_new_target(new_target)
else:
messagebox.showwarning("Warning", "Please enter a target name!", parent=dialog)
ttk.Button(dialog, text="OK", command=confirm).pack(pady=10)
dialog.bind('<Return>', lambda e: confirm())
self.root.wait_window(dialog)
def _create_new_target(self, new_name):
celestial_bodies = self.config_service.get_celestial_bodies()
if new_name not in celestial_bodies:
reply = messagebox.askyesno("New Target",
f"Target '{new_name}' not found in list.\nAdd it to the list?",
parent=self.root)
if reply:
self.config_service.add_celestial_body(new_name)
self.target_combo['values'] = self.config_service.get_celestial_bodies()
else:
return
self.session_service.create_new_object(new_name)
self.target_combo.set(new_name)
self.file_count = 0
self.stats_label.configure(text="Files received: 0")
def open_equipment_dialog(self):
from ui.dialogs.equipment_dialog import EquipmentDialog
dialog = EquipmentDialog(self.root, self.config_service)
self.root.wait_window(dialog)
# Refresh comboboxes
cameras = self.config_service.get_cameras()
lenses = self.config_service.get_lenses()
telescopes = self.config_service.get_telescopes()
all_optics = lenses + telescopes
self.camera_combo['values'] = cameras
self.lens_combo['values'] = all_optics
def open_celestial_dialog(self):
from ui.dialogs.celestial_dialog import CelestialDialog
dialog = CelestialDialog(self.root, self.config_service)
self.root.wait_window(dialog)
celestial_bodies = self.config_service.get_celestial_bodies()
self.target_combo['values'] = celestial_bodies
def open_calibration_dialog(self):
from ui.dialogs.calibration_dialog import CalibrationDialog
dialog = CalibrationDialog(self.root, self.config_service)
self.root.wait_window(dialog)
def open_session_folder(self):
if self.running and self.current_session_folder:
try:
if platform.system() == "Windows":
subprocess.Popen(['explorer', self.current_session_folder])
elif platform.system() == "Darwin":
subprocess.Popen(['open', self.current_session_folder])
else:
subprocess.Popen(['xdg-open', self.current_session_folder])
except Exception as e:
messagebox.showerror("Error", f"Failed to open folder: {e}", parent=self.root)
else:
messagebox.showinfo("Info", "No active session", parent=self.root)
def show_instructions(self):
from ui.dialogs.instructions_dialog import InstructionsDialog
InstructionsDialog(self.root)
def show_info(self):
messagebox.showinfo("About",
"Astro Session Watcher v0.4.0\n\n"
"Application for astrophotographers\n\n"
"Features:\n"
"• Automatic file tracking\n"
"• Sorting by targets\n"
"• Session logging\n"
"• Equipment management\n\n"
"Made by Vic Sergeev\n2026",
parent=self.root)
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
dialog = tk.Toplevel(self.root)
dialog.title("Session Completed")
dialog.geometry("500x250")
dialog.transient(self.root)
dialog.grab_set()
ttk.Label(dialog, text="✅ Session finished!", font=('Segoe UI', 14, 'bold')).pack(pady=15)
ttk.Label(dialog, text=f"Target: {object_name}").pack()
ttk.Label(dialog, text=f"Files received: {photo_count}").pack()
ttk.Label(dialog, text=f"Saved to: {session_folder}", wraplength=450).pack(pady=10)
def open_folder():
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:
messagebox.showerror("Error", f"Failed to open folder: {e}", parent=dialog)
dialog.destroy()
def close():
dialog.destroy()
btn_frame = ttk.Frame(dialog)
btn_frame.pack(pady=20)
ttk.Button(btn_frame, text="Open Folder", command=open_folder).pack(side='left', padx=10)
ttk.Button(btn_frame, text="Close", command=close).pack(side='left', padx=10)
def _on_closing(self):
if self.running:
reply = messagebox.askyesno("Exit", "Session is active. Stop session and exit?",
parent=self.root)
if reply:
self.stop()
self.root.destroy()
else:
self.root.destroy()

View file

@ -1,4 +0,0 @@
# Utils package
from utils.sound_manager import SoundManager
__all__ = ['SoundManager']