Compare commits
No commits in common. "77e94b2188d7dcc4aa53e39dadf5bbbe93f319de" and "b2331818ce0026a337329c060be5a9ceee62a3cd" have entirely different histories.
77e94b2188
...
b2331818ce
77 changed files with 25951 additions and 12 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Виртуальное окружение
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# Скомпилированные файлы
|
||||||
|
*.exe
|
||||||
|
*.pyc
|
||||||
|
*.pyd
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
# PyCharm (опционально, но полезно)
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
|
||||||
|
# Системные файлы
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
10
.idea/AstroSessionWatcher.iml
generated
Normal file
10
.idea/AstroSessionWatcher.iml
generated
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?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
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?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
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?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
Normal file
78
.idea/workspace.xml
generated
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?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">{
|
||||||
|
"associatedIndex": 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>
|
||||||
38
AstroSessionWatcher.spec
Normal file
38
AstroSessionWatcher.spec
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# -*- 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
9
LICENSE
|
|
@ -1,9 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
# Astro-Session-Watcher
|
|
||||||
|
|
||||||
Автоматизированная сортировка фотографий с астросессии
|
|
||||||
14
TODO.txt
Normal file
14
TODO.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
Что можно улучшить в следующих версиях:
|
||||||
|
Увеличение шрифтов для лучшей читаемости
|
||||||
|
|
||||||
|
Ночной режим (красная тема)
|
||||||
|
|
||||||
|
Автодополнение в комбобоксах
|
||||||
|
|
||||||
|
Предпросмотр изображений
|
||||||
|
|
||||||
|
Экспорт отчётов (PDF/HTML)
|
||||||
|
|
||||||
|
FITS поддержка
|
||||||
|
|
||||||
|
Dark/Flat/Bias калькулятор (рекомендации по количеству кадров)
|
||||||
17
astro_settings.json
Normal file
17
astro_settings.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
5079
build/AstroSessionWatcher/Analysis-00.toc
Normal file
5079
build/AstroSessionWatcher/Analysis-00.toc
Normal file
File diff suppressed because it is too large
Load diff
BIN
build/AstroSessionWatcher/AstroSessionWatcher.pkg
Normal file
BIN
build/AstroSessionWatcher/AstroSessionWatcher.pkg
Normal file
Binary file not shown.
3855
build/AstroSessionWatcher/EXE-00.toc
Normal file
3855
build/AstroSessionWatcher/EXE-00.toc
Normal file
File diff suppressed because it is too large
Load diff
3832
build/AstroSessionWatcher/PKG-00.toc
Normal file
3832
build/AstroSessionWatcher/PKG-00.toc
Normal file
File diff suppressed because it is too large
Load diff
BIN
build/AstroSessionWatcher/PYZ-00.pyz
Normal file
BIN
build/AstroSessionWatcher/PYZ-00.pyz
Normal file
Binary file not shown.
647
build/AstroSessionWatcher/PYZ-00.toc
Normal file
647
build/AstroSessionWatcher/PYZ-00.toc
Normal file
|
|
@ -0,0 +1,647 @@
|
||||||
|
('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')])
|
||||||
BIN
build/AstroSessionWatcher/base_library.zip
Normal file
BIN
build/AstroSessionWatcher/base_library.zip
Normal file
Binary file not shown.
BIN
build/AstroSessionWatcher/localpycs/pyimod01_archive.pyc
Normal file
BIN
build/AstroSessionWatcher/localpycs/pyimod01_archive.pyc
Normal file
Binary file not shown.
BIN
build/AstroSessionWatcher/localpycs/pyimod02_importers.pyc
Normal file
BIN
build/AstroSessionWatcher/localpycs/pyimod02_importers.pyc
Normal file
Binary file not shown.
BIN
build/AstroSessionWatcher/localpycs/pyimod03_ctypes.pyc
Normal file
BIN
build/AstroSessionWatcher/localpycs/pyimod03_ctypes.pyc
Normal file
Binary file not shown.
BIN
build/AstroSessionWatcher/localpycs/pyimod04_pywin32.pyc
Normal file
BIN
build/AstroSessionWatcher/localpycs/pyimod04_pywin32.pyc
Normal file
Binary file not shown.
BIN
build/AstroSessionWatcher/localpycs/struct.pyc
Normal file
BIN
build/AstroSessionWatcher/localpycs/struct.pyc
Normal file
Binary file not shown.
29
build/AstroSessionWatcher/warn-AstroSessionWatcher.txt
Normal file
29
build/AstroSessionWatcher/warn-AstroSessionWatcher.txt
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
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)
|
||||||
9168
build/AstroSessionWatcher/xref-AstroSessionWatcher.html
Normal file
9168
build/AstroSessionWatcher/xref-AstroSessionWatcher.html
Normal file
File diff suppressed because it is too large
Load diff
40
build_exe.spec
Normal file
40
build_exe.spec
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[
|
||||||
|
('resources/done.mp3', 'resources'),
|
||||||
|
],
|
||||||
|
hiddenimports=['customtkinter', 'watchdog', 'pygame', 'ctypes', 'queue'],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
[],
|
||||||
|
name='AstroSessionWatcher',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=False, # Без консоли
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
icon='resources/icon.ico', # Если есть иконка
|
||||||
|
)
|
||||||
16
celestial_bodies.json
Normal file
16
celestial_bodies.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[
|
||||||
|
"M31 (Andromeda Galaxy)",
|
||||||
|
"M42 (Orion Nebula)",
|
||||||
|
"M45 (Pleiades)",
|
||||||
|
"M57 (Ring Nebula)",
|
||||||
|
"Солнце",
|
||||||
|
"Moon",
|
||||||
|
"Jupiter",
|
||||||
|
"Сатурн",
|
||||||
|
"M89",
|
||||||
|
"Венера",
|
||||||
|
"Меркурий",
|
||||||
|
"Нептун",
|
||||||
|
"Saturn",
|
||||||
|
"NGC"
|
||||||
|
]
|
||||||
BIN
dist/AstroSessionWatcher.exe
vendored
Normal file
BIN
dist/AstroSessionWatcher.exe
vendored
Normal file
Binary file not shown.
12
dist/astro_settings.json
vendored
Normal file
12
dist/astro_settings.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"cameras": [
|
||||||
|
"ыфв"
|
||||||
|
],
|
||||||
|
"lenses": [
|
||||||
|
"фы"
|
||||||
|
],
|
||||||
|
"telescopes": [],
|
||||||
|
"last_watch_folder": "C:/Users/Juliette/Documents/prodTest",
|
||||||
|
"last_camera": "ыфв",
|
||||||
|
"last_lens": "фы"
|
||||||
|
}
|
||||||
11
dist/celestial_bodies.json
vendored
Normal file
11
dist/celestial_bodies.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[
|
||||||
|
"M31 (Andromeda Galaxy)",
|
||||||
|
"M42 (Orion Nebula)",
|
||||||
|
"M45 (Pleiades)",
|
||||||
|
"M57 (Ring Nebula)",
|
||||||
|
"Sun",
|
||||||
|
"Moon",
|
||||||
|
"Jupiter",
|
||||||
|
"Saturn",
|
||||||
|
"МКС"
|
||||||
|
]
|
||||||
22
main.py
Normal file
22
main.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
4
models/__init__.py
Normal file
4
models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from models.astro_object import AstroObject
|
||||||
|
from models.session import Session
|
||||||
|
|
||||||
|
__all__ = ['AstroObject', 'Session']
|
||||||
BIN
models/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
models/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/astro_object.cpython-313.pyc
Normal file
BIN
models/__pycache__/astro_object.cpython-313.pyc
Normal file
Binary file not shown.
BIN
models/__pycache__/session.cpython-313.pyc
Normal file
BIN
models/__pycache__/session.cpython-313.pyc
Normal file
Binary file not shown.
19
models/astro_object.py
Normal file
19
models/astro_object.py
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AstroObject:
|
||||||
|
"""Модель астрономического объекта"""
|
||||||
|
name: str
|
||||||
|
folder: Path
|
||||||
|
photo_count: int = 0
|
||||||
|
|
||||||
|
def increment_photo_count(self):
|
||||||
|
self.photo_count += 1
|
||||||
|
|
||||||
|
def get_object_log_path(self) -> Path:
|
||||||
|
return self.folder / "ObjectLog.txt"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"AstroObject(name='{self.name}', photos={self.photo_count})"
|
||||||
106
models/camera_profile.py
Normal file
106
models/camera_profile.py
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
35
models/equipment.py
Normal file
35
models/equipment.py
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
||||||
31
models/session.py
Normal file
31
models/session.py
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
from models.astro_object import AstroObject
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Session:
|
||||||
|
"""Модель сессии наблюдения"""
|
||||||
|
camera: str
|
||||||
|
optics: str
|
||||||
|
start_time: datetime
|
||||||
|
end_time: Optional[datetime] = None
|
||||||
|
objects: List[AstroObject] = field(default_factory=list)
|
||||||
|
session_folder: Optional[Path] = None
|
||||||
|
|
||||||
|
def add_object(self, astro_object: AstroObject):
|
||||||
|
self.objects.append(astro_object)
|
||||||
|
|
||||||
|
def get_current_object(self) -> Optional[AstroObject]:
|
||||||
|
return self.objects[-1] if self.objects else None
|
||||||
|
|
||||||
|
def get_session_name(self) -> str:
|
||||||
|
return f"AstroSession_{self.start_time.strftime('%Y-%m-%d')}"
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
self.end_time = datetime.now()
|
||||||
|
|
||||||
|
def is_active(self) -> bool:
|
||||||
|
return self.end_time is None
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
PySide6>=6.5.0
|
||||||
|
watchdog>=3.0.0
|
||||||
BIN
resources/icons/app-icon-128.png
Normal file
BIN
resources/icons/app-icon-128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
BIN
resources/icons/app-icon-16.png
Normal file
BIN
resources/icons/app-icon-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 910 B |
BIN
resources/icons/app-icon-256.png
Normal file
BIN
resources/icons/app-icon-256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
BIN
resources/icons/app-icon-32.png
Normal file
BIN
resources/icons/app-icon-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
resources/icons/app-icon-64.png
Normal file
BIN
resources/icons/app-icon-64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
resources/icons/app.ico
Normal file
BIN
resources/icons/app.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
6
services/__init__.py
Normal file
6
services/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from services.config_service import ConfigService
|
||||||
|
from services.file_service import FileService
|
||||||
|
from services.session_service import SessionService
|
||||||
|
from services.watch_service import WatchService
|
||||||
|
|
||||||
|
__all__ = ['ConfigService', 'FileService', 'SessionService', 'WatchService']
|
||||||
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
services/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/calibration_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/calibration_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/config_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/config_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/file_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/file_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/session_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/session_service.cpython-313.pyc
Normal file
Binary file not shown.
BIN
services/__pycache__/watch_service.cpython-313.pyc
Normal file
BIN
services/__pycache__/watch_service.cpython-313.pyc
Normal file
Binary file not shown.
113
services/calibration_service.py
Normal file
113
services/calibration_service.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
"""
|
||||||
|
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)
|
||||||
194
services/config_service.py
Normal file
194
services/config_service.py
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
126
services/file_service.py
Normal file
126
services/file_service.py
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
159
services/session_service.py
Normal file
159
services/session_service.py
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
119
services/watch_service.py
Normal file
119
services/watch_service.py
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
4
ui/__init__.py
Normal file
4
ui/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# UI package
|
||||||
|
from ui.main_window import MainWindow
|
||||||
|
|
||||||
|
__all__ = ['MainWindow']
|
||||||
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/main_window.cpython-313.pyc
Normal file
BIN
ui/__pycache__/main_window.cpython-313.pyc
Normal file
Binary file not shown.
8
ui/dialogs/__init__.py
Normal file
8
ui/dialogs/__init__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
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']
|
||||||
BIN
ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/calibration_type_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/celestial_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/equipment_dialog.cpython-313.pyc
Normal file
Binary file not shown.
BIN
ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc
Normal file
BIN
ui/dialogs/__pycache__/instructions_dialog.cpython-313.pyc
Normal file
Binary file not shown.
180
ui/dialogs/calibration_dialog.py
Normal file
180
ui/dialogs/calibration_dialog.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
"""
|
||||||
|
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)
|
||||||
574
ui/dialogs/calibration_type_dialog.py
Normal file
574
ui/dialogs/calibration_type_dialog.py
Normal file
|
|
@ -0,0 +1,574 @@
|
||||||
|
"""
|
||||||
|
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}')
|
||||||
153
ui/dialogs/celestial_dialog.py
Normal file
153
ui/dialogs/celestial_dialog.py
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""
|
||||||
|
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())
|
||||||
469
ui/dialogs/equipment_dialog.py
Normal file
469
ui/dialogs/equipment_dialog.py
Normal file
|
|
@ -0,0 +1,469 @@
|
||||||
|
"""
|
||||||
|
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)
|
||||||
152
ui/dialogs/instructions_dialog.py
Normal file
152
ui/dialogs/instructions_dialog.py
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
"""
|
||||||
|
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}')
|
||||||
579
ui/main_window.py
Normal file
579
ui/main_window.py
Normal file
|
|
@ -0,0 +1,579 @@
|
||||||
|
"""
|
||||||
|
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()
|
||||||
4
utils/__init__.py
Normal file
4
utils/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Utils package
|
||||||
|
from utils.sound_manager import SoundManager
|
||||||
|
|
||||||
|
__all__ = ['SoundManager']
|
||||||
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue