diff --git a/Furious/Actions/Connection.py b/Furious/Actions/Connection.py index 2dfe7b1..559c01f 100644 --- a/Furious/Actions/Connection.py +++ b/Furious/Actions/Connection.py @@ -120,13 +120,11 @@ class ConnectAction(AppQAction): self.setDisabled(value) APP().systemTray.RoutingAction.setDisabled(value) - APP().systemTray.SystemProxyAction.setDisabled(value) - if PLATFORM == 'Linux' or SystemRuntime.isAdmin(): - try: - APP().mainWindow.settingsPage.setTUNModeControlEnabled(not value) - except (AttributeError, RuntimeError): - pass + try: + APP().mainWindow.settingsPage.setConnectionControlsEnabled(not value) + except (AttributeError, RuntimeError): + pass def isConnected(self) -> bool: """Return whether connected.""" diff --git a/Furious/Actions/Configuration.py b/Furious/Actions/Home.py similarity index 92% rename from Furious/Actions/Configuration.py rename to Furious/Actions/Home.py index 83fc86c..c257684 100644 --- a/Furious/Actions/Configuration.py +++ b/Furious/Actions/Home.py @@ -37,8 +37,14 @@ class ShowHomePageAction(AppQAction): **kwargs, ) - self.setToolTip(_('Show the server management Home page')) - self.setStatusTip(_('Show the server management Home page')) + self._setDescription() + + def _setDescription(self): + """Apply translated explanatory text to hover/status surfaces.""" + description = _('Show the server management Home page') + + self.setToolTip(description) + self.setStatusTip(description) def triggeredCallback(self, checked): """Handle activation of the action.""" @@ -57,7 +63,4 @@ class ShowHomePageAction(AppQAction): """Refresh the action text and explanatory hover text.""" super().retranslate() - description = _('Show the server management Home page') - - self.setToolTip(description) - self.setStatusTip(description) + self._setDescription() diff --git a/Furious/Actions/Settings.py b/Furious/Actions/Settings.py deleted file mode 100644 index 486394b..0000000 --- a/Furious/Actions/Settings.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (C) 2024–present Loren Eteval & contributors -# -# This file is part of Furious. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Provide the compatibility action used by the Settings-page TUN control.""" - -from __future__ import annotations - -from Furious.Controllers import SettingsController -from Furious.Frozenlib import * -from Furious.Qt import * -from Furious.Qt import gettext as _ - -__all__ = ['TUNModeAction'] - - -class TUNModeAction(AppQAction): - """Handle the TUN mode action.""" - - def __init__(self, **kwargs): - """Initialize the TUNModeAction.""" - if PLATFORM == 'Linux': - super().__init__(_('TUN Mode'), **kwargs) - else: - if SystemRuntime.isAdmin(): - super().__init__(_('TUN Mode'), **kwargs) - else: - if ADMINISTRATOR_NAME == 'Administrator': - text = _('TUN Mode Disabled (Administrator)') - else: - text = _('TUN Mode Disabled (Superuser)') - - super().__init__(text, **kwargs) - - self.setDisabled(True) - - def triggeredCallback(self, checked): - """Handle activation of the action.""" - SettingsController.setTUNMode(checked) diff --git a/Furious/Actions/SystemProxy.py b/Furious/Actions/SystemProxy.py deleted file mode 100644 index acb9c87..0000000 --- a/Furious/Actions/SystemProxy.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (C) 2024–present Loren Eteval & contributors -# -# This file is part of Furious. -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -"""Implement tray actions for system proxy.""" - -from __future__ import annotations - -from Furious.Frozenlib import * -from Furious.Qt import * -from Furious.Qt import gettext as _ - -__all__ = ['SystemProxyAction'] - -registerAppSettings( - 'SystemProxyMode', validRange=list(mode.value for mode in AppBuiltinProxyMode) -) - - -class SystemProxyChildAction(AppQAction): - """Handle the system proxy child action.""" - - def __init__(self, *args, **kwargs): - """Initialize the SystemProxyChildAction.""" - super().__init__(*args, **kwargs) - - def triggeredCallback(self, checked): - """Handle activation of the action.""" - if self.textCompare('Automatically Configure System Proxy'): - AppSettings.set('SystemProxyMode', AppBuiltinProxyMode.Auto.value) - elif self.textCompare('Do Not Change System Proxy'): - AppSettings.set('SystemProxyMode', AppBuiltinProxyMode.NoChanges.value) - - -class SystemProxyAction(AppQAction): - """Handle the system proxy action.""" - - def __init__(self, **kwargs): - """Initialize the SystemProxyAction.""" - super().__init__( - _('System Proxy'), - icon=bootstrapIcon('hdd-network.svg'), - menu=AppQMenu( - SystemProxyChildAction( - _('Automatically Configure System Proxy'), - checkable=True, - checked=AppSettings.get('SystemProxyMode') - == AppBuiltinProxyMode.Auto.value, - ), - SystemProxyChildAction( - _('Do Not Change System Proxy'), - checkable=True, - checked=AppSettings.get('SystemProxyMode') - == AppBuiltinProxyMode.NoChanges.value, - ), - ), - **kwargs, - ) diff --git a/Furious/Actions/__init__.py b/Furious/Actions/__init__.py index acf7a98..7c0cf2b 100644 --- a/Furious/Actions/__init__.py +++ b/Furious/Actions/__init__.py @@ -19,9 +19,9 @@ from __future__ import annotations -from .Configuration import ShowHomePageAction from .Connection import ConnectAction from .Exit import ExitAction +from .Home import ShowHomePageAction from .Import import ( ImportAction, ImportFromFileAction, @@ -31,8 +31,6 @@ from .Import import ( ) from .Language import LanguageAction from .Routing import RoutingAction -from .Settings import TUNModeAction -from .SystemProxy import SystemProxyAction __all__ = [ 'ConnectAction', @@ -45,6 +43,4 @@ __all__ = [ 'LanguageAction', 'RoutingAction', 'ShowHomePageAction', - 'TUNModeAction', - 'SystemProxyAction', ] diff --git a/Furious/Application/TrayIcon.py b/Furious/Application/TrayIcon.py index 5fddb8b..9dc8235 100644 --- a/Furious/Application/TrayIcon.py +++ b/Furious/Application/TrayIcon.py @@ -56,9 +56,8 @@ class TrayIcon( actions = [ ConnectAction(isTrayAction=True), RoutingAction(isTrayAction=True), - SystemProxyAction(isTrayAction=True), - AppQSeperator(), ImportAction(isTrayAction=True), + AppQSeperator(), ShowHomePageAction(isTrayAction=True), AppQSeperator(), ExitAction(isTrayAction=True), diff --git a/Furious/Backends/Xray/Plugin.py b/Furious/Backends/Xray/Plugin.py index 5e0e07d..9692294 100644 --- a/Furious/Backends/Xray/Plugin.py +++ b/Furious/Backends/Xray/Plugin.py @@ -393,6 +393,17 @@ class XrayKernelFactory(KernelFactory): assetDownloadManager.download() +def _placeholder(x): + return x + + +_ = _placeholder + +_TRANSLATABLE = [ + _('Official Xray protocol, editor, and runtime support.'), +] + + class XrayPlugin(FuriousPlugin): """Bundle official Xray protocol handlers and its runtime backend.""" diff --git a/Furious/Controllers/SettingsController.py b/Furious/Controllers/SettingsController.py index a82c214..f9b5e4c 100644 --- a/Furious/Controllers/SettingsController.py +++ b/Furious/Controllers/SettingsController.py @@ -42,6 +42,10 @@ registerAppSettings( 'ShowProgressBarWhenConnecting', isBinary=True, default=AppBinarySettings.ON_ ) registerAppSettings('ShowTabAndSpacesInEditor', isBinary=True) +registerAppSettings( + 'SystemProxyMode', + validRange=list(mode.value for mode in AppBuiltinProxyMode), +) class SettingsController: @@ -135,6 +139,14 @@ class SettingsController: cls._setBinary('ForceToLocalhostWhenSettingLocalProxy', enabled) showMBoxNewChangesNextTime() + @staticmethod + def setSystemProxyMode(mode: str): + """Persist how Furious manages the operating-system proxy.""" + validModes = tuple(item.value for item in AppBuiltinProxyMode) + + if mode in validModes: + AppSettings.set('SystemProxyMode', mode) + @classmethod def setAutoUpdateAssets(cls, enabled: bool): """Persist automatic core-asset updates.""" diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index 8a56c98..72917ae 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -968,7 +968,6 @@ TRANSLATION = { }, "TUN Mode": { "source": [ - "Furious.Actions.Settings", "Furious.Window.SettingsPage" ], "RU": "TUN режим", @@ -977,7 +976,6 @@ TRANSLATION = { }, "TUN Mode Disabled (Administrator)": { "source": [ - "Furious.Actions.Settings", "Furious.Window.SettingsPage" ], "RU": "TUN режим отключен (Administrator)", @@ -986,7 +984,6 @@ TRANSLATION = { }, "TUN Mode Disabled (Superuser)": { "source": [ - "Furious.Actions.Settings", "Furious.Window.SettingsPage" ], "RU": "TUN режим отключен (Superuser)", @@ -1052,7 +1049,7 @@ TRANSLATION = { }, "System Proxy": { "source": [ - "Furious.Actions.SystemProxy" + "Furious.Window.SettingsPage" ], "RU": "Системный прокси", "ZH": "系统代理", @@ -1060,7 +1057,7 @@ TRANSLATION = { }, "Automatically Configure System Proxy": { "source": [ - "Furious.Actions.SystemProxy" + "Furious.Window.SettingsPage" ], "RU": "Автоматическая настройка системного прокси-сервера", "ZH": "自动配置系统代理", @@ -1068,7 +1065,7 @@ TRANSLATION = { }, "Do Not Change System Proxy": { "source": [ - "Furious.Actions.SystemProxy" + "Furious.Window.SettingsPage" ], "RU": "Не изменять системный прокси-сервер", "ZH": "不修改系统代理", @@ -2664,160 +2661,184 @@ TRANSLATION = { "source": [ "Furious.Window.SettingsPage" ], + "RU": "Очищать статистику трафика при переподключении", "ZH": "重新连接时清除流量使用统计", - "isReviewed": "True", - "RU": "Очищать статистику трафика при переподключении" + "isReviewed": "True" }, "Start accumulated upload and download usage from zero after reconnecting.": { "source": [ "Furious.Window.SettingsPage" ], + "RU": "После переподключения начинать подсчёт накопленного исходящего и входящего трафика с нуля.", "ZH": "重新连接后,将累计上传和下载流量从零开始计算。", - "isReviewed": "True", - "RU": "После переподключения начинать подсчёт накопленного исходящего и входящего трафика с нуля." + "isReviewed": "True" }, "Show Home Page...": { "source": [ - "Furious.Actions.Configuration" + "Furious.Actions.Home" ], + "RU": "Показать главную страницу...", "ZH": "显示主页...", - "isReviewed": "True", - "RU": "Показать главную страницу..." + "isReviewed": "True" }, "Show the server management Home page": { "source": [ - "Furious.Actions.Configuration" + "Furious.Actions.Home" ], + "RU": "Показать главную страницу управления серверами", "ZH": "显示服务器管理主页", - "isReviewed": "True", - "RU": "Показать главную страницу управления серверами" + "isReviewed": "True" }, "Last Updated": { "source": [ "Furious.Widget.SubscriptionTableView" ], + "RU": "Последнее обновление", "ZH": "上次更新", - "isReviewed": "True", - "RU": "Последнее обновление" + "isReviewed": "True" }, "Profiles": { "source": [ "Furious.Widget.SubscriptionTableView" ], + "RU": "Профили", "ZH": "配置文件", - "isReviewed": "True", - "RU": "Профили" + "isReviewed": "True" }, "Plugin Settings": { "source": [ "Furious.Window.SettingsPage" ], + "RU": "Настройки плагинов", "ZH": "插件设置", - "isReviewed": "True", - "RU": "Настройки плагинов" + "isReviewed": "True" }, "Enable Metrics Collection": { "source": [ "Furious.Window.SettingsPage" ], + "RU": "Включить сбор метрик", "ZH": "启用指标收集", - "isReviewed": "True", - "RU": "Включить сбор метрик" + "isReviewed": "True" }, "Collect network speed and traffic history while connected.": { "source": [ "Furious.Window.SettingsPage" ], + "RU": "Собирать историю скорости сети и трафика при подключении.", "ZH": "连接时收集网络速度和流量历史记录。", - "isReviewed": "True", - "RU": "Собирать историю скорости сети и трафика при подключении." + "isReviewed": "True" }, "URL": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "URL", "ZH": "URL", - "isReviewed": "True", - "RU": "URL" + "isReviewed": "True" }, "User Agent": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "User-Agent", "ZH": "用户代理", - "isReviewed": "True", - "RU": "User-Agent" + "isReviewed": "True" }, "Profile Filter (Regex)": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Фильтр профилей (регулярное выражение)", "ZH": "配置文件筛选器(正则表达式)", - "isReviewed": "True", - "RU": "Фильтр профилей (регулярное выражение)" + "isReviewed": "True" }, "Please enter a subscription remark.": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Введите название подписки.", "ZH": "请输入订阅备注。", - "isReviewed": "True", - "RU": "Введите название подписки." + "isReviewed": "True" }, "Please enter a valid subscription URL.": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Введите корректный URL подписки.", "ZH": "请输入有效的订阅 URL。", - "isReviewed": "True", - "RU": "Введите корректный URL подписки." + "isReviewed": "True" }, "Subscriptions": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Подписки", "ZH": "订阅管理", - "isReviewed": "True", - "RU": "Подписки" + "isReviewed": "True" }, "Update Using": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Способ обновления", "ZH": "更新方式", - "isReviewed": "True", - "RU": "Способ обновления" + "isReviewed": "True" }, "Add From Clipboard": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Добавить из буфера обмена", "ZH": "从剪贴板添加", - "isReviewed": "True", - "RU": "Добавить из буфера обмена" + "isReviewed": "True" }, "Copy URL": { "source": [ "Furious.Window.SubscriptionPage" ], - "ZH": "复制 URL", - "isReviewed": "True", - "RU": "Копировать URL" + "RU": "Копировать URL", + "ZH": "复制URL", + "isReviewed": "True" }, "Update Selected": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Обновить выбранные", "ZH": "更新所选项", - "isReviewed": "True", - "RU": "Обновить выбранные" + "isReviewed": "True" }, "Update All": { "source": [ "Furious.Window.SubscriptionPage" ], + "RU": "Обновить все", "ZH": "全部更新", - "isReviewed": "True", - "RU": "Обновить все" + "isReviewed": "True" + }, + "Official Hysteria 2 protocol, editor, and runtime support.": { + "source": [ + "Furious.Backends.Hysteria2.Plugin" + ], + "RU": "Официальная поддержка протокола, редактора и среды выполнения Hysteria 2.", + "ZH": "Hysteria 2官方协议、编辑器和运行时支持。", + "isReviewed": "True" + }, + "Official Xray protocol, editor, and runtime support.": { + "source": [ + "Furious.Backends.Xray.Plugin" + ], + "RU": "Официальная поддержка протокола, редактора и среды выполнения Xray.", + "ZH": "Xray官方协议、编辑器和运行时支持。", + "isReviewed": "True" + }, + "Choose whether Furious automatically configures the operating system proxy.": { + "source": [ + "Furious.Window.SettingsPage" + ], + "RU": "Выберите, должен ли Furious автоматически настраивать системный прокси.", + "ZH": "选择Furious是否自动配置操作系统代理。", + "isReviewed": "True" } } diff --git a/Furious/Repository/Subscriptions.py b/Furious/Repository/Subscriptions.py index 4ce8dad..e114496 100644 --- a/Furious/Repository/Subscriptions.py +++ b/Furious/Repository/Subscriptions.py @@ -23,22 +23,25 @@ from Furious.Frozenlib import * from Furious.Interface import * from Furious.Models.Encoding import * +from dataclasses import dataclass + __all__ = ['UserSubs'] registerAppSettings('CustomSubscription') +@dataclass class UserSubEntry: """Describe one user sub entry.""" - remark: str - webURL: str - enabled: bool - autoupdate: str - proxy: str - userAgent: str - filter: str - lastUpdated: str + remark: str = '' + webURL: str = '' + enabled: bool = True + autoupdate: str = '' + proxy: str = '' + userAgent: str = '' + filter: str = '' + lastUpdated: str = '' class UserSub: diff --git a/Furious/Window/SettingsPage.py b/Furious/Window/SettingsPage.py index 852955d..e4e432c 100644 --- a/Furious/Window/SettingsPage.py +++ b/Furious/Window/SettingsPage.py @@ -19,7 +19,6 @@ from __future__ import annotations -from Furious.Actions.Settings import TUNModeAction from Furious.Controllers import SettingsController from Furious.Frozenlib import * from Furious.Plugins import ( @@ -48,6 +47,25 @@ __all__ = ['SettingsPage'] logger = logging.getLogger(__name__) +def _tunModeTitle() -> str: + """Return the platform-appropriate translated TUN setting title.""" + if PLATFORM == 'Linux' or SystemRuntime.isAdmin(): + return _('TUN Mode') + + if ADMINISTRATOR_NAME == 'Administrator': + return _('TUN Mode Disabled (Administrator)') + + return _('TUN Mode Disabled (Superuser)') + + +def _restartApplicationTitle() -> str: + """Return the translated privilege-restart title for this platform.""" + if ADMINISTRATOR_NAME == 'Administrator': + return _('Restart The Application As Administrator') + + return _('Restart The Application As Superuser') + + class _SettingsSwitch(QCheckBox): """Paint a lightweight Fluent-style binary switch with Qt.""" @@ -187,7 +205,16 @@ class _SettingsCard(Mixins.ThemeAware, QFrame): IconSize = QtCore.QSize(20, 20) - def __init__(self, iconFileName: str, control: QWidget, parent=None): + def __init__( + self, + iconFileName: str, + control: QWidget, + title='', + description='', + *, + translatable=True, + parent=None, + ): """Initialize one reusable settings row.""" super().__init__(parent) @@ -196,9 +223,17 @@ class _SettingsCard(Mixins.ThemeAware, QFrame): self.iconLabel = QLabel(parent=self) self.iconLabel.setObjectName('SettingsCardIcon') self.iconLabel.setFixedSize(self.IconSize) - self.titleLabel = QLabel(parent=self) + self.titleLabel = AppQLabel( + title, + translatable=translatable, + parent=self, + ) self.titleLabel.setObjectName('SettingsCardTitle') - self.descriptionLabel = QLabel(parent=self) + self.descriptionLabel = AppQLabel( + description, + translatable=translatable, + parent=self, + ) self.descriptionLabel.setObjectName('SettingsCardDescription') self.descriptionLabel.setWordWrap(True) self.control = control @@ -241,14 +276,31 @@ class _SettingsCard(Mixins.ThemeAware, QFrame): class _ToggleSettingsCard(_SettingsCard): """Bind one binary application preference to a trailing checkbox.""" - def __init__(self, iconFileName, settingName, callback, parent=None): + def __init__( + self, + iconFileName, + settingName, + callback, + title='', + description='', + *, + translatable=True, + parent=None, + ): """Initialize a persistent binary setting card.""" self.settingName = settingName self.checkBox = _SettingsSwitch() self.checkBox.syncChecked(AppSettings.isStateON_(settingName)) self.checkBox.toggled.connect(callback) - super().__init__(iconFileName, self.checkBox, parent) + super().__init__( + iconFileName, + self.checkBox, + title, + description, + translatable=translatable, + parent=parent, + ) def sync(self): """Refresh the control from persistent state without applying it.""" @@ -258,7 +310,16 @@ class _ToggleSettingsCard(_SettingsCard): class _ActionToggleSettingsCard(_SettingsCard): """Present a plugin-provided checkable action as a Fluent switch.""" - def __init__(self, iconFileName, action, parent=None): + def __init__( + self, + iconFileName, + action, + title='', + description='', + *, + translatable=False, + parent=None, + ): """Bind switch requests and external action-state changes.""" self.action = action self.checkBox = _SettingsSwitch() @@ -269,7 +330,14 @@ class _ActionToggleSettingsCard(_SettingsCard): action.toggled.connect(self.sync) action.changed.connect(self.sync) - super().__init__(iconFileName, self.checkBox, parent) + super().__init__( + iconFileName, + self.checkBox, + title, + description, + translatable=translatable, + parent=parent, + ) @QtCore.Slot(bool) def _requestedState(self, checked: bool): @@ -289,13 +357,37 @@ class _ActionToggleSettingsCard(_SettingsCard): class _ActionSettingsCard(_SettingsCard): """Expose an existing command through one settings row.""" - def __init__(self, iconFileName, callback: Callable, parent=None): + def __init__( + self, + iconFileName, + callback: Callable, + title='', + description='', + buttonText='', + *, + translatable=True, + buttonTranslatable=None, + parent=None, + ): """Initialize a card with a compact trailing action button.""" - self.button = QPushButton() + if buttonTranslatable is None: + buttonTranslatable = translatable + + self.button = AppQPushButton( + buttonText, + translatable=buttonTranslatable, + ) self.button.setObjectName('SettingsActionButton') self.button.clicked.connect(callback) - super().__init__(iconFileName, self.button, parent) + super().__init__( + iconFileName, + self.button, + title, + description, + translatable=translatable, + parent=parent, + ) class _LineEditSettingsCard(_SettingsCard): @@ -305,16 +397,19 @@ class _LineEditSettingsCard(_SettingsCard): self, iconFileName, settingName, + title='', + description='', *, placeholder='', secret=False, strip=True, + translatable=True, parent=None, ): """Initialize one bounded settings text editor.""" self.settingName = settingName self.strip = strip - self.lineEdit = QLineEdit() + self.lineEdit = AppQLineEdit(translatable=False) self.lineEdit.setObjectName('SettingsLineEdit') self.lineEdit.setMaximumWidth(420) self.lineEdit.setMinimumWidth(260) @@ -326,7 +421,14 @@ class _LineEditSettingsCard(_SettingsCard): self.lineEdit.editingFinished.connect(self.persist) - super().__init__(iconFileName, self.lineEdit, parent) + super().__init__( + iconFileName, + self.lineEdit, + title, + description, + translatable=translatable, + parent=parent, + ) def persist(self): """Store the current text without coupling it to graph code.""" @@ -341,7 +443,7 @@ class _LineEditSettingsCard(_SettingsCard): class _LanguageSettingsCard(_SettingsCard): """Select and apply one supported application language.""" - def __init__(self, parent=None): + def __init__(self, title='', description='', parent=None): """Initialize the stable language-name selector.""" self.comboBox = QComboBox() self.comboBox.setObjectName('SettingsComboBox') @@ -353,7 +455,13 @@ class _LanguageSettingsCard(_SettingsCard): self.sync() self.comboBox.currentIndexChanged.connect(self._selectionChanged) - super().__init__('globe2.svg', self.comboBox, parent) + super().__init__( + 'globe2.svg', + self.comboBox, + title, + description, + parent=parent, + ) def sync(self): """Select the persisted language without retriggering translation.""" @@ -376,14 +484,69 @@ class _LanguageSettingsCard(_SettingsCard): SettingsController.setLanguage(language) +class _SystemProxySettingsCard(_SettingsCard): + """Select how Furious manages the operating-system proxy.""" + + Options = ( + ('Automatically Configure System Proxy', AppBuiltinProxyMode.Auto.value), + ('Do Not Change System Proxy', AppBuiltinProxyMode.NoChanges.value), + ) + + _TranslatableOptions = ( + _('Automatically Configure System Proxy'), + _('Do Not Change System Proxy'), + ) + + def __init__(self, title='', description='', parent=None): + """Initialize the translated system-proxy mode selector.""" + self.comboBox = AppQComboBox() + self.comboBox.setObjectName('SettingsComboBox') + self.comboBox.setMinimumWidth(260) + + for label, mode in self.Options: + self.comboBox.addItem(_(label), mode) + + self.sync() + self.comboBox.currentIndexChanged.connect(self._selectionChanged) + + super().__init__( + 'hdd-network.svg', + self.comboBox, + title, + description, + parent=parent, + ) + + def sync(self): + """Select the persisted proxy mode without writing it again.""" + blocker = QtCore.QSignalBlocker(self.comboBox) + index = self.comboBox.findData(AppSettings.get('SystemProxyMode')) + + self.comboBox.setCurrentIndex(max(index, 0)) + + del blocker + + @QtCore.Slot(int) + def _selectionChanged(self, _index: int): + """Persist the selected proxy-management mode.""" + mode = self.comboBox.currentData() + + if isinstance(mode, str): + SettingsController.setSystemProxyMode(mode) + + class _SettingsSection(QWidget): """Group a translated heading and a stack of settings cards.""" - def __init__(self, parent=None): + def __init__(self, title='', *, translatable=True, parent=None): """Initialize an empty settings section.""" super().__init__(parent) - self.titleLabel = QLabel(parent=self) + self.titleLabel = AppQLabel( + title, + translatable=translatable, + parent=self, + ) self.titleLabel.setObjectName('SettingsSectionTitle') self.cards = [] @@ -436,32 +599,26 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): openApplicationFolder, ) - self.pageTitleLabel = QLabel() + self.pageTitleLabel = AppQLabel(_('Settings')) self.pageTitleLabel.setObjectName('SettingsPageTitle') - self.generalSection = _SettingsSection() - self.connectionSection = _SettingsSection() - self.applicationSection = _SettingsSection() - self.pluginSettingsTitleLabel = QLabel() + self.generalSection = _SettingsSection(_('General')) + self.connectionSection = _SettingsSection(_('Connection and Interface')) + self.applicationSection = _SettingsSection(_('Application')) + self.pluginSettingsTitleLabel = AppQLabel(_('Plugin Settings')) self.pluginSettingsTitleLabel.setObjectName('SettingsSectionTitle') self.pluginSections = [] - self._pluginDescriptorCards = [] - self._pluginActionCards = [] self._pluginActions = [] - self.tunModeAction = TUNModeAction( - checkable=True, - checked=AppSettings.isStateON_('VPNMode'), - ) + self._tunModeAvailable = PLATFORM == 'Linux' or SystemRuntime.isAdmin() self.tunModeCard = _ToggleSettingsCard( 'shield-check.svg', 'VPNMode', - self._setTUNMode, + SettingsController.setTUNMode, + _tunModeTitle(), + _('Route system traffic through the active proxy connection.'), ) - self.tunModeCard.checkBox.setEnabled(self.tunModeAction.isEnabled()) - - self.tunModeAction.changed.connect(self._syncTUNModeAction) - self.tunModeAction.toggled.connect(self._syncTUNModeAction) + self.tunModeCard.checkBox.setEnabled(self._tunModeAvailable) ( self.darkModeCard, @@ -474,22 +631,33 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): 'moon-stars.svg', 'DarkMode', SettingsController.setDarkMode, + _('Dark Mode'), + _('Use the application dark theme instead of automatic appearance.'), + ), + _LanguageSettingsCard( + _('Language'), + _('Choose the language used by the application interface.'), ), - _LanguageSettingsCard(), _ToggleSettingsCard( 'circle-half.svg', 'UseMonochromeTrayIcon', SettingsController.setMonochromeTrayIcon, + _('Use Monochrome Tray Icon'), + _('Use a theme-aware single-color system tray icon.'), ), _ToggleSettingsCard( 'power.svg', 'StartupOnBoot', SettingsController.setStartupOnBoot, + _('Startup On Boot'), + _('Start the application automatically after signing in.'), ), _ToggleSettingsCard( 'battery-half.svg', 'PowerSaveMode', SettingsController.setPowerSaveMode, + _('Power Save Mode'), + _('Reduce background activity when the application is idle.'), ), ) @@ -503,6 +671,8 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): 'window.svg', 'HideDockIcon', SettingsController.setDockIconHidden, + _('Hide Dock Icon'), + _('Keep the application available from the menu bar only.'), ) self.generalSection.addCard(self.hideDockCard) else: @@ -513,6 +683,7 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): ( self.tunSettingsCard, + self.systemProxyCard, self.proxyBypassCard, self.networkTestCard, self.forceLocalhostCard, @@ -524,45 +695,73 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): _ActionSettingsCard( 'diagram-3.svg', self._openTUNSettings, + _('Customize Tun2socks Settings...'), + _('Configure the external Tun2socks network interface and routing.'), + _('Open'), + ), + _SystemProxySettingsCard( + _('System Proxy'), + _( + 'Choose whether Furious automatically configures the operating system proxy.' + ), ), _ActionSettingsCard( 'signpost-split.svg', self._proxyBypassDialog.open, + _('Customize System Proxy Bypass Address...'), + _('Choose destinations that bypass the operating system proxy.'), + _('Open'), ), _ActionSettingsCard( 'link-45deg.svg', self._networkTestDialog.open, + _('Customize Network Test URL...'), + _('Choose the URL used for download speed tests.'), + _('Open'), ), _ToggleSettingsCard( 'pc-display-horizontal.svg', 'ForceToLocalhostWhenSettingLocalProxy', SettingsController.setForceLocalProxy, + _('Force To 127.0.0.1 When Setting Local Proxy'), + _('Use the IPv4 loopback address when configuring the system proxy.'), ), _ToggleSettingsCard( 'hourglass-split.svg', 'ShowProgressBarWhenConnecting', SettingsController.setConnectionProgressVisible, + _('Show Progress Bar When Connecting'), + _('Show connection progress while proxy services are starting.'), ), _ToggleSettingsCard( 'speedometer2.svg', METRICS_COLLECTION_SETTING, SettingsController.setMetricsCollectionEnabled, + _('Enable Metrics Collection'), + _('Collect network speed and traffic history while connected.'), ), _ToggleSettingsCard( 'arrow-repeat.svg', CLEAR_TRAFFIC_USAGE_ON_RECONNECT_SETTING, SettingsController.setClearTrafficUsageOnReconnect, + _('Clear Traffic Usage Statistics On Reconnect'), + _( + 'Start accumulated upload and download usage from zero after reconnecting.' + ), ), _ToggleSettingsCard( 'text-paragraph.svg', 'ShowTabAndSpacesInEditor', SettingsController.setEditorWhitespaceVisible, + _('Show Tab And Spaces In Editor'), + _('Display whitespace markers in configuration editors.'), ), ) if not SystemRuntime.flatpakID(): self.connectionSection.addCard(self.tunSettingsCard) + self.connectionSection.addCard(self.systemProxyCard) self.connectionSection.addCard(self.proxyBypassCard) self.connectionSection.addCard(self.networkTestCard) self.connectionSection.addCard(self.forceLocalhostCard) @@ -576,6 +775,8 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): 'cloud-arrow-down.svg', 'AutoUpdateAssetFiles', SettingsController.setAutoUpdateAssets, + _('Automatically Update Asset Files'), + _('Keep supported proxy-core data files up to date.'), ) self.connectionSection.addCard(self.autoAssetsCard) else: @@ -590,10 +791,16 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): _ActionSettingsCard( 'download.svg', lambda: self._checkForUpdates(parent=self), + _('Check For Updates'), + _('Check for a newer application and proxy-core release.'), + _('Check'), ), _ActionSettingsCard( 'info-circle.svg', self._openAboutPage, + _('About'), + f'{APPLICATION_NAME} {APPLICATION_VERSION}', + _('View'), ), ) @@ -604,6 +811,9 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): self.restartCard = _ActionSettingsCard( 'arrow-clockwise.svg', self._restartAsAdmin, + _restartApplicationTitle(), + _('Restart with privileges required by system-level networking.'), + _('Restart'), ) self.applicationSection.addCard(self.restartCard) else: @@ -613,6 +823,9 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): self.openFolderCard = _ActionSettingsCard( 'folder2-open.svg', self._openApplicationFolder, + _('Open Application Folder'), + _('Open the folder containing the current application.'), + _('Open'), ) self.applicationSection.addCard(self.openFolderCard) else: @@ -663,6 +876,17 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): f'unsupported plugin setting control: {descriptor.control!r}' ) from ex + title, description = ( + self._translatedPluginText( + descriptor.title, + descriptor.translatable, + ), + self._translatedPluginText( + descriptor.description, + descriptor.translatable, + ), + ) + if control == PluginSettingControl.Toggle: if not descriptor.settingName: raise ValueError('toggle plugin settings require settingName') @@ -682,6 +906,9 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): descriptor.iconFileName, descriptor.settingName, callback, + title, + description, + translatable=descriptor.translatable, ) elif control in (PluginSettingControl.Text, PluginSettingControl.Password): if not descriptor.settingName: @@ -690,9 +917,12 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): card = _LineEditSettingsCard( descriptor.iconFileName, descriptor.settingName, + title, + description, placeholder=descriptor.placeholder, secret=control == PluginSettingControl.Password, strip=descriptor.strip, + translatable=descriptor.translatable, ) else: if not callable(descriptor.callback): @@ -701,10 +931,15 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): card = _ActionSettingsCard( descriptor.iconFileName, descriptor.callback, + title, + description, + self._translatedPluginText( + descriptor.buttonText, + descriptor.translatable, + ), + translatable=descriptor.translatable, ) - self._pluginDescriptorCards.append((card, descriptor)) - return card def _addPluginDescriptorSection( @@ -715,7 +950,13 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): if not isinstance(sectionDescriptor, PluginSettingsSection): raise TypeError('plugin settings providers must return sections') - section = _SettingsSection() + section = _SettingsSection( + self._translatedPluginText( + sectionDescriptor.title, + sectionDescriptor.translatable, + ), + translatable=sectionDescriptor.translatable, + ) section._pluginDescriptor = sectionDescriptor for descriptor in sectionDescriptor.settings: @@ -735,9 +976,16 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): isCoreActive=isCoreActive, ) - section = _SettingsSection() + section = _SettingsSection( + metadata.displayName, + translatable=False, + ) section._pluginActionMetadata = metadata + description = ( + _(metadata.description) if metadata.description else metadata.displayName + ) + for action in actions: if isinstance(action, AppQSeperator): continue @@ -750,19 +998,32 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): iconFileName = action.iconFileName or 'plugin.svg' if action.isCheckable(): - card = _ActionToggleSettingsCard(iconFileName, action) + card = _ActionToggleSettingsCard( + iconFileName, + action, + action.text(), + description, + translatable=True, + ) else: card = _ActionSettingsCard( iconFileName, lambda _checked=False, target=action: target.trigger(), + action.text(), + description, + _('Open'), + translatable=True, + buttonTranslatable=True, ) - self._pluginActionCards.append((card, action, metadata)) + # Plugin/action names are literal identities; descriptions may be + # host translation keys contributed through the static catalog. + card.titleLabel.translatable = False action.changed.connect( lambda target=card, source=action, owner=metadata: target.setTexts( source.text(), - owner.description or owner.displayName, + _(owner.description) if owner.description else owner.displayName, ) ) @@ -802,33 +1063,14 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): f'{metadata.id!r}: {ex}' ) - def _setTUNMode(self, enabled: bool): - """Trigger the compatibility action used by connection state.""" - if self.tunModeAction.isChecked() != enabled: - self.tunModeAction.trigger() - - @QtCore.Slot() - def _syncTUNModeAction(self, *_args): - """Mirror compatibility-action state into the settings control.""" - if self.tunModeCard.checkBox.isChecked() != self.tunModeAction.isChecked(): - self.tunModeCard.checkBox.syncChecked(self.tunModeAction.isChecked()) - - self.tunModeCard.checkBox.setEnabled(self.tunModeAction.isEnabled()) - def _openTUNSettings(self): """Create and retain the existing Tun2socks settings dialog.""" self._tunSettingsDialogFactory(parent=self).open() - def setTUNModeControlEnabled(self, enabled: bool): - """Disable TUN changes while a connection transition is active.""" - self.tunModeAction.setEnabled(enabled) - self.tunModeCard.checkBox.setEnabled(enabled) - - @staticmethod - def _setActionCardText(card, buttonText, title, description): - """Apply translated action-card text consistently.""" - card.button.setText(buttonText) - card.setTexts(title, description) + def setConnectionControlsEnabled(self, enabled: bool): + """Disable connection-sensitive settings during a transition.""" + self.tunModeCard.checkBox.setEnabled(bool(enabled) and self._tunModeAvailable) + self.systemProxyCard.comboBox.setEnabled(bool(enabled)) def showEvent(self, event): """Synchronize controls in case a legacy action changed a setting.""" @@ -849,170 +1091,5 @@ class SettingsPage(Mixins.QTranslatable, QMainWindow): self.languageCard.sync() def retranslate(self): - """Refresh section, card, button, and accessibility text.""" - self.pageTitleLabel.setText(_('Settings')) - self.generalSection.titleLabel.setText(_('General')) - self.connectionSection.titleLabel.setText(_('Connection and Interface')) - self.pluginSettingsTitleLabel.setText(_('Plugin Settings')) - self.applicationSection.titleLabel.setText(_('Application')) - - tunTitle = _('TUN Mode') - - if PLATFORM != 'Linux' and not SystemRuntime.isAdmin(): - if ADMINISTRATOR_NAME == 'Administrator': - tunTitle = _('TUN Mode Disabled (Administrator)') - else: - tunTitle = _('TUN Mode Disabled (Superuser)') - - self.tunModeCard.setTexts( - tunTitle, - _('Route system traffic through the active proxy connection.'), - ) - self.darkModeCard.setTexts( - _('Dark Mode'), - _('Use the application dark theme instead of automatic appearance.'), - ) - self.languageCard.setTexts( - _('Language'), - _('Choose the language used by the application interface.'), - ) - self.monochromeCard.setTexts( - _('Use Monochrome Tray Icon'), - _('Use a theme-aware single-color system tray icon.'), - ) - self.startupCard.setTexts( - _('Startup On Boot'), - _('Start the application automatically after signing in.'), - ) - self.powerSaveCard.setTexts( - _('Power Save Mode'), - _('Reduce background activity when the application is idle.'), - ) - - if self.hideDockCard is not None: - self.hideDockCard.setTexts( - _('Hide Dock Icon'), - _('Keep the application available from the menu bar only.'), - ) - - self._setActionCardText( - self.tunSettingsCard, - _('Open'), - _('Customize Tun2socks Settings...'), - _('Configure the external Tun2socks network interface and routing.'), - ) - self._setActionCardText( - self.proxyBypassCard, - _('Open'), - _('Customize System Proxy Bypass Address...'), - _('Choose destinations that bypass the operating system proxy.'), - ) - self._setActionCardText( - self.networkTestCard, - _('Open'), - _('Customize Network Test URL...'), - _('Choose the URL used for download speed tests.'), - ) - self.forceLocalhostCard.setTexts( - _('Force To 127.0.0.1 When Setting Local Proxy'), - _('Use the IPv4 loopback address when configuring the system proxy.'), - ) - self.connectionProgressCard.setTexts( - _('Show Progress Bar When Connecting'), - _('Show connection progress while proxy services are starting.'), - ) - self.metricsCollectionCard.setTexts( - _('Enable Metrics Collection'), - _('Collect network speed and traffic history while connected.'), - ) - self.clearTrafficUsageCard.setTexts( - _('Clear Traffic Usage Statistics On Reconnect'), - _( - 'Start accumulated upload and download usage from zero after reconnecting.' - ), - ) - self.editorWhitespaceCard.setTexts( - _('Show Tab And Spaces In Editor'), - _('Display whitespace markers in configuration editors.'), - ) - - if self.autoAssetsCard is not None: - self.autoAssetsCard.setTexts( - _('Automatically Update Asset Files'), - _('Keep supported proxy-core data files up to date.'), - ) - - for section in self.pluginSections: - descriptor = getattr(section, '_pluginDescriptor', None) - metadata = getattr(section, '_pluginActionMetadata', None) - - if descriptor is not None: - section.titleLabel.setText( - self._translatedPluginText( - descriptor.title, - descriptor.translatable, - ) - ) - elif metadata is not None: - section.titleLabel.setText(metadata.displayName) - - for card, descriptor in self._pluginDescriptorCards: - title = self._translatedPluginText( - descriptor.title, - descriptor.translatable, - ) - description = self._translatedPluginText( - descriptor.description, - descriptor.translatable, - ) - card.setTexts(title, description) - - if isinstance(card, _ActionSettingsCard): - card.button.setText( - self._translatedPluginText( - descriptor.buttonText, - descriptor.translatable, - ) - ) - - for card, action, metadata in self._pluginActionCards: - card.setTexts(action.text(), metadata.description or metadata.displayName) - - if isinstance(card, _ActionSettingsCard): - card.button.setText(_('Open')) - - self._setActionCardText( - self.updateCard, - _('Check'), - _('Check For Updates'), - _('Check for a newer application and proxy-core release.'), - ) - self._setActionCardText( - self.aboutCard, - _('View'), - _('About'), - f'{APPLICATION_NAME} {APPLICATION_VERSION}', - ) - - if self.restartCard is not None: - if ADMINISTRATOR_NAME == 'Administrator': - restartTitle = _('Restart The Application As Administrator') - else: - restartTitle = _('Restart The Application As Superuser') - - self._setActionCardText( - self.restartCard, - _('Restart'), - restartTitle, - _('Restart with privileges required by system-level networking.'), - ) - - if self.openFolderCard is not None: - self._setActionCardText( - self.openFolderCard, - _('Open'), - _('Open Application Folder'), - _('Open the folder containing the current application.'), - ) - + """Synchronize page-level dynamic state after a language change.""" self.languageCard.sync() diff --git a/Furious/Window/SubscriptionPage.py b/Furious/Window/SubscriptionPage.py index 6e9d149..d9dceda 100644 --- a/Furious/Window/SubscriptionPage.py +++ b/Furious/Window/SubscriptionPage.py @@ -34,6 +34,7 @@ class _SubscriptionEditorDialog(AppQDialog): super().__init__(parent) subscription = dict(subscription or {}) + self.remarkEdit = QLineEdit(subscription.get('remark', '')) self.urlEdit = QLineEdit(subscription.get('webURL', '')) self.enabledCheckBox = QCheckBox() @@ -56,6 +57,7 @@ class _SubscriptionEditorDialog(AppQDialog): autoIndex = self.autoUpdateComboBox.findData(subscription.get('autoupdate', '')) proxyIndex = self.proxyComboBox.findData(subscription.get('proxy', '')) + self.autoUpdateComboBox.setCurrentIndex(max(autoIndex, 0)) self.proxyComboBox.setCurrentIndex(max(proxyIndex, 0)) @@ -79,6 +81,7 @@ class _SubscriptionEditorDialog(AppQDialog): form.addRow(_('User Agent'), self.userAgentEdit) form.addRow(_('Profile Filter (Regex)'), self.filterEdit) form.addRow(self.buttons) + self.setLayout(form) self.setWindowTitle( @@ -155,6 +158,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): parent=self, ) self.table.doubleClicked.connect(lambda _index: self.editSelected()) + self.serverTable.subsManager.subscriptionsChanged.connect(self.table.flushAll) self.addButton.clicked.connect(self.addSubscription) @@ -190,12 +194,14 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): content = QWidget() content.setObjectName('SubscriptionPageContent') + layout = QVBoxLayout(content) layout.setContentsMargins(20, 18, 20, 20) layout.setSpacing(12) layout.addLayout(controls) layout.addLayout(actions) layout.addWidget(self.table, 1) + self.setCentralWidget(content) self.setIconsByTheme(APP().theme()) @@ -208,6 +214,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): for index, server in enumerate(Storage.UserServers()) if server.itemSubscription == unique ] + self.serverTable.deleteItemByIndex(indexes, showProgress=False) def _selectedUnique(self): @@ -219,6 +226,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): def _openEditor(self, unique=None, initial=None): """Open and retain an asynchronous add/edit dialog.""" source = Storage.UserSubs().get(unique, initial or {}) + dialog = _SubscriptionEditorDialog(source, parent=self) dialog.setWindowTitle( _('Edit Subscription') if unique else _('Add Subscription') @@ -232,6 +240,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): subscriptionUnique = unique or str(uuid.uuid4()) existing = Storage.UserSubs().get(subscriptionUnique, {}) + self.table.appendNewItem( unique=subscriptionUnique, lastUpdated=existing.get('lastUpdated', ''), @@ -366,6 +375,7 @@ class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): self.proxyComboBox.addItem(_(key), key) index = self.proxyComboBox.findData(selectedProxy) + self.proxyComboBox.setCurrentIndex(max(index, 0)) self.pageTitleLabel.setText(_('Subscriptions'))