579 lines
No EOL
24 KiB
Python
579 lines
No EOL
24 KiB
Python
"""
|
|
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() |