From 71546327ae5754912950d44b8b846e881087e870 Mon Sep 17 00:00:00 2001 From: Pepe44DEV Date: Sun, 6 Sep 2026 23:14:59 +0000 Subject: [PATCH] =?UTF-8?q?GUI-Politur:=20Hover-Effekte,=20edlere=20Karten?= =?UTF-8?q?,=20verfeinerte=20Farbpaletten,=20d=C3=BCnnere=20Scrollbar,=20a?= =?UTF-8?q?ufger=C3=A4umte=20Sidebar-Stats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Buttons/Nav/Filter-Chips mit Hover-Farbwechsel und aktiver Akzent-Kante - Server-Karten mit Status rechts, Verbinden-Hinweis und Akzent-Rahmen bei Hover - Shell-Karten mit '»'-Badge - Verfeinerte Farbpaletten (kontrastreicher) + eigener Hover-Akzent pro Theme - Sidebar-Stats mit Version, farbigem Terminal-Status und Server-Zähler - Empty-States mit Symbol, Scrollbar schlanker (6px, Akzent-Scrollbalken) - Version 1.2.0 --- README.md | 1 + desktop/pulsegate_desktop.py | 252 +++++++++++++++++++++++------------ installer/installer.iss | 2 +- 3 files changed, 168 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 7fbbae5..b6ca7b6 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Läuft nativ auf **Linux** und **Windows**. Für Windows liegt ein fertiger Inst ## Features - Desktop-UI ohne Browser +- Aufpolierte Oberfläche mit Hover-Effekten, Akzent-Design und edlen Karten - Serverliste mit Suche - Server hinzufügen, bearbeiten, duplizieren und löschen - Gruppenfilter in der Sidebar diff --git a/desktop/pulsegate_desktop.py b/desktop/pulsegate_desktop.py index 22f9d7b..70b48cb 100644 --- a/desktop/pulsegate_desktop.py +++ b/desktop/pulsegate_desktop.py @@ -17,7 +17,7 @@ import yaml APP_TITLE = "PulseGate Desktop" -APP_VERSION = "1.1.0" +APP_VERSION = "1.2.0" APP_URL = "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI" IS_WINDOWS = sys.platform == "win32" CONNECT_TIMEOUT = 2.5 @@ -25,60 +25,64 @@ STATUS_OK = "#00e07a" THEMES = { "neon-green": { - "bg": "#07110d", - "sidebar": "#050c09", - "surface": "#0d1b15", - "surface_2": "#13251e", - "surface_3": "#172c24", - "field": "#09150f", - "line": "#1f4b38", - "text": "#d7ffe9", - "muted": "#8aa99b", + "bg": "#050d09", + "sidebar": "#040b08", + "surface": "#0c1a13", + "surface_2": "#12241b", + "surface_3": "#193127", + "field": "#08130e", + "line": "#1d4a37", + "text": "#d9ffe9", + "muted": "#86a998", "accent": "#00ff99", - "accent_2": "#33ccff", - "danger": "#ff6666", + "accent_hi": "#5cffb9", + "accent_2": "#3ecfff", + "danger": "#ff7070", }, "ocean-blue": { - "bg": "#071018", - "sidebar": "#040b12", - "surface": "#0e1b26", - "surface_2": "#142638", - "surface_3": "#1a324a", - "field": "#091420", - "line": "#1e4a6e", - "text": "#d7ecff", - "muted": "#8aa7c0", + "bg": "#061019", + "sidebar": "#040b13", + "surface": "#0d1d29", + "surface_2": "#13293a", + "surface_3": "#1a364c", + "field": "#081420", + "line": "#1c4a6e", + "text": "#d8edff", + "muted": "#86a7c2", "accent": "#3ddcff", - "accent_2": "#5a8cff", - "danger": "#ff6a6a", + "accent_hi": "#8fecff", + "accent_2": "#6f9bff", + "danger": "#ff7a7a", }, "sunset-orange": { - "bg": "#120d07", - "sidebar": "#0b0804", - "surface": "#221711", - "surface_2": "#2f2215", - "surface_3": "#3b2d1d", - "field": "#17100a", - "line": "#6e4a1e", - "text": "#ffe9d7", - "muted": "#c0a98a", + "bg": "#130d06", + "sidebar": "#0e0904", + "surface": "#241911", + "surface_2": "#312419", + "surface_3": "#3f2f20", + "field": "#181009", + "line": "#6c4a1d", + "text": "#ffead6", + "muted": "#c3ab8b", "accent": "#ff9d3d", - "accent_2": "#ff6a5a", - "danger": "#ff6a6a", + "accent_hi": "#ffbd7c", + "accent_2": "#ff7a5c", + "danger": "#ff7a7a", }, "cyber-purple": { - "bg": "#0f0711", - "sidebar": "#09050b", - "surface": "#1d0e22", - "surface_2": "#2a1133", - "surface_3": "#371a44", - "field": "#140918", - "line": "#5a1f6e", - "text": "#f0d7ff", - "muted": "#b38ac0", + "bg": "#0f0712", + "sidebar": "#0a050c", + "surface": "#1e0f24", + "surface_2": "#2b1534", + "surface_3": "#3a1d46", + "field": "#150a19", + "line": "#5c2172", + "text": "#f2d9ff", + "muted": "#b590c2", "accent": "#c95aff", + "accent_hi": "#e0a0ff", "accent_2": "#6affcc", - "danger": "#ff6a6a", + "danger": "#ff7a7a", }, } @@ -358,10 +362,15 @@ class PulseGateDesktop(tk.Tk): stats = self._card(sidebar, bg=COLORS["surface"], padx=14, pady=12) stats.pack(fill="x", padx=22, pady=(28, 10)) - self.server_count_label = self._label(stats, "", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD) - self.server_count_label.pack(anchor="w") + head = tk.Frame(stats, bg=COLORS["surface"]) + head.pack(fill="x") + self._label(head, "PulseGate", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(side="left") + self._label(head, "v" + APP_VERSION, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL).pack(side="right", anchor="e", pady=(3, 0)) terminal_text = "Terminal bereit" if self.terminal else "Kein Terminal gefunden" - self._label(stats, terminal_text, bg=COLORS["surface"], fg=COLORS["accent_2"], font=FONT_SMALL).pack(anchor="w", pady=(7, 0)) + terminal_fg = STATUS_OK if self.terminal else COLORS["danger"] + self._label(stats, f"● {terminal_text}", bg=COLORS["surface"], fg=terminal_fg, font=FONT_SMALL).pack(anchor="w", pady=(8, 0)) + self.server_count_label = self._label(stats, "0 Server", bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 10, "bold")) + self.server_count_label.pack(anchor="w", pady=(7, 0)) self._label(stats, str(self.path), bg=COLORS["surface"], fg=COLORS["muted"], font=FONT_SMALL, wraplength=210).pack(anchor="w", pady=(8, 0)) self._button(sidebar, "Über PulseGate", self._about, variant="ghost").pack(fill="x", padx=22, side="bottom", pady=(0, 16)) @@ -399,7 +408,17 @@ class PulseGateDesktop(tk.Tk): left.pack_propagate(False) self.server_canvas = tk.Canvas(left, bg=COLORS["bg"], highlightthickness=0, bd=0) - scrollbar = tk.Scrollbar(left, orient="vertical", command=self.server_canvas.yview, bg=COLORS["bg"], troughcolor=COLORS["bg"]) + scrollbar = tk.Scrollbar( + left, + orient="vertical", + command=self.server_canvas.yview, + bg=COLORS["surface_3"], + activebackground=COLORS["accent"], + troughcolor=COLORS["bg"], + relief="flat", + bd=0, + width=6, + ) self.server_cards = tk.Frame(self.server_canvas, bg=COLORS["bg"]) self.server_cards.bind("", lambda _event: self.server_canvas.configure(scrollregion=self.server_canvas.bbox("all"))) self.server_canvas.create_window((0, 0), window=self.server_cards, anchor="nw", width=382) @@ -567,16 +586,27 @@ class PulseGateDesktop(tk.Tk): text=group, bg=COLORS["accent"] if selected else COLORS["surface_2"], fg="#001a10" if selected else COLORS["text"], - font=FONT_SMALL, - padx=9, - pady=4, + font=(UI_FONT[0], 9, "bold"), + padx=10, + pady=5, cursor="hand2", highlightthickness=1, - highlightbackground=COLORS["line"], + highlightbackground=COLORS["line"] if not selected else COLORS["accent_hi"], ) chip.pack(side="left", padx=(0, 6)) chip.bind("", lambda _event, value=group: self._set_group_filter(value)) + def _enter(_event, label=chip, is_selected=selected) -> None: + if not is_selected: + label.configure(bg=COLORS["surface_3"], highlightbackground=COLORS["accent_hi"]) + + def _leave(_event, label=chip, is_selected=selected) -> None: + if not is_selected: + label.configure(bg=COLORS["surface_2"], highlightbackground=COLORS["line"]) + + chip.bind("", _enter) + chip.bind("", _leave) + def _set_group_filter(self, group: str) -> None: self.group_filter = group self._refresh_group_chips() @@ -611,43 +641,61 @@ class PulseGateDesktop(tk.Tk): selected = index == self.selected_index bg = COLORS["surface_2"] if selected else COLORS["surface"] border = COLORS["accent"] if selected else COLORS["line"] + card = tk.Frame(parent, bg=border, bd=0) inner = tk.Frame(card, bg=bg, padx=14, pady=12) inner.pack(fill="both", expand=True, padx=1, pady=1) - target = f"{server.get('user')}@{server.get('host')}:{server.get('port') or 22}" - name = server.get("name") or "Unbenannt" - group = server.get("group") or "Keine Gruppe" + head = tk.Frame(inner, bg=bg) + head.pack(fill="x", pady=(0, 6)) - self._label(inner, name, bg=bg, fg=COLORS["text"], font=FONT_CARD).pack(anchor="w") - self._label(inner, target, bg=bg, fg=COLORS["muted"], font=FONT_SMALL).pack(anchor="w", pady=(5, 8)) + name = server.get("name") or "Unbenannt" + self._label(head, name, bg=bg, fg=COLORS["text"], font=FONT_CARD).pack(side="left") + + status = self.reachability.get(index) + if status is None: + status_text, status_fg = "…", COLORS["muted"] + elif status: + status_text, status_fg = "● erreichbar", STATUS_OK + else: + status_text, status_fg = "● offline", COLORS["danger"] + status_label = self._label(head, status_text, bg=bg, fg=status_fg, font=FONT_SMALL) + status_label.pack(side="right", anchor="e", pady=(2, 0)) + self.reach_labels[index] = status_label + + target = f"{server.get('user')}@{server.get('host')}:{server.get('port') or 22}" + self._label(inner, target, bg=bg, fg=COLORS["muted"], font=UI_FONT_MONO).pack(anchor="w", pady=(0, 10)) chips = tk.Frame(inner, bg=bg) chips.pack(fill="x") - self._chip(chips, group, COLORS["accent_2"], bg=bg).pack(side="left") + self._chip(chips, server.get("group") or "Keine Gruppe", COLORS["accent_2"], bg=bg).pack(side="left") self._chip(chips, server.get("auth") or "key", COLORS["accent"], bg=bg).pack(side="left", padx=(6, 0)) if server.get("kitty_fix"): self._chip(chips, "kitty", COLORS["muted"], bg=bg).pack(side="left", padx=(6, 0)) - status = self.reachability.get(index) - if status is None: - status_text, status_fg = "● prüfe ...", COLORS["muted"] - elif status: - status_text, status_fg = "● erreichbar", STATUS_OK - else: - status_text, status_fg = "● nicht erreichbar", COLORS["danger"] - status_label = self._label(inner, status_text, bg=bg, fg=status_fg, font=FONT_SMALL) - status_label.pack(anchor="w", pady=(8, 0)) - self.reach_labels[index] = status_label + connect_hint = tk.Label(inner, text="Verbinden ›", bg=bg, fg=COLORS["accent_hi"], font=FONT_SMALL) + connect_hint.place(relx=1.0, rely=0.0, anchor="ne", x=-14, y=6) + + def _card_enter(_event) -> None: + card.configure(bg=COLORS["accent_hi"]) + connect_hint.lift() + + def _card_leave(_event) -> None: + card.configure(bg=border) + connect_hint.lower() for widget in (card, inner): widget.bind("", lambda _event, value=index: self._select_server(value)) widget.bind("", lambda _event, value=index: self._connect_at(value)) widget.bind("", lambda event, value=index: self._server_menu(event, value)) + widget.bind("", _card_enter) + widget.bind("", _card_leave) for child in inner.winfo_children(): child.bind("", lambda _event, value=index: self._select_server(value)) child.bind("", lambda _event, value=index: self._connect_at(value)) child.bind("", lambda event, value=index: self._server_menu(event, value)) + child.bind("", _card_enter) + child.bind("", _card_leave) return card @@ -698,8 +746,13 @@ class PulseGateDesktop(tk.Tk): for command in commands: card = self._card(self.commands_host, bg=COLORS["surface"], padx=16, pady=14) card.pack(fill="x", pady=(0, 10)) - self._label(card, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(anchor="w") - self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).pack(anchor="w", pady=(7, 0)) + + head = tk.Frame(card, bg=COLORS["surface"]) + head.pack(fill="x") + self._label(head, "»", bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 14, "bold")).pack(side="left", padx=(0, 8)) + self._label(head, command.get("name") or "Command", bg=COLORS["surface"], fg=COLORS["text"], font=FONT_CARD).pack(side="left") + + self._label(card, command.get("command") or "", bg=COLORS["surface"], fg=COLORS["accent"], font=UI_FONT_MONO).pack(anchor="w", pady=(9, 0)) actions = tk.Frame(card, bg=COLORS["surface"]) actions.pack(fill="x", pady=(12, 0)) @@ -1009,36 +1062,44 @@ class PulseGateDesktop(tk.Tk): return widget def _button(self, parent: tk.Widget, text: str, command, *, variant: str = "primary") -> tk.Button: - bg = COLORS["accent"] - fg = "#001a10" - active_bg = COLORS["accent"] - if variant == "ghost": + if variant == "primary": + bg = COLORS["accent"] + fg = "#001a10" + hover_bg = COLORS["accent_hi"] + hover_fg = "#001a10" + elif variant == "ghost": bg = COLORS["surface_2"] fg = COLORS["text"] - active_bg = COLORS["surface_3"] - elif variant == "danger": - bg = "#351414" + hover_bg = COLORS["surface_3"] + hover_fg = COLORS["text"] + else: + bg = "#3a1717" fg = COLORS["danger"] - active_bg = "#4a1d1d" + hover_bg = "#522020" + hover_fg = "#ffd0d0" - return tk.Button( + button = tk.Button( parent, text=text, command=command, bg=bg, fg=fg, - activebackground=active_bg, - activeforeground=fg, + activebackground=hover_bg, + activeforeground=hover_fg, relief="flat", bd=0, padx=14, pady=9, font=(UI_FONT[0], 10, "bold"), cursor="hand2", + highlightthickness=0, ) + button.bind("", lambda _event: button.configure(bg=hover_bg, fg=hover_fg)) + button.bind("", lambda _event: button.configure(bg=bg, fg=fg)) + return button def _nav_button(self, parent: tk.Widget, text: str, command) -> tk.Button: - return tk.Button( + button = tk.Button( parent, text=text, command=command, @@ -1057,6 +1118,24 @@ class PulseGateDesktop(tk.Tk): highlightbackground=COLORS["sidebar"], ) + def _enter(_event) -> None: + if not self._view_active(text): + button.configure(bg=COLORS["surface_2"]) + + def _leave(_event) -> None: + if self._view_active(text): + button.configure(bg=COLORS["surface_2"], highlightbackground=COLORS["line"]) + else: + button.configure(bg=COLORS["sidebar"], highlightbackground=COLORS["sidebar"]) + + button.bind("", _enter) + button.bind("", _leave) + return button + + def _view_active(self, name: str) -> bool: + mapping = {"Server": "servers", "Commands": "commands", "Settings": "settings"} + return mapping.get(name) == self.current_view + def _card(self, parent: tk.Widget, *, bg: str, padx: int, pady: int) -> tk.Frame: return tk.Frame(parent, bg=bg, padx=padx, pady=pady, highlightthickness=1, highlightbackground=COLORS["line"]) @@ -1066,16 +1145,17 @@ class PulseGateDesktop(tk.Tk): text=text, bg=bg or parent.cget("bg"), fg=fg, - font=FONT_SMALL, + font=(UI_FONT[0], 9, "bold"), padx=7, pady=3, highlightthickness=1, - highlightbackground=COLORS["line"], + highlightbackground=COLORS["surface_3"], ) - def _empty_state(self, parent: tk.Widget, text: str) -> tk.Frame: + def _empty_state(self, parent: tk.Widget, text: str, symbol: str = "◦") -> tk.Frame: card = self._card(parent, bg=COLORS["surface"], padx=16, pady=18) - self._label(card, text, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT).pack(anchor="w") + self._label(card, symbol, bg=COLORS["surface"], fg=COLORS["accent"], font=(UI_FONT[0], 18, "bold")).pack(side="left", padx=(0, 12)) + self._label(card, text, bg=COLORS["surface"], fg=COLORS["muted"], font=FONT).pack(side="left") return card diff --git a/installer/installer.iss b/installer/installer.iss index 9402f30..be21fd3 100644 --- a/installer/installer.iss +++ b/installer/installer.iss @@ -2,7 +2,7 @@ ; Kompilieren: iscc installer\installer.iss (Inno Setup 6) #define MyAppName "PulseGate Desktop" -#define MyAppVersion "1.1.0" +#define MyAppVersion "1.2.0" #define MyAppPublisher "Pepe44DEV" #define MyAppURL "https://git.pepe44.dev/Pepe44DEV/PulseGate-GUI" #define MyAppExeName "PulseGate.exe"