v1.9.1: PNG Material Design icons — 28 icons, dark/light theme, HiDPI, graceful Unicode fallback

- 56 PNG icons (28 unique × 2 color variants) from Material Design Icons (round style, 96×96px)
- core/icons.py: ctk_icon(), make_icon_button(), reconfigure_icon_button() with CTkImage cache
- Updated 15 GUI files: app.py, sidebar.py, server_dialog.py, all tabs
- build.py: auto-include assets/icons/ in PyInstaller bundle, patch rollover at 99→minor+1
- tools/download_icons.py: icon download script
- Automatic dark↔light theme switching via CTkImage dual-image support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
chrome-storm-c442
2026-03-03 07:27:49 -05:00
parent 9b0e4c76a3
commit 1e729fcf3a
76 changed files with 397 additions and 148 deletions

BIN
assets/icons/dark/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 808 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/dark/check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 B

BIN
assets/icons/dark/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 B

BIN
assets/icons/dark/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

BIN
assets/icons/dark/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 B

BIN
assets/icons/dark/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
assets/icons/dark/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/icons/dark/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/icons/light/add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

BIN
assets/icons/light/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

BIN
assets/icons/light/edit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

BIN
assets/icons/light/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
assets/icons/light/lock.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/icons/light/save.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -36,6 +36,12 @@ def auto_bump_version() -> str:
sys.exit(1)
major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3))
# 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}"
@@ -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",

View File

@@ -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))

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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 ──────────────────────────────

View File

@@ -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)

View File

@@ -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,7 +33,7 @@ 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,
self._refresh_btn = make_icon_button(header_frame, "refresh", t("grafana_refresh"), width=110,
command=self._refresh)
self._refresh_btn.pack(side="right")
@@ -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()

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

@@ -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("<Up>", lambda e: self._history_navigate(-1))
self._entry.bind("<Down>", 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")

View File

@@ -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,7 +37,7 @@ class PrometheusTab(ctk.CTkFrame):
self._query_entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
self._query_entry.bind("<Return>", lambda e: self._execute_query())
self._exec_btn = ctk.CTkButton(query_frame, text=icon_text("execute", t("prom_execute")), width=100,
self._exec_btn = make_icon_button(query_frame, "execute", t("prom_execute"), width=100,
command=self._execute_query)
self._exec_btn.pack(side="left")
@@ -59,7 +59,7 @@ 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,
self._refresh_btn = make_icon_button(targets_header, "refresh", t("prom_refresh"), width=100,
command=self._refresh_all)
self._refresh_btn.pack(side="right")
@@ -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()

View File

@@ -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",

View File

@@ -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,26 +67,26 @@ 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,
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,
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,
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,
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,
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")

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -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("<Button-1>", 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"))

Binary file not shown.

64
tools/download_icons.py Normal file
View File

@@ -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()

View File

@@ -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"