server groups: grouped sidebar, GroupDialog, context menus, i18n + cleanup old releases
- Groups CRUD in sidebar (create, rename, change color, reorder, delete) - Collapsible group headers with color dots and server count - "Move to Group" context menu on servers - Group dropdown in ServerDialog (add/edit) - 17 i18n keys (EN/RU/ZH) - Search auto-expands groups - Cleaned up old release binaries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
110
gui/group_dialog.py
Normal file
110
gui/group_dialog.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Dialog for creating / editing server groups.
|
||||
"""
|
||||
|
||||
import customtkinter as ctk
|
||||
from typing import Optional
|
||||
|
||||
from core.i18n import t
|
||||
|
||||
GROUP_COLORS = [
|
||||
"#ef4444", # red
|
||||
"#f97316", # orange
|
||||
"#f59e0b", # amber
|
||||
"#22c55e", # green
|
||||
"#3b82f6", # blue
|
||||
"#6366f1", # indigo
|
||||
"#a855f7", # purple
|
||||
"#ec4899", # pink
|
||||
]
|
||||
|
||||
|
||||
class GroupDialog(ctk.CTkToplevel):
|
||||
"""Small dialog: group name + color picker (8 circles)."""
|
||||
|
||||
def __init__(self, master, store, group: Optional[dict] = None):
|
||||
super().__init__(master)
|
||||
self.store = store
|
||||
self._group = group # None = add, dict = edit
|
||||
self.result: Optional[dict] = None
|
||||
|
||||
self.title(t("edit_group") if group else t("add_group"))
|
||||
self.geometry("340x200")
|
||||
self.resizable(False, False)
|
||||
self.transient(master)
|
||||
self.grab_set()
|
||||
|
||||
# ── Name ──
|
||||
ctk.CTkLabel(self, text=t("group_name"), anchor="w").pack(
|
||||
fill="x", padx=20, pady=(15, 2))
|
||||
self._name_var = ctk.StringVar(value=group["name"] if group else "")
|
||||
self._name_entry = ctk.CTkEntry(self, textvariable=self._name_var)
|
||||
self._name_entry.pack(fill="x", padx=20)
|
||||
self._name_entry.focus()
|
||||
|
||||
# ── Color picker ──
|
||||
ctk.CTkLabel(self, text=t("group_color"), anchor="w").pack(
|
||||
fill="x", padx=20, pady=(10, 2))
|
||||
|
||||
color_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
color_frame.pack(fill="x", padx=20)
|
||||
|
||||
self._selected_color = group["color"] if group else GROUP_COLORS[0]
|
||||
self._color_buttons: list[ctk.CTkButton] = []
|
||||
|
||||
for color in GROUP_COLORS:
|
||||
btn = ctk.CTkButton(
|
||||
color_frame, text="", width=28, height=28,
|
||||
fg_color=color, hover_color=color,
|
||||
border_width=3,
|
||||
border_color=color,
|
||||
corner_radius=14,
|
||||
command=lambda c=color: self._pick_color(c),
|
||||
)
|
||||
btn.pack(side="left", padx=2)
|
||||
self._color_buttons.append(btn)
|
||||
|
||||
self._highlight_selected_color()
|
||||
|
||||
# ── Buttons ──
|
||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||
btn_frame.pack(fill="x", padx=20, pady=(15, 10))
|
||||
|
||||
ctk.CTkButton(btn_frame, text=t("cancel"), width=80,
|
||||
fg_color="gray", command=self.destroy).pack(side="left")
|
||||
ctk.CTkButton(btn_frame, text=t("save"), width=80,
|
||||
command=self._save).pack(side="right")
|
||||
|
||||
# Enter to save
|
||||
self.bind("<Return>", lambda e: self._save())
|
||||
|
||||
def _pick_color(self, color: str):
|
||||
self._selected_color = color
|
||||
self._highlight_selected_color()
|
||||
|
||||
def _highlight_selected_color(self):
|
||||
for btn in self._color_buttons:
|
||||
fg = btn.cget("fg_color")
|
||||
if fg == self._selected_color:
|
||||
btn.configure(border_color="white")
|
||||
else:
|
||||
btn.configure(border_color=fg)
|
||||
|
||||
def _save(self):
|
||||
name = self._name_var.get().strip()
|
||||
if not name:
|
||||
self._name_entry.focus()
|
||||
return
|
||||
|
||||
if self._group:
|
||||
# Edit mode
|
||||
self.store.update_group(
|
||||
self._group["id"], name=name, color=self._selected_color)
|
||||
self.result = {"id": self._group["id"], "name": name,
|
||||
"color": self._selected_color}
|
||||
else:
|
||||
# Add mode
|
||||
group = self.store.add_group(name, self._selected_color)
|
||||
self.result = group
|
||||
|
||||
self.destroy()
|
||||
Reference in New Issue
Block a user