diff --git a/__pycache__/astrometry_gui.cpython-312.pyc b/__pycache__/astrometry_gui.cpython-312.pyc index 44e363e..84a327f 100644 Binary files a/__pycache__/astrometry_gui.cpython-312.pyc and b/__pycache__/astrometry_gui.cpython-312.pyc differ diff --git a/astrometry_gui.py b/astrometry_gui.py index 5f552be..fa15cd1 100644 --- a/astrometry_gui.py +++ b/astrometry_gui.py @@ -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("<>", 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) # ----------------------------------------------------------------------- diff --git a/profiles/HU 0815.json b/profiles/HU 0815.json new file mode 100644 index 0000000..0f1da39 --- /dev/null +++ b/profiles/HU 0815.json @@ -0,0 +1,60 @@ +{ + "format": 2, + "variables": { + "v_host": "10.42.44.25", + "v_port": 22, + "v_user": "eskimue", + "v_pass": "", + "v_local_file": "", + "v_remote_dir": "/tmp/astrometry", + "v_input_file": "", + "v_out_prefix": "", + "v_out_dir": "/tmp/astrometry", + "v_new_fits": "", + "v_wcs": "", + "v_corr": "", + "v_rdls": "", + "v_axy": "", + "v_solved": "", + "v_match": "", + "v_scale_units": "degwidth", + "v_scale_low": "20", + "v_scale_high": "30", + "v_scale_est": "", + "v_scale_err": "", + "v_ra": "", + "v_dec": "", + "v_radius": "1.0", + "v_depth": "", + "v_cpulimit": "300", + "v_plot_scale": "", + "v_plot_scale_auto": true, + "v_plot_target_long_edge": "900", + "v_downsample": "6", + "v_tweak_order": "2", + "v_odds_solve": "", + "v_odds_reject": "", + "v_odds_tune": "", + "v_objs": "", + "v_pixel_error": "", + "v_parity": "both", + "v_overwrite": true, + "v_no_plots": false, + "v_no_tweak": false, + "v_resort": false, + "v_verbose": false, + "v_timestamp": true, + "v_no_del_temp": false, + "v_continue": false, + "v_sextractor": false, + "v_fits_image": false, + "v_no_verify": false, + "v_just_augment": false, + "v_invert": true, + "v_extra": "" + }, + "secrets": { + "v_pass_enc": "EQERAQYJAA==" + }, + "idx_text": "" +} \ No newline at end of file diff --git a/profiles/Test.json b/profiles/Test.json new file mode 100644 index 0000000..10b0bb5 --- /dev/null +++ b/profiles/Test.json @@ -0,0 +1,60 @@ +{ + "format": 2, + "variables": { + "v_host": "10.42.44.25", + "v_port": 22, + "v_user": "eskimue", + "v_pass": "", + "v_local_file": "C:/Users/ich/Cloud__Museum/Museumsentwicklung/Shop/Faksimile/2026/Heinlein Dieter/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png", + "v_remote_dir": "/tmp/astrometry", + "v_input_file": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.png", + "v_out_prefix": "", + "v_out_dir": "/tmp/astrometry", + "v_new_fits": "/tmp/astrometry/IMG_410328-1956_08_30-20000+2000-Te3-26-50.fits", + "v_wcs": "", + "v_corr": "", + "v_rdls": "", + "v_axy": "", + "v_solved": "", + "v_match": "", + "v_scale_units": "degwidth", + "v_scale_low": "20", + "v_scale_high": "30", + "v_scale_est": "", + "v_scale_err": "", + "v_ra": "", + "v_dec": "", + "v_radius": "1.0", + "v_depth": "", + "v_cpulimit": "300", + "v_plot_scale": "0.15", + "v_plot_scale_auto": true, + "v_plot_target_long_edge": "900", + "v_downsample": "6", + "v_tweak_order": "2", + "v_odds_solve": "", + "v_odds_reject": "", + "v_odds_tune": "", + "v_objs": "", + "v_pixel_error": "", + "v_parity": "both", + "v_overwrite": true, + "v_no_plots": false, + "v_no_tweak": false, + "v_resort": false, + "v_verbose": false, + "v_timestamp": true, + "v_no_del_temp": false, + "v_continue": false, + "v_sextractor": false, + "v_fits_image": false, + "v_no_verify": false, + "v_just_augment": false, + "v_invert": true, + "v_extra": "" + }, + "secrets": { + "v_pass_enc": "EQERAQYJAA==" + }, + "idx_text": "" +} \ No newline at end of file