Centralize application settings UI

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-12 12:23:19 +08:00
parent 6d0f75d1ea
commit bee149a8b0
12 changed files with 436 additions and 429 deletions
+4 -6
View File
@@ -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."""
@@ -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()
-52
View File
@@ -1,52 +0,0 @@
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
#
# 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 <https://www.gnu.org/licenses/>.
"""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)
-71
View File
@@ -1,71 +0,0 @@
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
#
# 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 <https://www.gnu.org/licenses/>.
"""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,
)
+1 -5
View File
@@ -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',
]
+1 -2
View File
@@ -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),
+11
View File
@@ -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."""
+12
View File
@@ -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."""
+70 -49
View File
@@ -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"
}
}
+11 -8
View File
@@ -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:
+307 -230
View File
@@ -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()
+10
View File
@@ -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'))