mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-30 19:08:03 +03:00
Introduce page-based navigation
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -31,7 +31,7 @@ from Furious.Qt import gettext as _
|
||||
from Furious.Repository import *
|
||||
from Furious.Service import ApplicationLogHandler, LogManager
|
||||
from Furious.Application.TrayIcon import *
|
||||
from Furious.Window.LogWindow import *
|
||||
from Furious.Window.LogPage import *
|
||||
from Furious.Window.MainWindow import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -182,7 +182,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
|
||||
# Unified logging service and presentation
|
||||
self.logManager = None
|
||||
self.logWindow = None
|
||||
self.logPage = None
|
||||
|
||||
# Protected storage access
|
||||
self._userActivatedItemIndex = None
|
||||
@@ -231,7 +231,7 @@ class DesktopApplication(ApplicationRunner, SingletonApplication):
|
||||
"""Configure logging."""
|
||||
self.logManager = LogManager(parent=self)
|
||||
|
||||
self.logWindow = LogWindow(
|
||||
self.logPage = LogPage(
|
||||
manager=self.logManager,
|
||||
fontFamily=self.customFontName,
|
||||
)
|
||||
|
||||
@@ -184,7 +184,7 @@ class MsgQueue(multiprocessing.queues.Queue):
|
||||
self.callback(msg)
|
||||
|
||||
if self.optimizer is not None and self.optimizer.isVisible():
|
||||
# Logger window is visible: maximum loading speed for user
|
||||
# Log page is visible: maximum loading speed for user
|
||||
|
||||
# 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 2, 2, ...
|
||||
# For timeout value 2 Furious can handle at about 500 messages per second
|
||||
@@ -193,7 +193,7 @@ class MsgQueue(multiprocessing.queues.Queue):
|
||||
)
|
||||
self.startTimer()
|
||||
else:
|
||||
# Logger window does not exist or is not visible: low speed in background
|
||||
# Log page is unavailable or hidden: low speed in background
|
||||
# to avoid consuming too much CPU resources
|
||||
|
||||
# 1024, 512, 256, 256, 256, ...
|
||||
@@ -300,8 +300,8 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the CoreProcessWorker."""
|
||||
msgCallback = kwargs.pop('msgCallback', None)
|
||||
# Drain output more frequently while the unified log window is visible.
|
||||
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLogWindow)
|
||||
# Drain output more frequently while the unified log page is visible.
|
||||
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLogPage)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ class Tun2socks(CoreProcessWorker):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Initialize the Tun2socks."""
|
||||
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLogWindow)
|
||||
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLogPage)
|
||||
|
||||
super().__init__(**kwargs, backgroundOptimizer=backgroundOptimizer)
|
||||
|
||||
|
||||
Vendored
+69
-62
@@ -274,7 +274,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Save As...": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить как...",
|
||||
@@ -292,7 +292,7 @@ TRANSLATION = {
|
||||
"File": {
|
||||
"source": [
|
||||
"Furious.Backends.Xray.AssetWindow",
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Файл",
|
||||
@@ -325,7 +325,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Copy": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Копировать",
|
||||
@@ -343,7 +343,7 @@ TRANSLATION = {
|
||||
"Select All": {
|
||||
"source": [
|
||||
"Furious.Widget.ServerTableView",
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Выбрать все",
|
||||
@@ -360,7 +360,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Edit": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Редактировать",
|
||||
@@ -369,7 +369,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Zoom In": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Увеличить маcштаб",
|
||||
@@ -378,7 +378,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Zoom Out": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Уменьшить маcштаб",
|
||||
@@ -387,7 +387,7 @@ TRANSLATION = {
|
||||
},
|
||||
"View": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Вид",
|
||||
@@ -404,7 +404,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Save File": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow",
|
||||
"Furious.Window.LogPage",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить файл",
|
||||
@@ -508,7 +508,7 @@ TRANSLATION = {
|
||||
"Subscription": {
|
||||
"source": [
|
||||
"Furious.Widget.ServerTableView",
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Подписка",
|
||||
"ZH": "订阅",
|
||||
@@ -606,7 +606,7 @@ TRANSLATION = {
|
||||
"New Empty Configuration": {
|
||||
"source": [
|
||||
"Furious.Widget.ServerTableView",
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Создать новую пустую конфигурацию",
|
||||
"ZH": "新建空白配置",
|
||||
@@ -713,8 +713,7 @@ TRANSLATION = {
|
||||
"Exit": {
|
||||
"source": [
|
||||
"Furious.Actions.Exit",
|
||||
"Furious.Backends.Xray.AssetWindow",
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Backends.Xray.AssetWindow"
|
||||
],
|
||||
"RU": "Выход",
|
||||
"ZH": "退出",
|
||||
@@ -722,7 +721,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Unable to save log": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Не удалось сохранить журнал",
|
||||
"ZH": "无法保存日志",
|
||||
@@ -730,7 +729,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Text files (*.txt);;All files (*)": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Текстовые файлы (*.txt);;Все файлы (*)",
|
||||
"ZH": "文本文件 (*.txt);;所有文件 (*)",
|
||||
@@ -738,20 +737,12 @@ TRANSLATION = {
|
||||
},
|
||||
"Error saving log": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Ошибка сохранения журнала",
|
||||
"ZH": "保存日志出错",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Log Viewer": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
],
|
||||
"RU": "Средство просмотра журнала",
|
||||
"ZH": "日志查看器",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Asset file already exists. Overwrite?": {
|
||||
"source": [
|
||||
"Furious.Backends.Xray.AssetListWidget"
|
||||
@@ -812,7 +803,7 @@ TRANSLATION = {
|
||||
"source": [
|
||||
"Furious.Backends.Hysteria1.Editor",
|
||||
"Furious.Backends.Hysteria2.Editor",
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Сервер",
|
||||
"ZH": "服务器",
|
||||
@@ -826,17 +817,41 @@ TRANSLATION = {
|
||||
"ZH": "日志",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Update Subscription (Use Current Proxy)": {
|
||||
"Home": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
],
|
||||
"RU": "Главная",
|
||||
"ZH": "主页",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Expand Navigation": {
|
||||
"source": [
|
||||
"Furious.Widget.NavigationView"
|
||||
],
|
||||
"RU": "Развернуть навигацию",
|
||||
"ZH": "展开导航",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Collapse Navigation": {
|
||||
"source": [
|
||||
"Furious.Widget.NavigationView"
|
||||
],
|
||||
"RU": "Свернуть навигацию",
|
||||
"ZH": "折叠导航",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Update Subscription (Use Current Proxy)": {
|
||||
"source": [
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Обновить подписки (использовать текущий прокси-сервер)",
|
||||
"ZH": "更新订阅(使用当前代理)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Update Subscription (Force Proxy)": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Обновить подписки (обязательно через прокси-сервер)",
|
||||
"ZH": "更新订阅(强制代理)",
|
||||
@@ -844,7 +859,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Update Subscription (No Proxy)": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Обновить подписки (не использовать прокси)",
|
||||
"ZH": "更新订阅(无代理)",
|
||||
@@ -852,7 +867,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Edit Subscription...": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Редактировать подписки...",
|
||||
"ZH": "编辑订阅...",
|
||||
@@ -860,7 +875,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Tools": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Инструменты",
|
||||
"ZH": "工具",
|
||||
@@ -868,7 +883,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Plugins": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Плагины",
|
||||
"ZH": "插件",
|
||||
@@ -876,7 +891,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Core": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Core",
|
||||
"ZH": "Core",
|
||||
@@ -884,7 +899,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Open Application Folder": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Открыть папку приложения",
|
||||
"ZH": "打开应用程序文件夹",
|
||||
@@ -892,7 +907,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Check For Updates": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Проверка обновлений",
|
||||
"ZH": "检查更新",
|
||||
@@ -900,7 +915,7 @@ TRANSLATION = {
|
||||
},
|
||||
"About": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "О нас",
|
||||
"ZH": "关于",
|
||||
@@ -1197,7 +1212,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Help": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Помощь",
|
||||
"ZH": "帮助",
|
||||
@@ -1566,7 +1581,7 @@ TRANSLATION = {
|
||||
"Customize Tun2socks Settings...": {
|
||||
"source": [
|
||||
"Furious.Actions.Settings",
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Настроить параметры Tun2socks...",
|
||||
"ZH": "自定义Tun2socks设置...",
|
||||
@@ -1662,7 +1677,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Restart The Application As Administrator": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Перезапустите приложение от имени администратора",
|
||||
"ZH": "以管理员身份重新启动应用程序",
|
||||
@@ -1670,7 +1685,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Restart The Application As Superuser": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Перезапустите приложение от имени суперпользователя",
|
||||
"ZH": "以Superuser身份重新启动应用程序",
|
||||
@@ -1686,7 +1701,7 @@ TRANSLATION = {
|
||||
},
|
||||
"I am the supreme authority 👑": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Я — единственная высшая инстанция 👑",
|
||||
"ZH": "我就是至高无上的存在 👑",
|
||||
@@ -1702,7 +1717,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Customize System Proxy Bypass Address...": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Настроить адреса исключений системного прокси...",
|
||||
"ZH": "自定义系统代理绕过地址...",
|
||||
@@ -1735,7 +1750,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Customize Network Test URL...": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Настроить URL теста сети...",
|
||||
"ZH": "自定义网络测试URL...",
|
||||
@@ -1767,7 +1782,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Search servers with text or regex, e.g. trojan, hk|jp, ^vmess, (us|sg).*tls": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Поиск серверов по тексту или регулярному выражению, например: trojan, hk|jp, ^vmess, (us|sg).*tls",
|
||||
"ZH": "使用文本或正则表达式搜索服务器,例如:trojan、hk|jp、^vmess、(us|sg).*tls",
|
||||
@@ -1775,7 +1790,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Search": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Поиск",
|
||||
"ZH": "查找",
|
||||
@@ -1783,7 +1798,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Network OK": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Сеть работает",
|
||||
"ZH": "网络正常",
|
||||
@@ -1791,7 +1806,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Network error": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Ошибка сети",
|
||||
"ZH": "网络异常",
|
||||
@@ -2245,23 +2260,15 @@ TRANSLATION = {
|
||||
},
|
||||
"Extensions": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Расширения",
|
||||
"ZH": "扩展功能",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Show Logs...": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
],
|
||||
"RU": "Показать журналы...",
|
||||
"ZH": "显示日志...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Log Type": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Тип журнала",
|
||||
"ZH": "日志类型",
|
||||
@@ -2269,7 +2276,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Application": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Приложение",
|
||||
"ZH": "应用程序",
|
||||
@@ -2277,7 +2284,7 @@ TRANSLATION = {
|
||||
},
|
||||
"All Logs": {
|
||||
"source": [
|
||||
"Furious.Window.LogWindow"
|
||||
"Furious.Window.LogPage"
|
||||
],
|
||||
"RU": "Все журналы",
|
||||
"ZH": "全部日志",
|
||||
@@ -2285,7 +2292,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Upload Speed": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Скорость отдачи",
|
||||
"ZH": "上传速度",
|
||||
@@ -2293,7 +2300,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Download Speed": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Скорость загрузки",
|
||||
"ZH": "下载速度",
|
||||
@@ -2301,7 +2308,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Upload Traffic Usage": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Объём исходящего трафика",
|
||||
"ZH": "上传流量",
|
||||
@@ -2309,7 +2316,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Download Traffic Usage": {
|
||||
"source": [
|
||||
"Furious.Window.MainWindow"
|
||||
"Furious.Window.HomePage"
|
||||
],
|
||||
"RU": "Объём входящего трафика",
|
||||
"ZH": "下载流量",
|
||||
|
||||
@@ -27,7 +27,7 @@ __all__ = [
|
||||
'APP',
|
||||
'AppFontName',
|
||||
'AppLogManager',
|
||||
'AppLogWindow',
|
||||
'AppLogPage',
|
||||
'AppThreadPool',
|
||||
]
|
||||
|
||||
@@ -42,11 +42,11 @@ def getAppAttributes(name: str):
|
||||
(
|
||||
AppFontName,
|
||||
AppLogManager,
|
||||
AppLogWindow,
|
||||
AppLogPage,
|
||||
AppThreadPool,
|
||||
) = (
|
||||
functools.partial(getAppAttributes, 'customFontName'),
|
||||
functools.partial(getAppAttributes, 'logManager'),
|
||||
functools.partial(getAppAttributes, 'logWindow'),
|
||||
functools.partial(getAppAttributes, 'logPage'),
|
||||
functools.partial(getAppAttributes, 'threadPool'),
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ from .Constants import (
|
||||
XRAY_ASSET_PATH_GEOSITE,
|
||||
)
|
||||
from .Enum import AppBuiltinCommand, AppBuiltinProxyMode, AppBuiltinRouting
|
||||
from .Globals import APP, AppFontName, AppLogManager, AppLogWindow, AppThreadPool
|
||||
from .Globals import APP, AppFontName, AppLogManager, AppLogPage, AppThreadPool
|
||||
from .Mixins import Mixins
|
||||
from .PySide6Legacy import PySide6Legacy
|
||||
from .StartupOnBoot import StartupOnBoot
|
||||
@@ -112,7 +112,7 @@ __all__ = [
|
||||
'AppBuiltinRouting',
|
||||
'AppFontName',
|
||||
'AppLogManager',
|
||||
'AppLogWindow',
|
||||
'AppLogPage',
|
||||
'AppResources',
|
||||
'AppSettings',
|
||||
'AppThreadPool',
|
||||
|
||||
@@ -296,6 +296,17 @@ class AppStyleSheet:
|
||||
border-color: {palette['border']};
|
||||
}}
|
||||
|
||||
QFrame#NavigationPanel {{
|
||||
border: none;
|
||||
border-right: 1px solid {palette['border']};
|
||||
background-color: {palette['panel_alt']};
|
||||
}}
|
||||
|
||||
QStackedWidget#ApplicationPageStack {{
|
||||
border: none;
|
||||
background-color: {palette['window']};
|
||||
}}
|
||||
|
||||
QGroupBox {{
|
||||
margin-top: 16px;
|
||||
padding: 14px 12px 12px 12px;
|
||||
@@ -415,13 +426,15 @@ class AppStyleSheet:
|
||||
background-color: {palette['border']};
|
||||
}}
|
||||
|
||||
QToolBar#AppMainWindow_AppQToolBar {{
|
||||
QToolBar#AppMainWindow_AppQToolBar,
|
||||
QToolBar#HomePageToolBar {{
|
||||
padding: 6px 8px;
|
||||
spacing: 2px;
|
||||
background-color: {palette['panel']};
|
||||
}}
|
||||
|
||||
QToolBar#AppMainWindow_AppQToolBar::separator {{
|
||||
QToolBar#AppMainWindow_AppQToolBar::separator,
|
||||
QToolBar#HomePageToolBar::separator {{
|
||||
margin: 8px 4px;
|
||||
}}
|
||||
|
||||
@@ -434,13 +447,46 @@ class AppStyleSheet:
|
||||
color: {palette['text']};
|
||||
}}
|
||||
|
||||
QToolBar#AppMainWindow_AppQToolBar QToolButton {{
|
||||
QToolBar#AppMainWindow_AppQToolBar QToolButton,
|
||||
QToolBar#HomePageToolBar QToolButton {{
|
||||
min-width: 72px;
|
||||
min-height: 54px;
|
||||
padding: 5px 11px;
|
||||
border-radius: 8px;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationToggleButton,
|
||||
QToolButton#NavigationPageButton {{
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 7px;
|
||||
background-color: transparent;
|
||||
color: {palette['text']};
|
||||
text-align: left;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationToggleButton:hover,
|
||||
QToolButton#NavigationPageButton:hover {{
|
||||
border-color: transparent;
|
||||
background-color: {palette['hover']};
|
||||
color: {palette['text_strong']};
|
||||
}}
|
||||
|
||||
QToolButton#NavigationPageButton:checked {{
|
||||
border: none;
|
||||
border-left: 3px solid {palette['accent']};
|
||||
border-radius: 7px;
|
||||
background-color: {palette['accent_soft']};
|
||||
color: {palette['text_strong']};
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
QToolButton#NavigationPageButton:checked:hover {{
|
||||
background-color: {palette['accent_soft_hover']};
|
||||
}}
|
||||
|
||||
QToolButton:hover {{
|
||||
border-color: {palette['border']};
|
||||
background-color: {palette['hover']};
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
# 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 extensible collapsible navigation for application pages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import APP, Mixins
|
||||
from Furious.Qt import AppStyleSheet, bootstrapIcon, bootstrapIconWhite
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
__all__ = ['NavigationView']
|
||||
|
||||
|
||||
@dataclass
|
||||
class _NavigationPage:
|
||||
"""Describe one registered page and its navigation control."""
|
||||
|
||||
id: str
|
||||
title: str
|
||||
iconFileName: str
|
||||
widget: QWidget
|
||||
button: QToolButton
|
||||
translatable: bool = True
|
||||
|
||||
|
||||
class NavigationView(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
"""Route registered pages through a collapsible left navigation rail."""
|
||||
|
||||
pageChanged = QtCore.Signal(str)
|
||||
expandedChanged = QtCore.Signal(bool)
|
||||
|
||||
CollapsedWidth = 56
|
||||
ExpandedWidth = 220
|
||||
IconSize = QtCore.QSize(20, 20)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize an empty page registry and its navigation controls."""
|
||||
super().__init__(parent)
|
||||
|
||||
self._pages: dict[str, _NavigationPage] = {}
|
||||
self._currentPageId = ''
|
||||
self._expanded = False
|
||||
|
||||
self.setObjectName('NavigationView')
|
||||
|
||||
self.navigationPanel = QFrame(parent=self)
|
||||
self.navigationPanel.setObjectName('NavigationPanel')
|
||||
|
||||
self.toggleButton = QToolButton(parent=self.navigationPanel)
|
||||
self.toggleButton.setObjectName('NavigationToggleButton')
|
||||
self.toggleButton.setToolTip(_('Expand Navigation'))
|
||||
self.toggleButton.setIconSize(self.IconSize)
|
||||
self.toggleButton.clicked.connect(self.toggleExpanded)
|
||||
|
||||
self.navigationLayout = QVBoxLayout(self.navigationPanel)
|
||||
self.navigationLayout.setContentsMargins(6, 8, 6, 8)
|
||||
self.navigationLayout.setSpacing(4)
|
||||
self.navigationLayout.addWidget(self.toggleButton)
|
||||
|
||||
self.pageButtonLayout = QVBoxLayout()
|
||||
self.pageButtonLayout.setContentsMargins(0, 6, 0, 0)
|
||||
self.pageButtonLayout.setSpacing(4)
|
||||
self.navigationLayout.addLayout(self.pageButtonLayout)
|
||||
self.navigationLayout.addStretch()
|
||||
|
||||
self.pageButtonGroup = QButtonGroup(self)
|
||||
self.pageButtonGroup.setExclusive(True)
|
||||
|
||||
self.pageStack = QStackedWidget(parent=self)
|
||||
self.pageStack.setObjectName('ApplicationPageStack')
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSpacing(0)
|
||||
self._layout.addWidget(self.navigationPanel)
|
||||
self._layout.addWidget(self.pageStack, 1)
|
||||
|
||||
self.setExpanded(False)
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
def addPage(
|
||||
self,
|
||||
pageId: str,
|
||||
widget: QWidget,
|
||||
title: str,
|
||||
iconFileName: str,
|
||||
*,
|
||||
translatable: bool = True,
|
||||
):
|
||||
"""Register a page and create its navigation control."""
|
||||
if not isinstance(pageId, str) or not pageId:
|
||||
raise ValueError('pageId must be a non-empty string')
|
||||
|
||||
if pageId in self._pages:
|
||||
raise ValueError(f'page is already registered: {pageId}')
|
||||
|
||||
if not isinstance(widget, QWidget):
|
||||
raise TypeError('widget must be a QWidget')
|
||||
|
||||
button = QToolButton(parent=self.navigationPanel)
|
||||
button.setObjectName('NavigationPageButton')
|
||||
button.setProperty('pageId', pageId)
|
||||
button.setCheckable(True)
|
||||
button.setIconSize(self.IconSize)
|
||||
button.setToolButtonStyle(
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||
if self._expanded
|
||||
else QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||
)
|
||||
button.setSizePolicy(
|
||||
QSizePolicy.Policy.Expanding,
|
||||
QSizePolicy.Policy.Fixed,
|
||||
)
|
||||
button.clicked.connect(
|
||||
lambda _checked=False, selectedPageId=pageId: self.setCurrentPage(
|
||||
selectedPageId
|
||||
)
|
||||
)
|
||||
|
||||
page = _NavigationPage(
|
||||
pageId,
|
||||
title,
|
||||
iconFileName,
|
||||
widget,
|
||||
button,
|
||||
translatable,
|
||||
)
|
||||
self._pages[pageId] = page
|
||||
self.pageButtonGroup.addButton(button)
|
||||
self.pageButtonLayout.addWidget(button)
|
||||
self.pageStack.addWidget(widget)
|
||||
|
||||
self._updatePageText(page)
|
||||
self._updatePageIcon(page, APP().theme())
|
||||
|
||||
if not self._currentPageId:
|
||||
self.setCurrentPage(pageId)
|
||||
|
||||
def page(self, pageId: str) -> QWidget | None:
|
||||
"""Return the registered widget for *pageId*."""
|
||||
page = self._pages.get(pageId)
|
||||
|
||||
return page.widget if page is not None else None
|
||||
|
||||
def currentPageId(self) -> str:
|
||||
"""Return the identifier of the visible page."""
|
||||
return self._currentPageId
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def setCurrentPage(self, pageId: str):
|
||||
"""Show the page registered under *pageId*."""
|
||||
page = self._pages.get(pageId)
|
||||
|
||||
if page is None:
|
||||
return
|
||||
|
||||
changed = pageId != self._currentPageId
|
||||
self._currentPageId = pageId
|
||||
page.button.setChecked(True)
|
||||
self.pageStack.setCurrentWidget(page.widget)
|
||||
|
||||
if changed:
|
||||
self.pageChanged.emit(pageId)
|
||||
|
||||
def isExpanded(self) -> bool:
|
||||
"""Return whether labels are visible beside navigation icons."""
|
||||
return self._expanded
|
||||
|
||||
@QtCore.Slot()
|
||||
def toggleExpanded(self):
|
||||
"""Toggle between the compact rail and expanded navigation menu."""
|
||||
self.setExpanded(not self._expanded)
|
||||
|
||||
@QtCore.Slot(bool)
|
||||
def setExpanded(self, expanded: bool):
|
||||
"""Set the navigation menu expansion state."""
|
||||
expanded = bool(expanded)
|
||||
changed = expanded != self._expanded
|
||||
self._expanded = expanded
|
||||
|
||||
self.navigationPanel.setFixedWidth(
|
||||
self.ExpandedWidth if expanded else self.CollapsedWidth
|
||||
)
|
||||
self.toggleButton.setToolTip(
|
||||
_('Collapse Navigation') if expanded else _('Expand Navigation')
|
||||
)
|
||||
self.toggleButton.setProperty('expanded', expanded)
|
||||
self._refreshWidgetStyle(self.toggleButton)
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
style = (
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextBesideIcon
|
||||
if expanded
|
||||
else QtCore.Qt.ToolButtonStyle.ToolButtonIconOnly
|
||||
)
|
||||
|
||||
for page in self._pages.values():
|
||||
page.button.setToolButtonStyle(style)
|
||||
self._updatePageText(page)
|
||||
|
||||
if changed:
|
||||
self.expandedChanged.emit(expanded)
|
||||
|
||||
@staticmethod
|
||||
def _refreshWidgetStyle(widget: QWidget):
|
||||
"""Re-polish a widget after a dynamic property changes."""
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
widget.update()
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _iconFactory(theme: str):
|
||||
"""Return the icon factory appropriate for *theme*."""
|
||||
return bootstrapIconWhite if theme == AppStyleSheet.Dark else bootstrapIcon
|
||||
|
||||
def _updatePageIcon(self, page: _NavigationPage, theme: str):
|
||||
"""Apply one page icon for the active theme."""
|
||||
page.button.setIcon(self._iconFactory(theme)(page.iconFileName))
|
||||
|
||||
def setIconByTheme(self, theme: str):
|
||||
"""Refresh navigation icons for the active theme."""
|
||||
iconFactory = self._iconFactory(theme)
|
||||
toggleIcon = (
|
||||
'layout-sidebar-inset-reverse.svg'
|
||||
if self._expanded
|
||||
else 'layout-sidebar-inset.svg'
|
||||
)
|
||||
self.toggleButton.setIcon(iconFactory(toggleIcon))
|
||||
|
||||
for page in self._pages.values():
|
||||
self._updatePageIcon(page, theme)
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Refresh icons after an application theme change."""
|
||||
self.setIconByTheme(theme)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh navigation labels and expansion affordances."""
|
||||
self.toggleButton.setToolTip(
|
||||
_('Collapse Navigation') if self._expanded else _('Expand Navigation')
|
||||
)
|
||||
|
||||
for page in self._pages.values():
|
||||
self._updatePageText(page)
|
||||
@@ -20,12 +20,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .ConnectionProgressWidget import ConnectionProgressWidget
|
||||
from .NavigationView import NavigationView
|
||||
from .ServerTableView import ServerTableView
|
||||
from .SubscriptionTableView import SubscriptionTableView
|
||||
from .WaitingSpinner import WaitingSpinner
|
||||
|
||||
__all__ = [
|
||||
'ConnectionProgressWidget',
|
||||
'NavigationView',
|
||||
'ServerTableView',
|
||||
'SubscriptionTableView',
|
||||
'WaitingSpinner',
|
||||
|
||||
@@ -0,0 +1,960 @@
|
||||
# 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 server-management home page."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Models import *
|
||||
from Furious.Repository import *
|
||||
from Furious.Plugins import CapabilityKind, getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Service import (
|
||||
ConnectivityManager,
|
||||
TrafficStatsManager,
|
||||
UpdateManager,
|
||||
formatTrafficSpeed,
|
||||
formatTrafficUsage,
|
||||
)
|
||||
from Furious.Actions.Import import (
|
||||
ImportFromFileAction,
|
||||
ImportJSONFromClipboardAction,
|
||||
ImportQRCodeOnTheScreenAction,
|
||||
ImportURIFromClipboardAction,
|
||||
)
|
||||
from Furious.Widget.ServerTableView import *
|
||||
from Furious.Window.NetworkTestDialog import *
|
||||
from Furious.Window.ProxyBypassDialog import *
|
||||
from Furious.Window.QRCodeWindow import QRCodeWindow
|
||||
from Furious.Window.SubscriptionWindow import *
|
||||
from Furious.Window.TextEditorWindow import TextEditorWindow
|
||||
from Furious.Window.TunSettingsDialog import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from typing import Union
|
||||
|
||||
import os
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['HomePage']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _isCoreActive(coreType) -> bool:
|
||||
"""Return whether the tray's active connection owns *coreType*."""
|
||||
try:
|
||||
connectAction = APP().systemTray.ConnectAction
|
||||
|
||||
return connectAction.isConnected() and any(
|
||||
isinstance(process, coreType)
|
||||
for process in connectAction.coreManager.processesPool
|
||||
)
|
||||
except (AttributeError, RuntimeError):
|
||||
# The home page is built before the system tray is attached.
|
||||
return False
|
||||
|
||||
|
||||
class AppConnectivityManager(ConnectivityManager):
|
||||
"""Coordinate app network connectivity operations."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the application connectivity manager."""
|
||||
super().__init__(parent)
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
"""Handle a successful network operation."""
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, HomePage):
|
||||
parent.setNetworkState(True)
|
||||
|
||||
super().successCallback(networkReply, **kwargs)
|
||||
|
||||
def failureCallback(self, networkReply, **kwargs):
|
||||
"""Handle a failed network operation."""
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, HomePage):
|
||||
parent.setNetworkState(False, errorString=networkReply.errorString())
|
||||
|
||||
super().failureCallback(networkReply, **kwargs)
|
||||
|
||||
def startSingleTest(self):
|
||||
"""Start single test."""
|
||||
if not APP().isSystemTrayConnected():
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, HomePage):
|
||||
parent.resetNetworkState()
|
||||
|
||||
self.stopTest()
|
||||
|
||||
connectedHttpProxy = Storage.Extras.UserHttpProxy()
|
||||
|
||||
if connectedHttpProxy is None:
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, HomePage):
|
||||
parent.resetNetworkState()
|
||||
else:
|
||||
self.configureHttpProxy(connectedHttpProxy)
|
||||
|
||||
super().startSingleTest()
|
||||
|
||||
def disconnectedCallback(self):
|
||||
"""Update the app network connectivity manager for a disconnected state."""
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, HomePage):
|
||||
parent.resetNetworkState()
|
||||
|
||||
super().disconnectedCallback()
|
||||
|
||||
|
||||
class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
"""Provide the network state badge widget."""
|
||||
|
||||
DefaultIconFileName = 'reception-4.svg'
|
||||
StateIconFileName = {
|
||||
'success': 'reception-4.svg',
|
||||
'failure': 'reception-0.svg',
|
||||
}
|
||||
IconSize = QtCore.QSize(16, 16)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize the NetworkStateBadge."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.currentIconFileName = self.DefaultIconFileName
|
||||
self.currentState = ''
|
||||
self.currentRemark = ''
|
||||
self.currentErrorString = ''
|
||||
|
||||
self.setObjectName('NetworkStateBadge')
|
||||
self.setProperty('networkState', '')
|
||||
self.setVisible(False)
|
||||
|
||||
self.iconLabel = QLabel(parent=self)
|
||||
self.textLabel = AppQLabel(translatable=False, parent=self)
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(10, 3, 10, 3)
|
||||
self._layout.setSpacing(7)
|
||||
self._layout.addWidget(self.iconLabel)
|
||||
self._layout.addWidget(self.textLabel)
|
||||
|
||||
self.setIconByTheme(APP().theme())
|
||||
|
||||
def setIconByTheme(self, theme: str, iconFileName: Union[str, None] = None):
|
||||
"""Set icon by theme."""
|
||||
iconFileName = iconFileName or self.currentIconFileName
|
||||
|
||||
if theme == AppStyleSheet.Dark:
|
||||
icon = bootstrapIconWhite(iconFileName)
|
||||
else:
|
||||
icon = bootstrapIcon(iconFileName)
|
||||
|
||||
self.iconLabel.setPixmap(icon.pixmap(self.IconSize))
|
||||
|
||||
def statusText(self):
|
||||
"""Return the status text value used by the network state badge."""
|
||||
if self.currentState == 'success':
|
||||
return f'{_("Network OK")} - {self.currentRemark}'
|
||||
|
||||
if self.currentState == 'failure':
|
||||
return f'{_("Network error")} - {self.currentRemark}'
|
||||
|
||||
return ''
|
||||
|
||||
def statusToolTip(self):
|
||||
"""Return the status tool tip value used by the network state badge."""
|
||||
if self.currentState == 'success':
|
||||
return self.currentRemark
|
||||
|
||||
if self.currentState == 'failure':
|
||||
return f'{self.currentRemark}\n{self.currentErrorString}'.strip()
|
||||
|
||||
return ''
|
||||
|
||||
def updateStatusText(self):
|
||||
"""Update status text."""
|
||||
text = self.statusText()
|
||||
tooltip = self.statusToolTip()
|
||||
|
||||
self.textLabel.setText(text)
|
||||
self.setToolTip(tooltip)
|
||||
self.iconLabel.setToolTip(tooltip)
|
||||
self.textLabel.setToolTip(tooltip)
|
||||
|
||||
def setStatus(self, state: str, remark: str, errorString: str = ''):
|
||||
"""Set status."""
|
||||
self.currentState = state
|
||||
self.currentRemark = remark
|
||||
self.currentErrorString = errorString
|
||||
|
||||
self.updateStatusText()
|
||||
self.setProperty('networkState', state)
|
||||
self.currentIconFileName = self.StateIconFileName.get(
|
||||
state, self.DefaultIconFileName
|
||||
)
|
||||
self.setIconByTheme(APP().theme())
|
||||
self.refreshStyle()
|
||||
|
||||
def clearStatus(self):
|
||||
"""Clear status."""
|
||||
self.currentState = ''
|
||||
self.currentRemark = ''
|
||||
self.currentErrorString = ''
|
||||
|
||||
self.updateStatusText()
|
||||
self.setProperty('networkState', '')
|
||||
self.currentIconFileName = self.DefaultIconFileName
|
||||
self.setIconByTheme(APP().theme())
|
||||
self.setVisible(False)
|
||||
self.refreshStyle()
|
||||
|
||||
def refreshStyle(self):
|
||||
"""Refresh style."""
|
||||
for widget in [self, self.iconLabel, self.textLabel]:
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
widget.update()
|
||||
|
||||
self.setVisible(bool(self.textLabel.text()))
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Update the network state badge for a theme change."""
|
||||
self.setIconByTheme(theme)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the network state badge."""
|
||||
self.updateStatusText()
|
||||
|
||||
|
||||
class TrafficStatsBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
|
||||
"""Display independently updated traffic speeds and usage."""
|
||||
|
||||
UploadIconFileName = 'cloud-upload.svg'
|
||||
DownloadIconFileName = 'cloud-download.svg'
|
||||
IconSize = QtCore.QSize(16, 16)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize dedicated traffic-direction icons and labels."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('TrafficStatsBadge')
|
||||
self.setVisible(False)
|
||||
|
||||
self.uploadIconLabel = QLabel(parent=self)
|
||||
self.uploadTextLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.uploadUsageLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.downloadIconLabel = QLabel(parent=self)
|
||||
self.downloadTextLabel = AppQLabel(translatable=False, parent=self)
|
||||
self.downloadUsageLabel = AppQLabel(translatable=False, parent=self)
|
||||
|
||||
self.uploadUsageLabel.setObjectName('TrafficUsageLabel')
|
||||
self.downloadUsageLabel.setObjectName('TrafficUsageLabel')
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(8, 3, 8, 3)
|
||||
self._layout.setSpacing(6)
|
||||
self._layout.addWidget(self.uploadIconLabel)
|
||||
self._layout.addWidget(self.uploadTextLabel)
|
||||
self._layout.addWidget(self.uploadUsageLabel)
|
||||
self._layout.addSpacing(4)
|
||||
self._layout.addWidget(self.downloadIconLabel)
|
||||
self._layout.addWidget(self.downloadTextLabel)
|
||||
self._layout.addWidget(self.downloadUsageLabel)
|
||||
|
||||
self.setIconByTheme(APP().theme())
|
||||
self.updateToolTips()
|
||||
|
||||
def updateToolTips(self):
|
||||
"""Apply translated descriptions to each traffic statistic."""
|
||||
uploadSpeed, downloadSpeed, uploadUsage, downloadUsage = (
|
||||
_('Upload Speed'),
|
||||
_('Download Speed'),
|
||||
_('Upload Traffic Usage'),
|
||||
_('Download Traffic Usage'),
|
||||
)
|
||||
|
||||
self.uploadIconLabel.setToolTip(uploadSpeed)
|
||||
self.uploadTextLabel.setToolTip(uploadSpeed)
|
||||
self.uploadUsageLabel.setToolTip(uploadUsage)
|
||||
self.downloadIconLabel.setToolTip(downloadSpeed)
|
||||
self.downloadTextLabel.setToolTip(downloadSpeed)
|
||||
self.downloadUsageLabel.setToolTip(downloadUsage)
|
||||
|
||||
def setIconByTheme(self, theme: str):
|
||||
"""Apply theme-appropriate upload and download icons."""
|
||||
iconFactory = (
|
||||
bootstrapIconWhite if theme == AppStyleSheet.Dark else bootstrapIcon
|
||||
)
|
||||
|
||||
self.uploadIconLabel.setPixmap(
|
||||
iconFactory(self.UploadIconFileName).pixmap(self.IconSize)
|
||||
)
|
||||
self.downloadIconLabel.setPixmap(
|
||||
iconFactory(self.DownloadIconFileName).pixmap(self.IconSize)
|
||||
)
|
||||
|
||||
@QtCore.Slot(float, float)
|
||||
def setSpeeds(self, upload: float, download: float):
|
||||
"""Display formatted upload and download byte rates."""
|
||||
self.uploadTextLabel.setText(formatTrafficSpeed(upload))
|
||||
self.downloadTextLabel.setText(formatTrafficSpeed(download))
|
||||
self.setVisible(True)
|
||||
|
||||
@QtCore.Slot(object, object)
|
||||
def setUsage(self, upload: int, download: int):
|
||||
"""Display formatted cumulative upload and download usage."""
|
||||
self.uploadUsageLabel.setText(f'({formatTrafficUsage(upload)})')
|
||||
self.downloadUsageLabel.setText(f'({formatTrafficUsage(download)})')
|
||||
self.setVisible(True)
|
||||
|
||||
@QtCore.Slot()
|
||||
def clearStatistics(self):
|
||||
"""Hide stale speed and usage values when statistics are unavailable."""
|
||||
self.uploadTextLabel.clear()
|
||||
self.uploadUsageLabel.clear()
|
||||
self.downloadTextLabel.clear()
|
||||
self.downloadUsageLabel.clear()
|
||||
self.setVisible(False)
|
||||
|
||||
def themeChangedCallback(self, theme: str):
|
||||
"""Refresh traffic-direction icons after a theme change."""
|
||||
self.setIconByTheme(theme)
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated traffic-statistic tooltips."""
|
||||
self.updateToolTips()
|
||||
|
||||
|
||||
class ConnectionStatusWidget(QWidget):
|
||||
"""Compose network state and traffic speed as one permanent status item."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
"""Initialize separate network-state and traffic-speed components."""
|
||||
super().__init__(parent)
|
||||
|
||||
self.setObjectName('ConnectionStatusWidget')
|
||||
self.networkState = NetworkStateBadge(parent=self)
|
||||
self.trafficStats = TrafficStatsBadge(parent=self)
|
||||
|
||||
self._layout = QHBoxLayout(self)
|
||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._layout.setSpacing(6)
|
||||
self._layout.addWidget(self.trafficStats)
|
||||
self._layout.addWidget(self.networkState)
|
||||
|
||||
|
||||
class SearchButton(AppQPushButton):
|
||||
"""Represent search button."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the SearchButton."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setText(self.customText())
|
||||
self.setIcon(bootstrapIcon('search.svg'))
|
||||
|
||||
@staticmethod
|
||||
def customText():
|
||||
"""Return the user-facing message text for the search button."""
|
||||
return ' ' * 2 + _('Search')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated text for the search button."""
|
||||
self.setText(self.customText())
|
||||
|
||||
|
||||
class HomePage(Mixins.QTranslatable, QMainWindow):
|
||||
"""Own server management, its actions, and connection presentation."""
|
||||
|
||||
@staticmethod
|
||||
def serverImportActions():
|
||||
"""Build import actions contributed to the server table context menu."""
|
||||
return (
|
||||
ImportFromFileAction(),
|
||||
ImportURIFromClipboardAction(
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier,
|
||||
QtCore.Qt.Key.Key_V,
|
||||
),
|
||||
),
|
||||
ImportJSONFromClipboardAction(
|
||||
shortcut=QtCore.QKeyCombination(
|
||||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||||
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
||||
QtCore.Qt.Key.Key_J,
|
||||
),
|
||||
),
|
||||
ImportQRCodeOnTheScreenAction(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the server-management page."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setFocusPolicy(QtCore.Qt.FocusPolicy.StrongFocus)
|
||||
|
||||
self.updatesManager = UpdateManager(parent=self)
|
||||
self.networkConnectivityManager = AppConnectivityManager(parent=self)
|
||||
|
||||
self.userServersQTableWidget = ServerTableView(
|
||||
parent=self,
|
||||
configurationEditorFactory=lambda: TextEditorWindow(parent=self),
|
||||
qrCodeWindowFactory=QRCodeWindow,
|
||||
importActionsFactory=self.serverImportActions,
|
||||
)
|
||||
self.userSubsWindow = SubscriptionWindow(
|
||||
parent=self,
|
||||
deleteUniqueCallback=lambda unique: self.userServersQTableWidget.deleteItemByIndex(
|
||||
list(
|
||||
index
|
||||
for index, server in enumerate(Storage.UserServers())
|
||||
if server.itemSubscription == unique
|
||||
),
|
||||
showProgress=False,
|
||||
),
|
||||
)
|
||||
pluginRegistry = getPluginRegistry()
|
||||
self.customizeProxyBypassDialog = ProxyBypassDialog(parent=self)
|
||||
self.customizeNetworkTestDialog = NetworkTestDialog(parent=self)
|
||||
|
||||
serverActions = []
|
||||
|
||||
for descriptor in pluginRegistry.protocolDescriptors():
|
||||
if (
|
||||
not descriptor.addActionText
|
||||
or pluginRegistry.editorForProtocol(descriptor.id) is None
|
||||
):
|
||||
continue
|
||||
|
||||
if descriptor.separatorBefore and serverActions:
|
||||
serverActions.append(AppQSeperator())
|
||||
|
||||
actionText = (
|
||||
_(descriptor.addActionText)
|
||||
if descriptor.translatable
|
||||
else descriptor.addActionText
|
||||
)
|
||||
serverActions.append(
|
||||
AppQAction(
|
||||
actionText,
|
||||
callback=functools.partial(
|
||||
self.userServersQTableWidget.addServerViaGui,
|
||||
descriptor.id,
|
||||
actionText,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
if serverActions:
|
||||
serverActions.append(AppQSeperator())
|
||||
serverActions.append(
|
||||
AppQAction(
|
||||
_('New Empty Configuration'),
|
||||
callback=lambda: self.userServersQTableWidget.newEmptyItem(),
|
||||
)
|
||||
)
|
||||
|
||||
subsActions = [
|
||||
AppQAction(
|
||||
_('Update Subscription (Use Current Proxy)'),
|
||||
callback=lambda: self.userServersQTableWidget.updateSubs(
|
||||
Storage.Extras.UserHttpProxy(),
|
||||
parent=self,
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Update Subscription (Force Proxy)'),
|
||||
callback=lambda: self.userServersQTableWidget.updateSubs(
|
||||
'127.0.0.1:10809',
|
||||
parent=self,
|
||||
),
|
||||
),
|
||||
AppQAction(
|
||||
_('Update Subscription (No Proxy)'),
|
||||
callback=lambda: self.userServersQTableWidget.updateSubs(
|
||||
None,
|
||||
parent=self,
|
||||
),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Edit Subscription...'),
|
||||
icon=bootstrapIcon('star.svg'),
|
||||
callback=lambda: self.userSubsWindow.show(),
|
||||
),
|
||||
]
|
||||
|
||||
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(
|
||||
CapabilityKind.KernelFactory
|
||||
):
|
||||
pluginMetadata = pluginRegistry.metadataFor(plugin)
|
||||
managementActions = pluginRegistry.managementActions(
|
||||
plugin,
|
||||
parent=self,
|
||||
isCoreActive=_isCoreActive,
|
||||
)
|
||||
if managementActions:
|
||||
corePluginActions.append(
|
||||
AppQAction(
|
||||
pluginMetadata.displayName,
|
||||
menu=AppQMenu(*managementActions),
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
corePluginActions.append(
|
||||
AppQAction(
|
||||
pluginMetadata.displayName,
|
||||
checkable=False,
|
||||
)
|
||||
)
|
||||
|
||||
extensionPluginActions = []
|
||||
|
||||
corePlugins = set(
|
||||
pluginRegistry.pluginsWithCapability(CapabilityKind.KernelFactory)
|
||||
)
|
||||
|
||||
for plugin in pluginRegistry.pluginsWithCapability(
|
||||
CapabilityKind.ActionProvider
|
||||
):
|
||||
if plugin in corePlugins:
|
||||
continue
|
||||
|
||||
managementActions = pluginRegistry.managementActions(
|
||||
plugin,
|
||||
parent=self,
|
||||
isCoreActive=_isCoreActive,
|
||||
)
|
||||
|
||||
if managementActions:
|
||||
pluginMetadata = pluginRegistry.metadataFor(plugin)
|
||||
extensionPluginActions.append(
|
||||
AppQAction(
|
||||
pluginMetadata.displayName,
|
||||
menu=AppQMenu(*managementActions),
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
)
|
||||
)
|
||||
|
||||
pluginActions = [
|
||||
AppQAction(
|
||||
_('Core'),
|
||||
menu=AppQMenu(*corePluginActions),
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
]
|
||||
|
||||
if extensionPluginActions:
|
||||
pluginActions.append(
|
||||
AppQAction(
|
||||
_('Extensions'),
|
||||
menu=AppQMenu(*extensionPluginActions),
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
)
|
||||
)
|
||||
|
||||
pluginsToolbarActions = [
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Plugins'),
|
||||
icon=bootstrapIcon('plugin.svg'),
|
||||
menu=AppQMenu(*pluginActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
]
|
||||
|
||||
if hasattr(AppQAction, 'setMenu'):
|
||||
self.toolbar = AppQToolBar(
|
||||
AppQAction(
|
||||
_('Server'),
|
||||
icon=bootstrapIcon('server.svg'),
|
||||
menu=AppQMenu(*serverActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
checkable=False,
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Subscription'),
|
||||
icon=bootstrapIcon('collection.svg'),
|
||||
menu=AppQMenu(*subsActions),
|
||||
useSetMenu=False,
|
||||
useActionGroup=False,
|
||||
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.setIconSize(QtCore.QSize(64, 32))
|
||||
self.toolbar.setToolButtonStyle(
|
||||
QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon
|
||||
)
|
||||
self.addToolBar(self.toolbar)
|
||||
else:
|
||||
# Menu actions
|
||||
serverMenu = {
|
||||
'name': 'Server',
|
||||
'actions': [*serverActions],
|
||||
}
|
||||
|
||||
subsMenu = {
|
||||
'name': 'Subscription',
|
||||
'actions': [*subsActions],
|
||||
}
|
||||
|
||||
pluginsMenu = {
|
||||
'name': 'Plugins',
|
||||
'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
|
||||
for menuDict in (
|
||||
serverMenu,
|
||||
subsMenu,
|
||||
pluginsMenu,
|
||||
toolsMenu,
|
||||
helpMenu,
|
||||
):
|
||||
menuName = menuDict['name']
|
||||
menuObjName = f'_{menuName}Menu'
|
||||
menu = AppQMenu(
|
||||
*menuDict['actions'], title=_(menuName), parent=self.menuBar()
|
||||
)
|
||||
|
||||
# Set reference
|
||||
setattr(self, menuObjName, menu)
|
||||
|
||||
self.menuBar().addMenu(menu)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu)
|
||||
|
||||
# TODO: Custom status tip
|
||||
# self.setStatusBar(QStatusBar(self))
|
||||
|
||||
self.connectionStatus = ConnectionStatusWidget(parent=self)
|
||||
self.networkState = self.connectionStatus.networkState
|
||||
self.trafficStats = self.connectionStatus.trafficStats
|
||||
self.trafficStatsManager = TrafficStatsManager(parent=self)
|
||||
self.trafficStatsManager.speedChanged.connect(self.trafficStats.setSpeeds)
|
||||
self.trafficStatsManager.usageChanged.connect(self.trafficStats.setUsage)
|
||||
self.trafficStatsManager.statisticsUnavailable.connect(
|
||||
self.trafficStats.clearStatistics
|
||||
)
|
||||
|
||||
self.statusBar().addPermanentWidget(self.connectionStatus)
|
||||
|
||||
self._widget = QWidget()
|
||||
self._layout = QVBoxLayout(self._widget)
|
||||
|
||||
self.searchLayout = QHBoxLayout()
|
||||
|
||||
self.searchLineEdit = AppQLineEdit()
|
||||
self.searchLineEdit.setPlaceholderText(
|
||||
_(
|
||||
'Search servers with text or regex, e.g. trojan, hk|jp, ^vmess, (us|sg).*tls'
|
||||
)
|
||||
)
|
||||
|
||||
self.searchButton = SearchButton()
|
||||
|
||||
self.searchLayout.addWidget(self.searchLineEdit)
|
||||
self.searchLayout.addWidget(self.searchButton)
|
||||
|
||||
self._layout.addLayout(self.searchLayout)
|
||||
self._layout.addWidget(self.userServersQTableWidget)
|
||||
|
||||
self.searchButton.clicked.connect(
|
||||
lambda: self.userServersQTableWidget.search(self.searchLineEdit.text())
|
||||
)
|
||||
self.searchLineEdit.returnPressed.connect(
|
||||
lambda: self.userServersQTableWidget.search(self.searchLineEdit.text())
|
||||
)
|
||||
self.searchLineEdit.textChanged.connect(self.handleUserServersSearchTextChanged)
|
||||
|
||||
self.setCentralWidget(self._widget)
|
||||
|
||||
@QtCore.Slot(str)
|
||||
def handleUserServersSearchTextChanged(self, text: str):
|
||||
"""Handle user servers search text changed."""
|
||||
if not text:
|
||||
self.userServersQTableWidget.clearSearch()
|
||||
|
||||
def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
|
||||
"""Update subs by unique."""
|
||||
self.userServersQTableWidget.updateSubsByUnique(unique, httpProxy, **kwargs)
|
||||
|
||||
def appendNewItemByFactory(self, factory: ConfigFactory | ServerProfile):
|
||||
"""Append new item by factory."""
|
||||
self.userServersQTableWidget.appendNewItemByFactory(factory)
|
||||
|
||||
def flushRow(self, row: int, item: ServerProfile):
|
||||
"""Refresh row."""
|
||||
self.userServersQTableWidget.flushRow(row, item)
|
||||
|
||||
def showTabAndSpaces(self):
|
||||
"""Show tab and spaces."""
|
||||
self.userServersQTableWidget.showTabAndSpaces()
|
||||
|
||||
def hideTabAndSpaces(self):
|
||||
"""Hide tab and spaces."""
|
||||
self.userServersQTableWidget.hideTabAndSpaces()
|
||||
|
||||
def getGuiTUNSettings(self, **kwargs):
|
||||
"""Return GUI TUN settings."""
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def cachedGuiTUNSettings():
|
||||
"""Return the TUN settings editor owned by the home page."""
|
||||
parent = kwargs.pop('parent', self)
|
||||
|
||||
return TunSettingsDialog(parent=parent, **kwargs)
|
||||
|
||||
guiTUNSettings = cachedGuiTUNSettings()
|
||||
guiTUNSettings.factoryToInput(Storage.UserTUNSettings())
|
||||
|
||||
return guiTUNSettings
|
||||
|
||||
def checkForUpdates(self, **kwargs):
|
||||
"""Check for updates."""
|
||||
self.updatesManager.configureHttpProxy(Storage.Extras.UserHttpProxy())
|
||||
self.updatesManager.checkForUpdates(**kwargs)
|
||||
|
||||
def resetNetworkState(self):
|
||||
"""Reset network state."""
|
||||
self.networkState.clearStatus()
|
||||
|
||||
def setNetworkState(self, success: bool, **kwargs):
|
||||
"""Set network state."""
|
||||
remark = Storage.Extras.UserServerRemark()
|
||||
|
||||
if not remark:
|
||||
self.resetNetworkState()
|
||||
|
||||
return
|
||||
|
||||
if success:
|
||||
self.networkState.setStatus(
|
||||
'success',
|
||||
remark,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
errorString = kwargs.pop('errorString', '')
|
||||
|
||||
self.networkState.setStatus(
|
||||
'failure',
|
||||
remark,
|
||||
errorString,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def openApplicationFolder():
|
||||
"""Open application folder."""
|
||||
appFolder = os.path.dirname(APP().applicationFilePath())
|
||||
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(appFolder)):
|
||||
logger.info(f'open application folder \'{appFolder}\' success')
|
||||
else:
|
||||
logger.error(f'open application folder \'{appFolder}\' failed')
|
||||
|
||||
@staticmethod
|
||||
def restartAsAdmin():
|
||||
"""Handle the home-page request to restart with elevated privileges."""
|
||||
if not SystemRuntime.isScriptMode():
|
||||
if not SystemRuntime.isAdmin():
|
||||
process = QtCore.QProcess()
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
process.startDetached(
|
||||
'powershell',
|
||||
arguments=[
|
||||
'-Command',
|
||||
f'Start-Process \'{APP().applicationFilePath()}\' '
|
||||
f'\'{AppBuiltinCommand.RunAs.value}\' -Verb runAs',
|
||||
],
|
||||
)
|
||||
elif PLATFORM == 'Darwin':
|
||||
process.startDetached(
|
||||
'osascript',
|
||||
arguments=[
|
||||
'-e',
|
||||
f'do shell script \"{APP().applicationFilePath()} '
|
||||
f'{AppBuiltinCommand.RunAs.value}\" with administrator privileges',
|
||||
],
|
||||
)
|
||||
else:
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||||
mbox.setText(_('I am the supreme authority 👑'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
logger.info('ignored request to restart as admin as already is')
|
||||
else:
|
||||
logger.info(f'ignored request to restart as admin in script mode')
|
||||
|
||||
@staticmethod
|
||||
def openAboutPage():
|
||||
"""Open about page."""
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(APPLICATION_ABOUT_PAGE)):
|
||||
logger.info('open about page success')
|
||||
else:
|
||||
logger.error('open about page failed')
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh text owned directly by the home page."""
|
||||
pass
|
||||
@@ -15,11 +15,11 @@
|
||||
# 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 application window for log viewer window."""
|
||||
"""Provide the unified logging page."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import AppSettings, registerAppSettings
|
||||
from Furious.Frozenlib import AppSettings, Mixins, registerAppSettings
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Service.LogManager import (
|
||||
@@ -33,7 +33,7 @@ from Furious.Service.LogManager import (
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
__all__ = ['LogWindow']
|
||||
__all__ = ['LogPage']
|
||||
|
||||
registerAppSettings('LogViewerWidgetPointSize')
|
||||
registerAppSettings('LogViewerSelectedCategory', default=ALL_LOGS_FILTER)
|
||||
@@ -110,11 +110,11 @@ def saveAsFile(content: str):
|
||||
mbox.open()
|
||||
|
||||
|
||||
class LogWindow(AppQMainWindow):
|
||||
"""Present and filter the unified application log stream."""
|
||||
class LogPage(Mixins.QTranslatable, QMainWindow):
|
||||
"""Present and filter the unified application log stream as a page."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the log window."""
|
||||
"""Initialize the logging page."""
|
||||
manager, fontFamily = (
|
||||
kwargs.pop('manager', None),
|
||||
kwargs.pop('fontFamily', ''),
|
||||
@@ -132,8 +132,6 @@ class LogWindow(AppQMainWindow):
|
||||
self.manager = manager
|
||||
self._preferredFilter = str(AppSettings.get('LogViewerSelectedCategory'))
|
||||
|
||||
self.setWindowTitle(_('Log Viewer'))
|
||||
|
||||
self.filterLabel = AppQLabel(_('Log Type'))
|
||||
self.filterComboBox = QComboBox()
|
||||
self.filterComboBox.setMinimumWidth(180)
|
||||
@@ -163,11 +161,6 @@ class LogWindow(AppQMainWindow):
|
||||
_('Save As...'),
|
||||
callback=lambda: saveAsFile(self.textBrowser.toPlainText()),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Exit'),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self,
|
||||
)
|
||||
@@ -285,7 +278,7 @@ class LogWindow(AppQMainWindow):
|
||||
|
||||
@QtCore.Slot(object)
|
||||
def _categoryRegistered(self, category):
|
||||
"""Add a newly registered component without rebuilding the window."""
|
||||
"""Add a newly registered component without rebuilding the page."""
|
||||
if self.filterComboBox.findData(category.id) == -1:
|
||||
self.filterComboBox.addItem(self._categoryText(category), category.id)
|
||||
|
||||
@@ -307,12 +300,6 @@ class LogWindow(AppQMainWindow):
|
||||
"""Refresh the presentation after the underlying collection changes."""
|
||||
self._refreshEntries()
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Focus the window itself instead of an untouched child control."""
|
||||
super().showEvent(event)
|
||||
|
||||
self.setFocus(QtCore.Qt.FocusReason.OtherFocusReason)
|
||||
|
||||
def plainText(self) -> str:
|
||||
"""Return the plain text currently shown by the selected filter."""
|
||||
return self.textBrowser.toPlainText()
|
||||
@@ -322,8 +309,7 @@ class LogWindow(AppQMainWindow):
|
||||
self.manager.clear()
|
||||
|
||||
def retranslate(self):
|
||||
"""Refresh translated window and filter labels."""
|
||||
super().retranslate()
|
||||
"""Refresh translated page controls and filter labels."""
|
||||
|
||||
selectedCategoryId = self.filterComboBox.currentData()
|
||||
|
||||
+88
-928
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .IndentDialog import IndentDialog
|
||||
from .LogWindow import LogWindow
|
||||
from .HomePage import HomePage
|
||||
from .LogPage import LogPage
|
||||
from .MainWindow import MainWindow
|
||||
from .NetworkTestDialog import NetworkTestDialog
|
||||
from .ProxyBypassDialog import ProxyBypassDialog
|
||||
@@ -31,7 +32,8 @@ from .TunSettingsDialog import TunSettingsDialog
|
||||
|
||||
__all__ = [
|
||||
'IndentDialog',
|
||||
'LogWindow',
|
||||
'HomePage',
|
||||
'LogPage',
|
||||
'MainWindow',
|
||||
'NetworkTestDialog',
|
||||
'ProxyBypassDialog',
|
||||
|
||||
Reference in New Issue
Block a user