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>
BIN
assets/icons/dark/add.png
Normal file
|
After Width: | Height: | Size: 434 B |
BIN
assets/icons/dark/arrow_back.png
Normal file
|
After Width: | Height: | Size: 584 B |
BIN
assets/icons/dark/arrow_upward.png
Normal file
|
After Width: | Height: | Size: 808 B |
BIN
assets/icons/dark/backspace.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/dark/check.png
Normal file
|
After Width: | Height: | Size: 563 B |
BIN
assets/icons/dark/close.png
Normal file
|
After Width: | Height: | Size: 695 B |
BIN
assets/icons/dark/code.png
Normal file
|
After Width: | Height: | Size: 679 B |
BIN
assets/icons/dark/computer.png
Normal file
|
After Width: | Height: | Size: 723 B |
BIN
assets/icons/dark/content_copy.png
Normal file
|
After Width: | Height: | Size: 730 B |
BIN
assets/icons/dark/dashboard.png
Normal file
|
After Width: | Height: | Size: 505 B |
BIN
assets/icons/dark/delete.png
Normal file
|
After Width: | Height: | Size: 647 B |
BIN
assets/icons/dark/edit.png
Normal file
|
After Width: | Height: | Size: 557 B |
BIN
assets/icons/dark/file_download.png
Normal file
|
After Width: | Height: | Size: 654 B |
BIN
assets/icons/dark/file_upload.png
Normal file
|
After Width: | Height: | Size: 605 B |
BIN
assets/icons/dark/folder.png
Normal file
|
After Width: | Height: | Size: 603 B |
BIN
assets/icons/dark/folder_open.png
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
assets/icons/dark/info.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/icons/dark/language.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/icons/dark/lock.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/icons/dark/play_arrow.png
Normal file
|
After Width: | Height: | Size: 762 B |
BIN
assets/icons/dark/refresh.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/icons/dark/save.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/icons/dark/search.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icons/dark/settings.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
assets/icons/dark/storage.png
Normal file
|
After Width: | Height: | Size: 501 B |
BIN
assets/icons/dark/trending_up.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
assets/icons/dark/visibility.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
assets/icons/dark/vpn_key.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/light/add.png
Normal file
|
After Width: | Height: | Size: 392 B |
BIN
assets/icons/light/arrow_back.png
Normal file
|
After Width: | Height: | Size: 580 B |
BIN
assets/icons/light/arrow_upward.png
Normal file
|
After Width: | Height: | Size: 761 B |
BIN
assets/icons/light/backspace.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/icons/light/check.png
Normal file
|
After Width: | Height: | Size: 554 B |
BIN
assets/icons/light/close.png
Normal file
|
After Width: | Height: | Size: 656 B |
BIN
assets/icons/light/code.png
Normal file
|
After Width: | Height: | Size: 700 B |
BIN
assets/icons/light/computer.png
Normal file
|
After Width: | Height: | Size: 634 B |
BIN
assets/icons/light/content_copy.png
Normal file
|
After Width: | Height: | Size: 617 B |
BIN
assets/icons/light/dashboard.png
Normal file
|
After Width: | Height: | Size: 486 B |
BIN
assets/icons/light/delete.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
assets/icons/light/edit.png
Normal file
|
After Width: | Height: | Size: 591 B |
BIN
assets/icons/light/file_download.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
assets/icons/light/file_upload.png
Normal file
|
After Width: | Height: | Size: 531 B |
BIN
assets/icons/light/folder.png
Normal file
|
After Width: | Height: | Size: 552 B |
BIN
assets/icons/light/folder_open.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
assets/icons/light/info.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/light/language.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
assets/icons/light/lock.png
Normal file
|
After Width: | Height: | Size: 959 B |
BIN
assets/icons/light/play_arrow.png
Normal file
|
After Width: | Height: | Size: 687 B |
BIN
assets/icons/light/refresh.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
assets/icons/light/save.png
Normal file
|
After Width: | Height: | Size: 881 B |
BIN
assets/icons/light/search.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/light/settings.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
assets/icons/light/storage.png
Normal file
|
After Width: | Height: | Size: 452 B |
BIN
assets/icons/light/trending_up.png
Normal file
|
After Width: | Height: | Size: 765 B |
BIN
assets/icons/light/visibility.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/icons/light/vpn_key.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
59
build.py
@@ -36,7 +36,13 @@ def auto_bump_version() -> str:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3))
|
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}"
|
new_version = f"{major}.{minor}.{new_patch}"
|
||||||
|
|
||||||
content = re.sub(
|
content = re.sub(
|
||||||
@@ -111,6 +117,13 @@ def build():
|
|||||||
"--add-data", f"core/encryption.py{os.pathsep}core",
|
"--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
|
||||||
icon_path = os.path.join(PROJECT_DIR, "assets", "icon.ico")
|
icon_path = os.path.join(PROJECT_DIR, "assets", "icon.ico")
|
||||||
if os.path.exists(icon_path):
|
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"
|
_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):
|
def publish_gitea_release(exe_path: str):
|
||||||
"""Create a Gitea release and upload the exe as asset."""
|
"""Create a Gitea release and upload the exe as asset."""
|
||||||
auth = _get_gitea_auth()
|
auth = _get_gitea_auth()
|
||||||
@@ -203,13 +257,14 @@ def publish_gitea_release(exe_path: str):
|
|||||||
|
|
||||||
tag = f"v{__version__}"
|
tag = f"v{__version__}"
|
||||||
filename = os.path.basename(exe_path)
|
filename = os.path.basename(exe_path)
|
||||||
|
changelog = _generate_changelog()
|
||||||
|
|
||||||
# Create release
|
# Create release
|
||||||
try:
|
try:
|
||||||
data = json.dumps({
|
data = json.dumps({
|
||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
"name": tag,
|
"name": tag,
|
||||||
"body": f"Release {tag}",
|
"body": changelog,
|
||||||
}).encode()
|
}).encode()
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
f"{_GITEA_API}/releases",
|
f"{_GITEA_API}/releases",
|
||||||
|
|||||||
116
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.
|
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
|
# Semantic icon mapping
|
||||||
ICONS = {
|
ICONS = {
|
||||||
# Navigation
|
# Navigation
|
||||||
@@ -179,3 +213,83 @@ CTX_ICONS = {
|
|||||||
"edit": "edit",
|
"edit": "edit",
|
||||||
"delete": "delete",
|
"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))
|
||||||
|
|||||||
16
gui/app.py
@@ -11,7 +11,7 @@ from core.status_checker import StatusChecker
|
|||||||
from core.updater import UpdateChecker
|
from core.updater import UpdateChecker
|
||||||
from core import i18n
|
from core import i18n
|
||||||
from core.i18n import t, LANGUAGES
|
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 core.session_pool import SessionPool
|
||||||
from gui.sidebar import Sidebar
|
from gui.sidebar import Sidebar
|
||||||
from gui.server_dialog import ServerDialog
|
from gui.server_dialog import ServerDialog
|
||||||
@@ -147,7 +147,11 @@ class App(ctk.CTk):
|
|||||||
header_bar.pack_propagate(False)
|
header_bar.pack_propagate(False)
|
||||||
|
|
||||||
# Language selector
|
# 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))
|
self._lang_icon.pack(side="right", padx=(5, 0))
|
||||||
lang_values = list(LANGUAGES.values())
|
lang_values = list(LANGUAGES.values())
|
||||||
current_display = LANGUAGES.get(i18n.get_language(), "English")
|
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))
|
self.lang_menu.pack(side="right", padx=(5, 0))
|
||||||
|
|
||||||
# Check Updates button
|
# Check Updates button
|
||||||
|
_sync_img = ctk_icon("refresh", 18)
|
||||||
self._update_check_btn = ctk.CTkButton(
|
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",
|
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=self._check_updates_manual,
|
command=self._check_updates_manual,
|
||||||
)
|
)
|
||||||
self._update_check_btn.pack(side="right", padx=(5, 0))
|
self._update_check_btn.pack(side="right", padx=(5, 0))
|
||||||
|
|
||||||
# About button
|
# About button
|
||||||
|
_info_img = ctk_icon("info", 18)
|
||||||
self.about_btn = ctk.CTkButton(
|
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",
|
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=self._show_about
|
command=self._show_about
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Form adapts visible fields based on selected server type.
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.server_store import SERVER_TYPES, DEFAULT_PORTS
|
from core.server_store import SERVER_TYPES, DEFAULT_PORTS
|
||||||
from core.i18n import t
|
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.
|
# 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))
|
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 = ctk.CTkEntry(pass_inner, show="*", placeholder_text=t("placeholder_password"))
|
||||||
self.password_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
|
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.show_pass.pack(side="right")
|
||||||
self._pass_visible = False
|
self._pass_visible = False
|
||||||
self._field_frames["password"] = f
|
self._field_frames["password"] = f
|
||||||
@@ -286,8 +286,8 @@ class ServerDialog(ctk.CTkToplevel):
|
|||||||
# ── Always visible: Buttons ──
|
# ── Always visible: Buttons ──
|
||||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x", padx=20, pady=(15, 20))
|
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))
|
make_icon_button(btn_frame, "close", 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, "confirm", t("save"), command=self._save).pack(side="right", expand=True, padx=(5, 0))
|
||||||
|
|
||||||
# Fill values if editing
|
# Fill values if editing
|
||||||
if server:
|
if server:
|
||||||
@@ -360,7 +360,7 @@ class ServerDialog(ctk.CTkToplevel):
|
|||||||
def _toggle_password(self):
|
def _toggle_password(self):
|
||||||
self._pass_visible = not self._pass_visible
|
self._pass_visible = not self._pass_visible
|
||||||
self.password_entry.configure(show="" if self._pass_visible else "*")
|
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):
|
def _save(self):
|
||||||
alias = self.alias_entry.get().strip()
|
alias = self.alias_entry.get().strip()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import customtkinter as ctk
|
|||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
from core.icons import (
|
from core.icons import (
|
||||||
icon_text, TYPE_COLORS, TYPE_LABELS, CTX_ICONS, icon,
|
icon_text, TYPE_COLORS, TYPE_LABELS, CTX_ICONS, icon,
|
||||||
|
make_icon_button, reconfigure_icon_button,
|
||||||
)
|
)
|
||||||
from gui.widgets.status_badge import StatusBadge
|
from gui.widgets.status_badge import StatusBadge
|
||||||
|
|
||||||
@@ -80,11 +81,11 @@ class Sidebar(ctk.CTkFrame):
|
|||||||
# Buttons
|
# Buttons
|
||||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x", padx=10, pady=10)
|
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.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.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))
|
self.del_btn.pack(side="right", padx=(3, 0))
|
||||||
|
|
||||||
# Callbacks — set by app.py
|
# Callbacks — set by app.py
|
||||||
@@ -103,9 +104,9 @@ class Sidebar(ctk.CTkFrame):
|
|||||||
def update_language(self):
|
def update_language(self):
|
||||||
self.title_label.configure(text=t("servers"))
|
self.title_label.configure(text=t("servers"))
|
||||||
self.search_entry.configure(placeholder_text=t("search"))
|
self.search_entry.configure(placeholder_text=t("search"))
|
||||||
self.add_btn.configure(text=icon_text("add", t("add")))
|
reconfigure_icon_button(self.add_btn, "add", t("add"))
|
||||||
self.edit_btn.configure(text=icon_text("edit", t("edit")))
|
reconfigure_icon_button(self.edit_btn, "edit", t("edit"))
|
||||||
self.del_btn.configure(text=icon_text("delete", t("delete")))
|
reconfigure_icon_button(self.del_btn, "delete", t("delete"))
|
||||||
self._update_sessions_label()
|
self._update_sessions_label()
|
||||||
|
|
||||||
# ── Refresh / Render ──────────────────────────────
|
# ── Refresh / Render ──────────────────────────────
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from tkinter import messagebox, filedialog
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
from core.i18n import t
|
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 core.ssh_client import SFTPSession
|
||||||
from gui.widgets.file_list import FileListWidget
|
from gui.widgets.file_list import FileListWidget
|
||||||
|
|
||||||
@@ -90,28 +90,34 @@ class FilesTab(ctk.CTkFrame):
|
|||||||
ctk.CTkLabel(left_header, text=t("local_files"),
|
ctk.CTkLabel(left_header, text=t("local_files"),
|
||||||
font=ctk.CTkFont(size=13, weight="bold")).pack(side="left")
|
font=ctk.CTkFont(size=13, weight="bold")).pack(side="left")
|
||||||
|
|
||||||
|
_back_img = ctk_icon("back", 16)
|
||||||
self._local_back_btn = ctk.CTkButton(
|
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,
|
command=self._local_go_back,
|
||||||
)
|
)
|
||||||
self._local_back_btn.pack(side="left", padx=(8, 2))
|
self._local_back_btn.pack(side="left", padx=(8, 2))
|
||||||
|
|
||||||
|
_up_img = ctk_icon("up", 16)
|
||||||
self._local_up_btn = ctk.CTkButton(
|
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,
|
command=self._local_go_up,
|
||||||
)
|
)
|
||||||
self._local_up_btn.pack(side="left", padx=2)
|
self._local_up_btn.pack(side="left", padx=2)
|
||||||
|
|
||||||
# Local refresh button
|
# Local refresh button
|
||||||
|
_ref_img = ctk_icon("refresh", 16)
|
||||||
self._local_refresh_btn = ctk.CTkButton(
|
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,
|
command=self._refresh_local,
|
||||||
)
|
)
|
||||||
self._local_refresh_btn.pack(side="left", padx=2)
|
self._local_refresh_btn.pack(side="left", padx=2)
|
||||||
|
|
||||||
# Browse button
|
# Browse button
|
||||||
self._browse_btn = ctk.CTkButton(
|
self._browse_btn = make_icon_button(
|
||||||
left_header, text=icon_text("folder_open", t("browse")), width=75, height=28,
|
left_header, "folder_open", t("browse"), width=75, height=28,
|
||||||
command=self._browse_local,
|
command=self._browse_local,
|
||||||
)
|
)
|
||||||
self._browse_btn.pack(side="left", padx=2)
|
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")
|
font=ctk.CTkFont(size=13, weight="bold")).pack(side="left")
|
||||||
|
|
||||||
self._remote_back_btn = ctk.CTkButton(
|
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,
|
command=self._remote_go_back,
|
||||||
)
|
)
|
||||||
self._remote_back_btn.pack(side="left", padx=(8, 2))
|
self._remote_back_btn.pack(side="left", padx=(8, 2))
|
||||||
|
|
||||||
self._remote_up_btn = ctk.CTkButton(
|
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,
|
command=self._remote_go_up,
|
||||||
)
|
)
|
||||||
self._remote_up_btn.pack(side="left", padx=2)
|
self._remote_up_btn.pack(side="left", padx=2)
|
||||||
|
|
||||||
self._remote_refresh_btn = ctk.CTkButton(
|
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,
|
command=self._refresh_remote,
|
||||||
)
|
)
|
||||||
self._remote_refresh_btn.pack(side="left", padx=2)
|
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 = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
toolbar.pack(fill="x", padx=10, pady=4)
|
toolbar.pack(fill="x", padx=10, pady=4)
|
||||||
|
|
||||||
self._upload_btn = ctk.CTkButton(
|
self._upload_btn = make_icon_button(
|
||||||
toolbar, text=icon_text("upload", t("upload")), width=110, height=30,
|
toolbar, "upload", t("upload"), width=110, height=30,
|
||||||
command=self._upload_selected,
|
command=self._upload_selected,
|
||||||
)
|
)
|
||||||
self._upload_btn.pack(side="left", padx=(0, 4))
|
self._upload_btn.pack(side="left", padx=(0, 4))
|
||||||
|
|
||||||
self._download_btn = ctk.CTkButton(
|
self._download_btn = make_icon_button(
|
||||||
toolbar, text=icon_text("download", t("download")), width=110, height=30,
|
toolbar, "download", t("download"), width=110, height=30,
|
||||||
command=self._download_selected,
|
command=self._download_selected,
|
||||||
)
|
)
|
||||||
self._download_btn.pack(side="left", padx=4)
|
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 = ctk.CTkFrame(toolbar, width=2, height=24, fg_color="gray40")
|
||||||
sep.pack(side="left", padx=8)
|
sep.pack(side="left", padx=8)
|
||||||
|
|
||||||
self._mkdir_btn = ctk.CTkButton(
|
self._mkdir_btn = make_icon_button(
|
||||||
toolbar, text=icon_text("folder", t("new_folder")), width=110, height=30,
|
toolbar, "folder", t("new_folder"), width=110, height=30,
|
||||||
command=self._mkdir_remote,
|
command=self._mkdir_remote,
|
||||||
)
|
)
|
||||||
self._mkdir_btn.pack(side="left", padx=4)
|
self._mkdir_btn.pack(side="left", padx=4)
|
||||||
|
|
||||||
self._delete_btn = ctk.CTkButton(
|
self._delete_btn = make_icon_button(
|
||||||
toolbar, text=icon_text("delete", t("delete_files")), width=90, height=30,
|
toolbar, "delete", t("delete_files"), width=90, height=30,
|
||||||
fg_color="#dc2626", hover_color="#b91c1c",
|
fg_color="#dc2626", hover_color="#b91c1c",
|
||||||
command=self._delete_remote,
|
command=self._delete_remote,
|
||||||
)
|
)
|
||||||
self._delete_btn.pack(side="left", padx=4)
|
self._delete_btn.pack(side="left", padx=4)
|
||||||
|
|
||||||
self._rename_btn = ctk.CTkButton(
|
self._rename_btn = make_icon_button(
|
||||||
toolbar, text=icon_text("edit", t("rename_file")), width=110, height=30,
|
toolbar, "edit", t("rename_file"), width=110, height=30,
|
||||||
command=self._rename_remote,
|
command=self._rename_remote,
|
||||||
)
|
)
|
||||||
self._rename_btn.pack(side="left", padx=4)
|
self._rename_btn.pack(side="left", padx=4)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from tkinter import ttk
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.grafana_client import GrafanaClient
|
from core.grafana_client import GrafanaClient
|
||||||
from core.i18n import t
|
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
|
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"))
|
font=ctk.CTkFont(size=18, weight="bold"))
|
||||||
title.pack(side="left")
|
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)
|
command=self._refresh)
|
||||||
self._refresh_btn.pack(side="right")
|
self._refresh_btn.pack(side="right")
|
||||||
|
|
||||||
# ── Dashboards section ──
|
# ── Dashboards section ──
|
||||||
@@ -131,8 +131,10 @@ class GrafanaTab(ctk.CTkFrame):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
|
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
|
||||||
finally:
|
finally:
|
||||||
self.after(0, lambda: self._refresh_btn.configure(
|
self.after(0, lambda: (
|
||||||
state="normal", text=icon_text("refresh", t("grafana_refresh"))))
|
self._refresh_btn.configure(state="normal"),
|
||||||
|
reconfigure_icon_button(self._refresh_btn, "refresh", t("grafana_refresh")),
|
||||||
|
))
|
||||||
|
|
||||||
threading.Thread(target=_do, daemon=True).start()
|
threading.Thread(target=_do, daemon=True).start()
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Info tab — display server details, edit button.
|
|||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
from core.icons import icon_text
|
from core.icons import icon_text, make_icon_button
|
||||||
|
|
||||||
|
|
||||||
class InfoTab(ctk.CTkFrame):
|
class InfoTab(ctk.CTkFrame):
|
||||||
@@ -56,7 +56,7 @@ class InfoTab(ctk.CTkFrame):
|
|||||||
self._fields[key] = val
|
self._fields[key] = val
|
||||||
|
|
||||||
# Edit button
|
# 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)
|
self.edit_btn.pack(pady=15)
|
||||||
|
|
||||||
def set_server(self, alias: str | None):
|
def set_server(self, alias: str | None):
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import threading
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.ssh_client import SSHClientWrapper
|
from core.ssh_client import SSHClientWrapper
|
||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
from core.icons import icon_text
|
from core.icons import icon_text, make_icon_button
|
||||||
|
|
||||||
|
|
||||||
class KeysTab(ctk.CTkFrame):
|
class KeysTab(ctk.CTkFrame):
|
||||||
@@ -30,13 +30,13 @@ class KeysTab(ctk.CTkFrame):
|
|||||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x", padx=15, pady=5)
|
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.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.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")
|
self.copy_btn.pack(side="right")
|
||||||
|
|
||||||
# Status log
|
# Status log
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import threading
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.remote_desktop import RemoteDesktopLauncher
|
from core.remote_desktop import RemoteDesktopLauncher
|
||||||
from core.i18n import t
|
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
|
from core.logger import log
|
||||||
|
|
||||||
|
|
||||||
@@ -33,15 +33,15 @@ class LaunchTab(ctk.CTkFrame):
|
|||||||
# ── Toolbar (shown when RDP connected) ──
|
# ── Toolbar (shown when RDP connected) ──
|
||||||
self._toolbar = ctk.CTkFrame(self, height=36, fg_color="transparent")
|
self._toolbar = ctk.CTkFrame(self, height=36, fg_color="transparent")
|
||||||
|
|
||||||
self._disconnect_btn = ctk.CTkButton(
|
self._disconnect_btn = make_icon_button(
|
||||||
self._toolbar, text=icon_text("delete", t("rdp_disconnect")),
|
self._toolbar, "delete", t("rdp_disconnect"),
|
||||||
width=120, height=30, fg_color="#ef4444", hover_color="#dc2626",
|
width=120, height=30, fg_color="#ef4444", hover_color="#dc2626",
|
||||||
command=self._disconnect,
|
command=self._disconnect,
|
||||||
)
|
)
|
||||||
self._disconnect_btn.pack(side="left", padx=(8, 4))
|
self._disconnect_btn.pack(side="left", padx=(8, 4))
|
||||||
|
|
||||||
self._fullscreen_btn = ctk.CTkButton(
|
self._fullscreen_btn = make_icon_button(
|
||||||
self._toolbar, text=icon_text("launch", t("rdp_fullscreen")),
|
self._toolbar, "launch", t("rdp_fullscreen"),
|
||||||
width=130, height=30, fg_color="#6b7280", hover_color="#4b5563",
|
width=130, height=30, fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=self._toggle_fullscreen,
|
command=self._toggle_fullscreen,
|
||||||
)
|
)
|
||||||
@@ -135,8 +135,9 @@ class LaunchTab(ctk.CTkFrame):
|
|||||||
).pack(fill="x", padx=15, pady=(3, 12))
|
).pack(fill="x", padx=15, pady=(3, 12))
|
||||||
|
|
||||||
# Connect button
|
# Connect button
|
||||||
self._connect_btn = ctk.CTkButton(
|
self._connect_btn = make_icon_button(
|
||||||
self._settings_panel, text=icon_text("execute", t("launch_connect")),
|
self._settings_panel, "execute", t("launch_connect"),
|
||||||
|
icon_size=20,
|
||||||
font=ctk.CTkFont(size=18, weight="bold"),
|
font=ctk.CTkFont(size=18, weight="bold"),
|
||||||
width=220, height=50,
|
width=220, height=50,
|
||||||
command=self._on_connect,
|
command=self._on_connect,
|
||||||
@@ -374,9 +375,7 @@ class LaunchTab(ctk.CTkFrame):
|
|||||||
if self._is_fullscreen:
|
if self._is_fullscreen:
|
||||||
# Exit fullscreen — reattach
|
# Exit fullscreen — reattach
|
||||||
self._is_fullscreen = False
|
self._is_fullscreen = False
|
||||||
self._fullscreen_btn.configure(
|
reconfigure_icon_button(self._fullscreen_btn, "launch", t("rdp_fullscreen"))
|
||||||
text=icon_text("launch", t("rdp_fullscreen")),
|
|
||||||
)
|
|
||||||
self._rdp_frame.update_idletasks()
|
self._rdp_frame.update_idletasks()
|
||||||
parent_hwnd = self._rdp_frame.winfo_id()
|
parent_hwnd = self._rdp_frame.winfo_id()
|
||||||
w = self._rdp_frame.winfo_width()
|
w = self._rdp_frame.winfo_width()
|
||||||
@@ -385,9 +384,7 @@ class LaunchTab(ctk.CTkFrame):
|
|||||||
else:
|
else:
|
||||||
# Go fullscreen — detach, then maximize after event loop settles
|
# Go fullscreen — detach, then maximize after event loop settles
|
||||||
self._is_fullscreen = True
|
self._is_fullscreen = True
|
||||||
self._fullscreen_btn.configure(
|
reconfigure_icon_button(self._fullscreen_btn, "back", t("rdp_exit_fullscreen"))
|
||||||
text=icon_text("back", t("rdp_exit_fullscreen")),
|
|
||||||
)
|
|
||||||
self._embedded_rdp.detach()
|
self._embedded_rdp.detach()
|
||||||
self.after(300, self._maximize_detached)
|
self.after(300, self._maximize_detached)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import threading
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.winrm_client import WinRMClient
|
from core.winrm_client import WinRMClient
|
||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
from core.icons import icon_text
|
from core.icons import icon_text, make_icon_button
|
||||||
|
|
||||||
|
|
||||||
class PowershellTab(ctk.CTkFrame):
|
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("<Up>", lambda e: self._history_navigate(-1))
|
||||||
self._entry.bind("<Down>", lambda e: self._history_navigate(1))
|
self._entry.bind("<Down>", lambda e: self._history_navigate(1))
|
||||||
|
|
||||||
self._exec_btn = ctk.CTkButton(
|
self._exec_btn = make_icon_button(
|
||||||
input_row, text=icon_text("execute", t("ps_execute")), width=100,
|
input_row, "execute", t("ps_execute"), width=100,
|
||||||
command=self._execute,
|
command=self._execute,
|
||||||
)
|
)
|
||||||
self._exec_btn.pack(side="right")
|
self._exec_btn.pack(side="right")
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from tkinter import ttk
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.prometheus_client import PrometheusClient
|
from core.prometheus_client import PrometheusClient
|
||||||
from core.i18n import t
|
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
|
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.pack(side="left", fill="x", expand=True, padx=(0, 10))
|
||||||
self._query_entry.bind("<Return>", lambda e: self._execute_query())
|
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)
|
command=self._execute_query)
|
||||||
self._exec_btn.pack(side="left")
|
self._exec_btn.pack(side="left")
|
||||||
|
|
||||||
# ── Query results ──
|
# ── Query results ──
|
||||||
@@ -59,8 +59,8 @@ class PrometheusTab(ctk.CTkFrame):
|
|||||||
font=ctk.CTkFont(size=14, weight="bold"), anchor="w")
|
font=ctk.CTkFont(size=14, weight="bold"), anchor="w")
|
||||||
targets_label.pack(side="left")
|
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)
|
command=self._refresh_all)
|
||||||
self._refresh_btn.pack(side="right")
|
self._refresh_btn.pack(side="right")
|
||||||
|
|
||||||
targets_frame = ctk.CTkFrame(self, fg_color="transparent")
|
targets_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
@@ -201,8 +201,10 @@ class PrometheusTab(ctk.CTkFrame):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
|
self.after(0, lambda: self._set_status(f"(error) {e}", "#ef4444"))
|
||||||
finally:
|
finally:
|
||||||
self.after(0, lambda: self._refresh_btn.configure(
|
self.after(0, lambda: (
|
||||||
state="normal", text=icon_text("refresh", t("prom_refresh"))))
|
self._refresh_btn.configure(state="normal"),
|
||||||
|
reconfigure_icon_button(self._refresh_btn, "refresh", t("prom_refresh")),
|
||||||
|
))
|
||||||
|
|
||||||
threading.Thread(target=_do, daemon=True).start()
|
threading.Thread(target=_do, daemon=True).start()
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from tkinter import ttk, filedialog
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
from core.i18n import t
|
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
|
from core.sql_client import SQLClient
|
||||||
|
|
||||||
_TREE_THEME_APPLIED = False
|
_TREE_THEME_APPLIED = False
|
||||||
@@ -227,9 +227,8 @@ class QueryTab(ctk.CTkFrame):
|
|||||||
btn_row = ctk.CTkFrame(right_ctk, fg_color="transparent")
|
btn_row = ctk.CTkFrame(right_ctk, fg_color="transparent")
|
||||||
btn_row.pack(fill="x", padx=8, pady=4)
|
btn_row.pack(fill="x", padx=8, pady=4)
|
||||||
|
|
||||||
self._exec_btn = ctk.CTkButton(
|
self._exec_btn = make_icon_button(
|
||||||
btn_row,
|
btn_row, "execute", t("query_execute"),
|
||||||
text=icon_text("execute", t("query_execute")),
|
|
||||||
command=self._execute_query,
|
command=self._execute_query,
|
||||||
width=130,
|
width=130,
|
||||||
fg_color="#2563eb",
|
fg_color="#2563eb",
|
||||||
@@ -237,9 +236,8 @@ class QueryTab(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
self._exec_btn.pack(side="left", padx=(0, 6))
|
self._exec_btn.pack(side="left", padx=(0, 6))
|
||||||
|
|
||||||
self._clear_btn = ctk.CTkButton(
|
self._clear_btn = make_icon_button(
|
||||||
btn_row,
|
btn_row, "clear", t("query_clear"),
|
||||||
text=icon_text("clear", t("query_clear")),
|
|
||||||
command=self._clear_all,
|
command=self._clear_all,
|
||||||
width=80,
|
width=80,
|
||||||
fg_color="#6b7280",
|
fg_color="#6b7280",
|
||||||
@@ -247,9 +245,8 @@ class QueryTab(ctk.CTkFrame):
|
|||||||
)
|
)
|
||||||
self._clear_btn.pack(side="left", padx=(0, 6))
|
self._clear_btn.pack(side="left", padx=(0, 6))
|
||||||
|
|
||||||
self._export_btn = ctk.CTkButton(
|
self._export_btn = make_icon_button(
|
||||||
btn_row,
|
btn_row, "save", t("query_export_csv"),
|
||||||
text=icon_text("save", t("query_export_csv")),
|
|
||||||
command=self._export_csv,
|
command=self._export_csv,
|
||||||
width=110,
|
width=110,
|
||||||
fg_color="#059669",
|
fg_color="#059669",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import threading
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.redis_client import RedisClient
|
from core.redis_client import RedisClient
|
||||||
from core.i18n import t
|
from core.i18n import t
|
||||||
from core.icons import icon_text
|
from core.icons import icon_text, make_icon_button
|
||||||
|
|
||||||
|
|
||||||
class RedisTab(ctk.CTkFrame):
|
class RedisTab(ctk.CTkFrame):
|
||||||
@@ -67,28 +67,28 @@ class RedisTab(ctk.CTkFrame):
|
|||||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x", padx=15, pady=5)
|
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)
|
command=self._execute_command)
|
||||||
self._exec_btn.pack(side="left", padx=(0, 5))
|
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",
|
fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=lambda: self._run_quick("INFO"))
|
command=lambda: self._run_quick("INFO"))
|
||||||
self._info_btn.pack(side="left", padx=(0, 5))
|
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",
|
fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=lambda: self._run_quick("DBSIZE"))
|
command=lambda: self._run_quick("DBSIZE"))
|
||||||
self._dbsize_btn.pack(side="left", padx=(0, 5))
|
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",
|
fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=lambda: self._run_quick("SCAN 0 COUNT 100"))
|
command=lambda: self._run_quick("SCAN 0 COUNT 100"))
|
||||||
self._scan_btn.pack(side="left", padx=(0, 5))
|
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",
|
fg_color="#374151", hover_color="#1f2937",
|
||||||
command=self._clear_output)
|
command=self._clear_output)
|
||||||
self._clear_btn.pack(side="right")
|
self._clear_btn.pack(side="right")
|
||||||
|
|
||||||
# ── Output console ──
|
# ── Output console ──
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from tkinter import ttk, filedialog
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.s3_client import S3Client
|
from core.s3_client import S3Client
|
||||||
from core.i18n import t
|
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
|
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 = ctk.CTkFrame(header, fg_color="transparent")
|
||||||
btn_frame.pack(side="right")
|
btn_frame.pack(side="right")
|
||||||
|
|
||||||
self._back_btn = ctk.CTkButton(
|
self._back_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("back", t("s3_back")), width=80,
|
btn_frame, "back", t("s3_back"), width=80,
|
||||||
command=self._go_back, state="disabled",
|
command=self._go_back, state="disabled",
|
||||||
)
|
)
|
||||||
self._back_btn.pack(side="left", padx=(0, 5))
|
self._back_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self._refresh_btn = ctk.CTkButton(
|
self._refresh_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("refresh", t("s3_refresh")), width=100,
|
btn_frame, "refresh", t("s3_refresh"), width=100,
|
||||||
command=self._refresh,
|
command=self._refresh,
|
||||||
)
|
)
|
||||||
self._refresh_btn.pack(side="left", padx=(0, 5))
|
self._refresh_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self._upload_btn = ctk.CTkButton(
|
self._upload_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("upload", t("s3_upload")), width=100,
|
btn_frame, "upload", t("s3_upload"), width=100,
|
||||||
command=self._upload,
|
command=self._upload,
|
||||||
)
|
)
|
||||||
self._upload_btn.pack(side="left", padx=(0, 5))
|
self._upload_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self._download_btn = ctk.CTkButton(
|
self._download_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("download", t("s3_download")), width=110,
|
btn_frame, "download", t("s3_download"), width=110,
|
||||||
command=self._download,
|
command=self._download,
|
||||||
)
|
)
|
||||||
self._download_btn.pack(side="left", padx=(0, 5))
|
self._download_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self._delete_btn = ctk.CTkButton(
|
self._delete_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("delete", t("s3_delete")), width=100,
|
btn_frame, "delete", t("s3_delete"), width=100,
|
||||||
fg_color="#dc2626", hover_color="#b91c1c",
|
fg_color="#dc2626", hover_color="#b91c1c",
|
||||||
command=self._delete,
|
command=self._delete,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from tkinter import filedialog, messagebox
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key
|
from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key
|
||||||
from core.i18n import t
|
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
|
from core.logger import log
|
||||||
|
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ class SetupTab(ctk.CTkFrame):
|
|||||||
btn_frame = ctk.CTkFrame(self._scroll, fg_color="transparent")
|
btn_frame = ctk.CTkFrame(self._scroll, fg_color="transparent")
|
||||||
btn_frame.pack(fill="x", padx=20, pady=15)
|
btn_frame.pack(fill="x", padx=20, pady=15)
|
||||||
|
|
||||||
self.install_all_btn = ctk.CTkButton(
|
self.install_all_btn = make_icon_button(
|
||||||
btn_frame, text=icon_text("confirm", t("install_everything")),
|
btn_frame, "confirm", t("install_everything"),
|
||||||
font=ctk.CTkFont(size=14, weight="bold"),
|
font=ctk.CTkFont(size=14, weight="bold"),
|
||||||
height=40, fg_color="#22c55e", hover_color="#16a34a",
|
height=40, fg_color="#22c55e", hover_color="#16a34a",
|
||||||
command=self._install_all
|
command=self._install_all
|
||||||
@@ -82,16 +82,16 @@ class SetupTab(ctk.CTkFrame):
|
|||||||
ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent")
|
ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent")
|
||||||
ind_frame.pack(fill="x")
|
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)
|
command=self._install_script)
|
||||||
self.ssh_py_btn.pack(side="left", padx=(0, 5))
|
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)
|
command=self._install_skill)
|
||||||
self.skill_btn.pack(side="left", padx=5)
|
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)
|
command=self._gen_key)
|
||||||
self.ssh_key_btn.pack(side="left", padx=5)
|
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)
|
command=self._refresh_status)
|
||||||
self.refresh_btn.pack(side="right")
|
self.refresh_btn.pack(side="right")
|
||||||
|
|
||||||
@@ -185,8 +185,8 @@ class SetupTab(ctk.CTkFrame):
|
|||||||
font=ctk.CTkFont(family="Consolas", size=11)
|
font=ctk.CTkFont(family="Consolas", size=11)
|
||||||
)
|
)
|
||||||
self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10))
|
self._path_label.pack(side="left", fill="x", expand=True, padx=(5, 10))
|
||||||
self.change_path_btn = ctk.CTkButton(
|
self.change_path_btn = make_icon_button(
|
||||||
path_row, text=icon_text("folder", t("change_path")), width=120, fg_color="#6b7280",
|
path_row, "folder", t("change_path"), width=120, fg_color="#6b7280",
|
||||||
command=self._change_config_path
|
command=self._change_config_path
|
||||||
)
|
)
|
||||||
self.change_path_btn.pack(side="right")
|
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 = ctk.CTkFrame(config_frame, fg_color="transparent")
|
||||||
backup_row.pack(fill="x", padx=15, pady=(5, 10))
|
backup_row.pack(fill="x", padx=15, pady=(5, 10))
|
||||||
|
|
||||||
self.backup_btn = ctk.CTkButton(
|
self.backup_btn = make_icon_button(
|
||||||
backup_row, text=icon_text("save", t("backup_now")), width=120, fg_color="#3b82f6",
|
backup_row, "save", t("backup_now"), width=120, fg_color="#3b82f6",
|
||||||
command=self._backup_now
|
command=self._backup_now
|
||||||
)
|
)
|
||||||
self.backup_btn.pack(side="left", padx=(0, 10))
|
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._backup_menu.pack(side="left", padx=(0, 10))
|
||||||
|
|
||||||
self.restore_btn = ctk.CTkButton(
|
self.restore_btn = make_icon_button(
|
||||||
backup_row, text=icon_text("refresh", t("restore")), width=100, fg_color="#ef4444", hover_color="#dc2626",
|
backup_row, "refresh", t("restore"), width=100, fg_color="#ef4444", hover_color="#dc2626",
|
||||||
command=self._restore_backup
|
command=self._restore_backup
|
||||||
)
|
)
|
||||||
self.restore_btn.pack(side="left")
|
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 = ctk.CTkFrame(config_frame, fg_color="transparent")
|
||||||
ie_row.pack(fill="x", padx=15, pady=(0, 10))
|
ie_row.pack(fill="x", padx=15, pady=(0, 10))
|
||||||
|
|
||||||
self.export_config_btn = ctk.CTkButton(
|
self.export_config_btn = make_icon_button(
|
||||||
ie_row, text=icon_text("upload", t("export_config")), width=130, fg_color="#6b7280",
|
ie_row, "upload", t("export_config"), width=130, fg_color="#6b7280",
|
||||||
command=self._export_config
|
command=self._export_config
|
||||||
)
|
)
|
||||||
self.export_config_btn.pack(side="left", padx=(0, 5))
|
self.export_config_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self.import_config_btn = ctk.CTkButton(
|
self.import_config_btn = make_icon_button(
|
||||||
ie_row, text=icon_text("download", t("import_config")), width=130, fg_color="#6b7280",
|
ie_row, "download", t("import_config"), width=130, fg_color="#6b7280",
|
||||||
command=self._import_config
|
command=self._import_config
|
||||||
)
|
)
|
||||||
self.import_config_btn.pack(side="left", padx=5)
|
self.import_config_btn.pack(side="left", padx=5)
|
||||||
|
|
||||||
self.export_backup_btn = ctk.CTkButton(
|
self.export_backup_btn = make_icon_button(
|
||||||
ie_row, text=icon_text("upload", t("export_backup")), width=130, fg_color="#6b7280",
|
ie_row, "upload", t("export_backup"), width=130, fg_color="#6b7280",
|
||||||
command=self._export_backup
|
command=self._export_backup
|
||||||
)
|
)
|
||||||
self.export_backup_btn.pack(side="left", padx=5)
|
self.export_backup_btn.pack(side="left", padx=5)
|
||||||
|
|
||||||
self.import_backup_btn = ctk.CTkButton(
|
self.import_backup_btn = make_icon_button(
|
||||||
ie_row, text=icon_text("download", t("import_backup")), width=130, fg_color="#6b7280",
|
ie_row, "download", t("import_backup"), width=130, fg_color="#6b7280",
|
||||||
command=self._import_backup
|
command=self._import_backup
|
||||||
)
|
)
|
||||||
self.import_backup_btn.pack(side="left", padx=5)
|
self.import_backup_btn.pack(side="left", padx=5)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Live countdown, one-click copy, per-server secrets.
|
|||||||
import threading
|
import threading
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
from core.i18n import t
|
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):
|
class TOTPTab(ctk.CTkFrame):
|
||||||
@@ -81,8 +81,8 @@ class TOTPTab(ctk.CTkFrame):
|
|||||||
widget.bind("<Button-1>", lambda e: self._copy_code())
|
widget.bind("<Button-1>", lambda e: self._copy_code())
|
||||||
|
|
||||||
# Copy button
|
# Copy button
|
||||||
self.copy_btn = ctk.CTkButton(
|
self.copy_btn = make_icon_button(
|
||||||
self, text=icon_text("copy", t("totp_copy")), width=200, height=40,
|
self, "copy", t("totp_copy"), width=200, height=40,
|
||||||
font=ctk.CTkFont(size=14),
|
font=ctk.CTkFont(size=14),
|
||||||
fg_color="#22c55e", hover_color="#16a34a",
|
fg_color="#22c55e", hover_color="#16a34a",
|
||||||
command=self._copy_code
|
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.secret_entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
|
||||||
|
|
||||||
self.show_secret_btn = ctk.CTkButton(
|
self.show_secret_btn = make_icon_button(
|
||||||
entry_row, text=icon_text("eye", t("show")), width=80,
|
entry_row, "eye", t("show"), width=80,
|
||||||
fg_color="#6b7280", hover_color="#4b5563",
|
fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=self._toggle_secret
|
command=self._toggle_secret
|
||||||
)
|
)
|
||||||
self.show_secret_btn.pack(side="left", padx=(0, 5))
|
self.show_secret_btn.pack(side="left", padx=(0, 5))
|
||||||
self._secret_visible = False
|
self._secret_visible = False
|
||||||
|
|
||||||
self.save_secret_btn = ctk.CTkButton(
|
self.save_secret_btn = make_icon_button(
|
||||||
entry_row, text=icon_text("confirm", t("totp_save_secret")), width=110,
|
entry_row, "confirm", t("totp_save_secret"), width=110,
|
||||||
command=self._save_secret
|
command=self._save_secret
|
||||||
)
|
)
|
||||||
self.save_secret_btn.pack(side="left", padx=(0, 5))
|
self.save_secret_btn.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
self.remove_secret_btn = ctk.CTkButton(
|
self.remove_secret_btn = make_icon_button(
|
||||||
entry_row, text=icon_text("delete", t("totp_remove_secret")), width=110,
|
entry_row, "delete", t("totp_remove_secret"), width=110,
|
||||||
fg_color="#ef4444", hover_color="#dc2626",
|
fg_color="#ef4444", hover_color="#dc2626",
|
||||||
command=self._remove_secret
|
command=self._remove_secret
|
||||||
)
|
)
|
||||||
self.remove_secret_btn.pack(side="left")
|
self.remove_secret_btn.pack(side="left")
|
||||||
|
|
||||||
# Generate random secret button
|
# Generate random secret button
|
||||||
self.gen_secret_btn = ctk.CTkButton(
|
self.gen_secret_btn = make_icon_button(
|
||||||
secret_frame, text=icon_text("key", t("totp_generate_secret")), width=200,
|
secret_frame, "key", t("totp_generate_secret"), width=200,
|
||||||
fg_color="#6b7280", hover_color="#4b5563",
|
fg_color="#6b7280", hover_color="#4b5563",
|
||||||
command=self._generate_secret
|
command=self._generate_secret
|
||||||
)
|
)
|
||||||
@@ -267,8 +267,8 @@ class TOTPTab(ctk.CTkFrame):
|
|||||||
def _toggle_secret(self):
|
def _toggle_secret(self):
|
||||||
self._secret_visible = not self._secret_visible
|
self._secret_visible = not self._secret_visible
|
||||||
self.secret_entry.configure(show="" if self._secret_visible else "*")
|
self.secret_entry.configure(show="" if self._secret_visible else "*")
|
||||||
self.show_secret_btn.configure(
|
reconfigure_icon_button(
|
||||||
text=icon_text("eye", t("hide") if self._secret_visible else t("show"))
|
self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show")
|
||||||
)
|
)
|
||||||
|
|
||||||
def _save_secret(self):
|
def _save_secret(self):
|
||||||
@@ -331,12 +331,12 @@ class TOTPTab(ctk.CTkFrame):
|
|||||||
def update_language(self):
|
def update_language(self):
|
||||||
self.title_label.configure(text=t("totp_title"))
|
self.title_label.configure(text=t("totp_title"))
|
||||||
self.desc_label.configure(text=t("totp_desc"))
|
self.desc_label.configure(text=t("totp_desc"))
|
||||||
self.copy_btn.configure(text=icon_text("copy", t("totp_copy")))
|
reconfigure_icon_button(self.copy_btn, "copy", t("totp_copy"))
|
||||||
self.save_secret_btn.configure(text=icon_text("confirm", t("totp_save_secret")))
|
reconfigure_icon_button(self.save_secret_btn, "confirm", t("totp_save_secret"))
|
||||||
self.remove_secret_btn.configure(text=icon_text("delete", t("totp_remove_secret")))
|
reconfigure_icon_button(self.remove_secret_btn, "delete", t("totp_remove_secret"))
|
||||||
self.gen_secret_btn.configure(text=icon_text("key", t("totp_generate_secret")))
|
reconfigure_icon_button(self.gen_secret_btn, "key", t("totp_generate_secret"))
|
||||||
self.show_secret_btn.configure(
|
reconfigure_icon_button(
|
||||||
text=icon_text("eye", t("hide") if self._secret_visible else t("show"))
|
self.show_secret_btn, "eye", t("hide") if self._secret_visible else t("show")
|
||||||
)
|
)
|
||||||
if not self._current_alias:
|
if not self._current_alias:
|
||||||
self.server_label.configure(text=t("no_server_selected"))
|
self.server_label.configure(text=t("no_server_selected"))
|
||||||
|
|||||||
BIN
releases/ServerManager-v1.9.1-win-x64.exe
Normal file
64
tools/download_icons.py
Normal 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()
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Version info for ServerManager."""
|
"""Version info for ServerManager."""
|
||||||
|
|
||||||
__version__ = "1.9.0"
|
__version__ = "1.9.1"
|
||||||
__app_name__ = "ServerManager"
|
__app_name__ = "ServerManager"
|
||||||
__author__ = "aibot777"
|
__author__ = "aibot777"
|
||||||
__description__ = "Desktop GUI for managing remote servers"
|
__description__ = "Desktop GUI for managing remote servers"
|
||||||
|
|||||||