mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-13 17:58:19 +03:00
Reorganize application packages
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -60,7 +60,7 @@ NUITKA_BINARY_VERSION_OPTION = (
|
||||
f'--product-version={APPLICATION_VERSION} '
|
||||
)
|
||||
|
||||
PLATFORM_MACHINE_LOWER = PLATFORM_MACHINE.lower()
|
||||
PLATFORM_MACHINE_LOWER = PLATFORM_MACHINE.casefold()
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
NUITKA_BINARY_VERSION_OPTION += f'--file-description=\"{APPLICATION_DESCRIPTION}\" '
|
||||
@@ -117,9 +117,9 @@ else:
|
||||
if PLATFORM == 'Windows':
|
||||
if PLATFORM_RELEASE.endswith('Server'):
|
||||
# Windows server. Fixed to windows10
|
||||
winVerCompatible = f'{PLATFORM.lower()}10'
|
||||
winVerCompatible = f'{PLATFORM.casefold()}10'
|
||||
else:
|
||||
winVerCompatible = f'{PLATFORM.lower()}{PLATFORM_RELEASE}'
|
||||
winVerCompatible = f'{PLATFORM.casefold()}{PLATFORM_RELEASE}'
|
||||
|
||||
if os.environ.get('WIN_VER_COMPATIBLE', ''):
|
||||
winVerCompatible = os.environ['WIN_VER_COMPATIBLE']
|
||||
|
||||
@@ -21,11 +21,13 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Core import Tun2socks
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import AppStyleSheet
|
||||
from Furious.Qt.TextEditorTheme import configureEditorLogMetadata
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Library import *
|
||||
from Furious.Widget.SystemTrayIcon import *
|
||||
from Furious.Application.SystemTrayIcon import *
|
||||
from Furious.Window.AppMainWindow import *
|
||||
from Furious.Window.LogViewerWindow import *
|
||||
|
||||
@@ -296,7 +298,12 @@ class Application(ApplicationFactory, SingletonApplication):
|
||||
@staticmethod
|
||||
def addEnviron():
|
||||
"""Add environ."""
|
||||
getPluginRegistry().configureEnvironment()
|
||||
pluginRegistry = getPluginRegistry()
|
||||
configureEditorLogMetadata(
|
||||
lambda: (*pluginRegistry.coreVersions(), Tun2socks.version()),
|
||||
pluginRegistry.logTimestampPatterns,
|
||||
)
|
||||
pluginRegistry.configureEnvironment()
|
||||
|
||||
if SystemRuntime.flatpakID():
|
||||
# https://github.com/flatpak/flatpak/issues/3438
|
||||
@@ -0,0 +1,25 @@
|
||||
# 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/>.
|
||||
|
||||
"""Expose the GUI composition root for the Furious desktop application."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Application import Application
|
||||
from .SystemTrayIcon import SystemTrayIcon
|
||||
|
||||
__all__ = ['Application', 'SystemTrayIcon']
|
||||
@@ -15,9 +15,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Expose the public core-process integration API."""
|
||||
"""Expose reusable proxy-process primitives without application orchestration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Tun2socks import *
|
||||
from .CoreManager import *
|
||||
from .CoreProcessWorker import (
|
||||
CoreLaunchSpec,
|
||||
CoreProcessMonitor,
|
||||
CoreProcessState,
|
||||
CoreProcessWorker,
|
||||
MsgQueue,
|
||||
ProcessOutputRedirector,
|
||||
)
|
||||
from .Tun2socks import Tun2socks
|
||||
|
||||
__all__ = [
|
||||
'CoreLaunchSpec',
|
||||
'CoreProcessMonitor',
|
||||
'CoreProcessState',
|
||||
'CoreProcessWorker',
|
||||
'MsgQueue',
|
||||
'ProcessOutputRedirector',
|
||||
'Tun2socks',
|
||||
]
|
||||
|
||||
Vendored
+32
-32
@@ -154,7 +154,7 @@ TRANSLATION = {
|
||||
"Text files (*.json);;All files (*)": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Import",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Текст файлов (*.json);;Остальные файлы (*)",
|
||||
"ZH": "文本文件 (*.json);;所有文件 (*)",
|
||||
@@ -225,7 +225,7 @@ TRANSLATION = {
|
||||
"Furious.Widget.GuiCustomizeProxyBypass",
|
||||
"Furious.Widget.IndentSpinBox",
|
||||
"Furious.Widget.UserServersQTableView",
|
||||
"Furious.Window.TextEditorWindow",
|
||||
"Furious.Widget.TextEditorWindow",
|
||||
"Furious.Window.UserSubsWindow"
|
||||
],
|
||||
"RU": "Отмена",
|
||||
@@ -234,7 +234,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Save Changes": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить изменения",
|
||||
"ZH": "保存更改",
|
||||
@@ -242,7 +242,7 @@ TRANSLATION = {
|
||||
},
|
||||
"The content has been modified. Save changes?": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Содержимое было изменено. Сохранить изменения?",
|
||||
"ZH": "内容已修改。保存更改?",
|
||||
@@ -250,7 +250,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Save": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить",
|
||||
"ZH": "保存",
|
||||
@@ -258,7 +258,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Discard": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Отказаться",
|
||||
"ZH": "丢弃",
|
||||
@@ -266,7 +266,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Please check if the configuration is in valid JSON format": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Проверьте, имеет ли конфигурация допустимый формат JSON",
|
||||
"ZH": "请检查配置是否为有效的JSON格式",
|
||||
@@ -275,7 +275,7 @@ TRANSLATION = {
|
||||
"Save As...": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить как...",
|
||||
"ZH": "另存为...",
|
||||
@@ -283,7 +283,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Close Window": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Закрыть окно",
|
||||
"ZH": "关闭窗口",
|
||||
@@ -293,7 +293,7 @@ TRANSLATION = {
|
||||
"source": [
|
||||
"Furious.Plugins.Official.Xray.XrayAssetViewerWindow",
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Файл",
|
||||
"ZH": "文件",
|
||||
@@ -301,7 +301,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Undo": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Отменить",
|
||||
"ZH": "撤销",
|
||||
@@ -309,7 +309,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Redo": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Повторить",
|
||||
"ZH": "重做",
|
||||
@@ -317,7 +317,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Cut": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Вырезать",
|
||||
"ZH": "剪切",
|
||||
@@ -326,7 +326,7 @@ TRANSLATION = {
|
||||
"Copy": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Копировать",
|
||||
"ZH": "复制",
|
||||
@@ -334,7 +334,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Paste": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Вставить",
|
||||
"ZH": "粘贴",
|
||||
@@ -344,7 +344,7 @@ TRANSLATION = {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableView",
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Выбрать все",
|
||||
"ZH": "全选",
|
||||
@@ -352,7 +352,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Indent...": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Настройка отступа...",
|
||||
"ZH": "缩进...",
|
||||
@@ -361,7 +361,7 @@ TRANSLATION = {
|
||||
"Edit": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Редактировать",
|
||||
"ZH": "编辑",
|
||||
@@ -370,7 +370,7 @@ TRANSLATION = {
|
||||
"Zoom In": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Увеличить маcштаб",
|
||||
"ZH": "变大",
|
||||
@@ -379,7 +379,7 @@ TRANSLATION = {
|
||||
"Zoom Out": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Уменьшить маcштаб",
|
||||
"ZH": "变小",
|
||||
@@ -388,7 +388,7 @@ TRANSLATION = {
|
||||
"View": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Вид",
|
||||
"ZH": "显示",
|
||||
@@ -396,7 +396,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Error saving configuration": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка сохранения конфигурации",
|
||||
"ZH": "保存配置出错",
|
||||
@@ -405,7 +405,7 @@ TRANSLATION = {
|
||||
"Save File": {
|
||||
"source": [
|
||||
"Furious.Window.LogViewerWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Сохранить файл",
|
||||
"ZH": "保存文件",
|
||||
@@ -413,7 +413,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Error Saving File": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка сохранения файла",
|
||||
"ZH": "保存文件出错",
|
||||
@@ -422,7 +422,7 @@ TRANSLATION = {
|
||||
"Invalid server configuration": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Connect",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Неверная конфигурация сервера",
|
||||
"ZH": "无效的服务器配置",
|
||||
@@ -430,7 +430,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Error setting indent": {
|
||||
"source": [
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Ошибка при установке отступов",
|
||||
"ZH": "设置缩进出错",
|
||||
@@ -935,7 +935,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Already started": {
|
||||
"source": [
|
||||
"Furious.Widget.Application"
|
||||
"Furious.Application.Application"
|
||||
],
|
||||
"RU": "Активированный",
|
||||
"ZH": "已启动",
|
||||
@@ -943,7 +943,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Furious Log": {
|
||||
"source": [
|
||||
"Furious.Widget.Application"
|
||||
"Furious.Application.Application"
|
||||
],
|
||||
"RU": "Лог Furious",
|
||||
"ZH": "Furious日志",
|
||||
@@ -951,7 +951,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Core Log": {
|
||||
"source": [
|
||||
"Furious.Widget.Application"
|
||||
"Furious.Application.Application"
|
||||
],
|
||||
"RU": "Лог ядра",
|
||||
"ZH": "内核日志",
|
||||
@@ -959,7 +959,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Tun2socks Log": {
|
||||
"source": [
|
||||
"Furious.Widget.Application"
|
||||
"Furious.Application.Application"
|
||||
],
|
||||
"RU": "Лог Tun2socks",
|
||||
"ZH": "Tun2socks日志",
|
||||
@@ -1729,7 +1729,7 @@ TRANSLATION = {
|
||||
},
|
||||
"Administrator": {
|
||||
"source": [
|
||||
"Furious.Widget.SystemTrayIcon"
|
||||
"Furious.Application.SystemTrayIcon"
|
||||
],
|
||||
"RU": "Administrator",
|
||||
"ZH": "管理员",
|
||||
@@ -2116,7 +2116,7 @@ TRANSLATION = {
|
||||
"Close": {
|
||||
"source": [
|
||||
"Furious.Plugins.Official.Xray.UserRoutingWindow",
|
||||
"Furious.Window.TextEditorWindow"
|
||||
"Furious.Widget.TextEditorWindow"
|
||||
],
|
||||
"RU": "Закрыть",
|
||||
"ZH": "关闭",
|
||||
|
||||
Vendored
+3
-1
@@ -19,4 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .GenTranslation import *
|
||||
from .GenTranslation import TRANSLATION
|
||||
|
||||
__all__ = ['TRANSLATION']
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Version import __version__
|
||||
from .Version import __version__
|
||||
|
||||
from PySide6 import QtCore, __version__ as _pyside6_version
|
||||
|
||||
@@ -28,6 +28,51 @@ import math
|
||||
import pathlib
|
||||
import platform
|
||||
|
||||
__all__ = [
|
||||
'ADMINISTRATOR_NAME',
|
||||
'APPLICATION_ABOUT_PAGE',
|
||||
'APPLICATION_AUTHOR_EMAIL',
|
||||
'APPLICATION_AUTHOR_NAME',
|
||||
'APPLICATION_DESCRIPTION',
|
||||
'APPLICATION_FLATPAK_ID',
|
||||
'APPLICATION_MACOS_SIGNATURE',
|
||||
'APPLICATION_NAME',
|
||||
'APPLICATION_REPO_NAME',
|
||||
'APPLICATION_REPO_OWNER_NAME',
|
||||
'APPLICATION_TUN2SOCKS_GATEWAY_ADDRESS',
|
||||
'APPLICATION_TUN2SOCKS_INTERFACE_DNS_ADDRESS',
|
||||
'APPLICATION_TUN2SOCKS_NETWORK_INTERFACE_NAME',
|
||||
'APPLICATION_VERSION',
|
||||
'CORE_CHECK_ALIVE_INTERVAL',
|
||||
'CRASH_LOG_DIR',
|
||||
'DATA_DIR',
|
||||
'GEN_TRANSLATION_FILE',
|
||||
'GOLDEN_RATIO',
|
||||
'LOCAL_SERVER_NAME',
|
||||
'NETWORK_CONNECTIVITY_TEST_URL',
|
||||
'NETWORK_SPEED_TEST_URL',
|
||||
'ORGANIZATION_DOMAIN',
|
||||
'ORGANIZATION_NAME',
|
||||
'OS_CPU_COUNT',
|
||||
'PACKAGE_DIR',
|
||||
'PLATFORM',
|
||||
'PLATFORM_MACHINE',
|
||||
'PLATFORM_PYTHON_VERSION',
|
||||
'PLATFORM_RELEASE',
|
||||
'PROXY_OUTBOUND_USER_EMAIL',
|
||||
'PROXY_SERVER_BYPASS',
|
||||
'PYSIDE6_VERSION',
|
||||
'ROOT_DIR',
|
||||
'SYSTEM_LANGUAGE',
|
||||
'URL_GEOIP',
|
||||
'URL_GEOIP_SHA256',
|
||||
'URL_GEOSITE',
|
||||
'URL_GEOSITE_SHA256',
|
||||
'XRAY_ASSET_DIR',
|
||||
'XRAY_ASSET_PATH_GEOIP',
|
||||
'XRAY_ASSET_PATH_GEOSITE',
|
||||
]
|
||||
|
||||
APPLICATION_NAME = 'Furious'
|
||||
APPLICATION_VERSION = __version__
|
||||
APPLICATION_AUTHOR_NAME = 'Loren Eteval'
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# 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 bundled py side6 legacy."""
|
||||
"""Provide bundled PySide6 legacy."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -29,12 +29,12 @@ __all__ = ['PySide6Legacy']
|
||||
|
||||
|
||||
class PySide6Legacy:
|
||||
"""Represent py side6 legacy."""
|
||||
"""Represent PySide6 legacy."""
|
||||
|
||||
@staticmethod
|
||||
def enumValueWrapper(enum) -> int:
|
||||
# Protect PySide6 enum wrapper behavior changes
|
||||
"""Return the enum value wrapper value used by the py side6 legacy."""
|
||||
"""Return the enum value wrapper value used by the PySide6 legacy."""
|
||||
if versionToValue(PYSIDE6_VERSION) < versionToValue('6.2.2'):
|
||||
return enum
|
||||
else:
|
||||
@@ -44,7 +44,7 @@ class PySide6Legacy:
|
||||
def eventLoopWait(ms) -> None:
|
||||
# Protect qWait method does not exist in some
|
||||
# old PySide6 version
|
||||
"""Handle event loop wait for the py side6 legacy."""
|
||||
"""Handle event loop wait for the PySide6 legacy."""
|
||||
if versionToValue(PYSIDE6_VERSION) < versionToValue('6.3.1'):
|
||||
for counter in range(0, ms, 10):
|
||||
time.sleep(10 / 1000)
|
||||
|
||||
@@ -67,19 +67,19 @@ class Protocol(Enum):
|
||||
if not isinstance(protocol, str):
|
||||
return Protocol.Unknown
|
||||
|
||||
if protocol.lower() == 'vmess':
|
||||
if protocol.casefold() == 'vmess':
|
||||
return Protocol.VMess
|
||||
|
||||
if protocol.lower() == 'vless':
|
||||
if protocol.casefold() == 'vless':
|
||||
return Protocol.VLESS
|
||||
|
||||
if protocol.lower() == 'shadowsocks':
|
||||
if protocol.casefold() == 'shadowsocks':
|
||||
return Protocol.Shadowsocks
|
||||
|
||||
if protocol.lower() == 'socks':
|
||||
if protocol.casefold() == 'socks':
|
||||
return Protocol.Socks
|
||||
|
||||
if protocol.lower() == 'trojan':
|
||||
if protocol.casefold() == 'trojan':
|
||||
return Protocol.Trojan
|
||||
|
||||
return Protocol.Unknown
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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/>.
|
||||
|
||||
"""Define the package version without importing higher application layers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
__version__ = '0.7.0'
|
||||
+143
-14
@@ -15,20 +15,149 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Expose bundled platform, settings, and compatibility helpers."""
|
||||
"""Expose the self-contained foundation API used by higher layers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .AppSettings import *
|
||||
from .Constants import *
|
||||
from .Enum import *
|
||||
from .Globals import *
|
||||
from .Mixins import *
|
||||
from .PySide6Legacy import *
|
||||
from .StartupOnBoot import *
|
||||
from .SystemProxy import *
|
||||
from .SystemRoutingTable import *
|
||||
from .SystemRuntime import *
|
||||
from .Tcping import *
|
||||
from .Utility import *
|
||||
from .Win32Session import *
|
||||
from . import AppResources
|
||||
from .AppSettings import AppBinarySettings, AppSettings, registerAppSettings
|
||||
from .Constants import (
|
||||
ADMINISTRATOR_NAME,
|
||||
APPLICATION_ABOUT_PAGE,
|
||||
APPLICATION_AUTHOR_EMAIL,
|
||||
APPLICATION_AUTHOR_NAME,
|
||||
APPLICATION_DESCRIPTION,
|
||||
APPLICATION_FLATPAK_ID,
|
||||
APPLICATION_MACOS_SIGNATURE,
|
||||
APPLICATION_NAME,
|
||||
APPLICATION_REPO_NAME,
|
||||
APPLICATION_REPO_OWNER_NAME,
|
||||
APPLICATION_TUN2SOCKS_GATEWAY_ADDRESS,
|
||||
APPLICATION_TUN2SOCKS_INTERFACE_DNS_ADDRESS,
|
||||
APPLICATION_TUN2SOCKS_NETWORK_INTERFACE_NAME,
|
||||
APPLICATION_VERSION,
|
||||
CORE_CHECK_ALIVE_INTERVAL,
|
||||
CRASH_LOG_DIR,
|
||||
DATA_DIR,
|
||||
GEN_TRANSLATION_FILE,
|
||||
GOLDEN_RATIO,
|
||||
LOCAL_SERVER_NAME,
|
||||
NETWORK_CONNECTIVITY_TEST_URL,
|
||||
NETWORK_SPEED_TEST_URL,
|
||||
ORGANIZATION_DOMAIN,
|
||||
ORGANIZATION_NAME,
|
||||
OS_CPU_COUNT,
|
||||
PACKAGE_DIR,
|
||||
PLATFORM,
|
||||
PLATFORM_MACHINE,
|
||||
PLATFORM_PYTHON_VERSION,
|
||||
PLATFORM_RELEASE,
|
||||
PROXY_OUTBOUND_USER_EMAIL,
|
||||
PROXY_SERVER_BYPASS,
|
||||
PYSIDE6_VERSION,
|
||||
ROOT_DIR,
|
||||
SYSTEM_LANGUAGE,
|
||||
URL_GEOIP,
|
||||
URL_GEOIP_SHA256,
|
||||
URL_GEOSITE,
|
||||
URL_GEOSITE_SHA256,
|
||||
XRAY_ASSET_DIR,
|
||||
XRAY_ASSET_PATH_GEOIP,
|
||||
XRAY_ASSET_PATH_GEOSITE,
|
||||
)
|
||||
from .Enum import AppBuiltinCommand, AppBuiltinProxyMode, AppBuiltinRouting
|
||||
from .Globals import APP, AppFontName, AppLoggerWindow, AppThreadPool
|
||||
from .Mixins import Mixins
|
||||
from .PySide6Legacy import PySide6Legacy
|
||||
from .StartupOnBoot import StartupOnBoot
|
||||
from .SystemProxy import SystemProxy
|
||||
from .SystemRoutingTable import SystemRoutingTable
|
||||
from .SystemRuntime import SystemRuntime
|
||||
from .Tcping import tcping
|
||||
from .Utility import (
|
||||
Protocol,
|
||||
absolutePath,
|
||||
callOnceOnly,
|
||||
callRateLimited,
|
||||
classname,
|
||||
forceToLocalhostIfPossible,
|
||||
isValidIPAddress,
|
||||
parseHostPort,
|
||||
runExternalCommand,
|
||||
versionToValue,
|
||||
)
|
||||
from .Win32Session import Win32Session
|
||||
|
||||
__all__ = [
|
||||
'ADMINISTRATOR_NAME',
|
||||
'APP',
|
||||
'APPLICATION_ABOUT_PAGE',
|
||||
'APPLICATION_AUTHOR_EMAIL',
|
||||
'APPLICATION_AUTHOR_NAME',
|
||||
'APPLICATION_DESCRIPTION',
|
||||
'APPLICATION_FLATPAK_ID',
|
||||
'APPLICATION_MACOS_SIGNATURE',
|
||||
'APPLICATION_NAME',
|
||||
'APPLICATION_REPO_NAME',
|
||||
'APPLICATION_REPO_OWNER_NAME',
|
||||
'APPLICATION_TUN2SOCKS_GATEWAY_ADDRESS',
|
||||
'APPLICATION_TUN2SOCKS_INTERFACE_DNS_ADDRESS',
|
||||
'APPLICATION_TUN2SOCKS_NETWORK_INTERFACE_NAME',
|
||||
'APPLICATION_VERSION',
|
||||
'AppBinarySettings',
|
||||
'AppBuiltinCommand',
|
||||
'AppBuiltinProxyMode',
|
||||
'AppBuiltinRouting',
|
||||
'AppFontName',
|
||||
'AppLoggerWindow',
|
||||
'AppResources',
|
||||
'AppSettings',
|
||||
'AppThreadPool',
|
||||
'CORE_CHECK_ALIVE_INTERVAL',
|
||||
'CRASH_LOG_DIR',
|
||||
'DATA_DIR',
|
||||
'GEN_TRANSLATION_FILE',
|
||||
'GOLDEN_RATIO',
|
||||
'LOCAL_SERVER_NAME',
|
||||
'Mixins',
|
||||
'NETWORK_CONNECTIVITY_TEST_URL',
|
||||
'NETWORK_SPEED_TEST_URL',
|
||||
'ORGANIZATION_DOMAIN',
|
||||
'ORGANIZATION_NAME',
|
||||
'OS_CPU_COUNT',
|
||||
'PACKAGE_DIR',
|
||||
'PLATFORM',
|
||||
'PLATFORM_MACHINE',
|
||||
'PLATFORM_PYTHON_VERSION',
|
||||
'PLATFORM_RELEASE',
|
||||
'PROXY_OUTBOUND_USER_EMAIL',
|
||||
'PROXY_SERVER_BYPASS',
|
||||
'PYSIDE6_VERSION',
|
||||
'Protocol',
|
||||
'PySide6Legacy',
|
||||
'ROOT_DIR',
|
||||
'SYSTEM_LANGUAGE',
|
||||
'StartupOnBoot',
|
||||
'SystemProxy',
|
||||
'SystemRoutingTable',
|
||||
'SystemRuntime',
|
||||
'URL_GEOIP',
|
||||
'URL_GEOIP_SHA256',
|
||||
'URL_GEOSITE',
|
||||
'URL_GEOSITE_SHA256',
|
||||
'Win32Session',
|
||||
'XRAY_ASSET_DIR',
|
||||
'XRAY_ASSET_PATH_GEOIP',
|
||||
'XRAY_ASSET_PATH_GEOSITE',
|
||||
'absolutePath',
|
||||
'callOnceOnly',
|
||||
'callRateLimited',
|
||||
'classname',
|
||||
'forceToLocalhostIfPossible',
|
||||
'isValidIPAddress',
|
||||
'parseHostPort',
|
||||
'registerAppSettings',
|
||||
'runExternalCommand',
|
||||
'tcping',
|
||||
'versionToValue',
|
||||
]
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface.ConfigFactory import *
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
@@ -68,14 +67,14 @@ class CoreProcessFactory(ABC):
|
||||
"""Handle the registered singledispatch variant."""
|
||||
return config
|
||||
|
||||
@toJSONString.register(ConfigFactory)
|
||||
def _(self, config, **kwargs) -> str:
|
||||
"""Handle the registered singledispatch variant."""
|
||||
return config.toJSONString(**kwargs)
|
||||
|
||||
@toJSONString.register(dict)
|
||||
def _(self, config, **kwargs) -> str:
|
||||
"""Handle the registered singledispatch variant."""
|
||||
serializer = getattr(config, 'toJSONString', None)
|
||||
|
||||
if callable(serializer):
|
||||
return serializer(**kwargs)
|
||||
|
||||
try:
|
||||
ensure_ascii = kwargs.pop('ensure_ascii', False)
|
||||
escape_forward_slashes = kwargs.pop('escape_forward_slashes', False)
|
||||
|
||||
@@ -15,14 +15,23 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Expose the public application interface contracts."""
|
||||
"""Expose low-level contracts that do not depend on application services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .ApplicationFactory import *
|
||||
from .EncoderFactory import *
|
||||
from .GuiEditorItemFactory import *
|
||||
from .StorageFactory import *
|
||||
from .UserServersTableItem import *
|
||||
from .ConfigFactory import *
|
||||
from .CoreProcessFactory import *
|
||||
from .ApplicationFactory import ApplicationFactory
|
||||
from .CoreProcessFactory import CoreProcessFactory
|
||||
from .EncoderFactory import EncoderFactory
|
||||
from .GuiEditorItemFactory import GuiEditorItemFactory, GuiEditorItemWidgetContainer
|
||||
from .StorageFactory import StorageFactory
|
||||
from .UserServersTableItem import UserServersTableItem
|
||||
|
||||
__all__ = [
|
||||
'ApplicationFactory',
|
||||
'CoreProcessFactory',
|
||||
'EncoderFactory',
|
||||
'GuiEditorItemFactory',
|
||||
'GuiEditorItemWidgetContainer',
|
||||
'StorageFactory',
|
||||
'UserServersTableItem',
|
||||
]
|
||||
|
||||
@@ -15,21 +15,28 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Define the shared configuration model and conversion interface."""
|
||||
"""Define configuration data and the registry used to construct it."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface.UserServersTableItem import *
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from Furious.Interface.UserServersTableItem import UserServersTableItem
|
||||
|
||||
from typing import Union
|
||||
|
||||
import copy
|
||||
import ujson
|
||||
import functools
|
||||
import threading
|
||||
import ujson
|
||||
|
||||
__all__ = ['ConfigFactory']
|
||||
__all__ = [
|
||||
'ConfigFactory',
|
||||
'ConfigurationRegistry',
|
||||
'configurationRegistry',
|
||||
'registerConfigurationProvider',
|
||||
'configFactoryFromDict',
|
||||
'configFactoryFromAny',
|
||||
'configFactoryBlank',
|
||||
]
|
||||
|
||||
|
||||
class ConfigFactory(UserServersTableItem, dict):
|
||||
@@ -149,21 +156,8 @@ class ConfigFactory(UserServersTableItem, dict):
|
||||
|
||||
@property
|
||||
def itemSubscription(self) -> str:
|
||||
"""Return the item subscription value."""
|
||||
try:
|
||||
app = QApplication.instance()
|
||||
|
||||
if app is None:
|
||||
return ''
|
||||
else:
|
||||
subsId = self.getExtras('subsId')
|
||||
subsOb = app._userSubs.get(subsId, {})
|
||||
|
||||
return subsOb.get('remark', '')
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return ''
|
||||
"""Return the persisted subscription identifier."""
|
||||
return self.getExtras('subsId') or ''
|
||||
|
||||
@property
|
||||
def itemLatency(self) -> str:
|
||||
@@ -265,3 +259,115 @@ class ConfigFactory(UserServersTableItem, dict):
|
||||
"""
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ConfigurationRegistry:
|
||||
"""Construct configuration objects through registered providers.
|
||||
|
||||
Providers are deliberately defined by behavior instead of by a plugin base
|
||||
class. This keeps configuration and persistence independent from the
|
||||
optional plugin system while allowing plugins to contribute parsers.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty provider registry."""
|
||||
self._providers = []
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def register(self, provider):
|
||||
"""Register *provider* once and return it."""
|
||||
requiredMethods = ('configFromString', 'configFromDict', 'blankConfig')
|
||||
|
||||
if not all(callable(getattr(provider, name, None)) for name in requiredMethods):
|
||||
raise TypeError(
|
||||
'configuration providers must implement configFromString, '
|
||||
'configFromDict, and blankConfig'
|
||||
)
|
||||
|
||||
with self._lock:
|
||||
if any(item is provider for item in self._providers):
|
||||
raise ValueError('configuration provider is already registered')
|
||||
|
||||
self._providers.append(provider)
|
||||
|
||||
return provider
|
||||
|
||||
def providers(self):
|
||||
"""Return registered providers in deterministic registration order."""
|
||||
with self._lock:
|
||||
return tuple(self._providers)
|
||||
|
||||
def fromString(self, config: str, **kwargs):
|
||||
"""Return the first provider result for textual *config*."""
|
||||
for provider in self.providers():
|
||||
factory = provider.configFromString(config, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
return None
|
||||
|
||||
def fromDict(self, config: dict, **kwargs):
|
||||
"""Return the first provider result for mapping *config*."""
|
||||
for provider in self.providers():
|
||||
factory = provider.configFromDict(config, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
return None
|
||||
|
||||
def blank(self, protocol, **kwargs):
|
||||
"""Return a blank configuration from the first matching provider."""
|
||||
for provider in self.providers():
|
||||
factory = provider.blankConfig(protocol, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
return None
|
||||
|
||||
|
||||
configurationRegistry = ConfigurationRegistry()
|
||||
|
||||
|
||||
def registerConfigurationProvider(provider):
|
||||
"""Register a configuration provider with the process-wide registry."""
|
||||
return configurationRegistry.register(provider)
|
||||
|
||||
|
||||
def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
|
||||
"""Construct a configuration mapping through registered providers."""
|
||||
if not isinstance(config, dict):
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
factory = configurationRegistry.fromDict(config, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory(config, **kwargs)
|
||||
|
||||
|
||||
def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
|
||||
"""Construct configuration data from text or a mapping."""
|
||||
if isinstance(config, str):
|
||||
factory = configurationRegistry.fromString(config, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
try:
|
||||
return configFactoryFromDict(ujson.loads(config), **kwargs)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
if isinstance(config, dict):
|
||||
return configFactoryFromDict(config, **kwargs)
|
||||
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
|
||||
def configFactoryBlank(protocol, **kwargs) -> ConfigFactory:
|
||||
"""Construct a blank configuration through a registered provider."""
|
||||
factory = configurationRegistry.blank(protocol, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory(**kwargs)
|
||||
@@ -21,8 +21,8 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library.Configuration import ConfigFactory, configFactoryFromAny
|
||||
from Furious.Library.Encoder import *
|
||||
from Furious.Plugins.Configuration import configFactoryFromAny
|
||||
|
||||
__all__ = ['UserServers']
|
||||
|
||||
|
||||
@@ -15,11 +15,33 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Expose configuration, encoding, and storage services."""
|
||||
"""Expose domain configuration, encoding, and persistent storage services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Plugins.Configuration import *
|
||||
from .Configuration import (
|
||||
ConfigFactory,
|
||||
ConfigurationRegistry,
|
||||
configFactoryBlank,
|
||||
configFactoryFromAny,
|
||||
configFactoryFromDict,
|
||||
configurationRegistry,
|
||||
registerConfigurationProvider,
|
||||
)
|
||||
from .Encoder import Base64Encoder, JSONEncoder, PyBase64Encoder, UJSONEncoder
|
||||
from .Storage import Storage
|
||||
|
||||
from .Encoder import *
|
||||
from .Storage import *
|
||||
__all__ = [
|
||||
'Base64Encoder',
|
||||
'ConfigFactory',
|
||||
'ConfigurationRegistry',
|
||||
'JSONEncoder',
|
||||
'PyBase64Encoder',
|
||||
'Storage',
|
||||
'UJSONEncoder',
|
||||
'configFactoryBlank',
|
||||
'configFactoryFromAny',
|
||||
'configFactoryFromDict',
|
||||
'configurationRegistry',
|
||||
'registerConfigurationProvider',
|
||||
]
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
# Copyright (C) 2024–present Loren Eteval & contributors <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Dispatch configuration creation through the process-wide plugin registry."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface.ConfigFactory import ConfigFactory
|
||||
|
||||
from .Registry import getPluginRegistry
|
||||
|
||||
from typing import Union
|
||||
|
||||
import ujson
|
||||
|
||||
__all__ = ['configFactoryFromDict', 'configFactoryFromAny', 'configFactoryBlank']
|
||||
|
||||
|
||||
def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
|
||||
"""Construct a configuration mapping through the first matching plugin."""
|
||||
if not isinstance(config, dict):
|
||||
return ConfigFactory()
|
||||
|
||||
factory = getPluginRegistry().configFromDict(config, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory(config, **kwargs)
|
||||
|
||||
|
||||
def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
|
||||
"""Construct configuration data through the first matching plugin."""
|
||||
if isinstance(config, str):
|
||||
factory = getPluginRegistry().configFromString(config, **kwargs)
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
try:
|
||||
return configFactoryFromDict(ujson.loads(config), **kwargs)
|
||||
except Exception:
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
if isinstance(config, dict):
|
||||
return configFactoryFromDict(config, **kwargs)
|
||||
|
||||
return ConfigFactory(**kwargs)
|
||||
|
||||
|
||||
def configFactoryBlank(protocol, **kwargs) -> ConfigFactory:
|
||||
"""Construct a blank configuration through the owning plugin."""
|
||||
factory = getPluginRegistry().blankConfig(protocol, **kwargs)
|
||||
|
||||
return factory if factory is not None else ConfigFactory()
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library.Configuration import ConfigFactory
|
||||
from Furious.Library.Encoder import *
|
||||
|
||||
from typing import Union, Tuple
|
||||
@@ -392,7 +393,7 @@ class ConfigXray(ConfigFactory):
|
||||
def getProxyOutboundServer(config: dict, protocol: Protocol, **kwargs) -> dict:
|
||||
"""Return proxy outbound server."""
|
||||
value, proxyOutbound = (
|
||||
protocol.value.lower(),
|
||||
protocol.value.casefold(),
|
||||
ConfigXray.getProxyOutboundObject(config, **kwargs),
|
||||
)
|
||||
|
||||
@@ -458,7 +459,7 @@ class ConfigXray(ConfigFactory):
|
||||
def proxyServerObject(self) -> dict:
|
||||
"""Return the proxy server object value."""
|
||||
try:
|
||||
proxyProtocol = self.proxyProtocol.lower()
|
||||
proxyProtocol = self.proxyProtocol.casefold()
|
||||
|
||||
if proxyProtocol == 'vmess' or proxyProtocol == 'vless':
|
||||
return self.proxyOutboundObject['settings']['vnext'][0]
|
||||
@@ -479,7 +480,7 @@ class ConfigXray(ConfigFactory):
|
||||
def proxyUserObject(self) -> dict:
|
||||
"""Return the proxy user object value."""
|
||||
try:
|
||||
if self.proxyProtocol.lower() == 'socks':
|
||||
if self.proxyProtocol.casefold() == 'socks':
|
||||
return self.proxyServerObject
|
||||
|
||||
return self.proxyServerObject['users'][0]
|
||||
@@ -1416,7 +1417,7 @@ class ConfigXray(ConfigFactory):
|
||||
else:
|
||||
override = remark
|
||||
|
||||
protocol = self.proxyProtocol.lower()
|
||||
protocol = self.proxyProtocol.casefold()
|
||||
|
||||
if protocol == 'vmess':
|
||||
netloc = PyBase64Encoder.encode(
|
||||
@@ -2311,7 +2312,7 @@ class ConfigHysteria2(ConfigFactory):
|
||||
|
||||
def configXrayEmptyProxyOutboundObject(protocol: Protocol) -> dict:
|
||||
"""Return the config Xray empty proxy outbound object value used by the application."""
|
||||
value = protocol.value.lower()
|
||||
value = protocol.value.casefold()
|
||||
|
||||
if value == 'vless' or value == 'vmess':
|
||||
return {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
from Furious.Core import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import ConfigFactory
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
|
||||
@@ -21,17 +21,14 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.DynamicTranslate import gettext as _
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol, PluginRouting
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_HYSTERIA1,
|
||||
ConfigHysteria1,
|
||||
)
|
||||
from Furious.Plugins.API import *
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
|
||||
from .Core import *
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .Core import Hysteria1
|
||||
|
||||
__all__ = ['Hysteria1Plugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -60,11 +57,10 @@ class Hysteria1Plugin(FuriousPlugin):
|
||||
|
||||
def configFromString(self, config: str, **kwargs):
|
||||
"""Parse a Hysteria 1 share URI."""
|
||||
return (
|
||||
ConfigHysteria1(config, **kwargs)
|
||||
if config.startswith('hysteria://')
|
||||
else None
|
||||
)
|
||||
if config.startswith('hysteria://'):
|
||||
return ConfigHysteria1(config, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
def configFromDict(self, config: dict, **kwargs):
|
||||
"""Recognize Hysteria 1 configuration mappings."""
|
||||
|
||||
@@ -19,4 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
from .Plugin import Hysteria1Plugin
|
||||
|
||||
__all__ = ['Hysteria1Plugin']
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
from Furious.Core import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import ConfigFactory
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
|
||||
@@ -23,15 +23,16 @@ from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
from .Plugin import *
|
||||
from .TUN import *
|
||||
|
||||
from .Plugin import isHysteria2ConnectionActive
|
||||
from .TUN import getHysteria2TUNSettings, saveHysteria2TUNSettings
|
||||
import copy
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiHysteria2TUNSettings']
|
||||
|
||||
@@ -41,6 +42,7 @@ logger = logging.getLogger(__name__)
|
||||
def _nestedValue(config: dict, path: tuple[str, ...], default):
|
||||
"""Return a nested settings value or *default*."""
|
||||
value = config
|
||||
|
||||
for key in path:
|
||||
if not isinstance(value, dict):
|
||||
return default
|
||||
@@ -53,8 +55,10 @@ def _nestedValue(config: dict, path: tuple[str, ...], default):
|
||||
def _setNestedValue(config: dict, path: tuple[str, ...], value):
|
||||
"""Set one nested settings value."""
|
||||
target = config
|
||||
|
||||
for key in path[:-1]:
|
||||
child = target.get(key)
|
||||
|
||||
if not isinstance(child, dict):
|
||||
child = {}
|
||||
target[key] = child
|
||||
@@ -70,7 +74,9 @@ class GuiHysteria2TUNItemText(GuiEditorItemTextInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize a Hysteria 2 TUN string editor."""
|
||||
self.path = tuple(kwargs.pop('path'))
|
||||
|
||||
maximumWidth = kwargs.pop('maximumWidth', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if maximumWidth is not None:
|
||||
@@ -79,6 +85,7 @@ class GuiHysteria2TUNItemText(GuiEditorItemTextInput):
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store the current text in a Hysteria 2 TUN settings mapping."""
|
||||
value = self.text().strip()
|
||||
|
||||
if _nestedValue(config, self.path, '') == value:
|
||||
return False
|
||||
|
||||
@@ -89,6 +96,7 @@ class GuiHysteria2TUNItemText(GuiEditorItemTextInput):
|
||||
def factoryToInput(self, config: dict):
|
||||
"""Load one Hysteria 2 TUN string setting into the editor."""
|
||||
value = _nestedValue(config, self.path, '')
|
||||
|
||||
self.setText(value if isinstance(value, str) else '')
|
||||
|
||||
|
||||
@@ -98,6 +106,7 @@ class GuiHysteria2TUNItemList(GuiHysteria2TUNItemText):
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store comma-separated values in the settings mapping."""
|
||||
value = [item.strip() for item in self.text().split(',') if item.strip()]
|
||||
|
||||
if _nestedValue(config, self.path, []) == value:
|
||||
return False
|
||||
|
||||
@@ -108,6 +117,7 @@ class GuiHysteria2TUNItemList(GuiHysteria2TUNItemText):
|
||||
def factoryToInput(self, config: dict):
|
||||
"""Load one list setting into the editor."""
|
||||
value = _nestedValue(config, self.path, [])
|
||||
|
||||
self.setText(','.join(value) if isinstance(value, list) else '')
|
||||
|
||||
|
||||
@@ -117,11 +127,13 @@ class GuiHysteria2TUNItemMTU(GuiEditorItemTextSpinBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the MTU editor."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(1, 65535)
|
||||
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store the MTU in the settings mapping."""
|
||||
value = self.value()
|
||||
|
||||
if config.get('mtu', 1500) == value:
|
||||
return False
|
||||
|
||||
@@ -132,6 +144,7 @@ class GuiHysteria2TUNItemMTU(GuiEditorItemTextSpinBox):
|
||||
def factoryToInput(self, config: dict):
|
||||
"""Load the MTU into the editor."""
|
||||
value = config.get('mtu', 1500)
|
||||
|
||||
self.setValue(value if isinstance(value, int) else 1500)
|
||||
|
||||
|
||||
@@ -213,6 +226,7 @@ class GuiHysteria2TUNSettingsGroupBoxNetwork(GuiEditorWidgetQGroupBox):
|
||||
row, field = divmod(index, 2)
|
||||
label, editor = container.widgets()
|
||||
column = field * 2
|
||||
|
||||
layout.addWidget(label, row, column)
|
||||
layout.addWidget(editor, row, column + 1)
|
||||
|
||||
@@ -253,32 +267,42 @@ class GuiHysteria2TUNSettings(GuiEditorWidgetQDialog):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the Hysteria 2 TUN settings dialog."""
|
||||
self._isConnectionActive = kwargs.pop('isConnectionActive', lambda: False)
|
||||
kwargs.setdefault('style', 'portrait')
|
||||
kwargs.setdefault('tabTranslatable', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(_('Customize Hysteria2 TUN Settings'))
|
||||
self.setFixedSize(int(650 * GOLDEN_RATIO), 650)
|
||||
|
||||
self._settings = getHysteria2TUNSettings()
|
||||
|
||||
self.factoryToInput(self._settings)
|
||||
|
||||
self.accepted.connect(self.handleAccepted)
|
||||
|
||||
self.layout().takeRow(self.layout().rowCount() - 1)
|
||||
|
||||
bottomLayout = QHBoxLayout()
|
||||
bottomLayout.addWidget(Hysteria2TUNDocumentationURL())
|
||||
bottomLayout.addStretch(1)
|
||||
bottomLayout.addWidget(self.dialogBtns)
|
||||
|
||||
self.layout().addRow(bottomLayout)
|
||||
|
||||
def handleAccepted(self):
|
||||
"""Persist changed settings and offer to reconnect when connected."""
|
||||
oldSettings = copy.deepcopy(self._settings)
|
||||
|
||||
self.inputToFactory(self._settings)
|
||||
|
||||
if self._settings == oldSettings:
|
||||
return
|
||||
|
||||
saveHysteria2TUNSettings(self._settings)
|
||||
if SystemRuntime.isTUNMode() and isHysteria2ConnectionActive():
|
||||
|
||||
if SystemRuntime.isTUNMode() and self._isConnectionActive():
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
@functools.lru_cache(None)
|
||||
|
||||
@@ -19,51 +19,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import APP, PLATFORM, SystemRuntime
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.DynamicTranslate import gettext as _
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_HYSTERIA2,
|
||||
ConfigHysteria2,
|
||||
)
|
||||
from Furious.Plugins.API import *
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
|
||||
from .Core import Hysteria2
|
||||
from .TUN import *
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .Core import Hysteria2
|
||||
from .TUN import (
|
||||
buildHysteria2TUNConfig,
|
||||
getHysteria2TUNSettings,
|
||||
isHysteria2TUNEnabled,
|
||||
resolveHysteria2ServerAddresses,
|
||||
setHysteria2TUNEnabled,
|
||||
)
|
||||
|
||||
__all__ = ['Hysteria2Plugin', 'isHysteria2ConnectionActive']
|
||||
__all__ = ['Hysteria2Plugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_TRANSLATABLE_ACTION_TEXT = [
|
||||
_('Add Hysteria2 Server...'),
|
||||
]
|
||||
|
||||
|
||||
def isHysteria2ConnectionActive() -> bool:
|
||||
"""Return whether the connected core process belongs to Hysteria 2."""
|
||||
try:
|
||||
connectAction = APP().systemTray.ConnectAction
|
||||
if not connectAction.isConnected():
|
||||
return False
|
||||
|
||||
return any(
|
||||
isinstance(process, Hysteria2)
|
||||
for process in connectAction.coreManager.processesPool
|
||||
)
|
||||
except Exception:
|
||||
# Plugin actions can be created before the tray is fully initialized.
|
||||
return False
|
||||
|
||||
|
||||
class Hysteria2Plugin(FuriousPlugin):
|
||||
"""Provide official Hysteria 2 support."""
|
||||
|
||||
@@ -133,6 +109,8 @@ class Hysteria2Plugin(FuriousPlugin):
|
||||
|
||||
def createManagementActions(self, parent=None, **kwargs):
|
||||
"""Create Hysteria 2 native TUN management actions."""
|
||||
isCoreActive = kwargs.pop('isCoreActive', lambda coreType: False)
|
||||
|
||||
# These modules require a fully initialized Furious.Qt package.
|
||||
from Furious.Qt import AppQAction, showMBoxNewChangesNextTime
|
||||
from Furious.Qt import gettext as _
|
||||
@@ -149,7 +127,7 @@ class Hysteria2Plugin(FuriousPlugin):
|
||||
"""Persist the native Hysteria 2 TUN action state."""
|
||||
setHysteria2TUNEnabled(useHysteria2TUNAction.isChecked())
|
||||
|
||||
if SystemRuntime.isTUNMode() and isHysteria2ConnectionActive():
|
||||
if SystemRuntime.isTUNMode() and isCoreActive(Hysteria2):
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
useHysteria2TUNAction.callback = updateUseHysteria2TUN
|
||||
@@ -158,7 +136,10 @@ class Hysteria2Plugin(FuriousPlugin):
|
||||
useHysteria2TUNAction,
|
||||
AppQAction(
|
||||
_('Customize Hysteria2 TUN Settings...'),
|
||||
callback=lambda: GuiHysteria2TUNSettings(parent=parent).open(),
|
||||
callback=lambda: GuiHysteria2TUNSettings(
|
||||
parent=parent,
|
||||
isConnectionActive=lambda: isCoreActive(Hysteria2),
|
||||
).open(),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -176,6 +157,7 @@ class Hysteria2Plugin(FuriousPlugin):
|
||||
# own process, so it cannot use CoreManager's privileged helper.
|
||||
# Keep the existing external tun2socks path available instead.
|
||||
config.pop('tun', None)
|
||||
|
||||
logger.warning(
|
||||
'Hysteria 2 native TUN requires superuser privileges on '
|
||||
'Linux; falling back to external tun2socks'
|
||||
@@ -190,6 +172,7 @@ class Hysteria2Plugin(FuriousPlugin):
|
||||
|
||||
if not serverAddresses and not hasManualExclusions:
|
||||
config.pop('tun', None)
|
||||
|
||||
logger.error(
|
||||
'Hysteria 2 native TUN disabled for this connection because '
|
||||
'the server address could not be resolved and no manual route '
|
||||
|
||||
@@ -21,11 +21,11 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
|
||||
import copy
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
import copy
|
||||
import socket
|
||||
import logging
|
||||
import ipaddress
|
||||
|
||||
__all__ = [
|
||||
'DEFAULT_HYSTERIA2_TUN_SETTINGS',
|
||||
@@ -84,29 +84,36 @@ def _normalizedStringList(value) -> list[str]:
|
||||
def _normalizedHysteria2TUNSettings(settings) -> dict:
|
||||
"""Return validated Hysteria 2 TUN settings with safe defaults."""
|
||||
result = copy.deepcopy(DEFAULT_HYSTERIA2_TUN_SETTINGS)
|
||||
|
||||
if not isinstance(settings, dict):
|
||||
return result
|
||||
|
||||
for key in ('name', 'timeout'):
|
||||
value = settings.get(key)
|
||||
|
||||
if isinstance(value, str):
|
||||
result[key] = value.strip()
|
||||
|
||||
mtu = settings.get('mtu')
|
||||
|
||||
if isinstance(mtu, int) and not isinstance(mtu, bool) and 1 <= mtu <= 65535:
|
||||
result['mtu'] = mtu
|
||||
|
||||
address = settings.get('address')
|
||||
|
||||
if isinstance(address, dict):
|
||||
for key in ('ipv4', 'ipv6'):
|
||||
value = address.get(key)
|
||||
|
||||
if isinstance(value, str):
|
||||
result['address'][key] = value.strip()
|
||||
|
||||
route = settings.get('route')
|
||||
|
||||
if isinstance(route, dict):
|
||||
for key in ('ipv4', 'ipv6', 'ipv4Exclude', 'ipv6Exclude'):
|
||||
value = route.get(key)
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
result['route'][key] = _normalizedStringList(value)
|
||||
|
||||
@@ -118,6 +125,8 @@ def getHysteria2TUNSettings() -> dict:
|
||||
try:
|
||||
settings = json.loads(AppSettings.get('Hysteria2TUNSettings'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
settings = None
|
||||
|
||||
return _normalizedHysteria2TUNSettings(settings)
|
||||
@@ -134,18 +143,23 @@ def saveHysteria2TUNSettings(settings: dict):
|
||||
def _serverHost(config) -> str:
|
||||
"""Return the Hysteria server host from a config or share-style server URI."""
|
||||
server = config.get('server', '')
|
||||
|
||||
if not isinstance(server, str) or not server.strip():
|
||||
return ''
|
||||
|
||||
try:
|
||||
host, _port = parseHostPort(server.strip())
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
host = None
|
||||
|
||||
if not host:
|
||||
try:
|
||||
host = config.itemAddress
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
host = server.rsplit(':', 1)[0]
|
||||
|
||||
if not isinstance(host, str):
|
||||
@@ -157,16 +171,19 @@ def _serverHost(config) -> str:
|
||||
def resolveHysteria2ServerAddresses(config) -> list[str]:
|
||||
"""Resolve addresses that must bypass native TUN to prevent a route loop."""
|
||||
host = _serverHost(config)
|
||||
|
||||
if not host:
|
||||
return []
|
||||
|
||||
unscopedHost = host.split('%', 1)[0]
|
||||
|
||||
try:
|
||||
return [str(ipaddress.ip_address(unscopedHost))]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
addresses = []
|
||||
|
||||
try:
|
||||
results = socket.getaddrinfo(
|
||||
host,
|
||||
@@ -184,6 +201,7 @@ def resolveHysteria2ServerAddresses(config) -> list[str]:
|
||||
continue
|
||||
|
||||
address = str(socketAddress[0]).split('%', 1)[0]
|
||||
|
||||
try:
|
||||
address = str(ipaddress.ip_address(address))
|
||||
except ValueError:
|
||||
@@ -218,6 +236,7 @@ def buildHysteria2TUNConfig(settings=None, serverAddresses=()) -> dict:
|
||||
continue
|
||||
|
||||
key, prefix = _hostPrefix(address)
|
||||
|
||||
if key is not None and prefix not in settings['route'][key]:
|
||||
settings['route'][key].append(prefix)
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
from .Plugin import Hysteria2Plugin
|
||||
|
||||
__all__ = ['Hysteria2Plugin']
|
||||
|
||||
@@ -21,8 +21,8 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library.Encoder import UJSONEncoder
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
from Furious.Library import *
|
||||
from Furious.Core import *
|
||||
|
||||
from enum import Enum
|
||||
from typing import Union
|
||||
|
||||
@@ -22,12 +22,9 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
|
||||
@@ -22,12 +22,9 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
|
||||
@@ -23,15 +23,16 @@ from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import logging
|
||||
from .Plugin import *
|
||||
from .TUN import *
|
||||
|
||||
from .Plugin import isXrayConnectionActive
|
||||
from .TUN import getXrayTUNSettings, saveXrayTUNSettings
|
||||
import copy
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['GuiXrayTUNSettings']
|
||||
|
||||
@@ -44,7 +45,9 @@ class GuiXrayTUNItemText(GuiEditorItemTextInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize an Xray TUN string editor."""
|
||||
self.key = kwargs.pop('key')
|
||||
|
||||
maximumWidth = kwargs.pop('maximumWidth', None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if maximumWidth is not None:
|
||||
@@ -53,6 +56,7 @@ class GuiXrayTUNItemText(GuiEditorItemTextInput):
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store the current text in an Xray TUN settings mapping."""
|
||||
value = self.text().strip()
|
||||
|
||||
if config.get(self.key, '') == value:
|
||||
return False
|
||||
|
||||
@@ -71,6 +75,7 @@ class GuiXrayTUNItemList(GuiXrayTUNItemText):
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store comma-separated values in the settings mapping."""
|
||||
value = [item.strip() for item in self.text().split(',') if item.strip()]
|
||||
|
||||
if config.get(self.key, []) == value:
|
||||
return False
|
||||
|
||||
@@ -81,6 +86,7 @@ class GuiXrayTUNItemList(GuiXrayTUNItemText):
|
||||
def factoryToInput(self, config: dict):
|
||||
"""Load one Xray TUN list setting into the editor."""
|
||||
value = config.get(self.key, [])
|
||||
|
||||
self.setText(','.join(value) if isinstance(value, list) else '')
|
||||
|
||||
|
||||
@@ -91,14 +97,18 @@ class GuiXrayTUNItemNumber(GuiEditorItemTextSpinBox):
|
||||
"""Initialize an Xray TUN numeric editor."""
|
||||
self.key = kwargs.pop('key')
|
||||
self.default = kwargs.pop('default')
|
||||
|
||||
minimum = kwargs.pop('minimum')
|
||||
maximum = kwargs.pop('maximum')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setRange(minimum, maximum)
|
||||
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
"""Store the current number in an Xray TUN settings mapping."""
|
||||
value = self.value()
|
||||
|
||||
if config.get(self.key, self.default) == value:
|
||||
return False
|
||||
|
||||
@@ -109,6 +119,7 @@ class GuiXrayTUNItemNumber(GuiEditorItemTextSpinBox):
|
||||
def factoryToInput(self, config: dict):
|
||||
"""Load one Xray TUN numeric setting into the editor."""
|
||||
value = config.get(self.key, self.default)
|
||||
|
||||
self.setValue(value if isinstance(value, int) else self.default)
|
||||
|
||||
|
||||
@@ -221,32 +232,42 @@ class GuiXrayTUNSettings(GuiEditorWidgetQDialog):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the Xray-core TUN settings dialog."""
|
||||
self._isConnectionActive = kwargs.pop('isConnectionActive', lambda: False)
|
||||
kwargs.setdefault('style', 'portrait')
|
||||
kwargs.setdefault('tabTranslatable', True)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setTabText(_('Customize Xray-core TUN Settings'))
|
||||
self.setFixedSize(int(650 * GOLDEN_RATIO), 650)
|
||||
|
||||
self._settings = getXrayTUNSettings()
|
||||
|
||||
self.factoryToInput(self._settings)
|
||||
|
||||
self.accepted.connect(self.handleAccepted)
|
||||
|
||||
self.layout().takeRow(self.layout().rowCount() - 1)
|
||||
|
||||
bottomLayout = QHBoxLayout()
|
||||
bottomLayout.addWidget(XrayTUNDocumentationURL())
|
||||
bottomLayout.addStretch(1)
|
||||
bottomLayout.addWidget(self.dialogBtns)
|
||||
|
||||
self.layout().addRow(bottomLayout)
|
||||
|
||||
def handleAccepted(self):
|
||||
"""Persist changed settings and offer to reconnect when connected."""
|
||||
oldSettings = copy.deepcopy(self._settings)
|
||||
|
||||
self.inputToFactory(self._settings)
|
||||
|
||||
if self._settings == oldSettings:
|
||||
return
|
||||
|
||||
saveXrayTUNSettings(self._settings)
|
||||
if SystemRuntime.isTUNMode() and isXrayConnectionActive():
|
||||
|
||||
if SystemRuntime.isTUNMode() and self._isConnectionActive():
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
@functools.lru_cache(None)
|
||||
|
||||
@@ -22,12 +22,9 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
|
||||
@@ -22,12 +22,9 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
|
||||
@@ -22,12 +22,9 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTransport import *
|
||||
from Furious.Plugins.Official.Xray.GuiVTLS import *
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import ConfigXray
|
||||
from Furious.Qt import *
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
@@ -21,9 +21,9 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.Official.Configuration import ConfigXray
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
|
||||
@@ -19,32 +19,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Core.CoreProcessWorker import ProcessOutputRedirector
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.DynamicTranslate import gettext as _
|
||||
from Furious.Library.Storage import Storage
|
||||
from Furious.Plugins.API import FuriousPlugin, PluginProtocol, PluginRouting
|
||||
from Furious.Plugins.Official.Configuration import (
|
||||
BLANK_CONFIG_XRAY,
|
||||
ConfigXray,
|
||||
configXrayEmptyProxyOutboundObject,
|
||||
)
|
||||
from Furious.Core import *
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins.API import *
|
||||
from Furious.Plugins.Official.Configuration import *
|
||||
|
||||
from .Core import *
|
||||
from .Routing import *
|
||||
from .TUN import *
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from .Core import XrayCore
|
||||
from .Routing import customRoutingObjectFromSettings
|
||||
from .TUN import (
|
||||
buildXrayTUNInbound,
|
||||
hasXrayTUNInbound,
|
||||
isXrayTUNEnabled,
|
||||
setXrayTUNEnabled,
|
||||
)
|
||||
|
||||
__all__ = ['XrayPlugin', 'isXrayConnectionActive']
|
||||
__all__ = ['XrayPlugin']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -96,22 +87,6 @@ _TRANSLATABLE_ACTION_TEXT = [
|
||||
]
|
||||
|
||||
|
||||
def isXrayConnectionActive() -> bool:
|
||||
"""Return whether the connected core process belongs to Xray-core."""
|
||||
try:
|
||||
connectAction = APP().systemTray.ConnectAction
|
||||
if not connectAction.isConnected():
|
||||
return False
|
||||
|
||||
return any(
|
||||
isinstance(process, XrayCore)
|
||||
for process in connectAction.coreManager.processesPool
|
||||
)
|
||||
except Exception:
|
||||
# Plugin actions can be created before the tray is fully initialized.
|
||||
return False
|
||||
|
||||
|
||||
class XrayPlugin(FuriousPlugin):
|
||||
"""Provide official Xray-core support."""
|
||||
|
||||
@@ -209,6 +184,8 @@ class XrayPlugin(FuriousPlugin):
|
||||
|
||||
def createManagementActions(self, parent=None, **kwargs):
|
||||
"""Create Xray routing, TUN, and asset-management actions."""
|
||||
isCoreActive = kwargs.pop('isCoreActive', lambda coreType: False)
|
||||
|
||||
# These modules require a fully initialized Furious.Qt package.
|
||||
from Furious.Qt import (
|
||||
AppQAction,
|
||||
@@ -245,7 +222,7 @@ class XrayPlugin(FuriousPlugin):
|
||||
"""Persist the native Xray TUN action state."""
|
||||
setXrayTUNEnabled(useXrayTUNAction.isChecked())
|
||||
|
||||
if SystemRuntime.isTUNMode() and isXrayConnectionActive():
|
||||
if SystemRuntime.isTUNMode() and isCoreActive(XrayCore):
|
||||
showMBoxNewChangesNextTime()
|
||||
|
||||
useXrayTUNAction.callback = updateUseXrayTUN
|
||||
@@ -260,7 +237,10 @@ class XrayPlugin(FuriousPlugin):
|
||||
useXrayTUNAction,
|
||||
AppQAction(
|
||||
_('Customize Xray-core TUN Settings...'),
|
||||
callback=lambda: GuiXrayTUNSettings(parent=parent).open(),
|
||||
callback=lambda: GuiXrayTUNSettings(
|
||||
parent=parent,
|
||||
isConnectionActive=lambda: isCoreActive(XrayCore),
|
||||
).open(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
@@ -275,6 +255,7 @@ class XrayPlugin(FuriousPlugin):
|
||||
return False
|
||||
|
||||
inbounds = config.get('inbounds')
|
||||
|
||||
if not isinstance(inbounds, list):
|
||||
inbounds = []
|
||||
|
||||
@@ -344,6 +325,7 @@ class XrayPlugin(FuriousPlugin):
|
||||
config['log'] = {'access': '', 'error': '', 'loglevel': 'warning'}
|
||||
|
||||
logRedirectValue = str(uuid.uuid4())
|
||||
|
||||
for attr in ['access', 'error']:
|
||||
fixLogObjectPath(config, attr, logRedirectValue, log)
|
||||
|
||||
@@ -454,6 +436,7 @@ class XrayPlugin(FuriousPlugin):
|
||||
return
|
||||
|
||||
logger.info(f'assets folder \'{XRAY_ASSET_DIR}\' is writable. Continue')
|
||||
|
||||
if not AppSettings.isStateON_('AutoUpdateAssetFiles'):
|
||||
logger.info('skipped auto assets update due to settings')
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Library.Storage import Storage
|
||||
from Furious.Library import *
|
||||
|
||||
__all__ = ['cleanRoutingRule', 'customRoutingObjectFromSettings']
|
||||
|
||||
@@ -45,8 +45,10 @@ def cleanRoutingRule(rule: dict):
|
||||
'process',
|
||||
]:
|
||||
value = rule.get(key, [])
|
||||
|
||||
if isinstance(value, list):
|
||||
value = list(str(item).strip() for item in value if str(item).strip())
|
||||
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
@@ -60,6 +62,7 @@ def cleanRoutingRule(rule: dict):
|
||||
'ruleTag',
|
||||
]:
|
||||
value = str(rule.get(key, '')).strip()
|
||||
|
||||
if value:
|
||||
result[key] = value
|
||||
|
||||
@@ -69,14 +72,17 @@ def cleanRoutingRule(rule: dict):
|
||||
def customRoutingObjectFromSettings(routing: str):
|
||||
"""Load a named custom Xray routing object from persistent settings."""
|
||||
prefix = 'Custom:'
|
||||
|
||||
if not isinstance(routing, str) or not routing.startswith(prefix):
|
||||
return None
|
||||
|
||||
routingProfile = Storage.UserRoutings().get(routing[len(prefix) :])
|
||||
|
||||
if not isinstance(routingProfile, dict) or not routingProfile.get('enabled', True):
|
||||
return None
|
||||
|
||||
domainStrategy = routingProfile.get('domainStrategy', 'AsIs')
|
||||
|
||||
if domainStrategy not in ['AsIs', 'IPIfNonMatch', 'IPOnDemand']:
|
||||
domainStrategy = 'AsIs'
|
||||
|
||||
|
||||
@@ -66,26 +66,31 @@ def setXrayTUNEnabled(enabled: bool):
|
||||
def _normalizedXrayTUNSettings(settings) -> dict:
|
||||
"""Return validated Xray TUN settings with defaults for invalid fields."""
|
||||
result = copy.deepcopy(DEFAULT_XRAY_TUN_SETTINGS)
|
||||
|
||||
if not isinstance(settings, dict):
|
||||
return result
|
||||
|
||||
for key in ('name', 'desc', 'autoOutboundsInterface'):
|
||||
value = settings.get(key)
|
||||
|
||||
if isinstance(value, str):
|
||||
result[key] = value.strip()
|
||||
|
||||
for key in ('gateway', 'dns', 'autoSystemRoutingTable'):
|
||||
value = settings.get(key)
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
result[key] = [
|
||||
item.strip() for item in value if isinstance(item, str) and item.strip()
|
||||
]
|
||||
|
||||
mtu = settings.get('mtu')
|
||||
|
||||
if isinstance(mtu, int) and not isinstance(mtu, bool) and mtu > 0:
|
||||
result['mtu'] = mtu
|
||||
|
||||
userLevel = settings.get('userLevel')
|
||||
|
||||
if (
|
||||
isinstance(userLevel, int)
|
||||
and not isinstance(userLevel, bool)
|
||||
@@ -101,6 +106,8 @@ def getXrayTUNSettings() -> dict:
|
||||
try:
|
||||
settings = json.loads(AppSettings.get('XrayTUNSettings'))
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
settings = None
|
||||
|
||||
return _normalizedXrayTUNSettings(settings)
|
||||
@@ -137,6 +144,7 @@ def buildXrayTUNInbound(settings=None) -> dict:
|
||||
def hasXrayTUNInbound(config) -> bool:
|
||||
"""Return whether an Xray configuration contains a TUN inbound."""
|
||||
inbounds = config.get('inbounds', [])
|
||||
|
||||
if not isinstance(inbounds, list):
|
||||
return False
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt.WebGETManager import *
|
||||
from Furious.Qt import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ from __future__ import annotations
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from .XrayAssetViewerQListWidget import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
|
||||
@@ -19,4 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Plugin import *
|
||||
from .Plugin import XrayPlugin
|
||||
|
||||
__all__ = ['XrayPlugin']
|
||||
|
||||
+70
-18
@@ -19,12 +19,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Library.Configuration import configurationRegistry
|
||||
|
||||
from importlib import metadata
|
||||
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from .API import PLUGIN_API_VERSION, FuriousPlugin, PluginProtocol, PluginRouting
|
||||
from .API import *
|
||||
|
||||
__all__ = [
|
||||
'PLUGIN_ENTRY_POINT_GROUP',
|
||||
@@ -48,12 +50,17 @@ def _normalizeProtocol(protocol) -> str:
|
||||
class PluginRegistry:
|
||||
"""Store plugins and route host operations to the owning plugin."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize an empty plugin registry."""
|
||||
def __init__(self, configRegistry=None):
|
||||
"""Initialize an empty plugin registry.
|
||||
|
||||
``configRegistry`` is supplied only to the process-wide registry. This
|
||||
keeps independently constructed registries isolated for tools and tests.
|
||||
"""
|
||||
self._plugins = {}
|
||||
self._protocols = {}
|
||||
self._configurationTypes = {}
|
||||
self._coreTypes = {}
|
||||
self._configRegistry = configRegistry
|
||||
|
||||
def register(self, plugin: FuriousPlugin):
|
||||
"""Register a plugin after validating all of its public identifiers."""
|
||||
@@ -62,6 +69,7 @@ class PluginRegistry:
|
||||
|
||||
if not isinstance(plugin, FuriousPlugin):
|
||||
raise TypeError('plugin must be a FuriousPlugin instance')
|
||||
|
||||
if plugin.apiVersion != PLUGIN_API_VERSION:
|
||||
raise ValueError(
|
||||
f'plugin API {plugin.apiVersion!r} is not supported; '
|
||||
@@ -69,28 +77,35 @@ class PluginRegistry:
|
||||
)
|
||||
|
||||
pluginId = str(plugin.pluginId).strip()
|
||||
|
||||
if not pluginId:
|
||||
raise ValueError('plugin ID cannot be empty')
|
||||
|
||||
if pluginId in self._plugins:
|
||||
raise ValueError(f'plugin {pluginId!r} is already registered')
|
||||
|
||||
protocolKeys = []
|
||||
|
||||
for descriptor in plugin.protocols:
|
||||
if not isinstance(descriptor, PluginProtocol):
|
||||
raise TypeError('plugin protocols must contain PluginProtocol values')
|
||||
|
||||
key = _normalizeProtocol(descriptor.id)
|
||||
|
||||
if not key:
|
||||
raise ValueError('plugin protocol ID cannot be empty')
|
||||
|
||||
if key in self._protocols or key in protocolKeys:
|
||||
raise ValueError(f'protocol {descriptor.id!r} is already registered')
|
||||
|
||||
protocolKeys.append(key)
|
||||
|
||||
configurationTypes = []
|
||||
|
||||
for configType in plugin.configurationTypes:
|
||||
if not isinstance(configType, type):
|
||||
raise TypeError('plugin configuration types must be classes')
|
||||
|
||||
if any(
|
||||
issubclass(configType, registeredType)
|
||||
or issubclass(registeredType, configType)
|
||||
@@ -102,12 +117,15 @@ class PluginRegistry:
|
||||
raise ValueError(
|
||||
f'configuration type {configType.__name__!r} overlaps a registered type'
|
||||
)
|
||||
|
||||
configurationTypes.append(configType)
|
||||
|
||||
coreTypes = []
|
||||
|
||||
for coreType in plugin.coreTypes:
|
||||
if not isinstance(coreType, type):
|
||||
raise TypeError('plugin core types must be classes')
|
||||
|
||||
if any(
|
||||
issubclass(coreType, registeredType)
|
||||
or issubclass(registeredType, coreType)
|
||||
@@ -119,8 +137,12 @@ class PluginRegistry:
|
||||
raise ValueError(
|
||||
f'core type {coreType.__name__!r} overlaps a registered type'
|
||||
)
|
||||
|
||||
coreTypes.append(coreType)
|
||||
|
||||
if self._configRegistry is not None:
|
||||
self._configRegistry.register(plugin)
|
||||
|
||||
self._plugins[pluginId] = plugin
|
||||
|
||||
for key, descriptor in zip(protocolKeys, plugin.protocols):
|
||||
@@ -172,6 +194,7 @@ class PluginRegistry:
|
||||
"""Ask plugins to parse textual configuration data."""
|
||||
for plugin in self.plugins():
|
||||
factory = plugin.configFromString(config, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
@@ -181,6 +204,7 @@ class PluginRegistry:
|
||||
"""Ask plugins to parse configuration mapping data."""
|
||||
for plugin in self.plugins():
|
||||
factory = plugin.configFromDict(config, **kwargs)
|
||||
|
||||
if factory is not None:
|
||||
return factory
|
||||
|
||||
@@ -195,6 +219,7 @@ class PluginRegistry:
|
||||
def createEditorForProtocol(self, protocol, parent=None, **kwargs):
|
||||
"""Construct a protocol editor through the owning plugin."""
|
||||
plugin = self.pluginForProtocol(protocol)
|
||||
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
@@ -203,6 +228,7 @@ class PluginRegistry:
|
||||
def createEditorForConfig(self, config, parent=None, **kwargs):
|
||||
"""Construct a configuration editor through the owning plugin."""
|
||||
plugin = self.pluginForConfig(config)
|
||||
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
@@ -225,16 +251,20 @@ class PluginRegistry:
|
||||
def prepareTUN(self, config) -> bool:
|
||||
"""Ask a configuration's plugin to prepare its native TUN support."""
|
||||
plugin = self.pluginForConfig(config)
|
||||
|
||||
if plugin is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
handled = plugin.prepareTUN(config)
|
||||
|
||||
if not isinstance(handled, bool):
|
||||
raise TypeError('plugin TUN preparation result must be a boolean')
|
||||
|
||||
return handled
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'TUN preparation failed for {plugin.pluginId!r}: {ex}')
|
||||
|
||||
return False
|
||||
@@ -242,12 +272,14 @@ class PluginRegistry:
|
||||
def routingOptions(self, config):
|
||||
"""Return validated routing modes supported by a configuration's plugin."""
|
||||
plugin = self.pluginForConfig(config)
|
||||
|
||||
if plugin is None:
|
||||
return tuple()
|
||||
|
||||
try:
|
||||
options = tuple(plugin.routingOptions(config))
|
||||
optionIds = set()
|
||||
|
||||
for option in options:
|
||||
if not isinstance(option, PluginRouting):
|
||||
raise TypeError(
|
||||
@@ -256,18 +288,22 @@ class PluginRegistry:
|
||||
|
||||
if not isinstance(option.id, str):
|
||||
raise TypeError('plugin routing option ID must be a string')
|
||||
|
||||
if not option.id.strip():
|
||||
raise ValueError('plugin routing option ID cannot be empty')
|
||||
|
||||
if not isinstance(option.displayName, str):
|
||||
raise TypeError(
|
||||
'plugin routing option display name must be a string'
|
||||
)
|
||||
|
||||
if not isinstance(option.translatable, bool):
|
||||
raise TypeError(
|
||||
'plugin routing option translatable flag must be a boolean'
|
||||
)
|
||||
|
||||
optionId = option.id
|
||||
|
||||
if optionId in optionIds:
|
||||
raise ValueError(
|
||||
f'routing option {option.id!r} is already registered'
|
||||
@@ -277,6 +313,8 @@ class PluginRegistry:
|
||||
|
||||
return options
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'failed to obtain routing options for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
@@ -286,6 +324,7 @@ class PluginRegistry:
|
||||
def normalizeRouting(self, config, routing):
|
||||
"""Return a supported routing value or the plugin's first option."""
|
||||
options = self.routingOptions(config)
|
||||
|
||||
if not options:
|
||||
return routing
|
||||
|
||||
@@ -299,15 +338,20 @@ class PluginRegistry:
|
||||
try:
|
||||
plugin.configureEnvironment()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'environment hook failed for {plugin.pluginId!r}: {ex}')
|
||||
|
||||
def coreVersions(self):
|
||||
"""Return version strings reported by every registered plugin core."""
|
||||
versions = []
|
||||
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
versions.extend(plugin.coreVersions())
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'failed to obtain core versions for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
@@ -317,10 +361,13 @@ class PluginRegistry:
|
||||
def logTimestampPatterns(self):
|
||||
"""Return timestamp expressions contributed by registered plugins."""
|
||||
patterns = []
|
||||
|
||||
for plugin in self.plugins():
|
||||
try:
|
||||
patterns.extend(plugin.logTimestampPatterns())
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'failed to obtain log patterns for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
@@ -330,12 +377,15 @@ class PluginRegistry:
|
||||
def coreExitMessage(self, core, exitcode: int):
|
||||
"""Return the owning plugin's special exit message, if any."""
|
||||
plugin = self.pluginForCore(core)
|
||||
|
||||
if plugin is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return plugin.coreExitMessage(core, exitcode)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'failed to interpret core exit for {plugin.pluginId!r}: {ex}')
|
||||
|
||||
return None
|
||||
@@ -346,6 +396,8 @@ class PluginRegistry:
|
||||
try:
|
||||
plugin.afterConnected(httpProxy)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'post-connection hook failed for {plugin.pluginId!r}: {ex}'
|
||||
)
|
||||
@@ -354,11 +406,14 @@ class PluginRegistry:
|
||||
"""Load third-party plugins exposed through Python entry points."""
|
||||
try:
|
||||
entryPoints = metadata.entry_points()
|
||||
|
||||
if hasattr(entryPoints, 'select'):
|
||||
entryPoints = entryPoints.select(group=PLUGIN_ENTRY_POINT_GROUP)
|
||||
else:
|
||||
entryPoints = entryPoints.get(PLUGIN_ENTRY_POINT_GROUP, tuple())
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'failed to enumerate Furious plugins: {ex}')
|
||||
|
||||
return
|
||||
@@ -366,6 +421,7 @@ class PluginRegistry:
|
||||
for entryPoint in entryPoints:
|
||||
try:
|
||||
plugin = entryPoint.load()
|
||||
|
||||
if isinstance(plugin, type) and issubclass(plugin, FuriousPlugin):
|
||||
plugin = plugin()
|
||||
elif callable(plugin) and not isinstance(plugin, FuriousPlugin):
|
||||
@@ -377,24 +433,14 @@ class PluginRegistry:
|
||||
else:
|
||||
self.register(plugin)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'failed to load plugin {entryPoint.name!r}: {ex}')
|
||||
|
||||
|
||||
_registry = PluginRegistry()
|
||||
_registry = PluginRegistry(configurationRegistry)
|
||||
_registryLock = threading.RLock()
|
||||
_registryInitialized = False
|
||||
_officialPluginTypes = tuple()
|
||||
|
||||
|
||||
def _setOfficialPluginTypes(pluginTypes):
|
||||
"""Configure bundled plugin classes before registry initialization."""
|
||||
global _officialPluginTypes
|
||||
|
||||
with _registryLock:
|
||||
if _registryInitialized:
|
||||
raise RuntimeError('plugin registry is already initialized')
|
||||
|
||||
_officialPluginTypes = tuple(pluginTypes)
|
||||
|
||||
|
||||
def getPluginRegistry() -> PluginRegistry:
|
||||
@@ -403,8 +449,14 @@ def getPluginRegistry() -> PluginRegistry:
|
||||
|
||||
with _registryLock:
|
||||
if not _registryInitialized:
|
||||
registry = PluginRegistry()
|
||||
for pluginType in _officialPluginTypes:
|
||||
# Importing official plugins is an intentional extension-loading
|
||||
# boundary. Keeping it here prevents package initialization from
|
||||
# eagerly importing GUI and core implementations.
|
||||
from .Official import OFFICIAL_PLUGIN_TYPES
|
||||
|
||||
registry = PluginRegistry(configurationRegistry)
|
||||
|
||||
for pluginType in OFFICIAL_PLUGIN_TYPES:
|
||||
registry.register(pluginType())
|
||||
|
||||
registry.discover()
|
||||
|
||||
@@ -15,14 +15,25 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Expose the Furious plugin API and process-wide plugin registry."""
|
||||
"""Expose plugin contracts without eagerly loading plugin implementations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .API import *
|
||||
from .Registry import *
|
||||
from .Registry import _setOfficialPluginTypes
|
||||
from .Configuration import *
|
||||
from .Official import OFFICIAL_PLUGIN_TYPES
|
||||
from .API import PLUGIN_API_VERSION, FuriousPlugin, PluginProtocol, PluginRouting
|
||||
from .Registry import (
|
||||
PLUGIN_ENTRY_POINT_GROUP,
|
||||
PluginRegistry,
|
||||
getPluginRegistry,
|
||||
registerPlugin,
|
||||
)
|
||||
|
||||
_setOfficialPluginTypes(OFFICIAL_PLUGIN_TYPES)
|
||||
__all__ = [
|
||||
'PLUGIN_API_VERSION',
|
||||
'PLUGIN_ENTRY_POINT_GROUP',
|
||||
'FuriousPlugin',
|
||||
'PluginProtocol',
|
||||
'PluginRegistry',
|
||||
'PluginRouting',
|
||||
'getPluginRegistry',
|
||||
'registerPlugin',
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library import ConfigFactory
|
||||
from Furious.Qt.DynamicTranslate import gettext as _
|
||||
from Furious.Qt.QtWidgets import *
|
||||
|
||||
|
||||
@@ -19,9 +19,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Core.Tun2socks import Tun2socks
|
||||
from Furious.Frozenlib.Mixins import Mixins
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt.AppStyleSheet import AppStyleSheet
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -32,8 +30,22 @@ __all__ = [
|
||||
'DraculaEditorTheme',
|
||||
'DraculaJSONSyntaxHighlighter',
|
||||
'DraculaLoggerSyntaxHighlighter',
|
||||
'configureEditorLogMetadata',
|
||||
]
|
||||
|
||||
_coreVersionsProvider = lambda: tuple()
|
||||
_logTimestampPatternsProvider = lambda: tuple()
|
||||
|
||||
|
||||
def configureEditorLogMetadata(coreVersionsProvider, timestampPatternsProvider):
|
||||
"""Install application-owned metadata providers used for log highlighting."""
|
||||
if not callable(coreVersionsProvider) or not callable(timestampPatternsProvider):
|
||||
raise TypeError('editor log metadata providers must be callable')
|
||||
|
||||
global _coreVersionsProvider, _logTimestampPatternsProvider
|
||||
_coreVersionsProvider = coreVersionsProvider
|
||||
_logTimestampPatternsProvider = timestampPatternsProvider
|
||||
|
||||
|
||||
def _currentTheme():
|
||||
"""Return the application's active theme without coupling to its concrete type."""
|
||||
@@ -149,10 +161,7 @@ class AppQSyntaxHighlighter(Mixins.ThemeAware, QSyntaxHighlighter):
|
||||
captured = text[capturedStart : capturedStart + capturedLength]
|
||||
shouldHighlight = True
|
||||
|
||||
for version in [
|
||||
*getPluginRegistry().coreVersions(),
|
||||
Tun2socks.version(),
|
||||
]:
|
||||
for version in _coreVersionsProvider():
|
||||
if captured == version:
|
||||
# These x.y.z.u version values are not IPv4 addresses. Do not highlight
|
||||
shouldHighlight = False
|
||||
@@ -261,11 +270,15 @@ class DraculaLoggerSyntaxHighlighter(AppQSyntaxHighlighter):
|
||||
),
|
||||
EditorHighlightRules(
|
||||
# Application logging timestamp
|
||||
r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\]' + r'|'
|
||||
# Plugin-core logging timestamps
|
||||
+ r'|'.join(getPluginRegistry().logTimestampPatterns()) + r'|'
|
||||
# tun2socks logging timestamp
|
||||
+ r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}',
|
||||
r'|'.join(
|
||||
(
|
||||
# Application logging timestamp
|
||||
r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\]',
|
||||
*_logTimestampPatternsProvider(),
|
||||
# tun2socks logging timestamp
|
||||
r'\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}',
|
||||
)
|
||||
),
|
||||
palette['editor_timestamp'],
|
||||
),
|
||||
EditorHighlightRules(
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
# 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/>.
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
@@ -26,6 +43,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from PySide6.QtCore import QRect, Qt, QTimer
|
||||
from PySide6.QtGui import QColor, QPainter, QPaintEvent
|
||||
from PySide6.QtWidgets import QWidget
|
||||
+147
-13
@@ -19,16 +19,150 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .DNSResolver import *
|
||||
from .AppStyleSheet import *
|
||||
from .DynamicTheme import *
|
||||
from .DynamicTranslate import *
|
||||
from .GuiEditorXXX import *
|
||||
from .NetworkConnectivityManager import *
|
||||
from .QtGui import *
|
||||
from .QtNetwork import *
|
||||
from .QtWidgets import *
|
||||
from .TextEditor import *
|
||||
from .TextEditorTheme import *
|
||||
from .UpdatesManager import *
|
||||
from .WebGETManager import *
|
||||
from .AppStyleSheet import AppStyleSheet
|
||||
from .DNSResolver import DNSResolver
|
||||
from .DynamicTheme import AppHue
|
||||
from .DynamicTranslate import (
|
||||
ABBR_TO_LANGUAGE,
|
||||
LANGUAGE_TO_ABBR,
|
||||
SUPPORTED_LANGUAGE,
|
||||
gettext,
|
||||
)
|
||||
from .GuiEditorXXX import (
|
||||
GuiEditorItemBasicRemark,
|
||||
GuiEditorItemProxyHttp,
|
||||
GuiEditorItemProxySocks,
|
||||
GuiEditorItemTextCheckBox,
|
||||
GuiEditorItemTextComboBox,
|
||||
GuiEditorItemTextInput,
|
||||
GuiEditorItemTextSpinBox,
|
||||
GuiEditorWidgetQDialog,
|
||||
GuiEditorWidgetQGroupBox,
|
||||
GuiEditorWidgetQWidget,
|
||||
)
|
||||
from .NetworkConnectivityManager import NetworkConnectivityManager
|
||||
from .QtGui import (
|
||||
AppQAction,
|
||||
AppQActionGroup,
|
||||
AppQIcon,
|
||||
AppQSeperator,
|
||||
bootstrapIcon,
|
||||
bootstrapIconMask,
|
||||
bootstrapIconWhite,
|
||||
bootstrapIconWithOpacity,
|
||||
)
|
||||
from .QtNetwork import AppQNetworkAccessManager
|
||||
from .QtWidgets import (
|
||||
AppQCheckBox,
|
||||
AppQComboBox,
|
||||
AppQDialog,
|
||||
AppQDialogButtonBox,
|
||||
AppQGroupBox,
|
||||
AppQHeaderView,
|
||||
AppQLabel,
|
||||
AppQLineEdit,
|
||||
AppQListWidget,
|
||||
AppQMainWindow,
|
||||
AppQMenu,
|
||||
AppQMenuBar,
|
||||
AppQMessageBox,
|
||||
AppQPushButton,
|
||||
AppQSpinBox,
|
||||
AppQTableView,
|
||||
AppQTableWidget,
|
||||
AppQTabWidget,
|
||||
AppQToolBar,
|
||||
MBoxDirectRulesNotAllowed,
|
||||
MBoxNewChangesNextTime,
|
||||
MBoxQuestionDelete,
|
||||
MBoxUnrecognizedConfig,
|
||||
moveToCenter,
|
||||
showMBoxDirectRulesNotAllowed,
|
||||
showMBoxNewChangesNextTime,
|
||||
showMBoxUnrecognizedConfig,
|
||||
)
|
||||
from .TextEditor import (
|
||||
AppQPlainTextEdit,
|
||||
AppQTextBrowser,
|
||||
DraculaJSONTextEditor,
|
||||
DraculaTextBrowser,
|
||||
DraculaTextEditor,
|
||||
)
|
||||
from .TextEditorTheme import (
|
||||
DraculaEditorTheme,
|
||||
DraculaJSONSyntaxHighlighter,
|
||||
DraculaLoggerSyntaxHighlighter,
|
||||
configureEditorLogMetadata,
|
||||
)
|
||||
from .UpdatesManager import UpdatesManager
|
||||
from .WaitingSpinner import WaitingSpinner
|
||||
from .WebGETManager import WebGETManager
|
||||
|
||||
__all__ = [
|
||||
'ABBR_TO_LANGUAGE',
|
||||
'AppHue',
|
||||
'AppQAction',
|
||||
'AppQActionGroup',
|
||||
'AppQCheckBox',
|
||||
'AppQComboBox',
|
||||
'AppQDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
'AppQHeaderView',
|
||||
'AppQIcon',
|
||||
'AppQLabel',
|
||||
'AppQLineEdit',
|
||||
'AppQListWidget',
|
||||
'AppQMainWindow',
|
||||
'AppQMenu',
|
||||
'AppQMenuBar',
|
||||
'AppQMessageBox',
|
||||
'AppQNetworkAccessManager',
|
||||
'AppQPlainTextEdit',
|
||||
'AppQPushButton',
|
||||
'AppQSeperator',
|
||||
'AppQSpinBox',
|
||||
'AppQTabWidget',
|
||||
'AppQTableView',
|
||||
'AppQTableWidget',
|
||||
'AppQTextBrowser',
|
||||
'AppQToolBar',
|
||||
'AppStyleSheet',
|
||||
'DNSResolver',
|
||||
'DraculaEditorTheme',
|
||||
'DraculaJSONSyntaxHighlighter',
|
||||
'DraculaJSONTextEditor',
|
||||
'DraculaLoggerSyntaxHighlighter',
|
||||
'DraculaTextBrowser',
|
||||
'DraculaTextEditor',
|
||||
'GuiEditorItemBasicRemark',
|
||||
'GuiEditorItemProxyHttp',
|
||||
'GuiEditorItemProxySocks',
|
||||
'GuiEditorItemTextCheckBox',
|
||||
'GuiEditorItemTextComboBox',
|
||||
'GuiEditorItemTextInput',
|
||||
'GuiEditorItemTextSpinBox',
|
||||
'GuiEditorWidgetQDialog',
|
||||
'GuiEditorWidgetQGroupBox',
|
||||
'GuiEditorWidgetQWidget',
|
||||
'LANGUAGE_TO_ABBR',
|
||||
'MBoxDirectRulesNotAllowed',
|
||||
'MBoxNewChangesNextTime',
|
||||
'MBoxQuestionDelete',
|
||||
'MBoxUnrecognizedConfig',
|
||||
'NetworkConnectivityManager',
|
||||
'SUPPORTED_LANGUAGE',
|
||||
'UpdatesManager',
|
||||
'WaitingSpinner',
|
||||
'WebGETManager',
|
||||
'bootstrapIcon',
|
||||
'bootstrapIconMask',
|
||||
'bootstrapIconWhite',
|
||||
'bootstrapIconWithOpacity',
|
||||
'configureEditorLogMetadata',
|
||||
'gettext',
|
||||
'moveToCenter',
|
||||
'showMBoxDirectRulesNotAllowed',
|
||||
'showMBoxNewChangesNextTime',
|
||||
'showMBoxUnrecognizedConfig',
|
||||
]
|
||||
|
||||
@@ -21,8 +21,8 @@ from __future__ import annotations
|
||||
|
||||
from Furious.Frozenlib import *
|
||||
from Furious.Interface import *
|
||||
from Furious.Library.Storage import Storage
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Library import *
|
||||
from Furious.Plugins import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Core.CoreProcessWorker import *
|
||||
from Furious.Core.Tun2socks import *
|
||||
@@ -0,0 +1,24 @@
|
||||
# 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/>.
|
||||
|
||||
"""Expose application services that coordinate lower-level subsystems."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .CoreManager import CoreManager
|
||||
|
||||
__all__ = ['CoreManager']
|
||||
@@ -25,8 +25,8 @@ from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Core.CoreManager import CoreManager
|
||||
from Furious.Widget.ConnectProgressBar import ConnectProgressBar
|
||||
from Furious.Service import CoreManager
|
||||
from Furious.TrayActions.ConnectProgressBar import ConnectProgressBar
|
||||
|
||||
from PySide6 import QtCore
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from Furious.Frozenlib import *
|
||||
from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Widget.WaitingSpinner import *
|
||||
from Furious.Qt.WaitingSpinner import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
@@ -19,11 +19,34 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Connect import *
|
||||
from .EditConfiguration import *
|
||||
from .Exit import *
|
||||
from .Import import *
|
||||
from .Language import *
|
||||
from .Routing import *
|
||||
from .Settings import *
|
||||
from .SystemProxy import *
|
||||
from .Connect import ConnectAction
|
||||
from .ConnectProgressBar import ConnectProgressBar
|
||||
from .EditConfiguration import EditConfigurationAction
|
||||
from .Exit import ExitAction
|
||||
from .Import import (
|
||||
ImportAction,
|
||||
ImportFromFileAction,
|
||||
ImportJSONFromClipboardAction,
|
||||
ImportQRCodeOnTheScreenAction,
|
||||
ImportURIFromClipboardAction,
|
||||
)
|
||||
from .Language import LanguageAction
|
||||
from .Routing import RoutingAction
|
||||
from .Settings import SettingsAction
|
||||
from .SystemProxy import SystemProxyAction
|
||||
|
||||
__all__ = [
|
||||
'ConnectAction',
|
||||
'ConnectProgressBar',
|
||||
'EditConfigurationAction',
|
||||
'ExitAction',
|
||||
'ImportAction',
|
||||
'ImportFromFileAction',
|
||||
'ImportJSONFromClipboardAction',
|
||||
'ImportQRCodeOnTheScreenAction',
|
||||
'ImportURIFromClipboardAction',
|
||||
'LanguageAction',
|
||||
'RoutingAction',
|
||||
'SettingsAction',
|
||||
'SystemProxyAction',
|
||||
]
|
||||
|
||||
@@ -19,4 +19,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .AppMainProcess import *
|
||||
from .AppMainProcess import AppMainProcess
|
||||
|
||||
__all__ = ['AppMainProcess']
|
||||
|
||||
+4
-2
@@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
"""Define the Furious package version."""
|
||||
"""Provide the stable public import path for the Furious package version."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__version__ = '0.7.0'
|
||||
from Furious.Frozenlib.Version import __version__
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
@@ -25,6 +25,7 @@ from Furious.Library import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
@@ -242,9 +243,7 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
|
||||
key='tunAdapterInterfaceDNS',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_(
|
||||
'Bypass Tun2socks Adapter Interface IP (separated by commas)'
|
||||
),
|
||||
title=_('Bypass Tun2socks Adapter Interface IP (separated by commas)'),
|
||||
key='bypassTUNAdapterInterfaceIP',
|
||||
),
|
||||
GuiTUNSettingsItemCheckBoxXXX(
|
||||
|
||||
@@ -25,11 +25,11 @@ from Furious.Library import *
|
||||
from Furious.Plugins import getPluginRegistry
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Core.CoreManager import CoreManager
|
||||
from Furious.Service import CoreManager
|
||||
from Furious.TrayActions.Import import *
|
||||
from Furious.Widget.WaitingSpinner import *
|
||||
from Furious.Window.QRCodeWindow import *
|
||||
from Furious.Window.TextEditorWindow import *
|
||||
from Furious.Qt.WaitingSpinner import *
|
||||
from Furious.Widget.QRCodeWindow import *
|
||||
from Furious.Widget.TextEditorWindow import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
@@ -1066,6 +1066,13 @@ class UserServersQTableViewHeaders:
|
||||
return self.name
|
||||
|
||||
|
||||
def _subscriptionRemark(item: ConfigFactory) -> str:
|
||||
"""Resolve a persisted subscription ID to its user-visible remark."""
|
||||
subscription = Storage.UserSubs().get(item.itemSubscription, {})
|
||||
|
||||
return subscription.get('remark', '')
|
||||
|
||||
|
||||
class UserServersTableModel(QtCore.QAbstractTableModel):
|
||||
"""Expose user servers table data through a Qt item model."""
|
||||
|
||||
@@ -1404,7 +1411,7 @@ class UserServersQTableView(
|
||||
UserServersQTableViewHeaders('Port'),
|
||||
UserServersQTableViewHeaders('Transport'),
|
||||
UserServersQTableViewHeaders('TLS'),
|
||||
UserServersQTableViewHeaders('Subscription'),
|
||||
UserServersQTableViewHeaders('Subscription', _subscriptionRemark),
|
||||
UserServersQTableViewHeaders('Latency'),
|
||||
UserServersQTableViewHeaders('Speed'),
|
||||
]
|
||||
|
||||
+19
-10
@@ -19,13 +19,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .Application import *
|
||||
from .ConnectProgressBar import *
|
||||
from .GuiCustomizeNetworkTest import *
|
||||
from .GuiCustomizeProxyBypass import *
|
||||
from .GuiTUNSettings import *
|
||||
from .IndentSpinBox import *
|
||||
from .SystemTrayIcon import *
|
||||
from .UserServersQTableView import *
|
||||
from .UserSubsQTableView import *
|
||||
from .WaitingSpinner import *
|
||||
from .GuiCustomizeNetworkTest import GuiCustomizeNetworkTestDialog
|
||||
from .GuiCustomizeProxyBypass import GuiCustomizeProxyBypassDialog
|
||||
from .GuiTUNSettings import GuiTUNSettings
|
||||
from .IndentSpinBox import IndentSpinBox
|
||||
from .QRCodeWindow import QRCodeWindow
|
||||
from .TextEditorWindow import TextEditorWindow
|
||||
from .UserServersQTableView import UserServersQTableView
|
||||
from .UserSubsQTableView import UserSubsQTableView
|
||||
|
||||
__all__ = [
|
||||
'GuiCustomizeNetworkTestDialog',
|
||||
'GuiCustomizeProxyBypassDialog',
|
||||
'GuiTUNSettings',
|
||||
'IndentSpinBox',
|
||||
'QRCodeWindow',
|
||||
'TextEditorWindow',
|
||||
'UserServersQTableView',
|
||||
'UserSubsQTableView',
|
||||
]
|
||||
|
||||
@@ -45,6 +45,21 @@ __all__ = ['AppMainWindow']
|
||||
|
||||
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 main window is built before the system tray is attached.
|
||||
return False
|
||||
|
||||
|
||||
# Migrate legacy settings
|
||||
registerAppSettings('ServerWidgetWindowSize')
|
||||
registerAppSettings('AppMainWindowGeometry')
|
||||
@@ -440,6 +455,7 @@ class AppMainWindow(AppQMainWindow):
|
||||
managementActions = pluginRegistry.managementActions(
|
||||
plugin,
|
||||
parent=self,
|
||||
isCoreActive=_isCoreActive,
|
||||
)
|
||||
if managementActions:
|
||||
corePluginActions.append(
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .AppMainWindow import *
|
||||
from .LogViewerWindow import *
|
||||
from .QRCodeWindow import *
|
||||
from .TextEditorWindow import *
|
||||
from .UserSubsWindow import *
|
||||
from .AppMainWindow import AppMainWindow
|
||||
from .LogViewerWindow import LogViewerWindow
|
||||
from .UserSubsWindow import UserSubsWindow
|
||||
|
||||
__all__ = ['AppMainWindow', 'LogViewerWindow', 'UserSubsWindow']
|
||||
|
||||
@@ -20,3 +20,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .Frozenlib import AppResources
|
||||
|
||||
__all__ = ['AppResources']
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ from Furious.Interface import *
|
||||
from Furious.Qt import *
|
||||
from Furious.Qt import gettext as _
|
||||
from Furious.Utility import *
|
||||
from Furious.Widget.Application import Application
|
||||
from Furious.Application import Application
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QDesktopServices
|
||||
|
||||
Reference in New Issue
Block a user