GUI-Politur: Hover-Effekte, edlere Karten, verfeinerte Farbpaletten, dünnere Scrollbar, aufgeräumte Sidebar-Stats
- 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:
@@ -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
|
||||
|
||||
+166
-86
@@ -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:
|
||||
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("<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
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user