GUI-Politur: Hover-Effekte, edlere Karten, verfeinerte Farbpaletten, dünnere Scrollbar, aufgeräumte Sidebar-Stats
Build Windows / build (push) Has been cancelled

- 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
This commit is contained in:
2026-09-06 23:14:59 +00:00
parent 743b8d7a5e
commit 71546327ae
3 changed files with 168 additions and 87 deletions
+1
View File
@@ -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
+164 -84
View File
@@ -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("<Configure>", 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("<Button-1>", 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>", _enter)
chip.bind("<Leave>", _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("<Button-1>", lambda _event, value=index: self._select_server(value))
widget.bind("<Double-Button-1>", lambda _event, value=index: self._connect_at(value))
widget.bind("<Button-3>", lambda event, value=index: self._server_menu(event, value))
widget.bind("<Enter>", _card_enter)
widget.bind("<Leave>", _card_leave)
for child in inner.winfo_children():
child.bind("<Button-1>", lambda _event, value=index: self._select_server(value))
child.bind("<Double-Button-1>", lambda _event, value=index: self._connect_at(value))
child.bind("<Button-3>", lambda event, value=index: self._server_menu(event, value))
child.bind("<Enter>", _card_enter)
child.bind("<Leave>", _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:
if variant == "primary":
bg = COLORS["accent"]
fg = "#001a10"
active_bg = COLORS["accent"]
if variant == "ghost":
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("<Enter>", lambda _event: button.configure(bg=hover_bg, fg=hover_fg))
button.bind("<Leave>", 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>", _enter)
button.bind("<Leave>", _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
+1 -1
View File
@@ -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"