This commit is contained in:
2026-06-28 20:12:56 +02:00
parent 58ab0d8925
commit a8b6fa2fa6
4 changed files with 245 additions and 7 deletions
+125 -7
View File
@@ -8,6 +8,8 @@ Abhängigkeiten:
"""
import asyncio
import base64
import json
import tkinter as tk
from tkinter import ttk, filedialog, messagebox, scrolledtext
import threading
@@ -180,7 +182,7 @@ class AstrometryGUI:
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("Astrometry.net — solve-field GUI")
self.root.title("Astrometry.net — solve-field GUI vom Astronomiemuseum")
self.root.geometry("1000x840")
self.root.minsize(800, 620)
@@ -278,12 +280,15 @@ class AstrometryGUI:
# -----------------------------------------------------------------------
def _build_ui(self):
self._build_menu()
top = ttk.Frame(self.root, padding=4)
top.pack(fill=tk.X)
self._build_conn_bar(top)
self.nb = ttk.Notebook(self.root)
self.nb.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 4))
self.nb.bind("<<NotebookTabChanged>>", self._on_tab_changed)
for title, builder in [
("Input / Output", self._tab_io),
@@ -336,6 +341,20 @@ class AstrometryGUI:
self._current_task: asyncio.Future | None = None
def _build_menu(self):
menubar = tk.Menu(self.root)
profile_menu = tk.Menu(menubar, tearoff=False)
profile_menu.add_command(label="Profil speichern…", command=self._save_profile)
profile_menu.add_command(label="Profil laden…", command=self._load_profile)
menubar.add_cascade(label="Profil", menu=profile_menu)
self.root.config(menu=menubar)
def _on_tab_changed(self, _event=None):
if self.nb.select() == str(self.results_tab):
self._refresh_results_tab()
# -----------------------------------------------------------------------
# Verbindungs-Leiste
# -----------------------------------------------------------------------
@@ -605,7 +624,7 @@ class AstrometryGUI:
("Weitermachen (--continue)", self.v_continue,
"Überspringt bereits gelöste Bilder (prüft ob .solved-Datei existiert).\n"
"Ideal für unterbrochene Batch-Verarbeitungen."),
("SExtractor nutzen (--use-sextractor)", self.v_sextractor,
("SExtractor nutzen (--use-source-extractor)", self.v_sextractor,
"Verwendet SExtractor statt des eingebauten Quellenerkennungs-Algorithmus.\n"
"Muss auf dem Server installiert sein. Oft besser bei schwierigen Bildern."),
("Als FITS-Bild behandeln (--fits-image)", self.v_fits_image,
@@ -799,7 +818,7 @@ class AstrometryGUI:
flag("--resort", self.v_resort)
flag("--continue", self.v_continue)
flag("--no-delete-temp", self.v_no_del_temp)
flag("--use-sextractor", self.v_sextractor)
flag("--use-source-extractor", self.v_sextractor)
flag("--fits-image", self.v_fits_image)
flag("--no-verify", self.v_no_verify)
flag("--just-augment", self.v_just_augment)
@@ -967,11 +986,11 @@ class AstrometryGUI:
except Exception as exc:
self._log(f"[FEHLER] Remote-Inhalt l\u00f6schen: {exc}\n", "error")
def _show_results_tab(self):
self.nb.select(self.results_tab)
def _refresh_results_tab(self):
remote_dir = self.v_remote_dir.get().strip()
self.root.config(cursor="watch")
self.nb.config(cursor="watch")
self.results_tree.config(cursor="watch")
self._submit(self._async_refresh_results_tab(remote_dir))
async def _async_refresh_results_tab(self, remote_dir: str, returncode: int | None = None):
@@ -1015,9 +1034,11 @@ class AstrometryGUI:
await ssh.exec_stream(cmd, _capture)
await ssh.close()
self.root.after(0, lambda: self._set_results_items(entries))
self.root.after(0, self._clear_wait_cursor)
except Exception as exc:
self.root.after(0, lambda: self._set_results_info(f"[FEHLER] {exc}"))
self.root.after(0, lambda: self._set_results_items([]))
self.root.after(0, self._clear_wait_cursor)
def _set_results_info(self, text: str):
self.results_info.config(text=text, foreground="gray")
@@ -1040,6 +1061,11 @@ class AstrometryGUI:
),
)
def _clear_wait_cursor(self):
self.root.config(cursor="")
self.nb.config(cursor="")
self.results_tree.config(cursor="")
def _format_size(self, size_text: str) -> str:
try:
size = int(size_text)
@@ -1065,8 +1091,11 @@ class AstrometryGUI:
messagebox.showwarning("Datei fehlt", "Die ausgewählte Datei konnte nicht gefunden werden.")
return
suggested = os.path.basename(entry["remote"])
source_dir = os.path.dirname(self.v_local_file.get().strip()) if self.v_local_file.get().strip() else ""
initial_dir = source_dir if source_dir and os.path.isdir(source_dir) else os.getcwd()
local_dir = filedialog.askdirectory(
title="Zielordner für Download auswählen",
initialdir=initial_dir,
)
if not local_dir:
return
@@ -1102,7 +1131,6 @@ class AstrometryGUI:
rc = await ssh.exec_stream(cmd, self._log)
await ssh.close()
self._current_task = None
self.root.after(0, self._show_results_tab)
await self._async_refresh_results_tab(self.v_remote_dir.get().strip(), rc)
except Exception as exc:
self._log(f"[FEHLER] {exc}\n", "error")
@@ -1267,6 +1295,96 @@ class AstrometryGUI:
self.root.clipboard_clear()
self.root.clipboard_append(self.cmd_text.get("1.0", tk.END).strip())
def _encrypt_text(self, text: str) -> str:
if not text:
return ""
key = b"AstrometryGuiInternalKey-2026"
plain = text.encode("utf-8")
encrypted = bytes(b ^ key[i % len(key)] for i, b in enumerate(plain))
return base64.b64encode(encrypted).decode("ascii")
def _decrypt_text(self, token: str) -> str:
if not token:
return ""
key = b"AstrometryGuiInternalKey-2026"
encrypted = base64.b64decode(token.encode("ascii"))
plain = bytes(b ^ key[i % len(key)] for i, b in enumerate(encrypted))
return plain.decode("utf-8")
def _collect_profile_data(self) -> dict:
vars_data = {}
for name, value in self.__dict__.items():
if name.startswith("v_") and hasattr(value, "get"):
vars_data[name] = value.get()
if "v_pass" in vars_data:
vars_data["v_pass"] = ""
return {
"format": 2,
"variables": vars_data,
"secrets": {
"v_pass_enc": self._encrypt_text(self.v_pass.get()),
},
"idx_text": self.idx_text.get("1.0", tk.END).strip(),
}
def _apply_profile_data(self, data: dict):
vars_data = data.get("variables", {})
for name, value in vars_data.items():
var = getattr(self, name, None)
if var is not None and hasattr(var, "set"):
var.set(value)
secrets = data.get("secrets", {})
encrypted_pass = secrets.get("v_pass_enc") or secrets.get("v_pass_dpapi")
if encrypted_pass:
try:
self.v_pass.set(self._decrypt_text(encrypted_pass))
except Exception:
pass
self.idx_text.delete("1.0", tk.END)
idx_text = data.get("idx_text", "")
if idx_text:
self.idx_text.insert("1.0", idx_text)
self._refresh_cmd()
def _save_profile(self):
profile_dir = os.path.join(os.getcwd(), "profiles")
os.makedirs(profile_dir, exist_ok=True)
path = filedialog.asksaveasfilename(
title="Profil speichern",
initialdir=profile_dir,
defaultextension=".json",
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
)
if not path:
return
data = self._collect_profile_data()
try:
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
self._log(f"[OK] Profil gespeichert: {path}\n", "ok")
except Exception as exc:
messagebox.showerror("Profil speichern", f"Speichern fehlgeschlagen:\n{exc}")
def _load_profile(self):
profile_dir = os.path.join(os.getcwd(), "profiles")
path = filedialog.askopenfilename(
title="Profil laden",
initialdir=profile_dir if os.path.isdir(profile_dir) else os.getcwd(),
filetypes=[("JSON-Dateien", "*.json"), ("Alle Dateien", "*.*")],
)
if not path:
return
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
self._apply_profile_data(data)
self._log(f"[OK] Profil geladen: {path}\n", "ok")
except Exception as exc:
messagebox.showerror("Profil laden", f"Laden fehlgeschlagen:\n{exc}")
# -----------------------------------------------------------------------
# Log (thread-sicher via Queue)
# -----------------------------------------------------------------------