diff --git a/assets/icons/dark/add.png b/assets/icons/dark/add.png new file mode 100644 index 0000000..8d6da45 Binary files /dev/null and b/assets/icons/dark/add.png differ diff --git a/assets/icons/dark/arrow_back.png b/assets/icons/dark/arrow_back.png new file mode 100644 index 0000000..a052e61 Binary files /dev/null and b/assets/icons/dark/arrow_back.png differ diff --git a/assets/icons/dark/arrow_upward.png b/assets/icons/dark/arrow_upward.png new file mode 100644 index 0000000..53f2786 Binary files /dev/null and b/assets/icons/dark/arrow_upward.png differ diff --git a/assets/icons/dark/backspace.png b/assets/icons/dark/backspace.png new file mode 100644 index 0000000..0cc1311 Binary files /dev/null and b/assets/icons/dark/backspace.png differ diff --git a/assets/icons/dark/check.png b/assets/icons/dark/check.png new file mode 100644 index 0000000..63d33be Binary files /dev/null and b/assets/icons/dark/check.png differ diff --git a/assets/icons/dark/close.png b/assets/icons/dark/close.png new file mode 100644 index 0000000..b60716a Binary files /dev/null and b/assets/icons/dark/close.png differ diff --git a/assets/icons/dark/code.png b/assets/icons/dark/code.png new file mode 100644 index 0000000..590ee8b Binary files /dev/null and b/assets/icons/dark/code.png differ diff --git a/assets/icons/dark/computer.png b/assets/icons/dark/computer.png new file mode 100644 index 0000000..4a8bb1b Binary files /dev/null and b/assets/icons/dark/computer.png differ diff --git a/assets/icons/dark/content_copy.png b/assets/icons/dark/content_copy.png new file mode 100644 index 0000000..485f8d9 Binary files /dev/null and b/assets/icons/dark/content_copy.png differ diff --git a/assets/icons/dark/dashboard.png b/assets/icons/dark/dashboard.png new file mode 100644 index 0000000..df5659e Binary files /dev/null and b/assets/icons/dark/dashboard.png differ diff --git a/assets/icons/dark/delete.png b/assets/icons/dark/delete.png new file mode 100644 index 0000000..20f8f7c Binary files /dev/null and b/assets/icons/dark/delete.png differ diff --git a/assets/icons/dark/edit.png b/assets/icons/dark/edit.png new file mode 100644 index 0000000..67e51a0 Binary files /dev/null and b/assets/icons/dark/edit.png differ diff --git a/assets/icons/dark/file_download.png b/assets/icons/dark/file_download.png new file mode 100644 index 0000000..5ab8c83 Binary files /dev/null and b/assets/icons/dark/file_download.png differ diff --git a/assets/icons/dark/file_upload.png b/assets/icons/dark/file_upload.png new file mode 100644 index 0000000..f2cf3ff Binary files /dev/null and b/assets/icons/dark/file_upload.png differ diff --git a/assets/icons/dark/folder.png b/assets/icons/dark/folder.png new file mode 100644 index 0000000..d95e75d Binary files /dev/null and b/assets/icons/dark/folder.png differ diff --git a/assets/icons/dark/folder_open.png b/assets/icons/dark/folder_open.png new file mode 100644 index 0000000..60e209d Binary files /dev/null and b/assets/icons/dark/folder_open.png differ diff --git a/assets/icons/dark/info.png b/assets/icons/dark/info.png new file mode 100644 index 0000000..8667bdc Binary files /dev/null and b/assets/icons/dark/info.png differ diff --git a/assets/icons/dark/language.png b/assets/icons/dark/language.png new file mode 100644 index 0000000..1d9c5eb Binary files /dev/null and b/assets/icons/dark/language.png differ diff --git a/assets/icons/dark/lock.png b/assets/icons/dark/lock.png new file mode 100644 index 0000000..464290d Binary files /dev/null and b/assets/icons/dark/lock.png differ diff --git a/assets/icons/dark/play_arrow.png b/assets/icons/dark/play_arrow.png new file mode 100644 index 0000000..8852d7e Binary files /dev/null and b/assets/icons/dark/play_arrow.png differ diff --git a/assets/icons/dark/refresh.png b/assets/icons/dark/refresh.png new file mode 100644 index 0000000..d8b00d2 Binary files /dev/null and b/assets/icons/dark/refresh.png differ diff --git a/assets/icons/dark/save.png b/assets/icons/dark/save.png new file mode 100644 index 0000000..3cf95f4 Binary files /dev/null and b/assets/icons/dark/save.png differ diff --git a/assets/icons/dark/search.png b/assets/icons/dark/search.png new file mode 100644 index 0000000..5235c9f Binary files /dev/null and b/assets/icons/dark/search.png differ diff --git a/assets/icons/dark/settings.png b/assets/icons/dark/settings.png new file mode 100644 index 0000000..c9f8e55 Binary files /dev/null and b/assets/icons/dark/settings.png differ diff --git a/assets/icons/dark/storage.png b/assets/icons/dark/storage.png new file mode 100644 index 0000000..22f35b4 Binary files /dev/null and b/assets/icons/dark/storage.png differ diff --git a/assets/icons/dark/trending_up.png b/assets/icons/dark/trending_up.png new file mode 100644 index 0000000..d06e27c Binary files /dev/null and b/assets/icons/dark/trending_up.png differ diff --git a/assets/icons/dark/visibility.png b/assets/icons/dark/visibility.png new file mode 100644 index 0000000..6f6d1f5 Binary files /dev/null and b/assets/icons/dark/visibility.png differ diff --git a/assets/icons/dark/vpn_key.png b/assets/icons/dark/vpn_key.png new file mode 100644 index 0000000..8f537d8 Binary files /dev/null and b/assets/icons/dark/vpn_key.png differ diff --git a/assets/icons/light/add.png b/assets/icons/light/add.png new file mode 100644 index 0000000..6cfbad8 Binary files /dev/null and b/assets/icons/light/add.png differ diff --git a/assets/icons/light/arrow_back.png b/assets/icons/light/arrow_back.png new file mode 100644 index 0000000..6673fe4 Binary files /dev/null and b/assets/icons/light/arrow_back.png differ diff --git a/assets/icons/light/arrow_upward.png b/assets/icons/light/arrow_upward.png new file mode 100644 index 0000000..6cc4ce6 Binary files /dev/null and b/assets/icons/light/arrow_upward.png differ diff --git a/assets/icons/light/backspace.png b/assets/icons/light/backspace.png new file mode 100644 index 0000000..83e79b3 Binary files /dev/null and b/assets/icons/light/backspace.png differ diff --git a/assets/icons/light/check.png b/assets/icons/light/check.png new file mode 100644 index 0000000..b981533 Binary files /dev/null and b/assets/icons/light/check.png differ diff --git a/assets/icons/light/close.png b/assets/icons/light/close.png new file mode 100644 index 0000000..8fb9a5a Binary files /dev/null and b/assets/icons/light/close.png differ diff --git a/assets/icons/light/code.png b/assets/icons/light/code.png new file mode 100644 index 0000000..cd242e3 Binary files /dev/null and b/assets/icons/light/code.png differ diff --git a/assets/icons/light/computer.png b/assets/icons/light/computer.png new file mode 100644 index 0000000..7d77115 Binary files /dev/null and b/assets/icons/light/computer.png differ diff --git a/assets/icons/light/content_copy.png b/assets/icons/light/content_copy.png new file mode 100644 index 0000000..d160f86 Binary files /dev/null and b/assets/icons/light/content_copy.png differ diff --git a/assets/icons/light/dashboard.png b/assets/icons/light/dashboard.png new file mode 100644 index 0000000..ba1df99 Binary files /dev/null and b/assets/icons/light/dashboard.png differ diff --git a/assets/icons/light/delete.png b/assets/icons/light/delete.png new file mode 100644 index 0000000..430d9b4 Binary files /dev/null and b/assets/icons/light/delete.png differ diff --git a/assets/icons/light/edit.png b/assets/icons/light/edit.png new file mode 100644 index 0000000..4496703 Binary files /dev/null and b/assets/icons/light/edit.png differ diff --git a/assets/icons/light/file_download.png b/assets/icons/light/file_download.png new file mode 100644 index 0000000..253b96c Binary files /dev/null and b/assets/icons/light/file_download.png differ diff --git a/assets/icons/light/file_upload.png b/assets/icons/light/file_upload.png new file mode 100644 index 0000000..570bd0b Binary files /dev/null and b/assets/icons/light/file_upload.png differ diff --git a/assets/icons/light/folder.png b/assets/icons/light/folder.png new file mode 100644 index 0000000..997afff Binary files /dev/null and b/assets/icons/light/folder.png differ diff --git a/assets/icons/light/folder_open.png b/assets/icons/light/folder_open.png new file mode 100644 index 0000000..f16ba76 Binary files /dev/null and b/assets/icons/light/folder_open.png differ diff --git a/assets/icons/light/info.png b/assets/icons/light/info.png new file mode 100644 index 0000000..76b5a85 Binary files /dev/null and b/assets/icons/light/info.png differ diff --git a/assets/icons/light/language.png b/assets/icons/light/language.png new file mode 100644 index 0000000..da48f4c Binary files /dev/null and b/assets/icons/light/language.png differ diff --git a/assets/icons/light/lock.png b/assets/icons/light/lock.png new file mode 100644 index 0000000..0af6d4d Binary files /dev/null and b/assets/icons/light/lock.png differ diff --git a/assets/icons/light/play_arrow.png b/assets/icons/light/play_arrow.png new file mode 100644 index 0000000..2212b93 Binary files /dev/null and b/assets/icons/light/play_arrow.png differ diff --git a/assets/icons/light/refresh.png b/assets/icons/light/refresh.png new file mode 100644 index 0000000..2a94010 Binary files /dev/null and b/assets/icons/light/refresh.png differ diff --git a/assets/icons/light/save.png b/assets/icons/light/save.png new file mode 100644 index 0000000..27bcd97 Binary files /dev/null and b/assets/icons/light/save.png differ diff --git a/assets/icons/light/search.png b/assets/icons/light/search.png new file mode 100644 index 0000000..0987b39 Binary files /dev/null and b/assets/icons/light/search.png differ diff --git a/assets/icons/light/settings.png b/assets/icons/light/settings.png new file mode 100644 index 0000000..a7cd188 Binary files /dev/null and b/assets/icons/light/settings.png differ diff --git a/assets/icons/light/storage.png b/assets/icons/light/storage.png new file mode 100644 index 0000000..562d301 Binary files /dev/null and b/assets/icons/light/storage.png differ diff --git a/assets/icons/light/trending_up.png b/assets/icons/light/trending_up.png new file mode 100644 index 0000000..bb56640 Binary files /dev/null and b/assets/icons/light/trending_up.png differ diff --git a/assets/icons/light/visibility.png b/assets/icons/light/visibility.png new file mode 100644 index 0000000..a848614 Binary files /dev/null and b/assets/icons/light/visibility.png differ diff --git a/assets/icons/light/vpn_key.png b/assets/icons/light/vpn_key.png new file mode 100644 index 0000000..173b79a Binary files /dev/null and b/assets/icons/light/vpn_key.png differ diff --git a/build.py b/build.py index 8bf5f2c..34a031e 100644 --- a/build.py +++ b/build.py @@ -36,7 +36,13 @@ def auto_bump_version() -> str: sys.exit(1) major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3)) - new_patch = patch + 1 + + # Patch grows up to 99, then minor+1 and patch resets to 1 + if patch >= 99: + minor += 1 + new_patch = 1 + else: + new_patch = patch + 1 new_version = f"{major}.{minor}.{new_patch}" content = re.sub( @@ -111,6 +117,13 @@ def build(): "--add-data", f"core/encryption.py{os.pathsep}core", ] + # PNG icons for GUI (Material Design) + icons_dir = os.path.join(PROJECT_DIR, "assets", "icons") + if os.path.isdir(icons_dir): + cmd_parts.extend(["--add-data", f"assets/icons{os.pathsep}assets/icons"]) + else: + print("WARNING: assets/icons/ not found, building without PNG icons") + # Icon icon_path = os.path.join(PROJECT_DIR, "assets", "icon.ico") if os.path.exists(icon_path): @@ -194,6 +207,47 @@ def _get_gitea_auth() -> dict: _GITEA_API = "https://git.sensey24.ru/api/v1/repos/aibot777/server-manager" +def _generate_changelog() -> str: + """Generate changelog from git commits since previous tag.""" + try: + # Get all tags sorted by semver + raw = subprocess.check_output( + ["git", "tag", "--list", "v*"], + text=True, stderr=subprocess.DEVNULL, + ).strip() + tags = [t for t in raw.splitlines() if re.match(r'^v\d+\.\d+\.\d+$', t)] + tags.sort(key=lambda t: tuple(int(x) for x in t[1:].split("."))) + + current_tag = f"v{__version__}" + # Find previous tag (exclude current if it exists) + prev_tags = [t for t in tags if t != current_tag] + if prev_tags: + prev_tag = prev_tags[-1] + log_range = f"{prev_tag}..HEAD" + else: + log_range = "HEAD~20..HEAD" + + # Get commits + raw_log = subprocess.check_output( + ["git", "log", log_range, "--oneline", "--no-merges"], + text=True, stderr=subprocess.DEVNULL, + ).strip() + + if not raw_log: + return f"Release {current_tag}" + + # Format: strip commit hash, keep message + lines = [] + for line in raw_log.splitlines(): + parts = line.split(" ", 1) + if len(parts) == 2: + lines.append(f"- {parts[1]}") + return f"## What's New in {current_tag}\n\n" + "\n".join(lines) + except Exception as exc: + print(f"Changelog generation failed: {exc}") + return f"Release v{__version__}" + + def publish_gitea_release(exe_path: str): """Create a Gitea release and upload the exe as asset.""" auth = _get_gitea_auth() @@ -203,13 +257,14 @@ def publish_gitea_release(exe_path: str): tag = f"v{__version__}" filename = os.path.basename(exe_path) + changelog = _generate_changelog() # Create release try: data = json.dumps({ "tag_name": tag, "name": tag, - "body": f"Release {tag}", + "body": changelog, }).encode() req = urllib.request.Request( f"{_GITEA_API}/releases", diff --git a/core/icons.py b/core/icons.py index fbbdfbf..7183892 100644 --- a/core/icons.py +++ b/core/icons.py @@ -1,8 +1,42 @@ """ -Icon registry — semantic Unicode symbols for all GUI elements. +Icon registry — semantic Unicode symbols + PNG Material Design icons. Centralized icon management for buttons, tabs, menus, and type badges. + +PNG icons (assets/icons/dark/ + light/) auto-switch with CTk dark/light theme. +If PNG files or PIL are missing, all functions gracefully fall back to Unicode. """ +import os +import sys +from typing import Optional + +# ── Asset path resolution ────────────────────────────── +if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): + _ASSETS_DIR = os.path.join(sys._MEIPASS, "assets", "icons") +else: + _ASSETS_DIR = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "assets", "icons", + ) + +_HAS_PNG = os.path.isdir(_ASSETS_DIR) +_HAS_PIL: Optional[bool] = None # lazy-checked on first ctk_icon() call + +# ── Semantic name → Material icon filename ───────────── +ICON_FILES = { + "back": "arrow_back", "up": "arrow_upward", "refresh": "refresh", + "add": "add", "edit": "edit", "delete": "delete", "confirm": "check", + "upload": "file_upload", "download": "file_download", + "execute": "play_arrow", "info": "info", "clear": "backspace", + "search": "search", "folder": "folder", "folder_open": "folder_open", + "save": "save", "key": "vpn_key", "lock": "lock", "eye": "visibility", + "copy": "content_copy", "gear": "settings", "globe": "language", + "terminal": "code", "query": "play_arrow", "dashboards": "dashboard", + "metrics": "trending_up", "powershell": "code", "launch": "computer", + "totp": "lock", "objects": "storage", "connect": "play_arrow", + "browser": "language", "close": "close", +} + # Semantic icon mapping ICONS = { # Navigation @@ -179,3 +213,83 @@ CTX_ICONS = { "edit": "edit", "delete": "delete", } + + +# ── CTkImage cache + loader ──────────────────────────── +_icon_cache: dict[tuple, object] = {} + + +def ctk_icon(name: str, size: int = 20) -> Optional[object]: + """PNG icon as CTkImage, or None (fallback to Unicode). + + Lazy-imports PIL and customtkinter on first call so that + icons.py stays usable as a pure data module for CLI tools. + """ + global _HAS_PIL + if _HAS_PIL is None: + try: + from PIL import Image as _img # noqa: F401 + import customtkinter as _ctk # noqa: F401 + _HAS_PIL = True + except ImportError: + _HAS_PIL = False + if not _HAS_PIL or not _HAS_PNG: + return None + + cache_key = (name, size) + if cache_key in _icon_cache: + return _icon_cache[cache_key] + + file_stem = ICON_FILES.get(name) + if not file_stem: + return None + + from PIL import Image + import customtkinter as ctk + + dark_path = os.path.join(_ASSETS_DIR, "dark", f"{file_stem}.png") + light_path = os.path.join(_ASSETS_DIR, "light", f"{file_stem}.png") + + if not os.path.exists(dark_path) or not os.path.exists(light_path): + return None + try: + light_img = Image.open(light_path) # black icons for light bg + dark_img = Image.open(dark_path) # white icons for dark bg + result = ctk.CTkImage( + light_image=light_img, dark_image=dark_img, + size=(size, size), + ) + _icon_cache[cache_key] = result + return result + except Exception: + return None + + +def make_icon_button(parent, icon_name: str, label: str, + icon_size: int = 16, **kwargs): + """CTkButton with PNG icon or Unicode fallback.""" + import customtkinter as _ctk + img = ctk_icon(icon_name, icon_size) + if img: + return _ctk.CTkButton(parent, text=label, image=img, + compound="left", **kwargs) + return _ctk.CTkButton(parent, text=icon_text(icon_name, label), **kwargs) + + +def make_icon_label(parent, icon_name: str, icon_size: int = 18, **kwargs): + """CTkLabel with PNG icon or Unicode fallback.""" + import customtkinter as _ctk + img = ctk_icon(icon_name, icon_size) + if img: + return _ctk.CTkLabel(parent, text="", image=img, **kwargs) + return _ctk.CTkLabel(parent, text=ICONS.get(icon_name, ""), **kwargs) + + +def reconfigure_icon_button(btn, icon_name: str, label: str, + icon_size: int = 16): + """Update existing button text + image (for update_language).""" + img = ctk_icon(icon_name, icon_size) + if img: + btn.configure(text=label, image=img) + else: + btn.configure(text=icon_text(icon_name, label)) diff --git a/gui/app.py b/gui/app.py index 6d0e76b..3117dca 100644 --- a/gui/app.py +++ b/gui/app.py @@ -11,7 +11,7 @@ from core.status_checker import StatusChecker from core.updater import UpdateChecker from core import i18n from core.i18n import t, LANGUAGES -from core.icons import icon, TAB_ICONS +from core.icons import icon, TAB_ICONS, ctk_icon from core.session_pool import SessionPool from gui.sidebar import Sidebar from gui.server_dialog import ServerDialog @@ -147,7 +147,11 @@ class App(ctk.CTk): header_bar.pack_propagate(False) # Language selector - self._lang_icon = ctk.CTkLabel(header_bar, text="\U0001f310", font=ctk.CTkFont(size=14), width=20) + _lang_img = ctk_icon("globe", 18) + self._lang_icon = ctk.CTkLabel( + header_bar, text="" if _lang_img else "\U0001f310", + image=_lang_img, font=ctk.CTkFont(size=14), width=20, + ) self._lang_icon.pack(side="right", padx=(5, 0)) lang_values = list(LANGUAGES.values()) current_display = LANGUAGES.get(i18n.get_language(), "English") @@ -159,16 +163,20 @@ class App(ctk.CTk): self.lang_menu.pack(side="right", padx=(5, 0)) # Check Updates button + _sync_img = ctk_icon("refresh", 18) self._update_check_btn = ctk.CTkButton( - header_bar, text="\u21bb", width=30, height=30, + header_bar, text="" if _sync_img else "\u21bb", + image=_sync_img, width=30, height=30, corner_radius=15, fg_color="#6b7280", hover_color="#4b5563", command=self._check_updates_manual, ) self._update_check_btn.pack(side="right", padx=(5, 0)) # About button + _info_img = ctk_icon("info", 18) self.about_btn = ctk.CTkButton( - header_bar, text="ⓘ", width=30, height=30, + header_bar, text="" if _info_img else "ⓘ", + image=_info_img, width=30, height=30, corner_radius=15, fg_color="#6b7280", hover_color="#4b5563", command=self._show_about ) diff --git a/gui/server_dialog.py b/gui/server_dialog.py index e704bcd..52aea6c 100644 --- a/gui/server_dialog.py +++ b/gui/server_dialog.py @@ -6,7 +6,7 @@ Form adapts visible fields based on selected server type. import customtkinter as ctk from core.server_store import SERVER_TYPES, DEFAULT_PORTS from core.i18n import t -from core.icons import icon_text, type_display, type_from_display +from core.icons import icon_text, type_display, type_from_display, make_icon_button, reconfigure_icon_button # Which conditional fields to show for each server type. @@ -160,7 +160,7 @@ class ServerDialog(ctk.CTkToplevel): pass_inner.pack(fill="x", padx=20, pady=(2, 5)) self.password_entry = ctk.CTkEntry(pass_inner, show="*", placeholder_text=t("placeholder_password")) self.password_entry.pack(side="left", fill="x", expand=True, padx=(0, 5)) - self.show_pass = ctk.CTkButton(pass_inner, text=icon_text("eye", t("show")), width=70, command=self._toggle_password) + self.show_pass = make_icon_button(pass_inner, "eye", t("show"), width=70, command=self._toggle_password) self.show_pass.pack(side="right") self._pass_visible = False self._field_frames["password"] = f @@ -286,8 +286,8 @@ class ServerDialog(ctk.CTkToplevel): # ── Always visible: Buttons ── btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=20, pady=(15, 20)) - ctk.CTkButton(btn_frame, text=icon_text("delete", t("cancel")), fg_color="#6b7280", command=self.destroy).pack(side="left", expand=True, padx=(0, 5)) - ctk.CTkButton(btn_frame, text=icon_text("confirm", t("save")), command=self._save).pack(side="right", expand=True, padx=(5, 0)) + make_icon_button(btn_frame, "close", t("cancel"), fg_color="#6b7280", command=self.destroy).pack(side="left", expand=True, padx=(0, 5)) + make_icon_button(btn_frame, "confirm", t("save"), command=self._save).pack(side="right", expand=True, padx=(5, 0)) # Fill values if editing if server: @@ -360,7 +360,7 @@ class ServerDialog(ctk.CTkToplevel): def _toggle_password(self): self._pass_visible = not self._pass_visible self.password_entry.configure(show="" if self._pass_visible else "*") - self.show_pass.configure(text=icon_text("eye", t("hide") if self._pass_visible else t("show"))) + reconfigure_icon_button(self.show_pass, "eye", t("hide") if self._pass_visible else t("show")) def _save(self): alias = self.alias_entry.get().strip() diff --git a/gui/sidebar.py b/gui/sidebar.py index 2c51cf7..4caae3e 100644 --- a/gui/sidebar.py +++ b/gui/sidebar.py @@ -8,6 +8,7 @@ import customtkinter as ctk from core.i18n import t from core.icons import ( icon_text, TYPE_COLORS, TYPE_LABELS, CTX_ICONS, icon, + make_icon_button, reconfigure_icon_button, ) from gui.widgets.status_badge import StatusBadge @@ -80,11 +81,11 @@ class Sidebar(ctk.CTkFrame): # Buttons btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=10, pady=10) - self.add_btn = ctk.CTkButton(btn_frame, text=icon_text("add", t("add")), width=70, height=30, command=self._on_add) + self.add_btn = make_icon_button(btn_frame, "add", t("add"), width=70, height=30, command=self._on_add) self.add_btn.pack(side="left", padx=(0, 3)) - self.edit_btn = ctk.CTkButton(btn_frame, text=icon_text("edit", t("edit")), width=70, height=30, fg_color="#6b7280", command=self._on_edit) + self.edit_btn = make_icon_button(btn_frame, "edit", t("edit"), width=70, height=30, fg_color="#6b7280", command=self._on_edit) self.edit_btn.pack(side="left", padx=3) - self.del_btn = ctk.CTkButton(btn_frame, text=icon_text("delete", t("delete")), width=70, height=30, fg_color="#ef4444", hover_color="#dc2626", command=self._on_delete) + self.del_btn = make_icon_button(btn_frame, "delete", t("delete"), width=70, height=30, fg_color="#ef4444", hover_color="#dc2626", command=self._on_delete) self.del_btn.pack(side="right", padx=(3, 0)) # Callbacks — set by app.py @@ -103,9 +104,9 @@ class Sidebar(ctk.CTkFrame): def update_language(self): self.title_label.configure(text=t("servers")) self.search_entry.configure(placeholder_text=t("search")) - self.add_btn.configure(text=icon_text("add", t("add"))) - self.edit_btn.configure(text=icon_text("edit", t("edit"))) - self.del_btn.configure(text=icon_text("delete", t("delete"))) + reconfigure_icon_button(self.add_btn, "add", t("add")) + reconfigure_icon_button(self.edit_btn, "edit", t("edit")) + reconfigure_icon_button(self.del_btn, "delete", t("delete")) self._update_sessions_label() # ── Refresh / Render ────────────────────────────── diff --git a/gui/tabs/files_tab.py b/gui/tabs/files_tab.py index e2c05bb..52ab481 100644 --- a/gui/tabs/files_tab.py +++ b/gui/tabs/files_tab.py @@ -13,7 +13,7 @@ from tkinter import messagebox, filedialog import customtkinter as ctk from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, ctk_icon, make_icon_button from core.ssh_client import SFTPSession from gui.widgets.file_list import FileListWidget @@ -90,28 +90,34 @@ class FilesTab(ctk.CTkFrame): ctk.CTkLabel(left_header, text=t("local_files"), font=ctk.CTkFont(size=13, weight="bold")).pack(side="left") + _back_img = ctk_icon("back", 16) self._local_back_btn = ctk.CTkButton( - left_header, text="\u2190", width=30, height=28, + left_header, text="" if _back_img else "\u2190", + image=_back_img, width=30, height=28, command=self._local_go_back, ) self._local_back_btn.pack(side="left", padx=(8, 2)) + _up_img = ctk_icon("up", 16) self._local_up_btn = ctk.CTkButton( - left_header, text="\u2191", width=30, height=28, + left_header, text="" if _up_img else "\u2191", + image=_up_img, width=30, height=28, command=self._local_go_up, ) self._local_up_btn.pack(side="left", padx=2) # Local refresh button + _ref_img = ctk_icon("refresh", 16) self._local_refresh_btn = ctk.CTkButton( - left_header, text="\u21BB", width=30, height=28, + left_header, text="" if _ref_img else "\u21BB", + image=_ref_img, width=30, height=28, command=self._refresh_local, ) self._local_refresh_btn.pack(side="left", padx=2) # Browse button - self._browse_btn = ctk.CTkButton( - left_header, text=icon_text("folder_open", t("browse")), width=75, height=28, + self._browse_btn = make_icon_button( + left_header, "folder_open", t("browse"), width=75, height=28, command=self._browse_local, ) self._browse_btn.pack(side="left", padx=2) @@ -158,19 +164,22 @@ class FilesTab(ctk.CTkFrame): font=ctk.CTkFont(size=13, weight="bold")).pack(side="left") self._remote_back_btn = ctk.CTkButton( - right_header, text="\u2190", width=30, height=28, + right_header, text="" if _back_img else "\u2190", + image=_back_img, width=30, height=28, command=self._remote_go_back, ) self._remote_back_btn.pack(side="left", padx=(8, 2)) self._remote_up_btn = ctk.CTkButton( - right_header, text="\u2191", width=30, height=28, + right_header, text="" if _up_img else "\u2191", + image=_up_img, width=30, height=28, command=self._remote_go_up, ) self._remote_up_btn.pack(side="left", padx=2) self._remote_refresh_btn = ctk.CTkButton( - right_header, text="\u21BB", width=30, height=28, + right_header, text="" if _ref_img else "\u21BB", + image=_ref_img, width=30, height=28, command=self._refresh_remote, ) self._remote_refresh_btn.pack(side="left", padx=2) @@ -204,14 +213,14 @@ class FilesTab(ctk.CTkFrame): toolbar = ctk.CTkFrame(self, fg_color="transparent") toolbar.pack(fill="x", padx=10, pady=4) - self._upload_btn = ctk.CTkButton( - toolbar, text=icon_text("upload", t("upload")), width=110, height=30, + self._upload_btn = make_icon_button( + toolbar, "upload", t("upload"), width=110, height=30, command=self._upload_selected, ) self._upload_btn.pack(side="left", padx=(0, 4)) - self._download_btn = ctk.CTkButton( - toolbar, text=icon_text("download", t("download")), width=110, height=30, + self._download_btn = make_icon_button( + toolbar, "download", t("download"), width=110, height=30, command=self._download_selected, ) self._download_btn.pack(side="left", padx=4) @@ -219,21 +228,21 @@ class FilesTab(ctk.CTkFrame): sep = ctk.CTkFrame(toolbar, width=2, height=24, fg_color="gray40") sep.pack(side="left", padx=8) - self._mkdir_btn = ctk.CTkButton( - toolbar, text=icon_text("folder", t("new_folder")), width=110, height=30, + self._mkdir_btn = make_icon_button( + toolbar, "folder", t("new_folder"), width=110, height=30, command=self._mkdir_remote, ) self._mkdir_btn.pack(side="left", padx=4) - self._delete_btn = ctk.CTkButton( - toolbar, text=icon_text("delete", t("delete_files")), width=90, height=30, + self._delete_btn = make_icon_button( + toolbar, "delete", t("delete_files"), width=90, height=30, fg_color="#dc2626", hover_color="#b91c1c", command=self._delete_remote, ) self._delete_btn.pack(side="left", padx=4) - self._rename_btn = ctk.CTkButton( - toolbar, text=icon_text("edit", t("rename_file")), width=110, height=30, + self._rename_btn = make_icon_button( + toolbar, "edit", t("rename_file"), width=110, height=30, command=self._rename_remote, ) self._rename_btn.pack(side="left", padx=4) diff --git a/gui/tabs/grafana_tab.py b/gui/tabs/grafana_tab.py index 64196fe..9f97c9d 100644 --- a/gui/tabs/grafana_tab.py +++ b/gui/tabs/grafana_tab.py @@ -9,7 +9,7 @@ from tkinter import ttk import customtkinter as ctk from core.grafana_client import GrafanaClient from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button, reconfigure_icon_button from gui.tabs.query_tab import apply_dark_scrollbar_style @@ -33,8 +33,8 @@ class GrafanaTab(ctk.CTkFrame): font=ctk.CTkFont(size=18, weight="bold")) title.pack(side="left") - self._refresh_btn = ctk.CTkButton(header_frame, text=icon_text("refresh", t("grafana_refresh")), width=110, - command=self._refresh) + self._refresh_btn = make_icon_button(header_frame, "refresh", t("grafana_refresh"), width=110, + command=self._refresh) self._refresh_btn.pack(side="right") # ── Dashboards section ── @@ -131,8 +131,10 @@ class GrafanaTab(ctk.CTkFrame): except Exception as e: self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444")) finally: - self.after(0, lambda: self._refresh_btn.configure( - state="normal", text=icon_text("refresh", t("grafana_refresh")))) + self.after(0, lambda: ( + self._refresh_btn.configure(state="normal"), + reconfigure_icon_button(self._refresh_btn, "refresh", t("grafana_refresh")), + )) threading.Thread(target=_do, daemon=True).start() diff --git a/gui/tabs/info_tab.py b/gui/tabs/info_tab.py index 5c8e948..6fdedb1 100644 --- a/gui/tabs/info_tab.py +++ b/gui/tabs/info_tab.py @@ -4,7 +4,7 @@ Info tab — display server details, edit button. import customtkinter as ctk from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button class InfoTab(ctk.CTkFrame): @@ -56,7 +56,7 @@ class InfoTab(ctk.CTkFrame): self._fields[key] = val # Edit button - self.edit_btn = ctk.CTkButton(self, text=icon_text("edit", t("edit_server_btn")), command=self._on_edit) + self.edit_btn = make_icon_button(self, "edit", t("edit_server_btn"), command=self._on_edit) self.edit_btn.pack(pady=15) def set_server(self, alias: str | None): diff --git a/gui/tabs/keys_tab.py b/gui/tabs/keys_tab.py index ef0c1ef..6f7ff2a 100644 --- a/gui/tabs/keys_tab.py +++ b/gui/tabs/keys_tab.py @@ -7,7 +7,7 @@ import threading import customtkinter as ctk from core.ssh_client import SSHClientWrapper from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button class KeysTab(ctk.CTkFrame): @@ -30,13 +30,13 @@ class KeysTab(ctk.CTkFrame): btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=15, pady=5) - self.gen_btn = ctk.CTkButton(btn_frame, text=icon_text("key", t("generate_key")), command=self._generate) + self.gen_btn = make_icon_button(btn_frame, "key", t("generate_key"), command=self._generate) self.gen_btn.pack(side="left", padx=(0, 10)) - self.install_btn = ctk.CTkButton(btn_frame, text=icon_text("upload", t("install_on_server")), fg_color="#22c55e", hover_color="#16a34a", command=self._install) + self.install_btn = make_icon_button(btn_frame, "upload", t("install_on_server"), fg_color="#22c55e", hover_color="#16a34a", command=self._install) self.install_btn.pack(side="left") - self.copy_btn = ctk.CTkButton(btn_frame, text=icon_text("copy", t("copy_public_key")), fg_color="#6b7280", command=self._copy_key) + self.copy_btn = make_icon_button(btn_frame, "copy", t("copy_public_key"), fg_color="#6b7280", command=self._copy_key) self.copy_btn.pack(side="right") # Status log diff --git a/gui/tabs/launch_tab.py b/gui/tabs/launch_tab.py index 90e91d5..8938d65 100644 --- a/gui/tabs/launch_tab.py +++ b/gui/tabs/launch_tab.py @@ -10,7 +10,7 @@ import threading import customtkinter as ctk from core.remote_desktop import RemoteDesktopLauncher from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button, reconfigure_icon_button from core.logger import log @@ -33,15 +33,15 @@ class LaunchTab(ctk.CTkFrame): # ── Toolbar (shown when RDP connected) ── self._toolbar = ctk.CTkFrame(self, height=36, fg_color="transparent") - self._disconnect_btn = ctk.CTkButton( - self._toolbar, text=icon_text("delete", t("rdp_disconnect")), + self._disconnect_btn = make_icon_button( + self._toolbar, "delete", t("rdp_disconnect"), width=120, height=30, fg_color="#ef4444", hover_color="#dc2626", command=self._disconnect, ) self._disconnect_btn.pack(side="left", padx=(8, 4)) - self._fullscreen_btn = ctk.CTkButton( - self._toolbar, text=icon_text("launch", t("rdp_fullscreen")), + self._fullscreen_btn = make_icon_button( + self._toolbar, "launch", t("rdp_fullscreen"), width=130, height=30, fg_color="#6b7280", hover_color="#4b5563", command=self._toggle_fullscreen, ) @@ -135,8 +135,9 @@ class LaunchTab(ctk.CTkFrame): ).pack(fill="x", padx=15, pady=(3, 12)) # Connect button - self._connect_btn = ctk.CTkButton( - self._settings_panel, text=icon_text("execute", t("launch_connect")), + self._connect_btn = make_icon_button( + self._settings_panel, "execute", t("launch_connect"), + icon_size=20, font=ctk.CTkFont(size=18, weight="bold"), width=220, height=50, command=self._on_connect, @@ -374,9 +375,7 @@ class LaunchTab(ctk.CTkFrame): if self._is_fullscreen: # Exit fullscreen — reattach self._is_fullscreen = False - self._fullscreen_btn.configure( - text=icon_text("launch", t("rdp_fullscreen")), - ) + reconfigure_icon_button(self._fullscreen_btn, "launch", t("rdp_fullscreen")) self._rdp_frame.update_idletasks() parent_hwnd = self._rdp_frame.winfo_id() w = self._rdp_frame.winfo_width() @@ -385,9 +384,7 @@ class LaunchTab(ctk.CTkFrame): else: # Go fullscreen — detach, then maximize after event loop settles self._is_fullscreen = True - self._fullscreen_btn.configure( - text=icon_text("back", t("rdp_exit_fullscreen")), - ) + reconfigure_icon_button(self._fullscreen_btn, "back", t("rdp_exit_fullscreen")) self._embedded_rdp.detach() self.after(300, self._maximize_detached) diff --git a/gui/tabs/powershell_tab.py b/gui/tabs/powershell_tab.py index e433a70..1986a9d 100644 --- a/gui/tabs/powershell_tab.py +++ b/gui/tabs/powershell_tab.py @@ -8,7 +8,7 @@ import threading import customtkinter as ctk from core.winrm_client import WinRMClient from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button class PowershellTab(ctk.CTkFrame): @@ -68,8 +68,8 @@ class PowershellTab(ctk.CTkFrame): self._entry.bind("", lambda e: self._history_navigate(-1)) self._entry.bind("", lambda e: self._history_navigate(1)) - self._exec_btn = ctk.CTkButton( - input_row, text=icon_text("execute", t("ps_execute")), width=100, + self._exec_btn = make_icon_button( + input_row, "execute", t("ps_execute"), width=100, command=self._execute, ) self._exec_btn.pack(side="right") diff --git a/gui/tabs/prometheus_tab.py b/gui/tabs/prometheus_tab.py index 459651a..226c1a2 100644 --- a/gui/tabs/prometheus_tab.py +++ b/gui/tabs/prometheus_tab.py @@ -8,7 +8,7 @@ from tkinter import ttk import customtkinter as ctk from core.prometheus_client import PrometheusClient from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button, reconfigure_icon_button from gui.tabs.query_tab import apply_dark_scrollbar_style @@ -37,8 +37,8 @@ class PrometheusTab(ctk.CTkFrame): self._query_entry.pack(side="left", fill="x", expand=True, padx=(0, 10)) self._query_entry.bind("", lambda e: self._execute_query()) - self._exec_btn = ctk.CTkButton(query_frame, text=icon_text("execute", t("prom_execute")), width=100, - command=self._execute_query) + self._exec_btn = make_icon_button(query_frame, "execute", t("prom_execute"), width=100, + command=self._execute_query) self._exec_btn.pack(side="left") # ── Query results ── @@ -59,8 +59,8 @@ class PrometheusTab(ctk.CTkFrame): font=ctk.CTkFont(size=14, weight="bold"), anchor="w") targets_label.pack(side="left") - self._refresh_btn = ctk.CTkButton(targets_header, text=icon_text("refresh", t("prom_refresh")), width=100, - command=self._refresh_all) + self._refresh_btn = make_icon_button(targets_header, "refresh", t("prom_refresh"), width=100, + command=self._refresh_all) self._refresh_btn.pack(side="right") targets_frame = ctk.CTkFrame(self, fg_color="transparent") @@ -201,8 +201,10 @@ class PrometheusTab(ctk.CTkFrame): except Exception as e: self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444")) finally: - self.after(0, lambda: self._refresh_btn.configure( - state="normal", text=icon_text("refresh", t("prom_refresh")))) + self.after(0, lambda: ( + self._refresh_btn.configure(state="normal"), + reconfigure_icon_button(self._refresh_btn, "refresh", t("prom_refresh")), + )) threading.Thread(target=_do, daemon=True).start() diff --git a/gui/tabs/query_tab.py b/gui/tabs/query_tab.py index c585c9b..71bae17 100644 --- a/gui/tabs/query_tab.py +++ b/gui/tabs/query_tab.py @@ -14,7 +14,7 @@ from tkinter import ttk, filedialog import customtkinter as ctk from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button from core.sql_client import SQLClient _TREE_THEME_APPLIED = False @@ -227,9 +227,8 @@ class QueryTab(ctk.CTkFrame): btn_row = ctk.CTkFrame(right_ctk, fg_color="transparent") btn_row.pack(fill="x", padx=8, pady=4) - self._exec_btn = ctk.CTkButton( - btn_row, - text=icon_text("execute", t("query_execute")), + self._exec_btn = make_icon_button( + btn_row, "execute", t("query_execute"), command=self._execute_query, width=130, fg_color="#2563eb", @@ -237,9 +236,8 @@ class QueryTab(ctk.CTkFrame): ) self._exec_btn.pack(side="left", padx=(0, 6)) - self._clear_btn = ctk.CTkButton( - btn_row, - text=icon_text("clear", t("query_clear")), + self._clear_btn = make_icon_button( + btn_row, "clear", t("query_clear"), command=self._clear_all, width=80, fg_color="#6b7280", @@ -247,9 +245,8 @@ class QueryTab(ctk.CTkFrame): ) self._clear_btn.pack(side="left", padx=(0, 6)) - self._export_btn = ctk.CTkButton( - btn_row, - text=icon_text("save", t("query_export_csv")), + self._export_btn = make_icon_button( + btn_row, "save", t("query_export_csv"), command=self._export_csv, width=110, fg_color="#059669", diff --git a/gui/tabs/redis_tab.py b/gui/tabs/redis_tab.py index 28e7154..bd9a1e4 100644 --- a/gui/tabs/redis_tab.py +++ b/gui/tabs/redis_tab.py @@ -6,7 +6,7 @@ import threading import customtkinter as ctk from core.redis_client import RedisClient from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button class RedisTab(ctk.CTkFrame): @@ -67,28 +67,28 @@ class RedisTab(ctk.CTkFrame): btn_frame = ctk.CTkFrame(self, fg_color="transparent") btn_frame.pack(fill="x", padx=15, pady=5) - self._exec_btn = ctk.CTkButton(btn_frame, text=icon_text("execute", t("redis_execute")), width=100, - command=self._execute_command) + self._exec_btn = make_icon_button(btn_frame, "execute", t("redis_execute"), width=100, + command=self._execute_command) self._exec_btn.pack(side="left", padx=(0, 5)) - self._info_btn = ctk.CTkButton(btn_frame, text=icon_text("info", "INFO"), width=80, - fg_color="#6b7280", hover_color="#4b5563", - command=lambda: self._run_quick("INFO")) + self._info_btn = make_icon_button(btn_frame, "info", "INFO", width=80, + fg_color="#6b7280", hover_color="#4b5563", + command=lambda: self._run_quick("INFO")) self._info_btn.pack(side="left", padx=(0, 5)) - self._dbsize_btn = ctk.CTkButton(btn_frame, text=icon_text("hash", "DBSIZE"), width=90, - fg_color="#6b7280", hover_color="#4b5563", - command=lambda: self._run_quick("DBSIZE")) + self._dbsize_btn = make_icon_button(btn_frame, "info", "DBSIZE", width=90, + fg_color="#6b7280", hover_color="#4b5563", + command=lambda: self._run_quick("DBSIZE")) self._dbsize_btn.pack(side="left", padx=(0, 5)) - self._scan_btn = ctk.CTkButton(btn_frame, text=icon_text("search", "SCAN"), width=80, - fg_color="#6b7280", hover_color="#4b5563", - command=lambda: self._run_quick("SCAN 0 COUNT 100")) + self._scan_btn = make_icon_button(btn_frame, "search", "SCAN", width=80, + fg_color="#6b7280", hover_color="#4b5563", + command=lambda: self._run_quick("SCAN 0 COUNT 100")) self._scan_btn.pack(side="left", padx=(0, 5)) - self._clear_btn = ctk.CTkButton(btn_frame, text=icon_text("clear", t("redis_clear")), width=80, - fg_color="#374151", hover_color="#1f2937", - command=self._clear_output) + self._clear_btn = make_icon_button(btn_frame, "clear", t("redis_clear"), width=80, + fg_color="#374151", hover_color="#1f2937", + command=self._clear_output) self._clear_btn.pack(side="right") # ── Output console ── diff --git a/gui/tabs/s3_tab.py b/gui/tabs/s3_tab.py index 44ef9c0..8387b21 100644 --- a/gui/tabs/s3_tab.py +++ b/gui/tabs/s3_tab.py @@ -12,7 +12,7 @@ from tkinter import ttk, filedialog import customtkinter as ctk from core.s3_client import S3Client from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button from gui.tabs.query_tab import apply_dark_scrollbar_style @@ -103,32 +103,32 @@ class S3Tab(ctk.CTkFrame): btn_frame = ctk.CTkFrame(header, fg_color="transparent") btn_frame.pack(side="right") - self._back_btn = ctk.CTkButton( - btn_frame, text=icon_text("back", t("s3_back")), width=80, + self._back_btn = make_icon_button( + btn_frame, "back", t("s3_back"), width=80, command=self._go_back, state="disabled", ) self._back_btn.pack(side="left", padx=(0, 5)) - self._refresh_btn = ctk.CTkButton( - btn_frame, text=icon_text("refresh", t("s3_refresh")), width=100, + self._refresh_btn = make_icon_button( + btn_frame, "refresh", t("s3_refresh"), width=100, command=self._refresh, ) self._refresh_btn.pack(side="left", padx=(0, 5)) - self._upload_btn = ctk.CTkButton( - btn_frame, text=icon_text("upload", t("s3_upload")), width=100, + self._upload_btn = make_icon_button( + btn_frame, "upload", t("s3_upload"), width=100, command=self._upload, ) self._upload_btn.pack(side="left", padx=(0, 5)) - self._download_btn = ctk.CTkButton( - btn_frame, text=icon_text("download", t("s3_download")), width=110, + self._download_btn = make_icon_button( + btn_frame, "download", t("s3_download"), width=110, command=self._download, ) self._download_btn.pack(side="left", padx=(0, 5)) - self._delete_btn = ctk.CTkButton( - btn_frame, text=icon_text("delete", t("s3_delete")), width=100, + self._delete_btn = make_icon_button( + btn_frame, "delete", t("s3_delete"), width=100, fg_color="#dc2626", hover_color="#b91c1c", command=self._delete, ) diff --git a/gui/tabs/setup_tab.py b/gui/tabs/setup_tab.py index bd1e893..a784983 100644 --- a/gui/tabs/setup_tab.py +++ b/gui/tabs/setup_tab.py @@ -10,7 +10,7 @@ from tkinter import filedialog, messagebox import customtkinter as ctk from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button from core.logger import log @@ -70,8 +70,8 @@ class SetupTab(ctk.CTkFrame): btn_frame = ctk.CTkFrame(self._scroll, fg_color="transparent") btn_frame.pack(fill="x", padx=20, pady=15) - self.install_all_btn = ctk.CTkButton( - btn_frame, text=icon_text("confirm", t("install_everything")), + self.install_all_btn = make_icon_button( + btn_frame, "confirm", t("install_everything"), font=ctk.CTkFont(size=14, weight="bold"), height=40, fg_color="#22c55e", hover_color="#16a34a", command=self._install_all @@ -82,16 +82,16 @@ class SetupTab(ctk.CTkFrame): ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent") ind_frame.pack(fill="x") - self.ssh_py_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_ssh_py")), width=110, fg_color="#6b7280", + self.ssh_py_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_py"), width=110, fg_color="#6b7280", command=self._install_script) self.ssh_py_btn.pack(side="left", padx=(0, 5)) - self.skill_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_skill")), width=110, fg_color="#6b7280", + self.skill_btn = make_icon_button(ind_frame, "confirm", t("install_skill"), width=110, fg_color="#6b7280", command=self._install_skill) self.skill_btn.pack(side="left", padx=5) - self.ssh_key_btn = ctk.CTkButton(ind_frame, text=icon_text("confirm", t("install_ssh_key")), width=110, fg_color="#6b7280", + self.ssh_key_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_key"), width=110, fg_color="#6b7280", command=self._gen_key) self.ssh_key_btn.pack(side="left", padx=5) - self.refresh_btn = ctk.CTkButton(ind_frame, text=icon_text("refresh", t("refresh")), width=90, fg_color="#3b82f6", + self.refresh_btn = make_icon_button(ind_frame, "refresh", t("refresh"), width=90, fg_color="#3b82f6", command=self._refresh_status) self.refresh_btn.pack(side="right") @@ -185,8 +185,8 @@ class SetupTab(ctk.CTkFrame): font=ctk.CTkFont(family="Consolas", size=11) ) self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10)) - self.change_path_btn = ctk.CTkButton( - path_row, text=icon_text("folder", t("change_path")), width=120, fg_color="#6b7280", + self.change_path_btn = make_icon_button( + path_row, "folder", t("change_path"), width=120, fg_color="#6b7280", command=self._change_config_path ) self.change_path_btn.pack(side="right") @@ -195,8 +195,8 @@ class SetupTab(ctk.CTkFrame): backup_row = ctk.CTkFrame(config_frame, fg_color="transparent") backup_row.pack(fill="x", padx=15, pady=(5, 10)) - self.backup_btn = ctk.CTkButton( - backup_row, text=icon_text("save", t("backup_now")), width=120, fg_color="#3b82f6", + self.backup_btn = make_icon_button( + backup_row, "save", t("backup_now"), width=120, fg_color="#3b82f6", command=self._backup_now ) self.backup_btn.pack(side="left", padx=(0, 10)) @@ -210,8 +210,8 @@ class SetupTab(ctk.CTkFrame): ) self._backup_menu.pack(side="left", padx=(0, 10)) - self.restore_btn = ctk.CTkButton( - backup_row, text=icon_text("refresh", t("restore")), width=100, fg_color="#ef4444", hover_color="#dc2626", + self.restore_btn = make_icon_button( + backup_row, "refresh", t("restore"), width=100, fg_color="#ef4444", hover_color="#dc2626", command=self._restore_backup ) self.restore_btn.pack(side="left") @@ -220,26 +220,26 @@ class SetupTab(ctk.CTkFrame): ie_row = ctk.CTkFrame(config_frame, fg_color="transparent") ie_row.pack(fill="x", padx=15, pady=(0, 10)) - self.export_config_btn = ctk.CTkButton( - ie_row, text=icon_text("upload", t("export_config")), width=130, fg_color="#6b7280", + self.export_config_btn = make_icon_button( + ie_row, "upload", t("export_config"), width=130, fg_color="#6b7280", command=self._export_config ) self.export_config_btn.pack(side="left", padx=(0, 5)) - self.import_config_btn = ctk.CTkButton( - ie_row, text=icon_text("download", t("import_config")), width=130, fg_color="#6b7280", + self.import_config_btn = make_icon_button( + ie_row, "download", t("import_config"), width=130, fg_color="#6b7280", command=self._import_config ) self.import_config_btn.pack(side="left", padx=5) - self.export_backup_btn = ctk.CTkButton( - ie_row, text=icon_text("upload", t("export_backup")), width=130, fg_color="#6b7280", + self.export_backup_btn = make_icon_button( + ie_row, "upload", t("export_backup"), width=130, fg_color="#6b7280", command=self._export_backup ) self.export_backup_btn.pack(side="left", padx=5) - self.import_backup_btn = ctk.CTkButton( - ie_row, text=icon_text("download", t("import_backup")), width=130, fg_color="#6b7280", + self.import_backup_btn = make_icon_button( + ie_row, "download", t("import_backup"), width=130, fg_color="#6b7280", command=self._import_backup ) self.import_backup_btn.pack(side="left", padx=5) diff --git a/gui/tabs/totp_tab.py b/gui/tabs/totp_tab.py index ff357e6..f43dd98 100644 --- a/gui/tabs/totp_tab.py +++ b/gui/tabs/totp_tab.py @@ -6,7 +6,7 @@ Live countdown, one-click copy, per-server secrets. import threading import customtkinter as ctk from core.i18n import t -from core.icons import icon_text +from core.icons import icon_text, make_icon_button, reconfigure_icon_button class TOTPTab(ctk.CTkFrame): @@ -81,8 +81,8 @@ class TOTPTab(ctk.CTkFrame): widget.bind("", lambda e: self._copy_code()) # Copy button - self.copy_btn = ctk.CTkButton( - self, text=icon_text("copy", t("totp_copy")), width=200, height=40, + self.copy_btn = make_icon_button( + self, "copy", t("totp_copy"), width=200, height=40, font=ctk.CTkFont(size=14), fg_color="#22c55e", hover_color="#16a34a", command=self._copy_code @@ -108,30 +108,30 @@ class TOTPTab(ctk.CTkFrame): ) self.secret_entry.pack(side="left", fill="x", expand=True, padx=(0, 5)) - self.show_secret_btn = ctk.CTkButton( - entry_row, text=icon_text("eye", t("show")), width=80, + self.show_secret_btn = make_icon_button( + entry_row, "eye", t("show"), width=80, fg_color="#6b7280", hover_color="#4b5563", command=self._toggle_secret ) self.show_secret_btn.pack(side="left", padx=(0, 5)) self._secret_visible = False - self.save_secret_btn = ctk.CTkButton( - entry_row, text=icon_text("confirm", t("totp_save_secret")), width=110, + self.save_secret_btn = make_icon_button( + entry_row, "confirm", t("totp_save_secret"), width=110, command=self._save_secret ) self.save_secret_btn.pack(side="left", padx=(0, 5)) - self.remove_secret_btn = ctk.CTkButton( - entry_row, text=icon_text("delete", t("totp_remove_secret")), width=110, + self.remove_secret_btn = make_icon_button( + entry_row, "delete", t("totp_remove_secret"), width=110, fg_color="#ef4444", hover_color="#dc2626", command=self._remove_secret ) self.remove_secret_btn.pack(side="left") # Generate random secret button - self.gen_secret_btn = ctk.CTkButton( - secret_frame, text=icon_text("key", t("totp_generate_secret")), width=200, + self.gen_secret_btn = make_icon_button( + secret_frame, "key", t("totp_generate_secret"), width=200, fg_color="#6b7280", hover_color="#4b5563", command=self._generate_secret ) @@ -267,8 +267,8 @@ class TOTPTab(ctk.CTkFrame): def _toggle_secret(self): self._secret_visible = not self._secret_visible self.secret_entry.configure(show="" if self._secret_visible else "*") - self.show_secret_btn.configure( - text=icon_text("eye", t("hide") if self._secret_visible else t("show")) + reconfigure_icon_button( + self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show") ) def _save_secret(self): @@ -331,12 +331,12 @@ class TOTPTab(ctk.CTkFrame): def update_language(self): self.title_label.configure(text=t("totp_title")) self.desc_label.configure(text=t("totp_desc")) - self.copy_btn.configure(text=icon_text("copy", t("totp_copy"))) - self.save_secret_btn.configure(text=icon_text("confirm", t("totp_save_secret"))) - self.remove_secret_btn.configure(text=icon_text("delete", t("totp_remove_secret"))) - self.gen_secret_btn.configure(text=icon_text("key", t("totp_generate_secret"))) - self.show_secret_btn.configure( - text=icon_text("eye", t("hide") if self._secret_visible else t("show")) + reconfigure_icon_button(self.copy_btn, "copy", t("totp_copy")) + reconfigure_icon_button(self.save_secret_btn, "confirm", t("totp_save_secret")) + reconfigure_icon_button(self.remove_secret_btn, "delete", t("totp_remove_secret")) + reconfigure_icon_button(self.gen_secret_btn, "key", t("totp_generate_secret")) + reconfigure_icon_button( + self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show") ) if not self._current_alias: self.server_label.configure(text=t("no_server_selected")) diff --git a/releases/ServerManager-v1.9.1-win-x64.exe b/releases/ServerManager-v1.9.1-win-x64.exe new file mode 100644 index 0000000..2f57e38 Binary files /dev/null and b/releases/ServerManager-v1.9.1-win-x64.exe differ diff --git a/tools/download_icons.py b/tools/download_icons.py new file mode 100644 index 0000000..4fab92b --- /dev/null +++ b/tools/download_icons.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Download Material Design Icons PNG from GitHub. + +Source: github.com/material-icons/material-icons-png (Apache 2.0) +Style: round-4x (96x96 px) — enough for 4x HiDPI +""" +import os +import urllib.request +import time + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_URL = "https://raw.githubusercontent.com/material-icons/material-icons-png/master/png" + +ICONS = [ + "add", "arrow_back", "arrow_upward", "backspace", "check", "close", + "code", "computer", "content_copy", "dashboard", "delete", "edit", + "file_upload", "folder", "folder_open", "info", + "language", "lock", "play_arrow", "refresh", "save", "search", + "settings", "storage", "trending_up", "visibility", "vpn_key", +] + +# Icons with different names on GitHub vs local filename +ICON_RENAMES = { + "get_app": "file_download", # Material "get_app" = download icon +} + +# GitHub color dir → local theme dir +VARIANTS = {"white": "dark", "black": "light"} + + +def download(): + done = 0 + errors = 0 + all_icons = [(name, name) for name in ICONS] + all_icons += [(gh_name, local_name) for gh_name, local_name in ICON_RENAMES.items()] + total = len(all_icons) * len(VARIANTS) + + for gh_color, local_dir in VARIANTS.items(): + out_dir = os.path.join(PROJECT_DIR, "assets", "icons", local_dir) + os.makedirs(out_dir, exist_ok=True) + for gh_name, local_name in all_icons: + done += 1 + dst = os.path.join(out_dir, f"{local_name}.png") + if os.path.exists(dst): + print(f" [{done}/{total}] SKIP {local_dir}/{local_name}.png") + continue + url = f"{BASE_URL}/{gh_color}/{gh_name}/round-4x.png" + print(f" [{done}/{total}] GET {local_dir}/{local_name}.png ...", end=" ") + try: + urllib.request.urlretrieve(url, dst) + size_kb = os.path.getsize(dst) / 1024 + print(f"OK ({size_kb:.1f} KB)") + except Exception as e: + print(f"FAIL: {e}") + errors += 1 + time.sleep(0.1) + + print(f"\nDone: {total - errors}/{total} icons downloaded") + if errors: + print(f" {errors} errors — re-run to retry") + + +if __name__ == "__main__": + download() diff --git a/version.py b/version.py index 51c2381..69751a3 100755 --- a/version.py +++ b/version.py @@ -1,6 +1,6 @@ """Version info for ServerManager.""" -__version__ = "1.9.0" +__version__ = "1.9.1" __app_name__ = "ServerManager" __author__ = "aibot777" __description__ = "Desktop GUI for managing remote servers"