v1.2.0 + v1.3.0: Localization, About dialog, TOTP/2FA, stability improvements
v1.2.0: - GUI localization (EN/RU/ZH) with language switcher and persistent selection - About dialog (ⓘ) with app info, features, quick start guide - core/i18n.py — internationalization module with t() function - All GUI components translated via t() keys v1.3.0: - TOTP/2FA tab — Google Authenticator compatible codes with live 30s countdown, one-click copy, per-server secret management - core/totp.py — TOTP module (pyotp, RFC 6238) - core/logger.py — rotating file logger (5MB, 3 backups) - Stronger Fernet encryption key with automatic migration from old key - Thread-safe server store with locks, atomic writes, auto-restore on corruption - Parallel status checks via ThreadPoolExecutor (up to 10 concurrent) - SSH client: explicit channel cleanup, Unix key permissions - Server dialog: port validation (1-65535), TOTP secret field - Language change preserves active tab and server selection - pyotp dependency added Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
139
gui/app.py
139
gui/app.py
@@ -7,13 +7,17 @@ from tkinter import messagebox
|
||||
|
||||
from core.server_store import ServerStore
|
||||
from core.status_checker import StatusChecker
|
||||
from core import i18n
|
||||
from core.i18n import t, LANGUAGES
|
||||
from gui.sidebar import Sidebar
|
||||
from gui.server_dialog import ServerDialog
|
||||
from gui.about_dialog import AboutDialog
|
||||
from gui.tabs.terminal_tab import TerminalTab
|
||||
from gui.tabs.files_tab import FilesTab
|
||||
from gui.tabs.info_tab import InfoTab
|
||||
from gui.tabs.keys_tab import KeysTab
|
||||
from gui.tabs.setup_tab import SetupTab
|
||||
from gui.tabs.totp_tab import TOTPTab
|
||||
|
||||
|
||||
class App(ctk.CTk):
|
||||
@@ -55,30 +59,54 @@ class App(ctk.CTk):
|
||||
main = ctk.CTkFrame(self, fg_color="transparent")
|
||||
main.pack(side="right", fill="both", expand=True)
|
||||
|
||||
# Header bar (language + about)
|
||||
header_bar = ctk.CTkFrame(main, fg_color="transparent", height=40)
|
||||
header_bar.pack(fill="x", padx=10, pady=(8, 0))
|
||||
header_bar.pack_propagate(False)
|
||||
|
||||
# Language selector
|
||||
lang_values = list(LANGUAGES.values())
|
||||
current_display = LANGUAGES.get(i18n.get_language(), "English")
|
||||
self._lang_var = ctk.StringVar(value=current_display)
|
||||
self.lang_menu = ctk.CTkOptionMenu(
|
||||
header_bar, values=lang_values, variable=self._lang_var,
|
||||
width=110, height=30, command=self._change_language
|
||||
)
|
||||
self.lang_menu.pack(side="right", padx=(5, 0))
|
||||
|
||||
# About button
|
||||
self.about_btn = ctk.CTkButton(
|
||||
header_bar, text="ⓘ", width=30, height=30,
|
||||
corner_radius=15, fg_color="#6b7280", hover_color="#4b5563",
|
||||
command=self._show_about
|
||||
)
|
||||
self.about_btn.pack(side="right", padx=(5, 5))
|
||||
|
||||
# Tabview
|
||||
self.tabview = ctk.CTkTabview(main)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
# Tabs
|
||||
self.tabview.add("Terminal")
|
||||
self.tabview.add("Files")
|
||||
self.tabview.add("Info")
|
||||
self.tabview.add("Keys")
|
||||
self.tabview.add("Setup")
|
||||
# Tab names stored for language updates
|
||||
self._tab_keys = ["terminal", "files", "info", "keys", "totp", "setup"]
|
||||
for key in self._tab_keys:
|
||||
self.tabview.add(t(key))
|
||||
|
||||
self.terminal_tab = TerminalTab(self.tabview.tab("Terminal"), self.store)
|
||||
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store)
|
||||
self.terminal_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.files_tab = FilesTab(self.tabview.tab("Files"), self.store)
|
||||
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store)
|
||||
self.files_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.info_tab = InfoTab(self.tabview.tab("Info"), self.store, edit_callback=self._edit_server)
|
||||
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
||||
self.info_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.keys_tab = KeysTab(self.tabview.tab("Keys"), self.store)
|
||||
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
||||
self.keys_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.setup_tab = SetupTab(self.tabview.tab("Setup"), self.store)
|
||||
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
||||
self.totp_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
||||
self.setup_tab.pack(fill="both", expand=True)
|
||||
|
||||
def _on_server_select(self, alias: str):
|
||||
@@ -86,6 +114,7 @@ class App(ctk.CTk):
|
||||
self.files_tab.set_server(alias)
|
||||
self.info_tab.set_server(alias)
|
||||
self.keys_tab.set_server(alias)
|
||||
self.totp_tab.set_server(alias)
|
||||
|
||||
def _add_server(self):
|
||||
dialog = ServerDialog(self, self.store)
|
||||
@@ -99,7 +128,7 @@ class App(ctk.CTk):
|
||||
self.info_tab.refresh()
|
||||
|
||||
def _delete_server(self, alias: str):
|
||||
if messagebox.askyesno("Delete Server", f"Remove '{alias}'?"):
|
||||
if messagebox.askyesno(t("delete_server"), t("delete_confirm").format(alias=alias)):
|
||||
self.store.remove_server(alias)
|
||||
self._on_server_select(None)
|
||||
|
||||
@@ -107,6 +136,92 @@ class App(ctk.CTk):
|
||||
self.sidebar.update_statuses()
|
||||
self.info_tab.refresh()
|
||||
|
||||
def _show_about(self):
|
||||
AboutDialog(self)
|
||||
|
||||
def _get_current_tab_key(self) -> str:
|
||||
"""Get the i18n key of the currently active tab."""
|
||||
try:
|
||||
current_name = self.tabview.get()
|
||||
# Match against current language translations
|
||||
for key in self._tab_keys:
|
||||
if t(key) == current_name:
|
||||
return key
|
||||
except Exception:
|
||||
pass
|
||||
return self._tab_keys[0]
|
||||
|
||||
def _change_language(self, display_name: str):
|
||||
# Remember current tab KEY before language switch
|
||||
active_tab_key = self._get_current_tab_key()
|
||||
|
||||
# Find lang code from display name
|
||||
lang_code = "en"
|
||||
for code, name in LANGUAGES.items():
|
||||
if name == display_name:
|
||||
lang_code = code
|
||||
break
|
||||
i18n.set_language(lang_code)
|
||||
self.store._save_settings()
|
||||
self._apply_language(active_tab_key)
|
||||
|
||||
def _apply_language(self, restore_tab_key: str | None = None):
|
||||
# Remember selected server
|
||||
alias = self.sidebar.get_selected()
|
||||
# Use provided key or default to first tab
|
||||
current_key = restore_tab_key or self._tab_keys[0]
|
||||
|
||||
# Detach tab contents
|
||||
self.terminal_tab.pack_forget()
|
||||
self.files_tab.pack_forget()
|
||||
self.info_tab.pack_forget()
|
||||
self.keys_tab.pack_forget()
|
||||
self.totp_tab.pack_forget()
|
||||
self.setup_tab.pack_forget()
|
||||
|
||||
# Get the main frame and destroy old tabview
|
||||
main = self.tabview.master
|
||||
self.tabview.destroy()
|
||||
|
||||
# Create new tabview with translated names
|
||||
self.tabview = ctk.CTkTabview(main)
|
||||
self.tabview.pack(fill="both", expand=True, padx=10, pady=10)
|
||||
|
||||
for key in self._tab_keys:
|
||||
self.tabview.add(t(key))
|
||||
|
||||
# Re-parent tab contents
|
||||
self.terminal_tab = TerminalTab(self.tabview.tab(t("terminal")), self.store)
|
||||
self.terminal_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.files_tab = FilesTab(self.tabview.tab(t("files")), self.store)
|
||||
self.files_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.info_tab = InfoTab(self.tabview.tab(t("info")), self.store, edit_callback=self._edit_server)
|
||||
self.info_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.keys_tab = KeysTab(self.tabview.tab(t("keys")), self.store)
|
||||
self.keys_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.totp_tab = TOTPTab(self.tabview.tab(t("totp")), self.store)
|
||||
self.totp_tab.pack(fill="both", expand=True)
|
||||
|
||||
self.setup_tab = SetupTab(self.tabview.tab(t("setup")), self.store)
|
||||
self.setup_tab.pack(fill="both", expand=True)
|
||||
|
||||
# Restore active tab by key
|
||||
try:
|
||||
self.tabview.set(t(current_key))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Restore server selection
|
||||
if alias:
|
||||
self._on_server_select(alias)
|
||||
|
||||
# Update sidebar
|
||||
self.sidebar.update_language()
|
||||
|
||||
def _on_close(self):
|
||||
self.checker.stop()
|
||||
self.destroy()
|
||||
|
||||
Reference in New Issue
Block a user