mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-10 16:28:24 +03:00
Add unified application settings page
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+149
-181
@@ -23,7 +23,7 @@ from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
__all__ = ['SettingsAction']
|
||||
__all__ = ['SettingsAction', 'SettingsController', 'TUNModeAction']
|
||||
|
||||
registerAppSettings('VPNMode', isBinary=True)
|
||||
registerAppSettings('DarkMode', isBinary=True)
|
||||
@@ -47,11 +47,134 @@ registerAppSettings(
|
||||
)
|
||||
registerAppSettings('ShowTabAndSpacesInEditor', isBinary=True)
|
||||
|
||||
# Administrator, Superuser
|
||||
_TRANSLATABLE_TUN_MODE = [
|
||||
_('TUN Mode Disabled (Administrator)'),
|
||||
_('TUN Mode Disabled (Superuser)'),
|
||||
]
|
||||
|
||||
class SettingsController:
|
||||
"""Apply application settings independently from their presentation."""
|
||||
|
||||
@staticmethod
|
||||
def setTUNMode(enabled: bool):
|
||||
"""Persist the global TUN mode and notify active workflows."""
|
||||
if PLATFORM != 'Linux':
|
||||
assert SystemRuntime.isAdmin()
|
||||
|
||||
if enabled:
|
||||
AppSettings.turnON_('VPNMode')
|
||||
else:
|
||||
AppSettings.turnOFF('VPNMode')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
@staticmethod
|
||||
def setDarkMode(enabled: bool):
|
||||
"""Switch between the explicit dark theme and automatic mode."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('DarkMode')
|
||||
|
||||
try:
|
||||
APP().switchToDarkMode()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
AppSettings.turnOFF('DarkMode')
|
||||
|
||||
try:
|
||||
APP().switchToAutoMode()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def setLanguage(language: str):
|
||||
"""Persist a supported UI language and refresh translated objects."""
|
||||
if language not in SUPPORTED_LANGUAGE:
|
||||
return
|
||||
|
||||
if AppSettings.get('Language') != language:
|
||||
AppSettings.set('Language', language)
|
||||
Mixins.QTranslatable.retranslateAll()
|
||||
|
||||
@staticmethod
|
||||
def setMonochromeTrayIcon(enabled: bool):
|
||||
"""Persist and immediately refresh the tray-icon presentation."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('UseMonochromeTrayIcon')
|
||||
APP().systemTray.setMonochromeIcon()
|
||||
else:
|
||||
AppSettings.turnOFF('UseMonochromeTrayIcon')
|
||||
|
||||
if APP().isSystemTrayConnected():
|
||||
APP().systemTray.setConnectedIcon()
|
||||
else:
|
||||
APP().systemTray.setDisconnectedIcon()
|
||||
|
||||
@staticmethod
|
||||
def setDockIconHidden(enabled: bool):
|
||||
"""Apply the macOS dock-icon visibility preference."""
|
||||
if enabled:
|
||||
APP().installDockIconVisibilityFeature()
|
||||
AppSettings.turnON_('HideDockIcon')
|
||||
else:
|
||||
APP().installDockIconVisibilityFeature(remove=True)
|
||||
AppSettings.turnOFF('HideDockIcon')
|
||||
|
||||
@staticmethod
|
||||
def setStartupOnBoot(enabled: bool):
|
||||
"""Apply the platform startup registration preference."""
|
||||
if enabled:
|
||||
StartupOnBoot.on_()
|
||||
AppSettings.turnON_('StartupOnBoot')
|
||||
else:
|
||||
StartupOnBoot.off()
|
||||
AppSettings.turnOFF('StartupOnBoot')
|
||||
|
||||
@staticmethod
|
||||
def setPowerSaveMode(enabled: bool):
|
||||
"""Persist power-saving behavior for the next connection."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('PowerSaveMode')
|
||||
else:
|
||||
AppSettings.turnOFF('PowerSaveMode')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
@staticmethod
|
||||
def setForceLocalProxy(enabled: bool):
|
||||
"""Persist local system-proxy address normalization."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('ForceToLocalhostWhenSettingLocalProxy')
|
||||
else:
|
||||
AppSettings.turnOFF('ForceToLocalhostWhenSettingLocalProxy')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
@staticmethod
|
||||
def setAutoUpdateAssets(enabled: bool):
|
||||
"""Persist automatic core-asset updates."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('AutoUpdateAssetFiles')
|
||||
else:
|
||||
AppSettings.turnOFF('AutoUpdateAssetFiles')
|
||||
|
||||
@staticmethod
|
||||
def setConnectionProgressVisible(enabled: bool):
|
||||
"""Persist connection-progress visibility."""
|
||||
if enabled:
|
||||
AppSettings.turnON_('ShowProgressBarWhenConnecting')
|
||||
else:
|
||||
AppSettings.turnOFF('ShowProgressBarWhenConnecting')
|
||||
|
||||
@staticmethod
|
||||
def setEditorWhitespaceVisible(enabled: bool):
|
||||
"""Apply and persist editor whitespace visibility."""
|
||||
if enabled:
|
||||
APP().mainWindow.showTabAndSpaces()
|
||||
AppSettings.turnON_('ShowTabAndSpacesInEditor')
|
||||
else:
|
||||
APP().mainWindow.hideTabAndSpaces()
|
||||
AppSettings.turnOFF('ShowTabAndSpacesInEditor')
|
||||
|
||||
|
||||
class TUNModeAction(AppQAction):
|
||||
@@ -65,23 +188,18 @@ class TUNModeAction(AppQAction):
|
||||
if SystemRuntime.isAdmin():
|
||||
super().__init__(_('TUN Mode'), **kwargs)
|
||||
else:
|
||||
super().__init__(
|
||||
_(f'TUN Mode Disabled ({ADMINISTRATOR_NAME})'), **kwargs
|
||||
)
|
||||
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."""
|
||||
if PLATFORM != 'Linux':
|
||||
assert SystemRuntime.isAdmin()
|
||||
|
||||
if checked:
|
||||
AppSettings.turnON_('VPNMode')
|
||||
else:
|
||||
AppSettings.turnOFF('VPNMode')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
SettingsController.setTUNMode(checked)
|
||||
|
||||
|
||||
class SettingsChildAction(AppQAction):
|
||||
@@ -94,187 +212,37 @@ class SettingsChildAction(AppQAction):
|
||||
def triggeredCallback(self, checked):
|
||||
"""Handle activation of the action."""
|
||||
if self.textCompare('Dark Mode'):
|
||||
# Settings turn on/off order matters here
|
||||
if checked:
|
||||
AppSettings.turnON_('DarkMode')
|
||||
|
||||
try:
|
||||
APP().switchToDarkMode()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
else:
|
||||
AppSettings.turnOFF('DarkMode')
|
||||
|
||||
try:
|
||||
APP().switchToAutoMode()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
pass
|
||||
SettingsController.setDarkMode(checked)
|
||||
elif self.textCompare('Use Monochrome Tray Icon'):
|
||||
# Settings turn on/off order matters here
|
||||
if checked:
|
||||
AppSettings.turnON_('UseMonochromeTrayIcon')
|
||||
|
||||
APP().systemTray.setMonochromeIcon()
|
||||
else:
|
||||
AppSettings.turnOFF('UseMonochromeTrayIcon')
|
||||
|
||||
if APP().isSystemTrayConnected():
|
||||
APP().systemTray.setConnectedIcon()
|
||||
else:
|
||||
APP().systemTray.setDisconnectedIcon()
|
||||
SettingsController.setMonochromeTrayIcon(checked)
|
||||
elif self.textCompare('Hide Dock Icon'):
|
||||
if checked:
|
||||
APP().installDockIconVisibilityFeature()
|
||||
|
||||
AppSettings.turnON_('HideDockIcon')
|
||||
else:
|
||||
APP().installDockIconVisibilityFeature(remove=True)
|
||||
|
||||
AppSettings.turnOFF('HideDockIcon')
|
||||
SettingsController.setDockIconHidden(checked)
|
||||
elif self.textCompare('Startup On Boot'):
|
||||
if checked:
|
||||
StartupOnBoot.on_()
|
||||
|
||||
AppSettings.turnON_('StartupOnBoot')
|
||||
else:
|
||||
StartupOnBoot.off()
|
||||
|
||||
AppSettings.turnOFF('StartupOnBoot')
|
||||
SettingsController.setStartupOnBoot(checked)
|
||||
elif self.textCompare('Power Save Mode'):
|
||||
if checked:
|
||||
AppSettings.turnON_('PowerSaveMode')
|
||||
else:
|
||||
AppSettings.turnOFF('PowerSaveMode')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
SettingsController.setPowerSaveMode(checked)
|
||||
elif self.textCompare('Force To 127.0.0.1 When Setting Local Proxy'):
|
||||
if checked:
|
||||
AppSettings.turnON_('ForceToLocalhostWhenSettingLocalProxy')
|
||||
else:
|
||||
AppSettings.turnOFF('ForceToLocalhostWhenSettingLocalProxy')
|
||||
|
||||
showMBoxNewChangesNextTime()
|
||||
SettingsController.setForceLocalProxy(checked)
|
||||
elif self.textCompare('Automatically Update Asset Files'):
|
||||
if checked:
|
||||
AppSettings.turnON_('AutoUpdateAssetFiles')
|
||||
else:
|
||||
AppSettings.turnOFF('AutoUpdateAssetFiles')
|
||||
SettingsController.setAutoUpdateAssets(checked)
|
||||
elif self.textCompare('Show Progress Bar When Connecting'):
|
||||
if checked:
|
||||
AppSettings.turnON_('ShowProgressBarWhenConnecting')
|
||||
else:
|
||||
AppSettings.turnOFF('ShowProgressBarWhenConnecting')
|
||||
SettingsController.setConnectionProgressVisible(checked)
|
||||
elif self.textCompare('Show Tab And Spaces In Editor'):
|
||||
if checked:
|
||||
APP().mainWindow.showTabAndSpaces()
|
||||
|
||||
AppSettings.turnON_('ShowTabAndSpacesInEditor')
|
||||
else:
|
||||
APP().mainWindow.hideTabAndSpaces()
|
||||
|
||||
AppSettings.turnOFF('ShowTabAndSpacesInEditor')
|
||||
SettingsController.setEditorWhitespaceVisible(checked)
|
||||
|
||||
|
||||
class SettingsAction(AppQAction):
|
||||
"""Handle the settings action."""
|
||||
"""Navigate from the system tray to the page-based settings UI."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the SettingsAction."""
|
||||
if SystemRuntime.flatpakID():
|
||||
tunActions = []
|
||||
else:
|
||||
tunActions = [
|
||||
TUNModeAction(
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('VPNMode'),
|
||||
),
|
||||
AppQAction(
|
||||
_('Customize Tun2socks Settings...'),
|
||||
icon=bootstrapIcon('diagram-3.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: APP().mainWindow.getGuiTUNSettings().open(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
]
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
hideDockIconAction = [
|
||||
SettingsChildAction(
|
||||
_('Hide Dock Icon'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('HideDockIcon'),
|
||||
)
|
||||
]
|
||||
else:
|
||||
hideDockIconAction = []
|
||||
|
||||
if not SystemRuntime.isAssetsFolderWritable():
|
||||
autoUpdateAssetFilesAction = []
|
||||
else:
|
||||
autoUpdateAssetFilesAction = (
|
||||
SettingsChildAction(
|
||||
_('Automatically Update Asset Files'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('AutoUpdateAssetFiles'),
|
||||
),
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
_('Settings'),
|
||||
icon=bootstrapIcon('gear-wide-connected.svg'),
|
||||
menu=AppQMenu(
|
||||
*tunActions,
|
||||
SettingsChildAction(
|
||||
_('Dark Mode'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('DarkMode'),
|
||||
),
|
||||
SettingsChildAction(
|
||||
_('Use Monochrome Tray Icon'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('UseMonochromeTrayIcon'),
|
||||
),
|
||||
*hideDockIconAction,
|
||||
AppQSeperator(),
|
||||
SettingsChildAction(
|
||||
_('Startup On Boot'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('StartupOnBoot'),
|
||||
),
|
||||
SettingsChildAction(
|
||||
_('Power Save Mode'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('PowerSaveMode'),
|
||||
),
|
||||
AppQSeperator(),
|
||||
SettingsChildAction(
|
||||
_('Force To 127.0.0.1 When Setting Local Proxy'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_(
|
||||
'ForceToLocalhostWhenSettingLocalProxy'
|
||||
),
|
||||
),
|
||||
*autoUpdateAssetFilesAction,
|
||||
SettingsChildAction(
|
||||
_('Show Progress Bar When Connecting'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('ShowProgressBarWhenConnecting'),
|
||||
),
|
||||
SettingsChildAction(
|
||||
_('Show Tab And Spaces In Editor'),
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('ShowTabAndSpacesInEditor'),
|
||||
),
|
||||
),
|
||||
useActionGroup=False,
|
||||
callback=lambda: APP().mainWindow.showSettingsPage(),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def getTUNModeAction(self) -> AppQAction:
|
||||
# 1st action
|
||||
"""Return TUN mode action."""
|
||||
return self._menu.actions()[0]
|
||||
"""Return the Settings page's compatibility TUN action."""
|
||||
return APP().mainWindow.settingsPage.tunModeAction
|
||||
|
||||
@@ -61,7 +61,6 @@ class TrayIcon(
|
||||
ImportAction(isTrayAction=True),
|
||||
EditConfigurationAction(isTrayAction=True),
|
||||
AppQSeperator(),
|
||||
LanguageAction(isTrayAction=True),
|
||||
SettingsAction(isTrayAction=True),
|
||||
AppQSeperator(),
|
||||
ExitAction(isTrayAction=True),
|
||||
|
||||
Vendored
+264
-40
@@ -388,6 +388,7 @@ TRANSLATION = {
|
||||
"View": {
|
||||
"source": [
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.SettingsPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Вид",
|
||||
@@ -811,6 +812,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Log": {
|
||||
"source": [
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.MainWindow"
|
||||
],
|
||||
"RU": "Журнал событий",
|
||||
@@ -873,14 +875,6 @@ TRANSLATION = {
|
||||
"ZH": "编辑订阅...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Tools": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Инструменты",
|
||||
"ZH": "工具",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Plugins": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
@@ -899,7 +893,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Open Application Folder": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Открыть папку приложения",
|
||||
"ZH": "打开应用程序文件夹",
|
||||
@@ -907,7 +901,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Check For Updates": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Проверка обновлений",
|
||||
"ZH": "检查更新",
|
||||
@@ -915,7 +909,7 @@ TRANSLATION = {
|
||||
},
|
||||
"About": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "О нас",
|
||||
"ZH": "关于",
|
||||
@@ -1004,7 +998,8 @@ TRANSLATION = {
|
||||
},
|
||||
"Language": {
|
||||
"source": [
|
||||
"Furious.Actions.Language"
|
||||
"Furious.Actions.Language",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Язык",
|
||||
"ZH": "语言",
|
||||
@@ -1020,7 +1015,8 @@ TRANSLATION = {
|
||||
},
|
||||
"TUN Mode": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "TUN режим",
|
||||
"ZH": "TUN模式",
|
||||
@@ -1028,7 +1024,8 @@ TRANSLATION = {
|
||||
},
|
||||
"TUN Mode Disabled (Administrator)": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "TUN режим отключен (Administrator)",
|
||||
"ZH": "TUN模式已禁用(管理员)",
|
||||
@@ -1036,7 +1033,8 @@ TRANSLATION = {
|
||||
},
|
||||
"TUN Mode Disabled (Superuser)": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "TUN режим отключен (Superuser)",
|
||||
"ZH": "TUN模式已禁用(Superuser)",
|
||||
@@ -1044,7 +1042,9 @@ TRANSLATION = {
|
||||
},
|
||||
"Settings": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.MainWindow",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настройки",
|
||||
"ZH": "设置",
|
||||
@@ -1052,7 +1052,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Dark Mode": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Темный режим",
|
||||
"ZH": "深色模式",
|
||||
@@ -1060,7 +1060,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Startup On Boot": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Автозапуск при загрузке системы",
|
||||
"ZH": "开机启动",
|
||||
@@ -1068,7 +1068,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Force To 127.0.0.1 When Setting Local Proxy": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Принудительно 127.0.0.1 для локального прокси",
|
||||
"ZH": "设置本地代理时强制使用127.0.0.1",
|
||||
@@ -1076,7 +1076,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Automatically Update Asset Files": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Автоматически обновлять файлы активов",
|
||||
"ZH": "自动更新资源文件",
|
||||
@@ -1084,7 +1084,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Show Progress Bar When Connecting": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Отображение индикатора выполнения при подключении",
|
||||
"ZH": "连接时显示进度条",
|
||||
@@ -1092,7 +1092,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Show Tab And Spaces In Editor": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Показывать табуляцию и пробелы в редакторе",
|
||||
"ZH": "在编辑器中显示制表符和空格",
|
||||
@@ -1210,14 +1210,6 @@ TRANSLATION = {
|
||||
"ZH": "请完善服务器配置",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Help": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Помощь",
|
||||
"ZH": "帮助",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Test Tcping Latency": {
|
||||
"source": [
|
||||
"Furious.Widget.ServerTableView"
|
||||
@@ -1302,7 +1294,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Use Monochrome Tray Icon": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Использовать монохромный значок в трее",
|
||||
"ZH": "使用单色托盘图标",
|
||||
@@ -1310,7 +1302,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Hide Dock Icon": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Скрыть значок Dock",
|
||||
"ZH": "隐藏程序坞图标",
|
||||
@@ -1350,7 +1342,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Power Save Mode": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Режим энергосбережения",
|
||||
"ZH": "省电模式",
|
||||
@@ -1580,8 +1572,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Customize Tun2socks Settings...": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настроить параметры Tun2socks...",
|
||||
"ZH": "自定义Tun2socks设置...",
|
||||
@@ -1677,7 +1668,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Restart The Application As Administrator": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Перезапустите приложение от имени администратора",
|
||||
"ZH": "以管理员身份重新启动应用程序",
|
||||
@@ -1685,7 +1676,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Restart The Application As Superuser": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Перезапустите приложение от имени суперпользователя",
|
||||
"ZH": "以Superuser身份重新启动应用程序",
|
||||
@@ -1717,7 +1708,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Customize System Proxy Bypass Address...": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настроить адреса исключений системного прокси...",
|
||||
"ZH": "自定义系统代理绕过地址...",
|
||||
@@ -1750,7 +1741,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Customize Network Test URL...": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настроить URL теста сети...",
|
||||
"ZH": "自定义网络测试URL...",
|
||||
@@ -2292,7 +2283,8 @@ TRANSLATION = {
|
||||
},
|
||||
"Application": {
|
||||
"source": [
|
||||
"Furious.Window.LogPage"
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Приложение",
|
||||
"ZH": "应用程序",
|
||||
@@ -2485,5 +2477,237 @@ TRANSLATION = {
|
||||
"RU": "Ожидание сетевой статистики",
|
||||
"ZH": "正在等待网络统计数据",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"General": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Общие",
|
||||
"ZH": "常规",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Connection and Interface": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Подключение и интерфейс",
|
||||
"ZH": "连接和接口",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Hysteria2 Traffic Statistics": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Статистика трафика Hysteria2",
|
||||
"ZH": "Hysteria2流量统计",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Route system traffic through the active proxy connection.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Направлять системный трафик через активное прокси-подключение.",
|
||||
"ZH": "通过当前代理连接路由系统流量。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use the application dark theme instead of automatic appearance.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Использовать тёмную тему приложения вместо автоматического оформления.",
|
||||
"ZH": "使用应用程序深色主题,而不跟随自动外观。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Choose the language used by the application interface.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Выберите язык интерфейса приложения.",
|
||||
"ZH": "选择应用程序界面使用的语言。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use a theme-aware single-color system tray icon.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Использовать одноцветный значок в системном трее с учётом темы.",
|
||||
"ZH": "使用适配主题的单色系统托盘图标。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Start the application automatically after signing in.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Автоматически запускать приложение после входа в систему.",
|
||||
"ZH": "登录系统后自动启动应用程序。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Reduce background activity when the application is idle.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Снижать фоновую активность, когда приложение не используется.",
|
||||
"ZH": "应用程序空闲时减少后台活动。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Keep the application available from the menu bar only.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Оставлять приложение доступным только из строки меню.",
|
||||
"ZH": "仅通过菜单栏保留应用程序入口。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Open": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Открыть",
|
||||
"ZH": "打开",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Configure the external Tun2socks network interface and routing.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Настройте внешний сетевой интерфейс Tun2socks и маршрутизацию.",
|
||||
"ZH": "配置外部Tun2socks网络接口和路由。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Choose destinations that bypass the operating system proxy.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Выберите адреса, которые обходят системный прокси.",
|
||||
"ZH": "选择绕过操作系统代理的目标地址。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Choose the URL used for download speed tests.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Выберите URL для тестов скорости загрузки.",
|
||||
"ZH": "选择下载速度测试使用的URL。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use the IPv4 loopback address when configuring the system proxy.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Использовать IPv4-адрес обратной петли при настройке системного прокси.",
|
||||
"ZH": "配置系统代理时使用IPv4回环地址。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Show connection progress while proxy services are starting.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Показывать ход подключения во время запуска прокси-служб.",
|
||||
"ZH": "代理服务启动时显示连接进度。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Display whitespace markers in configuration editors.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Показывать маркеры пробелов в редакторах конфигурации.",
|
||||
"ZH": "在配置编辑器中显示空白字符标记。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Keep supported proxy-core data files up to date.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Поддерживать файлы данных прокси-ядер в актуальном состоянии.",
|
||||
"ZH": "保持受支持的代理核心数据文件为最新版本。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Traffic Stats API URL": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "URL API статистики трафика",
|
||||
"ZH": "流量统计API URL",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Hysteria2 server API address; /traffic is appended automatically.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Адрес API сервера Hysteria2; /traffic добавляется автоматически.",
|
||||
"ZH": "Hysteria2服务器API地址;将自动附加 /traffic。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Traffic Stats Client ID": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Идентификатор клиента статистики",
|
||||
"ZH": "流量统计客户端ID",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Traffic Stats API Secret": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Секрет API статистики трафика",
|
||||
"ZH": "流量统计API密钥",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Authorization value configured by the Hysteria2 server.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Значение авторизации, настроенное на сервере Hysteria2.",
|
||||
"ZH": "Hysteria2服务器配置的Authorization值。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Check": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Проверить",
|
||||
"ZH": "检查",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Check for a newer application and proxy-core release.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Проверить наличие новой версии приложения и прокси-ядра.",
|
||||
"ZH": "检查应用程序和代理核心是否有新版本。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Restart": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Перезапустить",
|
||||
"ZH": "重新启动",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Restart with privileges required by system-level networking.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Перезапустить с правами, необходимыми для системной настройки сети.",
|
||||
"ZH": "使用系统级网络所需的权限重新启动。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Open the folder containing the current application.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Открыть папку с текущим приложением.",
|
||||
"ZH": "打开包含当前应用程序的文件夹。",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Authentication ID reported by the Hysteria2 server.": {
|
||||
"source": [
|
||||
"Furious.Window.SettingsPage"
|
||||
],
|
||||
"RU": "Идентификатор аутентификации, указанный сервером Hysteria2.",
|
||||
"ZH": "Hysteria2服务器报告的身份验证 ID。",
|
||||
"isReviewed": "True"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,17 +322,79 @@ class AppStyleSheet:
|
||||
}}
|
||||
|
||||
QScrollArea#MetricsScrollArea,
|
||||
QWidget#MetricsPageContent {{
|
||||
QWidget#MetricsPageContent,
|
||||
QScrollArea#SettingsScrollArea,
|
||||
QWidget#SettingsPageContent,
|
||||
QWidget#LogPageContent {{
|
||||
border: none;
|
||||
background-color: {palette['window']};
|
||||
}}
|
||||
|
||||
QLabel#MetricsPageTitle {{
|
||||
QLabel#MetricsPageTitle,
|
||||
QLabel#LogPageTitle,
|
||||
QLabel#SettingsPageTitle {{
|
||||
color: {palette['text_strong']};
|
||||
font-size: 16pt;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QLabel#SettingsSectionTitle {{
|
||||
padding: 4px 2px;
|
||||
color: {palette['text_strong']};
|
||||
font-size: 12pt;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QFrame#SettingsCard {{
|
||||
border: 1px solid {palette['border']};
|
||||
border-radius: 9px;
|
||||
background-color: {palette['panel']};
|
||||
}}
|
||||
|
||||
QFrame#SettingsCard:hover {{
|
||||
border-color: {palette['border_strong']};
|
||||
background-color: {palette['panel_alt']};
|
||||
}}
|
||||
|
||||
QLabel#SettingsCardTitle {{
|
||||
color: {palette['text_strong']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QLabel#SettingsCardDescription {{
|
||||
color: {palette['muted']};
|
||||
}}
|
||||
|
||||
QPushButton#SettingsActionButton {{
|
||||
min-width: 82px;
|
||||
}}
|
||||
|
||||
QCheckBox#SettingsToggle::indicator {{
|
||||
width: 34px;
|
||||
height: 18px;
|
||||
border: 1px solid {palette['border_strong']};
|
||||
border-radius: 9px;
|
||||
background-color: {palette['raised']};
|
||||
image: none;
|
||||
}}
|
||||
|
||||
QCheckBox#SettingsToggle::indicator:hover {{
|
||||
border-color: {palette['accent']};
|
||||
background-color: {palette['hover']};
|
||||
}}
|
||||
|
||||
QCheckBox#SettingsToggle::indicator:checked {{
|
||||
border-color: {palette['accent']};
|
||||
background-color: {palette['accent']};
|
||||
image: none;
|
||||
}}
|
||||
|
||||
QCheckBox#SettingsToggle::indicator:disabled {{
|
||||
border-color: {palette['border']};
|
||||
background-color: {palette['raised']};
|
||||
image: none;
|
||||
}}
|
||||
|
||||
QFrame#MetricsSection {{
|
||||
border: 1px solid {palette['border']};
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -104,6 +104,7 @@ class _NavigationPage:
|
||||
widget: QWidget
|
||||
button: _NavigationButton
|
||||
translatable: bool = True
|
||||
placement: str = 'top'
|
||||
|
||||
|
||||
class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
@@ -145,9 +146,16 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
self.pageButtonLayout = QVBoxLayout()
|
||||
self.pageButtonLayout.setContentsMargins(0, 6, 0, 0)
|
||||
self.pageButtonLayout.setSpacing(4)
|
||||
|
||||
self.navigationLayout.addLayout(self.pageButtonLayout)
|
||||
self.navigationLayout.addStretch()
|
||||
|
||||
self.bottomPageButtonLayout = QVBoxLayout()
|
||||
self.bottomPageButtonLayout.setContentsMargins(0, 0, 0, 0)
|
||||
self.bottomPageButtonLayout.setSpacing(4)
|
||||
|
||||
self.navigationLayout.addLayout(self.bottomPageButtonLayout)
|
||||
|
||||
self.pageButtonGroup = QButtonGroup(self)
|
||||
self.pageButtonGroup.setExclusive(True)
|
||||
|
||||
@@ -180,6 +188,7 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
iconFileName: str,
|
||||
*,
|
||||
translatable: bool = True,
|
||||
placement: str = 'top',
|
||||
):
|
||||
"""Register a page and create its navigation control."""
|
||||
if not isinstance(pageId, str) or not pageId:
|
||||
@@ -191,6 +200,9 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
if not isinstance(widget, QWidget):
|
||||
raise TypeError('widget must be a QWidget')
|
||||
|
||||
if placement not in ('top', 'bottom'):
|
||||
raise ValueError("placement must be either 'top' or 'bottom'")
|
||||
|
||||
button = _NavigationButton(
|
||||
parent=self.navigationPanel,
|
||||
hasSelectionIndicator=True,
|
||||
@@ -216,10 +228,17 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
widget,
|
||||
button,
|
||||
translatable,
|
||||
placement,
|
||||
)
|
||||
|
||||
self._pages[pageId] = page
|
||||
self.pageButtonGroup.addButton(button)
|
||||
self.pageButtonLayout.addWidget(button)
|
||||
|
||||
if placement == 'bottom':
|
||||
self.bottomPageButtonLayout.addWidget(button)
|
||||
else:
|
||||
self.pageButtonLayout.addWidget(button)
|
||||
|
||||
self.pageStack.addWidget(widget)
|
||||
|
||||
self._updatePageText(page)
|
||||
@@ -336,6 +355,7 @@ class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
def _updatePageText(self, page: _NavigationPage):
|
||||
"""Apply translated navigation text and compact-mode tooltips."""
|
||||
text = _(page.title) if page.translatable else page.title
|
||||
|
||||
page.button.setText(text)
|
||||
page.button.setToolTip('' if self._expanded else text)
|
||||
|
||||
|
||||
@@ -511,66 +511,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
),
|
||||
]
|
||||
|
||||
if SystemRuntime.flatpakID():
|
||||
customizeTUNSettingsAction = []
|
||||
else:
|
||||
customizeTUNSettingsAction = [
|
||||
AppQAction(
|
||||
_('Customize Tun2socks Settings...'),
|
||||
icon=bootstrapIcon('diagram-3.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.getGuiTUNSettings().open(),
|
||||
),
|
||||
]
|
||||
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
_TRANSLATABLE_RESTART_AS_ADMIN = [
|
||||
_('Restart The Application As Administrator'),
|
||||
_('Restart The Application As Superuser'),
|
||||
]
|
||||
|
||||
restartAsAdminAction = [
|
||||
AppQAction(
|
||||
_(f'Restart The Application As {ADMINISTRATOR_NAME}'),
|
||||
icon=bootstrapIcon('arrow-clockwise.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.restartAsAdmin(),
|
||||
),
|
||||
]
|
||||
else:
|
||||
restartAsAdminAction = []
|
||||
|
||||
if PLATFORM == 'Darwin' or SystemRuntime.flatpakID():
|
||||
openAppFolderAction = []
|
||||
else:
|
||||
openAppFolderAction = [
|
||||
AppQAction(
|
||||
_('Open Application Folder'),
|
||||
icon=bootstrapIcon('folder2-open.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.openApplicationFolder(),
|
||||
),
|
||||
]
|
||||
|
||||
toolsActions = [
|
||||
*customizeTUNSettingsAction,
|
||||
AppQAction(
|
||||
_('Customize System Proxy Bypass Address...'),
|
||||
checkable=False,
|
||||
callback=lambda: self.customizeProxyBypassDialog.open(),
|
||||
),
|
||||
AppQAction(
|
||||
_('Customize Network Test URL...'),
|
||||
icon=bootstrapIcon('speedometer2.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.customizeNetworkTestDialog.open(),
|
||||
),
|
||||
]
|
||||
systemTools = [*restartAsAdminAction, *openAppFolderAction]
|
||||
|
||||
if systemTools:
|
||||
toolsActions.extend([AppQSeperator(), *systemTools])
|
||||
|
||||
corePluginActions = []
|
||||
|
||||
for plugin in pluginRegistry.pluginsWithCapability(
|
||||
@@ -679,29 +619,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
checkable=False,
|
||||
),
|
||||
*pluginsToolbarActions,
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Tools'),
|
||||
icon=bootstrapIcon('tools.svg'),
|
||||
menu=AppQMenu(*toolsActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Check For Updates'),
|
||||
icon=bootstrapIcon('download.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.checkForUpdates(parent=self),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('About'),
|
||||
icon=bootstrapIcon('info-circle.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.openAboutPage(),
|
||||
),
|
||||
)
|
||||
self.toolbar.setObjectName('HomePageToolBar')
|
||||
self.toolbar.setMovable(False)
|
||||
@@ -728,37 +645,11 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
'actions': [*pluginActions],
|
||||
}
|
||||
|
||||
toolsMenu = {
|
||||
'name': 'Tools',
|
||||
'actions': [*toolsActions],
|
||||
}
|
||||
|
||||
helpMenu = {
|
||||
'name': 'Help',
|
||||
'actions': [
|
||||
AppQAction(
|
||||
_('Check For Updates'),
|
||||
icon=bootstrapIcon('download.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.checkForUpdates(parent=self),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('About'),
|
||||
icon=bootstrapIcon('info-circle.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.openAboutPage(),
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
# Corresponds to menus defined above
|
||||
_TRANSLATABLE_MENU_NAME = [
|
||||
_('Server'),
|
||||
_('Subscription'),
|
||||
_('Plugins'),
|
||||
_('Tools'),
|
||||
_('Help'),
|
||||
]
|
||||
|
||||
# Menus
|
||||
@@ -766,8 +657,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
serverMenu,
|
||||
subsMenu,
|
||||
pluginsMenu,
|
||||
toolsMenu,
|
||||
helpMenu,
|
||||
):
|
||||
menuName = menuDict['name']
|
||||
menuObjName = f'_{menuName}Menu'
|
||||
|
||||
@@ -132,7 +132,9 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.manager = manager
|
||||
self._preferredFilter = str(AppSettings.get('LogViewerSelectedCategory'))
|
||||
|
||||
self.filterLabel = AppQLabel(_('Log Type'))
|
||||
self.pageTitleLabel = AppQLabel(translatable=False)
|
||||
self.pageTitleLabel.setObjectName('LogPageTitle')
|
||||
self.filterLabel = AppQLabel(translatable=False)
|
||||
self.filterComboBox = QComboBox()
|
||||
self.filterComboBox.setMinimumWidth(180)
|
||||
|
||||
@@ -143,15 +145,21 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.textBrowser.setLineWrapMode(DraculaTextBrowser.LineWrapMode.NoWrap)
|
||||
|
||||
filterLayout = QHBoxLayout()
|
||||
filterLayout.setContentsMargins(0, 0, 0, 0)
|
||||
filterLayout.setSpacing(8)
|
||||
filterLayout.addWidget(self.pageTitleLabel)
|
||||
filterLayout.addStretch(1)
|
||||
filterLayout.addWidget(self.filterLabel)
|
||||
filterLayout.addWidget(self.filterComboBox)
|
||||
filterLayout.addStretch()
|
||||
|
||||
centralLayout = QVBoxLayout()
|
||||
centralLayout.setContentsMargins(20, 18, 20, 20)
|
||||
centralLayout.setSpacing(14)
|
||||
centralLayout.addLayout(filterLayout)
|
||||
centralLayout.addWidget(self.textBrowser)
|
||||
|
||||
centralWidget = QWidget()
|
||||
centralWidget.setObjectName('LogPageContent')
|
||||
centralWidget.setLayout(centralLayout)
|
||||
|
||||
self.setCentralWidget(centralWidget)
|
||||
@@ -221,6 +229,8 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
self.manager.entryAdded.connect(self._entryAdded)
|
||||
self.manager.entriesCleared.connect(self._entriesCleared)
|
||||
|
||||
self.retranslate()
|
||||
|
||||
def _categoryText(self, category) -> str:
|
||||
"""Return a category's translated or literal display label."""
|
||||
if category.id == APPLICATION_LOG_CATEGORY:
|
||||
@@ -313,5 +323,7 @@ class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
|
||||
selectedCategoryId = self.filterComboBox.currentData()
|
||||
|
||||
self.pageTitleLabel.setText(_('Log'))
|
||||
self.filterLabel.setText(_('Log Type'))
|
||||
self._populateFilters(selectedCategoryId)
|
||||
self._refreshEntries()
|
||||
|
||||
@@ -28,6 +28,7 @@ from Furious.Widget.NavigationView import NavigationView
|
||||
from Furious.Window.HomePage import HomePage
|
||||
from Furious.Window.LogPage import LogPage
|
||||
from Furious.Window.MetricsPage import MetricsPage
|
||||
from Furious.Window.SettingsPage import SettingsPage
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
@@ -46,7 +47,12 @@ registerAppSettings('AppMainWindowState')
|
||||
registerAppSettings('AppMainWindowSelectedPage', default='home')
|
||||
registerAppSettings('AppMainWindowNavigationExpanded', isBinary=True)
|
||||
|
||||
_TRANSLATABLE_NAVIGATION_LABELS = (_('Home'), _('Metrics'), _('Log'))
|
||||
_TRANSLATABLE_NAVIGATION_LABELS = (
|
||||
_('Home'),
|
||||
_('Log'),
|
||||
_('Metrics'),
|
||||
_('Settings'),
|
||||
)
|
||||
|
||||
|
||||
class MainWindow(AppQMainWindow):
|
||||
@@ -75,6 +81,16 @@ class MainWindow(AppQMainWindow):
|
||||
parent=self.navigationView,
|
||||
)
|
||||
self.logPage = AppLogPage()
|
||||
self.settingsPage = SettingsPage(
|
||||
tunSettingsDialogFactory=self.homePage.getGuiTUNSettings,
|
||||
proxyBypassDialog=self.homePage.customizeProxyBypassDialog,
|
||||
networkTestDialog=self.homePage.customizeNetworkTestDialog,
|
||||
checkForUpdates=self.homePage.checkForUpdates,
|
||||
openAboutPage=self.homePage.openAboutPage,
|
||||
restartAsAdmin=self.homePage.restartAsAdmin,
|
||||
openApplicationFolder=self.homePage.openApplicationFolder,
|
||||
parent=self.navigationView,
|
||||
)
|
||||
|
||||
if not isinstance(self.logPage, LogPage):
|
||||
raise TypeError('application log page must be a LogPage')
|
||||
@@ -85,6 +101,12 @@ class MainWindow(AppQMainWindow):
|
||||
'Home',
|
||||
'house-door.svg',
|
||||
)
|
||||
self.navigationView.addPage(
|
||||
'log',
|
||||
self.logPage,
|
||||
'Log',
|
||||
'pin-angle.svg',
|
||||
)
|
||||
self.navigationView.addPage(
|
||||
'metrics',
|
||||
self.metricsPage,
|
||||
@@ -92,10 +114,11 @@ class MainWindow(AppQMainWindow):
|
||||
'speedometer2.svg',
|
||||
)
|
||||
self.navigationView.addPage(
|
||||
'log',
|
||||
self.logPage,
|
||||
'Log',
|
||||
'pin-angle.svg',
|
||||
'settings',
|
||||
self.settingsPage,
|
||||
'Settings',
|
||||
'gear-wide-connected.svg',
|
||||
placement='bottom',
|
||||
)
|
||||
self.navigationView.pageChanged.connect(self._pageChanged)
|
||||
self.navigationView.expandedChanged.connect(self._navigationExpandedChanged)
|
||||
@@ -140,6 +163,11 @@ class MainWindow(AppQMainWindow):
|
||||
"""Navigate directly to the unified logging page."""
|
||||
self.showPage('log')
|
||||
|
||||
def showSettingsPage(self):
|
||||
"""Navigate directly to application settings."""
|
||||
self.show()
|
||||
self.showPage('settings')
|
||||
|
||||
def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
|
||||
"""Forward a subscription update to the home page."""
|
||||
self.homePage.updateSubsByUnique(unique, httpProxy, **kwargs)
|
||||
|
||||
@@ -0,0 +1,728 @@
|
||||
# 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/>.
|
||||
|
||||
"""Present application configuration as a Fluent page of reusable cards."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Actions.Language import LANGUAGE_TO_ABBR
|
||||
from Furious.Actions.Settings import (
|
||||
SettingsController,
|
||||
TUNModeAction,
|
||||
)
|
||||
from Furious.Backends.Hysteria2.Stats import (
|
||||
HYSTERIA2_STATS_CLIENT_ID_SETTING,
|
||||
HYSTERIA2_STATS_SECRET_SETTING,
|
||||
HYSTERIA2_STATS_URL_SETTING,
|
||||
)
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore, QtGui
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
__all__ = ['SettingsPage']
|
||||
|
||||
|
||||
class _SettingsSwitch(QCheckBox):
|
||||
"""Paint a lightweight Fluent-style binary switch with Qt."""
|
||||
|
||||
ControlSize = QtCore.QSize(38, 22)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the compact switch control."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('SettingsToggle')
|
||||
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
|
||||
self.setFixedSize(self.ControlSize)
|
||||
self.toggled.connect(lambda _checked: self.update())
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""Draw the track and animated-position-equivalent thumb."""
|
||||
del event
|
||||
|
||||
palette = AppStyleSheet.Palettes[APP().theme()]
|
||||
painter = QtGui.QPainter(self)
|
||||
painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
|
||||
|
||||
track = QtCore.QRectF(1, 2, self.width() - 2, self.height() - 4)
|
||||
radius = track.height() / 2
|
||||
|
||||
if not self.isEnabled():
|
||||
background, border, thumb = (
|
||||
palette['raised'],
|
||||
palette['border'],
|
||||
palette['disabled'],
|
||||
)
|
||||
elif self.isChecked():
|
||||
background, border, thumb = (
|
||||
palette['accent'],
|
||||
palette['accent'],
|
||||
palette['accent_text'],
|
||||
)
|
||||
else:
|
||||
background, border, thumb = (
|
||||
palette['raised'],
|
||||
palette['border_strong'],
|
||||
palette['muted'],
|
||||
)
|
||||
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(border), 1))
|
||||
painter.setBrush(QtGui.QColor(background))
|
||||
painter.drawRoundedRect(track, radius, radius)
|
||||
|
||||
thumbDiameter = track.height() - 6
|
||||
thumbX = (
|
||||
track.right() - thumbDiameter - 3 if self.isChecked() else track.left() + 3
|
||||
)
|
||||
thumbRect = QtCore.QRectF(
|
||||
thumbX,
|
||||
track.top() + 3,
|
||||
thumbDiameter,
|
||||
thumbDiameter,
|
||||
)
|
||||
|
||||
painter.setPen(QtCore.Qt.PenStyle.NoPen)
|
||||
painter.setBrush(QtGui.QColor(thumb))
|
||||
painter.drawEllipse(thumbRect)
|
||||
|
||||
if self.hasFocus():
|
||||
focusRect = track.adjusted(-0.5, -0.5, 0.5, 0.5)
|
||||
painter.setBrush(QtCore.Qt.BrushStyle.NoBrush)
|
||||
painter.setPen(QtGui.QPen(QtGui.QColor(palette['accent']), 1))
|
||||
painter.drawRoundedRect(focusRect, radius, radius)
|
||||
|
||||
|
||||
class _SettingsCard(Mixins.ThemeAware, QFrame):
|
||||
"""Lay out a themed icon, title, description, and trailing control."""
|
||||
|
||||
IconSize = QtCore.QSize(20, 20)
|
||||
|
||||
def __init__(self, iconFileName: str, control: QWidget, parent=None):
|
||||
"""Initialize one reusable settings row."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('SettingsCard')
|
||||
self.iconFileName = iconFileName
|
||||
self.iconLabel = QLabel(parent=self)
|
||||
self.iconLabel.setObjectName('SettingsCardIcon')
|
||||
self.iconLabel.setFixedSize(self.IconSize)
|
||||
self.titleLabel = QLabel(parent=self)
|
||||
self.titleLabel.setObjectName('SettingsCardTitle')
|
||||
self.descriptionLabel = QLabel(parent=self)
|
||||
self.descriptionLabel.setObjectName('SettingsCardDescription')
|
||||
self.descriptionLabel.setWordWrap(True)
|
||||
self.control = control
|
||||
self.control.setParent(self)
|
||||
|
||||
textLayout = QVBoxLayout()
|
||||
textLayout.setContentsMargins(0, 0, 0, 0)
|
||||
textLayout.setSpacing(2)
|
||||
textLayout.addWidget(self.titleLabel)
|
||||
textLayout.addWidget(self.descriptionLabel)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(16, 12, 16, 12)
|
||||
layout.setSpacing(14)
|
||||
layout.addWidget(self.iconLabel, 0, QtCore.Qt.AlignmentFlag.AlignTop)
|
||||
layout.addLayout(textLayout, 1)
|
||||
layout.addSpacing(16)
|
||||
layout.addWidget(self.control, 0, QtCore.Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
def setTexts(self, title: str, description: str):
|
||||
"""Set already translated card text."""
|
||||
self.titleLabel.setText(title)
|
||||
self.descriptionLabel.setText(description)
|
||||
|
||||
def setIconByTheme(self, theme: str):
|
||||
"""Refresh the card icon for the active theme."""
|
||||
iconFactory = (
|
||||
bootstrapIconWhite if theme == AppStyleSheet.Dark else bootstrapIcon
|
||||
)
|
||||
self.iconLabel.setPixmap(iconFactory(self.iconFileName).pixmap(self.IconSize))
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Refresh themed card artwork."""
|
||||
self.setIconByTheme(theme)
|
||||
self.control.update()
|
||||
|
||||
|
||||
class _ToggleSettingsCard(_SettingsCard):
|
||||
"""Bind one binary application preference to a trailing checkbox."""
|
||||
|
||||
def __init__(self, iconFileName, settingName, callback, parent=None):
|
||||
"""Initialize a persistent binary setting card."""
|
||||
self.settingName = settingName
|
||||
self.checkBox = _SettingsSwitch()
|
||||
self.checkBox.setChecked(AppSettings.isStateON_(settingName))
|
||||
self.checkBox.toggled.connect(callback)
|
||||
|
||||
super().__init__(iconFileName, self.checkBox, parent)
|
||||
|
||||
def sync(self):
|
||||
"""Refresh the control from persistent state without applying it."""
|
||||
blocker = QtCore.QSignalBlocker(self.checkBox)
|
||||
|
||||
self.checkBox.setChecked(AppSettings.isStateON_(self.settingName))
|
||||
|
||||
del blocker
|
||||
|
||||
|
||||
class _ActionSettingsCard(_SettingsCard):
|
||||
"""Expose an existing command through one settings row."""
|
||||
|
||||
def __init__(self, iconFileName, callback: Callable, parent=None):
|
||||
"""Initialize a card with a compact trailing action button."""
|
||||
self.button = QPushButton()
|
||||
self.button.setObjectName('SettingsActionButton')
|
||||
self.button.clicked.connect(callback)
|
||||
|
||||
super().__init__(iconFileName, self.button, parent)
|
||||
|
||||
|
||||
class _LineEditSettingsCard(_SettingsCard):
|
||||
"""Persist one text setting when editing finishes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
iconFileName,
|
||||
settingName,
|
||||
*,
|
||||
placeholder='',
|
||||
secret=False,
|
||||
strip=True,
|
||||
parent=None,
|
||||
):
|
||||
"""Initialize one bounded settings text editor."""
|
||||
self.settingName = settingName
|
||||
self.strip = strip
|
||||
self.lineEdit = QLineEdit()
|
||||
self.lineEdit.setObjectName('SettingsLineEdit')
|
||||
self.lineEdit.setMaximumWidth(420)
|
||||
self.lineEdit.setMinimumWidth(260)
|
||||
self.lineEdit.setPlaceholderText(placeholder)
|
||||
self.lineEdit.setText(str(AppSettings.get(settingName) or ''))
|
||||
|
||||
if secret:
|
||||
self.lineEdit.setEchoMode(QLineEdit.EchoMode.Password)
|
||||
|
||||
self.lineEdit.editingFinished.connect(self.persist)
|
||||
|
||||
super().__init__(iconFileName, self.lineEdit, parent)
|
||||
|
||||
def persist(self):
|
||||
"""Store the current text without coupling it to graph code."""
|
||||
value = self.lineEdit.text()
|
||||
|
||||
if self.strip:
|
||||
value = value.strip()
|
||||
|
||||
AppSettings.set(self.settingName, value)
|
||||
|
||||
|
||||
class _LanguageSettingsCard(_SettingsCard):
|
||||
"""Select and apply one supported application language."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the stable language-name selector."""
|
||||
self.comboBox = QComboBox()
|
||||
self.comboBox.setObjectName('SettingsComboBox')
|
||||
self.comboBox.setMinimumWidth(180)
|
||||
|
||||
for languageName, language in LANGUAGE_TO_ABBR.items():
|
||||
self.comboBox.addItem(languageName, language)
|
||||
|
||||
self.sync()
|
||||
self.comboBox.currentIndexChanged.connect(self._selectionChanged)
|
||||
|
||||
super().__init__('globe2.svg', self.comboBox, parent)
|
||||
|
||||
def sync(self):
|
||||
"""Select the persisted language without retriggering translation."""
|
||||
if not hasattr(self, 'comboBox'):
|
||||
return
|
||||
|
||||
blocker = QtCore.QSignalBlocker(self.comboBox)
|
||||
index = self.comboBox.findData(AppSettings.get('Language'))
|
||||
|
||||
self.comboBox.setCurrentIndex(max(index, 0))
|
||||
|
||||
del blocker
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def _selectionChanged(self, _index: int):
|
||||
"""Delegate language application to the settings controller."""
|
||||
language = self.comboBox.currentData()
|
||||
|
||||
if isinstance(language, str):
|
||||
SettingsController.setLanguage(language)
|
||||
|
||||
|
||||
class _SettingsSection(QWidget):
|
||||
"""Group a translated heading and a stack of settings cards."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize an empty settings section."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.titleLabel = QLabel(parent=self)
|
||||
self.titleLabel.setObjectName('SettingsSectionTitle')
|
||||
self.cards = []
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.layout.setSpacing(6)
|
||||
self.layout.addWidget(self.titleLabel)
|
||||
|
||||
def addCard(self, card: _SettingsCard):
|
||||
"""Append a card while retaining it for translation and syncing."""
|
||||
self.cards.append(card)
|
||||
self.layout.addWidget(card)
|
||||
|
||||
|
||||
class SettingsPage(Mixins.QTranslatable, QMainWindow):
|
||||
"""Compose application settings without owning their operational logic."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
tunSettingsDialogFactory,
|
||||
proxyBypassDialog,
|
||||
networkTestDialog,
|
||||
checkForUpdates,
|
||||
openAboutPage,
|
||||
restartAsAdmin,
|
||||
openApplicationFolder,
|
||||
parent=None,
|
||||
):
|
||||
"""Build settings sections around existing actions and dialogs."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('SettingsPage')
|
||||
|
||||
(
|
||||
self._tunSettingsDialogFactory,
|
||||
self._proxyBypassDialog,
|
||||
self._networkTestDialog,
|
||||
self._checkForUpdates,
|
||||
self._openAboutPage,
|
||||
self._restartAsAdmin,
|
||||
self._openApplicationFolder,
|
||||
) = (
|
||||
tunSettingsDialogFactory,
|
||||
proxyBypassDialog,
|
||||
networkTestDialog,
|
||||
checkForUpdates,
|
||||
openAboutPage,
|
||||
restartAsAdmin,
|
||||
openApplicationFolder,
|
||||
)
|
||||
|
||||
self.pageTitleLabel = QLabel()
|
||||
self.pageTitleLabel.setObjectName('SettingsPageTitle')
|
||||
|
||||
self.generalSection = _SettingsSection()
|
||||
self.connectionSection = _SettingsSection()
|
||||
self.hysteriaSection = _SettingsSection()
|
||||
self.applicationSection = _SettingsSection()
|
||||
|
||||
self.tunModeAction = TUNModeAction(
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('VPNMode'),
|
||||
)
|
||||
self.tunModeCard = _ToggleSettingsCard(
|
||||
'shield-check.svg',
|
||||
'VPNMode',
|
||||
self._setTUNMode,
|
||||
)
|
||||
self.tunModeCard.checkBox.setEnabled(self.tunModeAction.isEnabled())
|
||||
|
||||
self.tunModeAction.changed.connect(self._syncTUNModeAction)
|
||||
self.tunModeAction.toggled.connect(self._syncTUNModeAction)
|
||||
|
||||
(
|
||||
self.darkModeCard,
|
||||
self.languageCard,
|
||||
self.monochromeCard,
|
||||
self.startupCard,
|
||||
self.powerSaveCard,
|
||||
) = (
|
||||
_ToggleSettingsCard(
|
||||
'moon-stars.svg',
|
||||
'DarkMode',
|
||||
SettingsController.setDarkMode,
|
||||
),
|
||||
_LanguageSettingsCard(),
|
||||
_ToggleSettingsCard(
|
||||
'circle-half.svg',
|
||||
'UseMonochromeTrayIcon',
|
||||
SettingsController.setMonochromeTrayIcon,
|
||||
),
|
||||
_ToggleSettingsCard(
|
||||
'power.svg',
|
||||
'StartupOnBoot',
|
||||
SettingsController.setStartupOnBoot,
|
||||
),
|
||||
_ToggleSettingsCard(
|
||||
'battery-half.svg',
|
||||
'PowerSaveMode',
|
||||
SettingsController.setPowerSaveMode,
|
||||
),
|
||||
)
|
||||
|
||||
self.generalSection.addCard(self.tunModeCard)
|
||||
self.generalSection.addCard(self.darkModeCard)
|
||||
self.generalSection.addCard(self.languageCard)
|
||||
self.generalSection.addCard(self.monochromeCard)
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
self.hideDockCard = _ToggleSettingsCard(
|
||||
'window.svg',
|
||||
'HideDockIcon',
|
||||
SettingsController.setDockIconHidden,
|
||||
)
|
||||
self.generalSection.addCard(self.hideDockCard)
|
||||
else:
|
||||
self.hideDockCard = None
|
||||
|
||||
self.generalSection.addCard(self.startupCard)
|
||||
self.generalSection.addCard(self.powerSaveCard)
|
||||
|
||||
(
|
||||
self.tunSettingsCard,
|
||||
self.proxyBypassCard,
|
||||
self.networkTestCard,
|
||||
self.forceLocalhostCard,
|
||||
self.connectionProgressCard,
|
||||
self.editorWhitespaceCard,
|
||||
) = (
|
||||
_ActionSettingsCard(
|
||||
'diagram-3.svg',
|
||||
self._openTUNSettings,
|
||||
),
|
||||
_ActionSettingsCard(
|
||||
'signpost-split.svg',
|
||||
self._proxyBypassDialog.open,
|
||||
),
|
||||
_ActionSettingsCard(
|
||||
'link-45deg.svg',
|
||||
self._networkTestDialog.open,
|
||||
),
|
||||
_ToggleSettingsCard(
|
||||
'pc-display-horizontal.svg',
|
||||
'ForceToLocalhostWhenSettingLocalProxy',
|
||||
SettingsController.setForceLocalProxy,
|
||||
),
|
||||
_ToggleSettingsCard(
|
||||
'hourglass-split.svg',
|
||||
'ShowProgressBarWhenConnecting',
|
||||
SettingsController.setConnectionProgressVisible,
|
||||
),
|
||||
_ToggleSettingsCard(
|
||||
'text-paragraph.svg',
|
||||
'ShowTabAndSpacesInEditor',
|
||||
SettingsController.setEditorWhitespaceVisible,
|
||||
),
|
||||
)
|
||||
|
||||
if not SystemRuntime.flatpakID():
|
||||
self.connectionSection.addCard(self.tunSettingsCard)
|
||||
|
||||
self.connectionSection.addCard(self.proxyBypassCard)
|
||||
self.connectionSection.addCard(self.networkTestCard)
|
||||
self.connectionSection.addCard(self.forceLocalhostCard)
|
||||
self.connectionSection.addCard(self.connectionProgressCard)
|
||||
self.connectionSection.addCard(self.editorWhitespaceCard)
|
||||
|
||||
if SystemRuntime.isAssetsFolderWritable():
|
||||
self.autoAssetsCard = _ToggleSettingsCard(
|
||||
'cloud-arrow-down.svg',
|
||||
'AutoUpdateAssetFiles',
|
||||
SettingsController.setAutoUpdateAssets,
|
||||
)
|
||||
self.connectionSection.addCard(self.autoAssetsCard)
|
||||
else:
|
||||
self.autoAssetsCard = None
|
||||
|
||||
(
|
||||
self.hysteriaURLCard,
|
||||
self.hysteriaClientIdCard,
|
||||
self.hysteriaSecretCard,
|
||||
) = (
|
||||
_LineEditSettingsCard(
|
||||
'activity.svg',
|
||||
HYSTERIA2_STATS_URL_SETTING,
|
||||
placeholder='http://server:9999',
|
||||
),
|
||||
_LineEditSettingsCard(
|
||||
'person-badge.svg',
|
||||
HYSTERIA2_STATS_CLIENT_ID_SETTING,
|
||||
),
|
||||
_LineEditSettingsCard(
|
||||
'key.svg',
|
||||
HYSTERIA2_STATS_SECRET_SETTING,
|
||||
secret=True,
|
||||
strip=False,
|
||||
),
|
||||
)
|
||||
|
||||
self.hysteriaSection.addCard(self.hysteriaURLCard)
|
||||
self.hysteriaSection.addCard(self.hysteriaClientIdCard)
|
||||
self.hysteriaSection.addCard(self.hysteriaSecretCard)
|
||||
|
||||
(
|
||||
self.updateCard,
|
||||
self.aboutCard,
|
||||
) = (
|
||||
_ActionSettingsCard(
|
||||
'download.svg',
|
||||
lambda: self._checkForUpdates(parent=self),
|
||||
),
|
||||
_ActionSettingsCard(
|
||||
'info-circle.svg',
|
||||
self._openAboutPage,
|
||||
),
|
||||
)
|
||||
|
||||
self.applicationSection.addCard(self.updateCard)
|
||||
self.applicationSection.addCard(self.aboutCard)
|
||||
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
self.restartCard = _ActionSettingsCard(
|
||||
'arrow-clockwise.svg',
|
||||
self._restartAsAdmin,
|
||||
)
|
||||
self.applicationSection.addCard(self.restartCard)
|
||||
else:
|
||||
self.restartCard = None
|
||||
|
||||
if PLATFORM != 'Darwin' and not SystemRuntime.flatpakID():
|
||||
self.openFolderCard = _ActionSettingsCard(
|
||||
'folder2-open.svg',
|
||||
self._openApplicationFolder,
|
||||
)
|
||||
self.applicationSection.addCard(self.openFolderCard)
|
||||
else:
|
||||
self.openFolderCard = None
|
||||
|
||||
contentWidget = QWidget()
|
||||
contentWidget.setObjectName('SettingsPageContent')
|
||||
|
||||
contentLayout = QVBoxLayout(contentWidget)
|
||||
contentLayout.setContentsMargins(20, 18, 20, 24)
|
||||
contentLayout.setSpacing(22)
|
||||
contentLayout.addWidget(self.pageTitleLabel)
|
||||
contentLayout.addWidget(self.generalSection)
|
||||
contentLayout.addWidget(self.connectionSection)
|
||||
contentLayout.addWidget(self.hysteriaSection)
|
||||
contentLayout.addWidget(self.applicationSection)
|
||||
contentLayout.addStretch(1)
|
||||
|
||||
self.scrollArea = QScrollArea()
|
||||
self.scrollArea.setObjectName('SettingsScrollArea')
|
||||
self.scrollArea.setWidgetResizable(True)
|
||||
self.scrollArea.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scrollArea.setWidget(contentWidget)
|
||||
self.setCentralWidget(self.scrollArea)
|
||||
|
||||
self.retranslate()
|
||||
|
||||
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."""
|
||||
blocker = QtCore.QSignalBlocker(self.tunModeCard.checkBox)
|
||||
|
||||
self.tunModeCard.checkBox.setChecked(self.tunModeAction.isChecked())
|
||||
self.tunModeCard.checkBox.setEnabled(self.tunModeAction.isEnabled())
|
||||
|
||||
del blocker
|
||||
|
||||
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 showEvent(self, event):
|
||||
"""Synchronize controls in case a legacy action changed a setting."""
|
||||
super().showEvent(event)
|
||||
|
||||
for section in (
|
||||
self.generalSection,
|
||||
self.connectionSection,
|
||||
self.applicationSection,
|
||||
):
|
||||
for card in section.cards:
|
||||
sync = getattr(card, 'sync', None)
|
||||
|
||||
if callable(sync):
|
||||
sync()
|
||||
|
||||
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.hysteriaSection.titleLabel.setText(_('Hysteria2 Traffic Statistics'))
|
||||
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.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.'),
|
||||
)
|
||||
|
||||
self.hysteriaURLCard.setTexts(
|
||||
_('Traffic Stats API URL'),
|
||||
_('Hysteria2 server API address; /traffic is appended automatically.'),
|
||||
)
|
||||
self.hysteriaClientIdCard.setTexts(
|
||||
_('Traffic Stats Client ID'),
|
||||
_('Authentication ID reported by the Hysteria2 server.'),
|
||||
)
|
||||
self.hysteriaSecretCard.setTexts(
|
||||
_('Traffic Stats API Secret'),
|
||||
_('Authorization value configured by the Hysteria2 server.'),
|
||||
)
|
||||
|
||||
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.'),
|
||||
)
|
||||
|
||||
self.languageCard.sync()
|
||||
@@ -24,6 +24,7 @@ from .HomePage import HomePage
|
||||
from .LogPage import LogPage
|
||||
from .MetricsPage import MetricsPage
|
||||
from .MainWindow import MainWindow
|
||||
from .SettingsPage import SettingsPage
|
||||
from .NetworkTestDialog import NetworkTestDialog
|
||||
from .ProxyBypassDialog import ProxyBypassDialog
|
||||
from .QRCodeWindow import QRCodeWindow
|
||||
@@ -37,6 +38,7 @@ __all__ = [
|
||||
'LogPage',
|
||||
'MetricsPage',
|
||||
'MainWindow',
|
||||
'SettingsPage',
|
||||
'NetworkTestDialog',
|
||||
'ProxyBypassDialog',
|
||||
'QRCodeWindow',
|
||||
|
||||
Reference in New Issue
Block a user