Add docstrings across Python modules

Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Loren Eteval
2026-08-12 12:20:13 +08:00
parent 8b94b273b2
commit 67f3c2841c
98 changed files with 2099 additions and 0 deletions
+24
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Coordinate proxy cores, TUN mode, DNS, and platform routing."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -41,6 +43,7 @@ logger = logging.getLogger(__name__)
def fixLogObjectPath(config: ConfigFactory, attr: str, value: str, log=True):
"""Resolve and normalize one log-file path in a core configuration."""
try:
path = config['log'][attr]
except Exception:
@@ -84,6 +87,7 @@ def fixLogObjectPath(config: ConfigFactory, attr: str, value: str, log=True):
def cleanRoutingRule(rule: dict):
"""Remove empty match fields from an Xray routing rule."""
if not isinstance(rule, dict):
return None
@@ -131,6 +135,7 @@ def cleanRoutingRule(rule: dict):
def customRoutingObjectFromSettings(routing: str):
"""Load a named custom routing object from persistent settings."""
prefix = 'Custom:'
if not isinstance(routing, str) or not routing.startswith(prefix):
@@ -165,6 +170,7 @@ def customRoutingObjectFromSettings(routing: str):
def routingObjectHasDirectRule(routingObject: dict) -> bool:
"""Return whether an Xray routing object contains a direct rule."""
try:
return any(
rule.get('outboundTag') == 'direct'
@@ -178,6 +184,7 @@ def routingObjectHasDirectRule(routingObject: dict) -> bool:
def getUserTUNSettings(*args, **kwargs):
"""Return one user-defined TUN setting or its fallback value."""
return Storage.UserTUNSettings().get(*args, **kwargs)
@@ -205,7 +212,9 @@ def getUserTUNSettings(*args, **kwargs):
class CoreManager(Mixins.CleanupOnExit):
"""Coordinate proxy cores, TUN setup, DNS changes, and routing cleanup."""
def __init__(self, *args, **kwargs):
"""Initialize the CoreManager."""
super().__init__(*args, **kwargs)
self.uniqueCleanup = False
@@ -222,6 +231,7 @@ class CoreManager(Mixins.CleanupOnExit):
log=True,
**kwargs,
) -> Tuple[Union[CoreProcessWorker, None], bool]:
"""Return the start core value used by the core manager."""
return None, False
@_startCore.register(ConfigXray)
@@ -235,6 +245,7 @@ class CoreManager(Mixins.CleanupOnExit):
log=True,
**kwargs,
):
"""Handle the registered singledispatch variant."""
if config.get('log') is None or not isinstance(config['log'], dict):
config['log'] = {
'access': '',
@@ -332,6 +343,7 @@ class CoreManager(Mixins.CleanupOnExit):
log=True,
**kwargs,
):
"""Handle the registered singledispatch variant."""
if routing == AppBuiltinRouting.BypassMainlandChina.value:
# TUN Mode handling
if not proxyModeOnly and SystemRuntime.isTUNMode():
@@ -385,6 +397,7 @@ class CoreManager(Mixins.CleanupOnExit):
log=True,
**kwargs,
):
"""Handle the registered singledispatch variant."""
if log:
logger.info(f'core {Hysteria2.name()} configured')
@@ -395,6 +408,7 @@ class CoreManager(Mixins.CleanupOnExit):
@staticmethod
def waitForTUNDeviceBroughtUp(func: Callable[[str], bool], deviceName: str) -> bool:
"""Wait for the platform to expose the configured TUN device."""
for counter in range(0, 10000, 100):
if func(deviceName):
logger.info(
@@ -421,12 +435,14 @@ class CoreManager(Mixins.CleanupOnExit):
log=True,
**kwargs,
) -> bool:
"""Start the selected proxy core and configure TUN mode when requested."""
if deepcopy:
configcopy = config.deepcopy()
else:
configcopy = config
def abortStart(message: str = ''):
"""Return the abort start value used by the core manager."""
if message:
logger.error(message)
@@ -639,6 +655,7 @@ class CoreManager(Mixins.CleanupOnExit):
if alias:
def _windowsCleanup(_alias):
"""Handle windows cleanup for the core manager."""
SystemRoutingTable.WIN32SetInterfaceDNS(_alias)
SystemRoutingTable.WIN32FlushDNSCache()
@@ -693,6 +710,7 @@ class CoreManager(Mixins.CleanupOnExit):
servers = SystemRoutingTable.DarwinGetDNSServers()
def _darwinCleanup(_servers):
"""Handle darwin cleanup for the core manager."""
for _service, _dnsserver in _servers:
SystemRoutingTable.DarwinSetDNSServers(_service, _dnsserver)
@@ -726,6 +744,7 @@ class CoreManager(Mixins.CleanupOnExit):
if PLATFORM == 'Linux':
def _linuxCleanup():
"""Handle linux cleanup for the core manager."""
SystemRoutingTable.LinuxDeleteTUNDevice(APPLICATION_TUN_DEVICE_NAME)
tun.cleanup = functools.partial(_linuxCleanup)
@@ -754,6 +773,7 @@ class CoreManager(Mixins.CleanupOnExit):
)
def route(source, destination) -> str:
"""Configure platform routing for the active proxy connection."""
return f'{source} via {destination} dev {interface}'
iproute = SystemRoutingTable.LinuxGetIpRoute()
@@ -802,12 +822,15 @@ class CoreManager(Mixins.CleanupOnExit):
return True
def allRunning(self) -> bool:
"""Return whether every managed process is running."""
return all(process.isAlive() for process in self.processesPool)
def anyRunning(self) -> bool:
"""Return whether any managed process is running."""
return any(process.isAlive() for process in self.processesPool)
def stopAll(self):
"""Stop every managed proxy and TUN process."""
try:
for process in list(self.processesPool):
if not isinstance(process, CoreProcessFactory):
@@ -821,4 +844,5 @@ class CoreManager(Mixins.CleanupOnExit):
self.processesPool.clear()
def cleanup(self):
"""Release resources owned by the core manager."""
self.stopAll()
+38
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Manage proxy-core child processes and their output queues."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -49,6 +51,7 @@ logger = logging.getLogger(__name__)
class CoreProcessState(Enum):
"""Enumerate core process state."""
Idle = 'idle'
Starting = 'starting'
Running = 'running'
@@ -59,6 +62,7 @@ class CoreProcessState(Enum):
@dataclass
class CoreLaunchSpec:
"""Describe the parameters required by a core launch operation."""
target: Callable
args: Tuple[Any, ...] = field(default_factory=tuple)
processKwargs: Dict[str, Any] = field(default_factory=dict)
@@ -67,6 +71,7 @@ class CoreLaunchSpec:
waitTime: int = 2500
def __post_init__(self):
"""Normalize the initialized core launch spec values."""
self.args = tuple(self.args)
self.processKwargs = dict(self.processKwargs)
@@ -78,6 +83,7 @@ class CoreLaunchSpec:
@classmethod
def fromProcessKwargs(cls, **kwargs):
"""Build a launch specification from multiprocessing keyword arguments."""
daemon, waitCore, waitTime, target, args = (
kwargs.pop('daemon', True),
kwargs.pop('waitCore', True),
@@ -96,6 +102,7 @@ class CoreLaunchSpec:
)
def toProcessKwargs(self):
"""Convert the launch specification to multiprocessing keyword arguments."""
kwargs = dict(self.processKwargs)
kwargs.update(
{
@@ -108,11 +115,13 @@ class CoreLaunchSpec:
class MsgQueue(multiprocessing.queues.Queue):
"""Deliver child-process log messages to Qt callbacks at an adaptive rate."""
MSG_PRODUCE_THRESHOLD = 1024
OPTIMIZER_MIN_FREQ = 2
OPTIMIZER_MAX_FREQ = 256
def __init__(self, **kwargs):
"""Initialize the MsgQueue."""
msgCallback = kwargs.pop('msgCallback', None)
backgroundOptimizer = kwargs.pop('backgroundOptimizer', None)
@@ -125,6 +134,7 @@ class MsgQueue(multiprocessing.queues.Queue):
self.backgroundOptimizer = backgroundOptimizer
def getNoWait(self) -> str:
"""Return no wait."""
try:
return self.get_nowait()
except Exception:
@@ -133,19 +143,24 @@ class MsgQueue(multiprocessing.queues.Queue):
return ''
def getTimeout(self) -> int:
"""Return timeout."""
return self.timeout
def setTimeout(self, timeout: int):
"""Set timeout."""
self.timeout = timeout
def startTimer(self):
"""Start timer."""
self.timer.start(self.getTimeout())
def stopTimer(self):
"""Stop timer."""
self.timer.stop()
@property
def optimizer(self):
"""Return the optimizer value."""
try:
return self.backgroundOptimizer()
except Exception:
@@ -154,6 +169,7 @@ class MsgQueue(multiprocessing.queues.Queue):
return None
def processMsg(self):
"""Process msg."""
msg = self.getNoWait()
if not callable(self.callback):
@@ -190,9 +206,11 @@ class MsgQueue(multiprocessing.queues.Queue):
class CoreProcessMonitor(CoreProcessFactory, ABC):
"""Track the state and lifetime of a proxy-core child process."""
StopJoinTimeout = 3
def __init__(self, **kwargs):
"""Initialize the CoreProcessMonitor."""
exitCallback = kwargs.pop('exitCallback', None)
super().__init__(exitCallback)
@@ -206,37 +224,46 @@ class CoreProcessMonitor(CoreProcessFactory, ABC):
@property
def process(self) -> Union[multiprocessing.Process, None]:
"""Return the process value."""
return self._process
@process.setter
def process(self, process: Union[multiprocessing.Process, None]):
"""Set the process value."""
self._process = process
@property
def daemon(self) -> QtCore.QTimer:
"""Return the daemon value."""
return self._daemon
@property
def state(self) -> CoreProcessState:
"""Return the state value."""
return self._state
def setState(self, state: CoreProcessState):
"""Set state."""
self._state = state
@property
def lastExitCode(self):
"""Return the last exit code value."""
return self._lastExitCode
def setLastExitCode(self, exitCode):
"""Set last exit code."""
self._lastExitCode = exitCode
def isAlive(self) -> bool:
"""Return whether alive."""
if isinstance(self.process, multiprocessing.Process):
return self.process.is_alive()
else:
return False
def queryIsAlive(self):
"""Query is alive."""
if isinstance(self.process, multiprocessing.Process):
if self.process.is_alive():
return True
@@ -248,9 +275,11 @@ class CoreProcessMonitor(CoreProcessFactory, ABC):
return False
def handleInternalProcessStopped(self, *args, **kwargs):
"""Handle internal process stopped."""
raise NotImplementedError
def closeProcess(self):
"""Close process."""
if isinstance(self.process, multiprocessing.Process):
try:
self.process.close()
@@ -262,7 +291,9 @@ class CoreProcessMonitor(CoreProcessFactory, ABC):
class CoreProcessWorker(CoreProcessMonitor, ABC):
"""Run and monitor a proxy core in a child process."""
def __init__(self, **kwargs):
"""Initialize the CoreProcessWorker."""
msgCallback = kwargs.pop('msgCallback', None)
# Optimizer is AppLoggerWindow.Core window
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLoggerWindow.Core)
@@ -274,6 +305,7 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
)
def handleInternalProcessStopped(self):
"""Handle internal process stopped."""
exitcode = self.process.exitcode
logger.error(f'{self.name()} stopped unexpectedly with exitcode {exitcode}')
@@ -289,9 +321,11 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
self.closeProcess()
def start(self, **kwargs) -> bool:
"""Start the core process worker."""
return self.startWithSpec(CoreLaunchSpec.fromProcessKwargs(**kwargs))
def startWithSpec(self, launchSpec: CoreLaunchSpec) -> bool:
"""Start and validate a child process from a launch specification."""
if not isinstance(launchSpec, CoreLaunchSpec):
logger.error(f'invalid launch spec for {self.name()}: {launchSpec}')
@@ -348,6 +382,7 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
return False
def stop(self):
"""Stop the core process worker."""
self.msgQueue.stopTimer()
self.daemon.stop()
@@ -379,12 +414,14 @@ class CoreProcessWorker(CoreProcessMonitor, ABC):
class ProcessOutputRedirector:
"""Redirect child-process output into the application message queue."""
TemporaryDir = QtCore.QTemporaryDir()
@staticmethod
def launch(
msgQueue: multiprocessing.Queue, entrypoint: Callable[[], None], redirect: bool
):
"""Run an entry point while forwarding its output to a message queue."""
if not callable(entrypoint):
return
@@ -415,6 +452,7 @@ class ProcessOutputRedirector:
sys.stderr = tmpFileStream
def produceMsg():
"""Forward one process-output message to the shared queue."""
with open(temporaryFile, 'rb') as file:
while True:
for line in iter(file.readline, b''):
+13
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Wrap the embedded Hysteria 1 core process."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -34,6 +36,7 @@ __all__ = ['Hysteria1']
def startHysteria1(jsonString, rule, mmdb, msgQueue: multiprocessing.Queue):
"""Start hysteria1."""
try:
import hysteria
except ImportError:
@@ -54,17 +57,21 @@ def startHysteria1(jsonString, rule, mmdb, msgQueue: multiprocessing.Queue):
class Hysteria1(CoreProcessWorker):
"""Manage the embedded Hysteria 1 core subprocess."""
class ExitCode(Enum):
"""Enumerate process exit codes."""
ConfigurationError = 23
RemoteNetworkError = 3
# Windows shutting down
SystemShuttingDown = 0x40010004
def __init__(self, **kwargs):
"""Initialize the Hysteria1."""
super().__init__(**kwargs)
@staticmethod
def loadOptionalFile(pathLike, fileType: str):
"""Load optional file."""
if isinstance(pathLike, str) and pathLike == '':
return ''
@@ -96,18 +103,22 @@ class Hysteria1(CoreProcessWorker):
@staticmethod
def rule(rulePath):
"""Return the rule value."""
return Hysteria1.loadOptionalFile(rulePath, 'rule')
@staticmethod
def mmdb(mmdbPath):
"""Return the mmdb value."""
return Hysteria1.loadOptionalFile(mmdbPath, 'mmdb')
@staticmethod
def name() -> str:
"""Return the process implementation name."""
return 'Hysteria1'
@staticmethod
def version() -> str:
"""Return the bundled core version."""
try:
import hysteria
@@ -120,6 +131,7 @@ class Hysteria1(CoreProcessWorker):
def launchSpec(
self, config: Union[str, dict], rule, mmdb, **kwargs
) -> Union[CoreLaunchSpec, None]:
"""Build the child-process launch specification."""
param = self.toJSONString(config)
if not param:
@@ -137,6 +149,7 @@ class Hysteria1(CoreProcessWorker):
)
def start(self, config: Union[str, dict], rule, mmdb, **kwargs) -> bool:
"""Start the hysteria1."""
launchSpec = self.launchSpec(config, rule, mmdb, **kwargs)
if launchSpec is None:
+10
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Wrap the embedded Hysteria 2 core process."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -34,6 +36,7 @@ __all__ = ['Hysteria2']
def startHysteria2(jsonString: str, msgQueue: multiprocessing.Queue):
"""Start hysteria2."""
try:
import hysteria2
except ImportError:
@@ -54,7 +57,9 @@ def startHysteria2(jsonString: str, msgQueue: multiprocessing.Queue):
class Hysteria2(CoreProcessWorker):
"""Manage the embedded Hysteria 2 core subprocess."""
class ExitCode(Enum):
"""Enumerate process exit codes."""
ConfigurationError = 23
# Windows: 4294967295. Darwin, Linux: 255 (-1)
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
@@ -62,14 +67,17 @@ class Hysteria2(CoreProcessWorker):
SystemShuttingDown = 0x40010004
def __init__(self, **kwargs):
"""Initialize the Hysteria2."""
super().__init__(**kwargs)
@staticmethod
def name() -> str:
"""Return the process implementation name."""
return 'Hysteria2'
@staticmethod
def version() -> str:
"""Return the bundled core version."""
try:
import hysteria2
@@ -82,6 +90,7 @@ class Hysteria2(CoreProcessWorker):
def launchSpec(
self, config: Union[str, dict], **kwargs
) -> Union[CoreLaunchSpec, None]:
"""Build the child-process launch specification."""
param = self.toJSONString(config)
if not param:
@@ -97,6 +106,7 @@ class Hysteria2(CoreProcessWorker):
)
def start(self, config: Union[str, dict], **kwargs) -> bool:
"""Start the hysteria2."""
launchSpec = self.launchSpec(config, **kwargs)
if launchSpec is None:
+11
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Wrap the embedded tun2socks process used by TUN mode."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -28,6 +30,7 @@ __all__ = ['Tun2socks']
def startTun2socks(msgQueue: multiprocessing.Queue, *args):
"""Start tun2socks."""
try:
import tun2socks
except ImportError:
@@ -48,12 +51,15 @@ def startTun2socks(msgQueue: multiprocessing.Queue, *args):
class Tun2socks(CoreProcessWorker):
"""Manage the tun2socks subprocess used by TUN mode."""
class ExitCode:
# Windows shutting down
"""Enumerate process exit codes."""
SystemShuttingDown = 0x40010004
def __init__(self, **kwargs):
# Optimizer is AppLoggerWindow.TUN_ window
"""Initialize the Tun2socks."""
backgroundOptimizer = kwargs.pop('backgroundOptimizer', AppLoggerWindow.TUN_)
super().__init__(**kwargs, backgroundOptimizer=backgroundOptimizer)
@@ -62,10 +68,12 @@ class Tun2socks(CoreProcessWorker):
@staticmethod
def name() -> str:
"""Return the process implementation name."""
return 'Tun2socks'
@staticmethod
def version() -> str:
"""Return the bundled core version."""
try:
import tun2socks
@@ -87,6 +95,7 @@ class Tun2socks(CoreProcessWorker):
tcpAutoTuning: bool = False,
**kwargs,
) -> CoreLaunchSpec:
"""Build the child-process launch specification."""
return CoreLaunchSpec(
target=startTun2socks,
args=(
@@ -115,6 +124,7 @@ class Tun2socks(CoreProcessWorker):
tcpAutoTuning: bool = False,
**kwargs,
) -> bool:
"""Start the tun2socks."""
launchSpec = self.launchSpec(
device,
networkInterface,
@@ -130,6 +140,7 @@ class Tun2socks(CoreProcessWorker):
return self.startWithSpec(launchSpec)
def stop(self):
"""Stop the tun2socks."""
if PLATFORM == 'Linux':
# Stop tunnel first
super().stop()
+11
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Wrap the embedded Xray core process."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,6 +37,7 @@ __all__ = ['XrayCore']
def startXrayCore(jsonString: str, msgQueue: multiprocessing.Queue):
"""Start Xray core."""
try:
import xray
except ImportError:
@@ -91,6 +94,7 @@ def startXrayCore(jsonString: str, msgQueue: multiprocessing.Queue):
fileStreams.append(stream)
def produceMsg():
"""Forward one process-output message to the shared queue."""
while True:
for file in fileStreams:
for line in iter(file.readline, b''):
@@ -116,7 +120,9 @@ def startXrayCore(jsonString: str, msgQueue: multiprocessing.Queue):
class XrayCore(CoreProcessWorker):
"""Manage the embedded Xray core subprocess."""
class ExitCode(Enum):
"""Enumerate process exit codes."""
ConfigurationError = 23
# Windows: 4294967295. Darwin, Linux: 255 (-1)
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
@@ -124,14 +130,17 @@ class XrayCore(CoreProcessWorker):
SystemShuttingDown = 0x40010004
def __init__(self, **kwargs):
"""Initialize the XrayCore."""
super().__init__(**kwargs)
@staticmethod
def name() -> str:
"""Return the process implementation name."""
return 'Xray-core'
@staticmethod
def version() -> str:
"""Return the bundled core version."""
try:
import xray
@@ -144,6 +153,7 @@ class XrayCore(CoreProcessWorker):
def launchSpec(
self, config: Union[str, ConfigFactory, dict], **kwargs
) -> Union[CoreLaunchSpec, None]:
"""Build the child-process launch specification."""
param = self.toJSONString(config)
if not param:
@@ -159,6 +169,7 @@ class XrayCore(CoreProcessWorker):
)
def start(self, config: Union[str, ConfigFactory, dict], **kwargs) -> bool:
"""Start the Xray core."""
launchSpec = self.launchSpec(config, **kwargs)
if launchSpec is None:
+2
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from .XrayCore import *
+2
View File
@@ -1,3 +1,5 @@
"""Define source strings and translations used by the localization workflow."""
TRANSLATION = {
"Delete": {
"source": [
+2
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Initialize helpers for generated external data."""
from __future__ import annotations
from .GenTranslation import *
+4
View File
@@ -3,6 +3,8 @@
# Created by: The Resource Compiler for Qt version 6.8.3
# WARNING! All changes made in this file will be lost!
"""Provide Qt resources generated from the application resource collection."""
from PySide6 import QtCore
qt_resource_data = b"\
@@ -179543,9 +179545,11 @@ qt_resource_struct = b"\
"
def qInitResources():
"""Register the generated resources with Qt."""
QtCore.qRegisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
def qCleanupResources():
"""Unregister the generated resources from Qt."""
QtCore.qUnregisterResourceData(0x03, qt_resource_struct, qt_resource_name, qt_resource_data)
qInitResources()
+13
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Register, validate, read, and write persistent application settings."""
from __future__ import annotations
from PySide6 import QtCore
@@ -31,6 +33,7 @@ logger = logging.getLogger(__name__)
class AppBinarySettings:
"""Store and validate app binary settings."""
OFF = '0'
ON_ = '1'
@@ -38,6 +41,7 @@ class AppBinarySettings:
class AppSettings:
"""Store and validate app settings."""
SettingsPool: dict[str, AppSettings] = dict()
def __init__(
@@ -47,6 +51,7 @@ class AppSettings:
validRange: list = None,
default=None,
):
"""Initialize the AppSettings."""
self.name = name
self.isBinary = isBinary
@@ -65,6 +70,7 @@ class AppSettings:
raise ValueError(f'\'{name}\' already exists in AppSettings')
def validate(self, value) -> bool:
"""Validate the app settings."""
if self.validRange is None:
return True
else:
@@ -72,6 +78,7 @@ class AppSettings:
@staticmethod
def get(key: str):
"""Return data managed by the app settings."""
settings = AppSettings.SettingsPool.get(key)
if settings is None:
@@ -110,6 +117,7 @@ class AppSettings:
@staticmethod
def isStateON_(key: str) -> bool:
"""Return whether state on."""
value = AppSettings.get(key)
if value == AppBinarySettings.ON_:
@@ -119,6 +127,7 @@ class AppSettings:
@staticmethod
def isStateOFF(key: str) -> bool:
"""Return whether state off."""
value = AppSettings.get(key)
if value == AppBinarySettings.OFF:
@@ -128,6 +137,7 @@ class AppSettings:
@staticmethod
def set(key: str, value):
"""Set data managed by the app settings."""
settings = AppSettings.SettingsPool.get(key)
if settings is None:
@@ -143,12 +153,15 @@ class AppSettings:
@staticmethod
def turnON_(key: str):
"""Handle turn on for the app settings."""
AppSettings.set(key, AppBinarySettings.ON_)
@staticmethod
def turnOFF(key: str):
"""Handle turn off for the app settings."""
AppSettings.set(key, AppBinarySettings.OFF)
def registerAppSettings(name: str, *args, **kwargs):
"""Handle register app settings for the application."""
AppSettings.SettingsPool[name] = AppSettings(name, *args, **kwargs)
+2
View File
@@ -15,6 +15,8 @@
# 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 application, platform, network, and filesystem constants."""
from __future__ import annotations
from Furious.Version import __version__
+5
View File
@@ -15,6 +15,8 @@
# 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 enum."""
from __future__ import annotations
from enum import Enum
@@ -27,17 +29,20 @@ __all__ = [
class AppBuiltinCommand(Enum):
"""Enumerate app builtin command."""
Empty = 'empty'
RunAs = 'runas'
Clear = 'clear'
class AppBuiltinRouting(Enum):
"""Enumerate app builtin routing."""
BypassMainlandChina = 'Bypass Mainland China'
Global = 'Global'
Custom = 'Custom'
class AppBuiltinProxyMode(Enum):
"""Enumerate app builtin proxy mode."""
Auto = 'Auto'
NoChanges = 'NoChanges'
+4
View File
@@ -15,6 +15,8 @@
# 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 globals."""
from __future__ import annotations
from PySide6.QtWidgets import QApplication
@@ -32,6 +34,7 @@ APP = functools.partial(QApplication.instance)
def getAppAttributes(name: str):
"""Return app attributes."""
return getattr(APP(), name)
@@ -45,6 +48,7 @@ def getAppAttributes(name: str):
class AppLoggerWindow:
"""Present the app logger window."""
Self = functools.partial(getAppAttributes, 'logViewerWindowSelf')
Core = functools.partial(getAppAttributes, 'logViewerWindowCore')
TUN_ = functools.partial(getAppAttributes, 'logViewerWindowTun_')
+33
View File
@@ -15,6 +15,8 @@
# 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 mixins."""
from __future__ import annotations
from PySide6 import QtCore
@@ -31,8 +33,10 @@ __all__ = ['Mixins']
class Mixins:
"""Group reusable lifecycle, translation, theme, and Qt context mixins."""
@staticmethod
def qObjectIsValid(qobject) -> bool:
"""Return the q object is valid value used by the mixins."""
if not isinstance(qobject, QtCore.QObject):
return True
@@ -42,21 +46,26 @@ class Mixins:
return False
class ConnectionAware:
"""Represent connection aware."""
ObjectsPool = list()
def __init__(self, *args, **kwargs):
"""Initialize the ConnectionAware."""
super().__init__(*args, **kwargs)
Mixins.ConnectionAware.ObjectsPool.append(self)
def disconnectedCallback(self):
"""Update the connection aware for a disconnected state."""
raise NotImplementedError
def connectedCallback(self):
"""Update the connection aware for a connected state."""
raise NotImplementedError
@staticmethod
def callConnectedCallback():
"""Call connected callback."""
for ob in Mixins.ConnectionAware.ObjectsPool:
assert isinstance(ob, Mixins.ConnectionAware)
@@ -64,24 +73,29 @@ class Mixins:
@staticmethod
def callDisconnectedCallback():
"""Call disconnected callback."""
for ob in Mixins.ConnectionAware.ObjectsPool:
assert isinstance(ob, Mixins.ConnectionAware)
ob.disconnectedCallback()
class ThemeAware:
"""Represent theme aware."""
ObjectsPool = list()
def __init__(self, *args, **kwargs):
"""Initialize the ThemeAware."""
super().__init__(*args, **kwargs)
Mixins.ThemeAware.ObjectsPool.append(self)
def themeChangedCallback(self, theme: str):
"""Update the theme aware for a theme change."""
raise NotImplementedError
@staticmethod
def callThemeChangedCallbackUnchecked(theme: str):
"""Call theme changed callback unchecked."""
for ob in Mixins.ThemeAware.ObjectsPool:
assert isinstance(ob, Mixins.ThemeAware)
@@ -89,6 +103,7 @@ class Mixins:
@staticmethod
def callThemeChangedCallback(theme: str):
"""Call theme changed callback."""
try:
app = QApplication.instance()
@@ -107,10 +122,12 @@ class Mixins:
Mixins.ThemeAware.callThemeChangedCallbackUnchecked(theme)
class CleanupOnExit:
"""Represent cleanup on exit."""
ObjectsPool = list()
VisitedType = dict()
def __init__(self, *args, **kwargs):
"""Initialize the CleanupOnExit."""
self.uniqueCleanup = kwargs.pop('uniqueCleanup', True)
super().__init__(*args, **kwargs)
@@ -118,10 +135,12 @@ class Mixins:
Mixins.CleanupOnExit.ObjectsPool.append(self)
def cleanup(self):
"""Release resources owned by the cleanup on exit."""
raise NotImplementedError
@staticmethod
def cleanupAll():
"""Handle cleanup all for the cleanup on exit."""
for ob in Mixins.CleanupOnExit.ObjectsPool:
assert isinstance(ob, Mixins.CleanupOnExit)
@@ -141,41 +160,51 @@ class Mixins:
Mixins.CleanupOnExit.VisitedType.clear()
class QSetDisabledContext:
"""Manage the q set disabled context."""
def __init__(self, qobject: QtCore.QObject):
"""Initialize the QSetDisabledContext."""
self.qobject = qobject
def __enter__(self):
"""Enter the q set disabled context context."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'setDisabled'
):
self.qobject.setDisabled(True)
def __exit__(self, exceptionType, exceptionValue, tb):
"""Exit the q set disabled context context and restore state."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'setDisabled'
):
self.qobject.setDisabled(False)
class QBlockSignalContext:
"""Manage the q block signal context."""
def __init__(self, qobject: QtCore.QObject):
"""Initialize the QBlockSignalContext."""
self.qobject = qobject
def __enter__(self):
"""Enter the q block signal context context."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'blockSignals'
):
self.qobject.blockSignals(True)
def __exit__(self, exceptionType, exceptionValue, tb):
"""Exit the q block signal context context and restore state."""
if Mixins.qObjectIsValid(self.qobject) and hasattr(
self.qobject, 'blockSignals'
):
self.qobject.blockSignals(False)
class QTranslatable:
"""Represent q translatable."""
ObjectsPool = list()
def __init__(self, *args, **kwargs):
"""Initialize the QTranslatable."""
self.translatable = kwargs.pop('translatable', True)
self.useQSetDisabled = kwargs.pop('useQSetDisabled', True)
@@ -192,10 +221,12 @@ class Mixins:
)
def retranslate(self):
"""Refresh translated text for the q translatable."""
raise NotImplementedError
@staticmethod
def unregister(ob):
"""Handle unregister for the q translatable."""
try:
Mixins.QTranslatable.ObjectsPool.remove(ob)
except ValueError:
@@ -203,12 +234,14 @@ class Mixins:
@staticmethod
def pruneObjectsPool():
"""Handle prune objects pool for the q translatable."""
Mixins.QTranslatable.ObjectsPool = list(
filter(Mixins.qObjectIsValid, Mixins.QTranslatable.ObjectsPool)
)
@staticmethod
def retranslateAll():
"""Handle retranslate all for the q translatable."""
Mixins.QTranslatable.pruneObjectsPool()
for ob in list(Mixins.QTranslatable.ObjectsPool):
+5
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -27,9 +29,11 @@ __all__ = ['PySide6Legacy']
class PySide6Legacy:
"""Represent py side6 legacy."""
@staticmethod
def enumValueWrapper(enum) -> int:
# Protect PySide6 enum wrapper behavior changes
"""Return the enum value wrapper value used by the py side6 legacy."""
if versionToValue(PYSIDE6_VERSION) < versionToValue('6.2.2'):
return enum
else:
@@ -39,6 +43,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."""
if versionToValue(PYSIDE6_VERSION) < versionToValue('6.3.1'):
for counter in range(0, ms, 10):
time.sleep(10 / 1000)
+7
View File
@@ -15,6 +15,8 @@
# 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 startup on boot."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -33,9 +35,12 @@ logger = logging.getLogger(__name__)
class StartupOnBoot:
"""Represent startup on boot."""
@staticmethod
def on_():
"""Enable the startup on boot."""
def _on_():
"""Return the on value used by the startup on boot."""
if SystemRuntime.isScriptMode():
# Script mode
logger.info('ignore turn on StartupOnBoot in script mode')
@@ -143,7 +148,9 @@ class StartupOnBoot:
@staticmethod
def off():
"""Disable the startup on boot."""
def _off():
"""Return the off value used by the startup on boot."""
if PLATFORM == 'Windows':
settings = QtCore.QSettings(
'HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
+18
View File
@@ -15,6 +15,8 @@
# 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 system proxy."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -32,6 +34,7 @@ logger = logging.getLogger(__name__)
def handleAppSystemProxyMode() -> bool:
"""Handle app system proxy mode."""
try:
if AppSettings.get('SystemProxyMode') == AppBuiltinProxyMode.Auto.value:
# Automatically configure
@@ -47,6 +50,7 @@ def handleAppSystemProxyMode() -> bool:
def linuxProxyConfig(proxy_args, arg0, arg1):
"""Handle linux proxy config for the application."""
command = 'gsettings'
if SystemRuntime.flatpakID():
@@ -67,7 +71,9 @@ def linuxProxyConfig(proxy_args, arg0, arg1):
def darwinProxyConfig(operation, *args):
"""Return the darwin proxy config value used by the application."""
def getNetworkServices():
"""Return network services."""
command = runExternalCommand(
['networksetup', '-listallnetworkservices'],
stdout=subprocess.PIPE,
@@ -92,12 +98,16 @@ def darwinProxyConfig(operation, *args):
class _SystemProxy:
"""Represent system proxy."""
def __init__(self):
"""Initialize the _SystemProxy."""
self._daemonThread = None
@staticmethod
def pac(pac_url):
"""Configure the system proxy with a PAC URL."""
def _pac():
"""Return the pac value used by the system proxy."""
if PLATFORM == 'Windows':
try:
import sysproxy
@@ -141,7 +151,9 @@ class _SystemProxy:
@staticmethod
def set(server, bypass):
"""Set data managed by the system proxy."""
def _set():
"""Return the set value used by the system proxy."""
if PLATFORM == 'Windows':
try:
import sysproxy
@@ -198,7 +210,9 @@ class _SystemProxy:
@staticmethod
def off():
"""Disable the system proxy."""
def _off():
"""Return the off value used by the system proxy."""
if PLATFORM == 'Windows':
try:
import sysproxy
@@ -242,7 +256,9 @@ class _SystemProxy:
logger.error('turn off proxy failed')
def daemonOn_(self):
"""Return the daemon on value used by the system proxy."""
def _daemonOn_():
"""Return the daemon on value used by the system proxy."""
if PLATFORM == 'Windows':
try:
import sysproxy
@@ -273,7 +289,9 @@ class _SystemProxy:
logger.error('turn on proxy daemon failed')
def daemonOff(self):
"""Return the daemon off value used by the system proxy."""
def _daemonOff():
"""Return the daemon off value used by the system proxy."""
if PLATFORM == 'Windows':
try:
import sysproxy
+25
View File
@@ -15,6 +15,8 @@
# 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 system routing table."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -39,6 +41,7 @@ else:
def dictRepr(returncode, stdout, stderr):
"""Return the dict repr value used by the application."""
return {
'returncode': returncode,
'stdout': stdout.decode(SYSTEM_PREFERRED_ENCODING, 'replace').strip(),
@@ -47,6 +50,7 @@ def dictRepr(returncode, stdout, stderr):
class SystemRoutingTable:
"""Represent system routing table."""
Relations = list()
DEFAULT_GATEWAY_WIN32 = re.compile(
@@ -61,7 +65,9 @@ class SystemRoutingTable:
@staticmethod
def add(sourceIP, destinationIP):
"""Add the system routing table."""
def _add():
"""Return the add value used by the system routing table."""
if PLATFORM == 'Windows':
try:
result = runExternalCommand(
@@ -112,11 +118,13 @@ class SystemRoutingTable:
@staticmethod
def addRelations():
"""Add relations."""
for sourceIP, destinationIP in SystemRoutingTable.Relations:
SystemRoutingTable.add(sourceIP, destinationIP)
@staticmethod
def WIN32GetInterfaceAliasByIP(ipaddress) -> str:
"""Return the win32 get interface alias by ip value."""
assert PLATFORM == 'Windows'
try:
@@ -151,6 +159,7 @@ class SystemRoutingTable:
@staticmethod
def WIN32SetInterfaceDNS(name, address=None, dhcp=True):
"""Return the win32 set interface DNS value."""
assert PLATFORM == 'Windows'
try:
@@ -189,6 +198,7 @@ class SystemRoutingTable:
@staticmethod
def WIN32FlushDNSCache():
"""Return the win32 flush DNS cache value."""
assert PLATFORM == 'Windows'
try:
@@ -215,6 +225,7 @@ class SystemRoutingTable:
@staticmethod
def WIN32IpconfigFindContent(content: str) -> bool:
"""Return the win32 ipconfig find content value."""
assert PLATFORM == 'Windows'
try:
@@ -244,7 +255,9 @@ class SystemRoutingTable:
@staticmethod
def DarwinGetDNSServers() -> list:
"""Return the darwin get DNS servers value."""
def getNetworkServices():
"""Return network services."""
_command = runExternalCommand(
['networksetup', '-listallnetworkservices'],
stdout=subprocess.PIPE,
@@ -295,6 +308,7 @@ class SystemRoutingTable:
@staticmethod
def DarwinSetDNSServers(service: str, dnsserver: str):
"""Return the darwin set DNS servers value."""
assert PLATFORM == 'Darwin'
dnsserverRepr = [dnsserver]
@@ -335,6 +349,7 @@ class SystemRoutingTable:
@staticmethod
def LinuxFindTUNDevice(deviceName: str) -> bool:
"""Return the linux find TUN device value."""
assert PLATFORM == 'Linux'
command = 'ip tuntap show'
@@ -369,6 +384,7 @@ class SystemRoutingTable:
@staticmethod
def LinuxDeleteTUNDevice(deviceName: str) -> bool:
"""Return the linux delete TUN device value."""
assert PLATFORM == 'Linux'
command = 'ip tuntap del mode tun dev'
@@ -406,6 +422,7 @@ class SystemRoutingTable:
@staticmethod
def LinuxExecutePrivilegedScript(filepath, shell='bash') -> bool:
"""Return the linux execute privileged script value."""
assert PLATFORM == 'Linux'
command = 'pkexec'
@@ -443,6 +460,7 @@ class SystemRoutingTable:
@staticmethod
def LinuxGetIpRoute() -> str:
"""Return the linux get ip route value."""
assert PLATFORM == 'Linux'
command = 'ip route show'
@@ -477,7 +495,9 @@ class SystemRoutingTable:
@staticmethod
def getDefaultGateway() -> list:
"""Return default gateway."""
def _get():
"""Return the get value used by the system routing table."""
if PLATFORM == 'Windows':
# Note: On Windows interface IP is also captured
@@ -543,7 +563,9 @@ class SystemRoutingTable:
@staticmethod
def setDeviceGateway(deviceName, deviceIP, deviceGateway):
"""Set device gateway."""
def _set():
"""Return the set value used by the system routing table."""
if PLATFORM == 'Windows':
try:
result = runExternalCommand(
@@ -602,7 +624,9 @@ class SystemRoutingTable:
@staticmethod
def delete(sourceIP, destinationIP):
"""Delete the system routing table."""
def _delete():
"""Return the delete value used by the system routing table."""
if PLATFORM == 'Windows':
try:
result = runExternalCommand(
@@ -657,6 +681,7 @@ class SystemRoutingTable:
@staticmethod
def deleteRelations(clear=True):
"""Delete relations."""
if PLATFORM == 'Windows':
if len(SystemRoutingTable.Relations):
SystemRoutingTable.delete('0.0.0.0', APPLICATION_TUN_GATEWAY_ADDRESS)
+13
View File
@@ -15,6 +15,8 @@
# 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 system runtime."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -31,9 +33,11 @@ __all__ = ['SystemRuntime']
class SystemRuntime:
"""Represent system runtime."""
@staticmethod
@functools.lru_cache(None)
def ubuntuRelease() -> str:
"""Return the ubuntu release value used by the system runtime."""
try:
if PLATFORM != 'Linux':
return ''
@@ -64,6 +68,7 @@ class SystemRuntime:
@staticmethod
def appImagePath() -> str:
"""Return the app image path value used by the system runtime."""
if PLATFORM != 'Linux':
return ''
@@ -74,6 +79,7 @@ class SystemRuntime:
@staticmethod
def flatpakID() -> str:
"""Return the flatpak id value used by the system runtime."""
if PLATFORM != 'Linux':
return ''
@@ -84,11 +90,13 @@ class SystemRuntime:
@staticmethod
def isAssetsFolderWritable() -> bool:
"""Return whether assets folder writable."""
return os.access(XRAY_ASSET_DIR, os.W_OK)
@staticmethod
@functools.lru_cache(None)
def isAdmin() -> bool:
"""Return whether admin."""
if PLATFORM == 'Windows':
return ctypes.windll.shell32.IsUserAnAdmin() == 1
else:
@@ -96,6 +104,7 @@ class SystemRuntime:
@staticmethod
def isTUNMode() -> bool:
"""Return whether TUN mode."""
if PLATFORM == 'Linux':
if SystemRuntime.flatpakID():
# TUN Mode disabled in flatpak
@@ -108,12 +117,15 @@ class SystemRuntime:
@staticmethod
@functools.lru_cache(None)
def isScriptMode() -> bool:
"""Return whether script mode."""
return sys.argv[0].endswith('.py')
@staticmethod
@functools.lru_cache(None)
def isPythonw() -> bool:
"""Return whether pythonw."""
def isRealFile(file):
"""Return whether real file."""
if not hasattr(file, 'fileno'):
return False
@@ -134,4 +146,5 @@ class SystemRuntime:
@staticmethod
@functools.lru_cache(None)
def isWindows7() -> bool:
"""Return whether windows7."""
return PLATFORM == 'Windows' and PLATFORM_RELEASE == '7'
+3
View File
@@ -15,6 +15,8 @@
# 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 tcping."""
from __future__ import annotations
from typing import Tuple
@@ -34,6 +36,7 @@ def tcping(
count: int,
interval: float,
) -> Tuple[int, list]:
"""Return the tcping value used by the application."""
host = socket.gethostbyname(address)
addressobject = ipaddress.ip_address(host)
+17
View File
@@ -15,6 +15,8 @@
# 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 utility."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -47,6 +49,7 @@ __all__ = [
class Protocol(Enum):
"""Enumerate proxy protocols recognized by Furious."""
Unknown = 'Unknown'
VMess = 'VMess'
VLESS = 'VLESS'
@@ -59,6 +62,7 @@ class Protocol(Enum):
@staticmethod
@functools.lru_cache(None)
def toEnum(protocol: str):
"""Return the to enum value used by the protocol."""
if not isinstance(protocol, str):
return Protocol.Unknown
@@ -88,9 +92,11 @@ def callRateLimited(maxCallPerSecond):
called = time.monotonic()
def decorator(func):
"""Decorate a callable with the enclosing behavior."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Previously called
"""Invoke the wrapped callable with the enclosing behavior."""
nonlocal called
elapsed = time.monotonic() - called
@@ -110,9 +116,12 @@ def callRateLimited(maxCallPerSecond):
def forceToLocalhostIfPossible():
"""Return the force to localhost if possible value used by the application."""
def decorator(func):
"""Decorate a callable with the enclosing behavior."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Invoke the wrapped callable with the enclosing behavior."""
result = func(*args, **kwargs)
if (
@@ -146,6 +155,7 @@ def callOnceOnly(func):
called = False
def wrapper(*args, **kwargs):
"""Invoke the wrapped callable with the enclosing behavior."""
nonlocal result, called
if not called:
@@ -158,11 +168,13 @@ def callOnceOnly(func):
def classname(ob) -> str:
"""Return the classname value used by the application."""
return ob.__class__.__name__
@functools.lru_cache(None)
def isValidIPAddress(address) -> bool:
"""Return whether valid ip address."""
try:
ipaddress.ip_address(address)
except Exception:
@@ -176,6 +188,7 @@ def isValidIPAddress(address) -> bool:
# Can throw exceptions
@functools.lru_cache(None)
def parseHostPort(address: str) -> Tuple[AnyStr | None, str | None]:
"""Parse host port."""
if address.find('//') == -1:
result = urllib.parse.urlsplit('//' + address)
else:
@@ -195,6 +208,7 @@ def parseHostPort(address: str) -> Tuple[AnyStr | None, str | None]:
def runExternalCommand(*args, **kwargs):
"""Run external command."""
if PLATFORM == 'Windows':
creationflags = kwargs.pop('creationflags', subprocess.CREATE_NO_WINDOW)
@@ -205,13 +219,16 @@ def runExternalCommand(*args, **kwargs):
@functools.lru_cache(None)
def absolutePath(path) -> pathlib.Path:
"""Return the absolute path value used by the application."""
return pathlib.Path(path) if os.path.isabs(path) else ROOT_DIR / path
@functools.lru_cache(None)
def versionToValue(version: str) -> int:
"""Return the version to value value used by the application."""
def split():
# x or x.y or x.y.z or x.y.z.u
"""Split the application."""
result = version.split('.')
while len(result) < 4:
+7
View File
@@ -15,6 +15,8 @@
# 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 win32 session."""
from __future__ import annotations
from Furious.Frozenlib.Constants import *
@@ -30,11 +32,14 @@ logger = logging.getLogger(__name__)
class _Win32Session:
"""Represent win32 session."""
def __init__(self):
"""Initialize the _Win32Session."""
self._daemonThread = None
@staticmethod
def set(callback: Callable[[], None]) -> bool:
"""Set data managed by the win32 session."""
if PLATFORM == 'Windows':
import win32session
@@ -45,6 +50,7 @@ class _Win32Session:
return False
def off(self) -> bool:
"""Disable the win32 session."""
if PLATFORM == 'Windows':
import win32session
@@ -62,6 +68,7 @@ class _Win32Session:
return False
def run(self) -> bool:
"""Run the win32 session task."""
if PLATFORM == 'Windows':
import win32session
+2
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from .AppSettings import *
+6
View File
@@ -15,6 +15,8 @@
# 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 contract for the top-level application runner."""
from __future__ import annotations
from enum import Enum
@@ -23,14 +25,18 @@ __all__ = ['ApplicationFactory']
class ApplicationFactory:
"""Define the lifecycle contract for the top-level application runner."""
class ExitCode(Enum):
"""Enumerate process exit codes."""
ExitSuccess = 0
UnknownException = 61
PlatformNotSupported = 62
AssertionError = 63
def __init__(self, *args, **kwargs):
"""Initialize the ApplicationFactory."""
super().__init__(*args, **kwargs)
def run(self):
"""Run the application factory task."""
raise NotImplementedError
+21
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from Furious.Interface.UserServersTableItem import *
@@ -57,10 +59,12 @@ class ConfigFactory(UserServersTableItem, dict):
@functools.singledispatchmethod
def _init_dispatch(self, config):
"""Initialize the configuration from a supported input type."""
super().__init__()
@_init_dispatch.register(str)
def _(self, config):
"""Handle the registered singledispatch variant."""
try:
jsonObject = ujson.loads(config)
except Exception:
@@ -77,61 +81,75 @@ class ConfigFactory(UserServersTableItem, dict):
@_init_dispatch.register(dict)
def _(self, config):
"""Handle the registered singledispatch variant."""
super().__init__(**config)
def __getitem__(self, item: str):
"""Return an item from the config factory."""
if not isinstance(item, str):
raise TypeError(f'Bad type {type(item)} for __getitem__ call')
return super().__getitem__(item)
def __setitem__(self, item: str, value):
"""Set an item on the config factory."""
if not isinstance(item, str):
raise TypeError(f'Bad type {type(item)} for __setitem__ call')
return super().__setitem__(item, value)
def deepcopy(self) -> ConfigFactory:
"""Return an independent copy of the configuration."""
return copy.deepcopy(self)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Unknown'
def isValid(self) -> bool:
"""Return whether valid."""
return bool(self)
def getExtras(self, item):
"""Return non-core metadata associated with the configuration."""
return self.kwargs.get(item, '')
def setExtras(self, item, value):
"""Store non-core metadata associated with the configuration."""
self.kwargs[item] = value
@property
def index(self) -> int:
"""Return the index value."""
return self._index
@index.setter
def index(self, value: int):
"""Set the index value."""
assert isinstance(value, int)
self._index = value
@property
def deleted(self) -> bool:
"""Return the deleted value."""
return self._deleted
@deleted.setter
def deleted(self, value: bool):
"""Set the deleted value."""
assert isinstance(value, bool)
self._deleted = value
@property
def itemRemark(self) -> str:
"""Return the item remark value."""
return self.getExtras('remark')
@property
def itemSubscription(self) -> str:
"""Return the item subscription value."""
try:
app = QApplication.instance()
@@ -149,10 +167,12 @@ class ConfigFactory(UserServersTableItem, dict):
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
@@ -182,6 +202,7 @@ class ConfigFactory(UserServersTableItem, dict):
return ''
def toStorageObject(self) -> dict:
"""Build the persisted representation of the configuration."""
if self.kwargs.get('remark') is None:
# compatibility: remark field is mandatory in previous application version
self.kwargs['remark'] = ''
+14
View File
@@ -15,6 +15,8 @@
# 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 common proxy-core process interface."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -31,7 +33,9 @@ __all__ = ['CoreProcessFactory']
class CoreProcessFactory(ABC):
"""Define the interface and shared behavior for core process objects."""
class ExitCode(Enum):
"""Enumerate process exit codes."""
ConfigurationError = 23
# Windows: 4294967295. Darwin, Linux: 255 (-1)
ServerStartFailure = 4294967295 if PLATFORM == 'Windows' else 255
@@ -42,28 +46,34 @@ class CoreProcessFactory(ABC):
self,
exitCallback: Union[Callable[[CoreProcessFactory, int], None], None] = None,
):
"""Initialize the CoreProcessFactory."""
super().__init__()
self._exitCallback = exitCallback
def callExitCallback(self, exitcode: int):
"""Call exit callback."""
if callable(self._exitCallback):
self._exitCallback(self, exitcode)
@functools.singledispatchmethod
def toJSONString(self, config, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
return ''
@toJSONString.register(str)
def _(self, config, **kwargs) -> str:
"""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."""
try:
ensure_ascii = kwargs.pop('ensure_ascii', False)
escape_forward_slashes = kwargs.pop('escape_forward_slashes', False)
@@ -82,17 +92,21 @@ class CoreProcessFactory(ABC):
@staticmethod
@abstractmethod
def name() -> str:
"""Return the process implementation name."""
raise NotImplementedError
@staticmethod
@abstractmethod
def version() -> str:
"""Return the bundled core version."""
raise NotImplementedError
@abstractmethod
def start(self, *args, **kwargs) -> bool:
"""Start the core process factory."""
raise NotImplementedError
@abstractmethod
def stop(self):
"""Stop the core process factory."""
raise NotImplementedError
+5
View File
@@ -15,6 +15,8 @@
# 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 common data encoder interface."""
from __future__ import annotations
from abc import ABC, abstractmethod
@@ -24,10 +26,13 @@ __all__ = ['EncoderFactory']
class EncoderFactory(ABC):
"""Define the interface and shared behavior for encoder objects."""
@abstractmethod
def encode(self, data: Any, **kwargs) -> Any:
"""Encode data with the encoder factory."""
raise NotImplementedError
@abstractmethod
def decode(self, data: Any, **kwargs) -> Any:
"""Decode data with the encoder factory."""
raise NotImplementedError
@@ -15,6 +15,8 @@
# 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 configuration-editor binding interfaces."""
from __future__ import annotations
from typing import Any
@@ -23,19 +25,26 @@ __all__ = ['GuiEditorItemFactory', 'GuiEditorItemWidgetContainer']
class GuiEditorItemFactory:
"""Define the interface and shared behavior for GUI editor item objects."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemFactory."""
super().__init__(*args, **kwargs)
def inputToFactory(self, *args, **kwargs) -> Any:
"""Apply the current editor value to the configuration."""
raise NotImplementedError
def factoryToInput(self, *args, **kwargs) -> Any:
"""Load the configuration value into the editor."""
raise NotImplementedError
class GuiEditorItemWidgetContainer(GuiEditorItemFactory):
"""Bind one or more editor widgets to configuration data."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemWidgetContainer."""
super().__init__(*args, **kwargs)
def widgets(self) -> Any:
"""Return the widgets owned by this editor item."""
raise NotImplementedError
+5
View File
@@ -15,6 +15,8 @@
# 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 persistent storage collection interface."""
from __future__ import annotations
from abc import ABC, abstractmethod
@@ -24,10 +26,13 @@ __all__ = ['StorageFactory']
class StorageFactory(ABC):
"""Define the interface and shared behavior for storage objects."""
@abstractmethod
def sync(self):
"""Persist the current storage factory data."""
raise NotImplementedError
@abstractmethod
def data(self) -> Any:
"""Return the data managed by the storage factory."""
raise NotImplementedError
+13
View File
@@ -15,47 +15,60 @@
# 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 display fields for server-table rows."""
from __future__ import annotations
__all__ = ['UserServersTableItem']
class UserServersTableItem:
"""Define the display fields required by a server-table row."""
def __init__(self, *args, **kwargs):
"""Initialize the UserServersTableItem."""
super().__init__(*args, **kwargs)
@property
def itemRemark(self) -> str:
"""Return the item remark value."""
return ''
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return ''
@property
def itemAddress(self) -> str:
"""Return the item address value."""
return ''
@property
def itemPort(self) -> str:
"""Return the item port value."""
return ''
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return ''
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return ''
@property
def itemSubscription(self) -> str:
"""Return the item subscription value."""
return ''
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return ''
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return ''
+2
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from .ApplicationFactory import *
+98
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Parse, build, import, and export supported proxy configurations."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,6 +37,7 @@ urlunparse = functools.partial(urllib.parse.urlunparse)
def queryStringFromItems(items):
"""Encode non-empty query items as a URI query string."""
return urllib.parse.urlencode(
list((key, value) for key, value in items if value != '' and value is not None),
doseq=True,
@@ -57,6 +60,7 @@ __all__ = [
class ConfigXrayProxyOutboundObjectV(dict):
"""Represent and transform Xray proxy outbound object v configuration data."""
def __init__(
self,
protocol,
@@ -68,6 +72,7 @@ class ConfigXrayProxyOutboundObjectV(dict):
security,
**kwargs,
):
"""Initialize the ConfigXrayProxyOutboundObjectV."""
(
networkObjectArgs,
securityArgs,
@@ -146,7 +151,9 @@ class ConfigXrayProxyOutboundObjectV(dict):
class ConfigXrayProxyOutboundObjectSS(dict):
"""Represent and transform Xray proxy outbound object ss configuration data."""
def __init__(self, method, password, address, port):
"""Initialize the ConfigXrayProxyOutboundObjectSS."""
super().__init__(
**{
'tag': 'proxy',
@@ -175,7 +182,9 @@ class ConfigXrayProxyOutboundObjectSS(dict):
class ConfigXrayProxyOutboundObjectSocks(dict):
"""Represent and transform Xray proxy outbound object SOCKS configuration data."""
def __init__(self, address, port, user='', password=''):
"""Initialize the ConfigXrayProxyOutboundObjectSocks."""
settings = {
'address': address,
'port': int(port),
@@ -202,7 +211,9 @@ class ConfigXrayProxyOutboundObjectSocks(dict):
class ConfigXrayProxyOutboundObjectTrojan(dict):
"""Represent and transform Xray proxy outbound object trojan configuration data."""
def __init__(self, password, address, port, type_, security, **kwargs):
"""Initialize the ConfigXrayProxyOutboundObjectTrojan."""
networkObjectArgs, securityArgs, TLSObjectArgs = {}, {}, {}
if type_ == 'h2':
@@ -337,14 +348,18 @@ BLANK_CONFIG_XRAY = {
class ConfigXray(ConfigFactory):
"""Represent Xray configuration and supported share-link formats."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigXray."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Xray-core'
@staticmethod
def getProxyOutboundObject(config: dict, default=None) -> dict:
"""Return proxy outbound object."""
if not isinstance(config.get('outbounds'), list):
config['outbounds'] = []
@@ -363,6 +378,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def getProxyOutboundStream(config: dict, **kwargs) -> dict:
"""Return proxy outbound stream."""
proxyOutbound = ConfigXray.getProxyOutboundObject(config, **kwargs)
if not isinstance(proxyOutbound.get('streamSettings'), dict):
@@ -372,6 +388,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def getProxyOutboundServer(config: dict, protocol: Protocol, **kwargs) -> dict:
"""Return proxy outbound server."""
value, proxyOutbound = (
protocol.value.lower(),
ConfigXray.getProxyOutboundObject(config, **kwargs),
@@ -398,6 +415,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def getProxyOutboundUser(config: dict, protocol: Protocol, **kwargs) -> dict:
"""Return proxy outbound user."""
proxyOutboundServer = ConfigXray.getProxyOutboundServer(
config, protocol, **kwargs
)
@@ -416,6 +434,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyOutboundObject(self) -> dict:
"""Return the proxy outbound object value."""
try:
return ConfigXray.getProxyOutboundObject(self)
except Exception:
@@ -425,6 +444,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyProtocol(self) -> str:
"""Return the proxy protocol value."""
try:
return self.proxyOutboundObject['protocol']
except Exception:
@@ -434,6 +454,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyServerObject(self) -> dict:
"""Return the proxy server object value."""
try:
proxyProtocol = self.proxyProtocol.lower()
@@ -454,6 +475,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyUserObject(self) -> dict:
"""Return the proxy user object value."""
try:
if self.proxyProtocol.lower() == 'socks':
return self.proxyServerObject
@@ -466,6 +488,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyStreamSettingsObject(self) -> dict:
"""Return the proxy stream settings object value."""
try:
return ConfigXray.getProxyOutboundStream(self)
except Exception:
@@ -475,6 +498,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyStreamSettingsTLS(self) -> str:
"""Return the proxy stream settings TLS value."""
try:
return self.proxyStreamSettingsObject.get('security', 'none')
except Exception:
@@ -484,6 +508,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyStreamSettingsTLSObject(self) -> dict:
"""Return the proxy stream settings TLS object value."""
try:
# tlsSettings, realitySettings
TLSKey = f'{self.proxyStreamSettingsTLS}Settings'
@@ -496,6 +521,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyStreamSettingsNetwork(self) -> str:
"""Return the proxy stream settings network value."""
try:
return self.proxyStreamSettingsObject['network']
except Exception:
@@ -505,6 +531,7 @@ class ConfigXray(ConfigFactory):
@property
def proxyStreamSettingsNetworkObject(self) -> dict:
"""Return the proxy stream settings network object value."""
try:
if self.proxyStreamSettingsNetwork == 'h2':
networkKey = 'httpSettings'
@@ -531,6 +558,7 @@ class ConfigXray(ConfigFactory):
@property
def kwargsFromVMessProxyStreamSettingsNetworkObject(self) -> dict:
"""Return the kwargs from v mess proxy stream settings network object value."""
kwargs = {}
try:
@@ -550,6 +578,7 @@ class ConfigXray(ConfigFactory):
return kwargs
def hasKey(key):
"""Return whether key."""
return networkObject.get(key) is not None
if network == 'tcp' or network == 'raw':
@@ -662,6 +691,7 @@ class ConfigXray(ConfigFactory):
@property
def kwargsFromVLESSProxyStreamSettingsNetworkObject(self) -> dict:
"""Return the kwargs from VLESS proxy stream settings network object value."""
kwargs = {}
try:
@@ -681,6 +711,7 @@ class ConfigXray(ConfigFactory):
return kwargs
def hasKey(key):
"""Return whether key."""
return networkObject.get(key) is not None
if network == 'tcp' or network == 'raw':
@@ -804,6 +835,7 @@ class ConfigXray(ConfigFactory):
# v2rayN share standard doesn't require unquote. Still
# unquote value according to (VMess AEAD / VLESS) standard
"""Build Xray transport settings from share-link keyword arguments."""
if type_ == 'tcp' or type_ == 'raw':
TcpObject = {}
@@ -982,6 +1014,7 @@ class ConfigXray(ConfigFactory):
@property
def kwargsFromProxyStreamSettingsTLSObject(self) -> dict:
"""Return the kwargs from proxy stream settings TLS object value."""
kwargs = {}
TLS = self.proxyStreamSettingsTLS
@@ -1029,6 +1062,7 @@ class ConfigXray(ConfigFactory):
def kwargs2ProxyStreamSettingsTLSObject(
protocol, remote_host, security, kwargs
) -> dict:
"""Build Xray TLS or REALITY settings from share-link keyword arguments."""
TLSObject = {}
if security == 'reality' or security == 'tls':
@@ -1094,6 +1128,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def kwargs2ProxyUserObject(protocol, uuid_, encryption, kwargs) -> dict:
"""Build an Xray proxy user object from share-link values."""
if protocol == 'vmess':
UserObject = {
'id': uuid_,
@@ -1125,6 +1160,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObjectVMess(URI: str) -> Tuple[str, dict]:
"""Parse a VMess URI into a remark and Xray proxy outbound."""
try:
myHead, myBody = URI.split('://')
except Exception:
@@ -1142,6 +1178,7 @@ class ConfigXray(ConfigFactory):
else:
def getOrDefault(key, default=''):
"""Return or default."""
return myJSON.get(key, default)
remark = unquote(getOrDefault('ps'))
@@ -1176,6 +1213,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObjectVLESS(URI: str, **kwargs) -> Tuple[str, dict]:
"""Parse a VLESS URI into a remark and Xray proxy outbound."""
result = urlparse(URI)
remark = unquote(result.fragment)
queryObject = {key: value for key, value in parse_qsl(result.query)}
@@ -1205,6 +1243,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObjectSS(URI: str) -> Tuple[str, dict]:
"""Parse a Shadowsocks URI into a remark and Xray proxy outbound."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
@@ -1215,6 +1254,7 @@ class ConfigXray(ConfigFactory):
def getSSParams():
# Begin SIP002...
"""Return ss params."""
try:
# Try pack with 3 element
userinfo, server = result.netloc.split('@')
@@ -1261,6 +1301,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObjectSocks(URI: str) -> Tuple[str, dict]:
"""Parse a SOCKS URI into a remark and Xray proxy outbound."""
result = urlparse(URI)
remark = unquote(result.fragment)
@@ -1279,6 +1320,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObjectTrojan(URI: str) -> Tuple[str, dict]:
"""Parse a Trojan URI into a remark and Xray proxy outbound."""
result = urlparse(URI)
remark = unquote(result.fragment)
queryObject = {key: value for key, value in parse_qsl(result.query)}
@@ -1300,6 +1342,7 @@ class ConfigXray(ConfigFactory):
@staticmethod
def URI2ProxyOutboundObject(URI: str) -> Tuple[str, dict]:
"""Dispatch a supported share URI to its Xray outbound parser."""
if URI.startswith('vmess://'):
return ConfigXray.URI2ProxyOutboundObjectVMess(URI)
@@ -1319,44 +1362,53 @@ class ConfigXray(ConfigFactory):
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.toEnum(self.proxyProtocol).value
@property
def itemAddress(self) -> str:
"""Return the item address value."""
addr = self.proxyServerObject.get('address', '')
return str(addr)
@property
def itemPort(self) -> str:
"""Return the item port value."""
port = self.proxyServerObject.get('port', '')
return str(port)
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return self.proxyStreamSettingsNetwork
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return self.proxyStreamSettingsTLS
@property
def itemLatency(self) -> str:
# Backward compatibility
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
# Backward compatibility
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 2)
return super().toJSONString(indent=indent)
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
@@ -1472,6 +1524,7 @@ class ConfigXray(ConfigFactory):
return ''
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
remark, proxyOutboundObject = ConfigXray.URI2ProxyOutboundObject(URI)
@@ -1508,6 +1561,7 @@ class ConfigXray(ConfigFactory):
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
for inbound in self['inbounds']:
if inbound['protocol'] == 'http':
@@ -1522,6 +1576,7 @@ class ConfigXray(ConfigFactory):
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
for inbound in self['inbounds']:
if inbound['protocol'] == 'socks':
@@ -1536,6 +1591,7 @@ class ConfigXray(ConfigFactory):
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if self.get('inbounds') is None:
self['inbounds'] = []
@@ -1593,6 +1649,7 @@ class ConfigXray(ConfigFactory):
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if self.get('inbounds') is None:
self['inbounds'] = []
@@ -1663,18 +1720,23 @@ BLANK_CONFIG_HYSTERIA1 = {
class ConfigHysteria1(ConfigFactory):
"""Represent Hysteria 1 client configuration and share links."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigHysteria1."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Hysteria1'
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.Hysteria1.value
@property
def itemAddress(self) -> str:
"""Return the item address value."""
server = self.get('server', '')
pos = server.rfind(':')
@@ -1686,6 +1748,7 @@ class ConfigHysteria1(ConfigFactory):
@property
def itemPort(self) -> str:
"""Return the item port value."""
server = self.get('server', '')
pos = server.rfind(':')
@@ -1697,26 +1760,32 @@ class ConfigHysteria1(ConfigFactory):
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return ''
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return ''
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 4)
return super().toJSONString(indent=indent)
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
@@ -1782,6 +1851,7 @@ class ConfigHysteria1(ConfigFactory):
return urlunparse(['hysteria', netloc, '', '', query, quote(override)])
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
@@ -1864,6 +1934,7 @@ class ConfigHysteria1(ConfigFactory):
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
return self['http']['listen']
except Exception:
@@ -1872,6 +1943,7 @@ class ConfigHysteria1(ConfigFactory):
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
return self['socks5']['listen']
except Exception:
@@ -1880,6 +1952,7 @@ class ConfigHysteria1(ConfigFactory):
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if endpoint == '':
self.pop('http', None)
@@ -1898,6 +1971,7 @@ class ConfigHysteria1(ConfigFactory):
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if endpoint == '':
self.pop('socks5', None)
@@ -1931,18 +2005,23 @@ BLANK_CONFIG_HYSTERIA2 = {
class ConfigHysteria2(ConfigFactory):
"""Represent Hysteria 2 client configuration and share links."""
def __init__(self, config: Union[str, dict] = '', **kwargs):
"""Initialize the ConfigHysteria2."""
super().__init__(config, **kwargs)
def coreName(self) -> str:
"""Return the core implementation name."""
return 'Hysteria2'
@property
def itemProtocol(self) -> str:
"""Return the item protocol value."""
return Protocol.Hysteria2.value
@staticmethod
def splitServerAddressPort(server: str) -> Tuple[str, str]:
"""Split a standard or Realm-mode server value into display fields."""
if server.startswith(('realm://', 'realm+http://')):
result = urlparse(server)
@@ -1962,39 +2041,47 @@ class ConfigHysteria2(ConfigFactory):
@property
def itemAddress(self) -> str:
"""Return the item address value."""
address, _port = self.splitServerAddressPort(self.get('server', ''))
return address
@property
def itemPort(self) -> str:
"""Return the item port value."""
_address, port = self.splitServerAddressPort(self.get('server', ''))
return port
@property
def itemTransport(self) -> str:
"""Return the item transport value."""
return ''
@property
def itemTLS(self) -> str:
"""Return the item TLS value."""
return ''
@property
def itemLatency(self) -> str:
"""Return the item latency value."""
return self.getExtras('delayResult')
@property
def itemSpeed(self) -> str:
"""Return the item speed value."""
return self.getExtras('speedResult')
def toJSONString(self, **kwargs) -> str:
"""Serialize the configuration as JSON text."""
indent = kwargs.pop('indent', 4)
return super().toJSONString(indent=indent)
@staticmethod
def splitHysteria2RealmQueryItems(queryItems):
"""Separate Realm transport parameters from Hysteria 2 parameters."""
realmItems = []
hysteria2Items = []
@@ -2007,6 +2094,7 @@ class ConfigHysteria2(ConfigFactory):
return realmItems, hysteria2Items
def toURI(self, remark: str = '') -> str:
"""Export the configuration as a share URI."""
if remark == '':
override = self.itemRemark
else:
@@ -2064,6 +2152,7 @@ class ConfigHysteria2(ConfigFactory):
return urlunparse(['hysteria2', netloc, '', '', query, quote(override)])
def fromURI(self, URI: str) -> bool:
"""Populate the configuration from a share URI."""
try:
result = urlparse(URI)
remark = unquote(result.fragment)
@@ -2160,6 +2249,7 @@ class ConfigHysteria2(ConfigFactory):
return False
def httpProxy(self) -> str:
"""Return the configured local HTTP proxy endpoint."""
try:
return self['http']['listen']
except Exception:
@@ -2168,6 +2258,7 @@ class ConfigHysteria2(ConfigFactory):
return ''
def socksProxy(self) -> str:
"""Return the configured local SOCKS proxy endpoint."""
try:
return self['socks5']['listen']
except Exception:
@@ -2176,6 +2267,7 @@ class ConfigHysteria2(ConfigFactory):
return ''
def setHttpProxy(self, endpoint: str) -> bool:
"""Set the local HTTP proxy endpoint."""
try:
if endpoint == '':
self.pop('http', None)
@@ -2194,6 +2286,7 @@ class ConfigHysteria2(ConfigFactory):
return False
def setSocksProxy(self, endpoint: str) -> bool:
"""Set the local SOCKS proxy endpoint."""
try:
if endpoint == '':
self.pop('socks5', None)
@@ -2213,6 +2306,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()
if value == 'vless' or value == 'vmess':
@@ -2307,10 +2401,12 @@ def configXrayEmptyProxyOutboundObject(protocol: Protocol) -> dict:
def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
"""Return the config factory from dict value used by the application."""
if not isinstance(config, dict):
return ConfigFactory()
def hasField(field):
"""Return whether field."""
return config.get(field) is not None
if hasField('inbounds') or hasField('outbounds'):
@@ -2354,6 +2450,7 @@ def configFactoryFromDict(config: dict, **kwargs) -> ConfigFactory:
def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
"""Return the config factory from any value used by the application."""
if isinstance(config, str):
if config.startswith(
(
@@ -2394,6 +2491,7 @@ def configFactoryFromAny(config: Union[str, dict], **kwargs) -> ConfigFactory:
def configFactoryBlank(protocol: Protocol) -> ConfigFactory:
"""Return the config factory blank value used by the application."""
if protocol == Protocol.VMess or protocol == Protocol.VLESS:
factory = configFactoryFromDict(BLANK_CONFIG_XRAY)
factory['outbounds'][0]['protocol'] = protocol.value.lower()
+14
View File
@@ -15,6 +15,8 @@
# 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 JSON and Base64 encoder implementations."""
from __future__ import annotations
from Furious.Interface import *
@@ -35,20 +37,25 @@ __all__ = [
class JSONEncoder(EncoderFactory):
"""Encode and decode data using JSON."""
@staticmethod
def encode(data: Any, **kwargs) -> str:
"""Encode data with the JSON encoder."""
ensure_ascii = kwargs.pop('ensure_ascii', False)
return json.dumps(data, ensure_ascii=ensure_ascii, **kwargs)
@staticmethod
def decode(data: AnyStr, **kwargs) -> Any:
"""Decode data with the JSON encoder."""
return json.loads(data, **kwargs)
class UJSONEncoder(EncoderFactory):
"""Encode and decode data using ujson."""
@staticmethod
def encode(data: Any, **kwargs) -> str:
"""Encode data with the ujson encoder."""
ensure_ascii = kwargs.pop('ensure_ascii', False)
escape_forward_slashes = kwargs.pop('escape_forward_slashes', False)
@@ -61,28 +68,35 @@ class UJSONEncoder(EncoderFactory):
@staticmethod
def decode(data: AnyStr, **kwargs) -> Any:
"""Decode data with the ujson encoder."""
return ujson.loads(data, **kwargs)
class Base64Encoder(EncoderFactory):
"""Encode and decode data using base64."""
@staticmethod
def encode(data: Any, **kwargs) -> bytes:
"""Encode data with the base64 encoder."""
return base64.b64encode(data, **kwargs)
@staticmethod
def decode(data: Any, **kwargs) -> bytes:
"""Decode data with the base64 encoder."""
validate = kwargs.pop('validate', False)
return base64.b64decode(data, validate=validate, **kwargs)
class PyBase64Encoder(EncoderFactory):
"""Encode and decode data using py base64."""
@staticmethod
def encode(data: Any, **kwargs) -> bytes:
"""Encode data with the py base64 encoder."""
return pybase64.b64encode(data, **kwargs)
@staticmethod
def decode(data: Any, **kwargs) -> bytes:
"""Decode data with the py base64 encoder."""
validate = kwargs.pop('validate', False)
return pybase64.b64decode(data, validate=validate, **kwargs)
+11
View File
@@ -15,6 +15,8 @@
# 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 cached access to persisted user data collections."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -32,8 +34,10 @@ __all__ = ['Storage']
class Storage:
"""Provide cached access to persisted user configuration collections."""
@staticmethod
def UserActivatedItemIndex() -> int:
"""Return the user activated item index value."""
try:
return int(AppSettings.get('ActivatedItemIndex'))
except Exception:
@@ -44,6 +48,7 @@ class Storage:
@staticmethod
@functools.lru_cache(None)
def UserServers() -> list[ConfigFactory]:
"""Return the user servers value."""
assert APP() is not None
return UserServers().data()
@@ -51,6 +56,7 @@ class Storage:
@staticmethod
@functools.lru_cache(None)
def UserSubs() -> dict[str, dict]:
"""Return the user subs value."""
assert APP() is not None
return UserSubs().data()
@@ -58,6 +64,7 @@ class Storage:
@staticmethod
@functools.lru_cache(None)
def UserTUNSettings() -> dict[str, str]:
"""Return the user TUN settings value."""
assert APP() is not None
return UserTUNSettings().data()
@@ -65,14 +72,17 @@ class Storage:
@staticmethod
@functools.lru_cache(None)
def UserRoutings() -> dict[str, dict]:
"""Return the user routings value."""
assert APP() is not None
return UserRoutings().data()
class Extras:
"""Derive display and proxy values from the active server."""
@staticmethod
@forceToLocalhostIfPossible()
def UserHttpProxy() -> Union[str, None]:
"""Return the user HTTP proxy value."""
try:
if APP().isSystemTrayConnected():
index, servers = (
@@ -94,6 +104,7 @@ class Storage:
@staticmethod
def UserServerRemark() -> Union[str, None]:
"""Return the user server remark value."""
try:
if APP().isSystemTrayConnected():
index, servers = (
+8
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Persist custom routing definitions."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -27,10 +29,13 @@ registerAppSettings('CustomRouting')
class UserRoutings(Mixins.CleanupOnExit, StorageFactory):
"""Manage the persisted custom-routing collection."""
def __init__(self, *args, **kwargs):
"""Initialize the UserRoutings."""
super().__init__(*args, **kwargs)
def restore():
"""Restore the user routings."""
try:
data = UJSONEncoder.decode(
PyBase64Encoder.decode(AppSettings.get('CustomRouting'))
@@ -48,6 +53,7 @@ class UserRoutings(Mixins.CleanupOnExit, StorageFactory):
self._data = restore()
def sync(self):
"""Persist the current user routings data."""
AppSettings.set(
'CustomRouting',
PyBase64Encoder.encode(UJSONEncoder.encode(self._data).encode()),
@@ -55,7 +61,9 @@ class UserRoutings(Mixins.CleanupOnExit, StorageFactory):
def data(self) -> dict[str, dict]:
# Shallow copy
"""Return the data managed by the user routings."""
return self._data
def cleanup(self):
"""Release resources owned by the user routings."""
self.sync()
+9
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Persist and reconstruct user server configurations."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -28,6 +30,7 @@ registerAppSettings('Configuration')
class UserServer:
"""Represent user server."""
remark: str
config: str
subsId: str
@@ -35,10 +38,13 @@ class UserServer:
class UserServers(Mixins.CleanupOnExit, StorageFactory):
# remark, config, subsId. (subsId corresponds to unique in user subscription)
"""Manage the persisted list of server configurations."""
def __init__(self, *args, **kwargs):
"""Initialize the UserServers."""
super().__init__(*args, **kwargs)
def restore():
"""Restore the user servers."""
try:
return UJSONEncoder.decode(
PyBase64Encoder.decode(AppSettings.get('Configuration'))
@@ -55,6 +61,7 @@ class UserServers(Mixins.CleanupOnExit, StorageFactory):
)
def sync(self):
"""Persist the current user servers data."""
AppSettings.set(
'Configuration',
PyBase64Encoder.encode(
@@ -66,7 +73,9 @@ class UserServers(Mixins.CleanupOnExit, StorageFactory):
def data(self) -> list[ConfigFactory]:
# Shallow copy
"""Return the data managed by the user servers."""
return self._list
def cleanup(self):
"""Release resources owned by the user servers."""
self.sync()
+10
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Persist subscription definitions."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -27,6 +29,7 @@ registerAppSettings('CustomSubscription')
class UserSubEntry:
"""Describe one user sub entry."""
remark: str
webURL: str
autoupdate: str
@@ -34,15 +37,19 @@ class UserSubEntry:
class UserSub:
"""Represent user sub."""
unique: dict[str, dict]
class UserSubs(Mixins.CleanupOnExit, StorageFactory):
# unique: { remark, webURL }
"""Manage the persisted subscription collection."""
def __init__(self, *args, **kwargs):
"""Initialize the UserSubs."""
super().__init__(*args, **kwargs)
def restore():
"""Restore the user subs."""
try:
return UJSONEncoder.decode(
PyBase64Encoder.decode(AppSettings.get('CustomSubscription'))
@@ -55,6 +62,7 @@ class UserSubs(Mixins.CleanupOnExit, StorageFactory):
self._data = restore()
def sync(self):
"""Persist the current user subs data."""
AppSettings.set(
'CustomSubscription',
PyBase64Encoder.encode(
@@ -64,7 +72,9 @@ class UserSubs(Mixins.CleanupOnExit, StorageFactory):
def data(self) -> dict[str, dict]:
# Shallow copy
"""Return the data managed by the user subs."""
return self._data
def cleanup(self):
"""Release resources owned by the user subs."""
self.sync()
+8
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Persist user-defined TUN settings."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -27,10 +29,13 @@ registerAppSettings('CustomTUNSettings')
class UserTUNSettings(Mixins.CleanupOnExit, StorageFactory):
"""Manage persisted TUN customization values."""
def __init__(self, *args, **kwargs):
"""Initialize the UserTUNSettings."""
super().__init__(*args, **kwargs)
def restore():
"""Restore the user TUN settings."""
try:
return UJSONEncoder.decode(
PyBase64Encoder.decode(AppSettings.get('CustomTUNSettings'))
@@ -43,6 +48,7 @@ class UserTUNSettings(Mixins.CleanupOnExit, StorageFactory):
self._data = restore()
def sync(self):
"""Persist the current user TUN settings data."""
AppSettings.set(
'CustomTUNSettings',
PyBase64Encoder.encode(
@@ -52,7 +58,9 @@ class UserTUNSettings(Mixins.CleanupOnExit, StorageFactory):
def data(self) -> dict[str, str]:
# Shallow copy
"""Return the data managed by the user TUN settings."""
return self._data
def cleanup(self):
"""Release resources owned by the user TUN settings."""
self.sync()
+2
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
from .Encoder import *
+5
View File
@@ -15,6 +15,8 @@
# 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 Qt support for app style sheet."""
from __future__ import annotations
from textwrap import dedent
@@ -23,6 +25,7 @@ __all__ = ['AppStyleSheet']
class AppStyleSheet:
"""Represent app style sheet."""
Light = 'Light'
Dark = 'Dark'
FontPointSize = 10
@@ -82,6 +85,7 @@ class AppStyleSheet:
@staticmethod
def normalizeTheme(theme):
"""Return the normalize theme value used by the app style sheet."""
if theme == AppStyleSheet.Dark:
return AppStyleSheet.Dark
@@ -89,6 +93,7 @@ class AppStyleSheet:
@staticmethod
def forTheme(theme):
"""Return the for theme value used by the app style sheet."""
normalizedTheme = AppStyleSheet.normalizeTheme(theme)
palette = AppStyleSheet.Palettes[normalizedTheme]
iconPrefix = ':/Icons/bootstrap'
+9
View File
@@ -15,6 +15,8 @@
# 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 Qt support for DNS resolver."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -34,13 +36,16 @@ logger = logging.getLogger(__name__)
class _DNSResolver(WebGETManager):
"""Represent DNS resolver."""
def __init__(self, parent=None, **kwargs):
"""Initialize the _DNSResolver."""
actionMessage = kwargs.pop('actionMessage', 'DNS resolution')
super().__init__(parent, actionMessage=actionMessage)
@staticmethod
def request(address) -> QNetworkRequest:
"""Return the request value used by the DNS resolver."""
request = QNetworkRequest(
QtCore.QUrl(f'https://cloudflare-dns.com/dns-query?name={address}')
)
@@ -49,6 +54,7 @@ class _DNSResolver(WebGETManager):
return request
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
domain = kwargs.pop('domain', '')
resultMap = kwargs.pop('resultMap', {})
@@ -87,6 +93,7 @@ class _DNSResolver(WebGETManager):
resultMap['depth'] -= 1
def failureCallback(self, networkReply: QNetworkReply, **kwargs):
"""Handle a failed network operation."""
domain = kwargs.pop('domain', '')
resultMap = kwargs.pop('resultMap', {})
@@ -98,6 +105,7 @@ class _DNSResolver(WebGETManager):
resultMap['depth'] -= 1
def resolve(self, domain, timeout=30000) -> Tuple[bool, list[str]]:
"""Resolve the DNS resolver."""
resultMap = {
'domain': domain,
'depth': 0,
@@ -123,6 +131,7 @@ class _DNSResolver(WebGETManager):
@staticmethod
def wait(resultMap, startCounter=0, timeout=30000, step=100):
"""Wait for the DNS resolver operation to complete."""
domain = resultMap.get('domain', '')
if not domain:
+10
View File
@@ -15,6 +15,8 @@
# 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 Qt support for dynamic theme."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -24,21 +26,26 @@ __all__ = ['AppHue']
class AppHue:
"""Represent app hue."""
class ColorRGB:
"""Represent color rgb."""
LIGHT_BLUE = '#43ACED'
LIGHT_RED = '#FF7276'
LIGHT_PURPLE = '#DA70D6'
@staticmethod
def disconnectedColor() -> str:
"""Disconnect ed color."""
return AppHue.ColorRGB.LIGHT_BLUE
@staticmethod
def disconnectedWindowIcon() -> AppQIcon:
"""Disconnect ed window icon."""
return bootstrapIcon('rocket-takeoff-window.svg')
@staticmethod
def connectedColor() -> str:
"""Connect ed color."""
if not SystemRuntime.isAdmin():
return AppHue.ColorRGB.LIGHT_RED
else:
@@ -46,6 +53,7 @@ class AppHue:
@staticmethod
def connectedWindowIcon() -> AppQIcon:
"""Connect ed window icon."""
if not SystemRuntime.isAdmin():
return bootstrapIcon('rocket-takeoff-connected-dark.svg')
else:
@@ -53,6 +61,7 @@ class AppHue:
@staticmethod
def currentColor() -> str:
"""Return the current color value."""
if APP().isSystemTrayConnected():
return AppHue.connectedColor()
else:
@@ -60,6 +69,7 @@ class AppHue:
@staticmethod
def currentWindowIcon() -> AppQIcon:
"""Return the current window icon value."""
if APP().isSystemTrayConnected():
return AppHue.connectedWindowIcon()
else:
+8
View File
@@ -15,6 +15,8 @@
# 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 Qt support for dynamic translate."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -29,13 +31,16 @@ __all__ = [
class Translator:
"""Represent translator."""
def __init__(self):
"""Initialize the Translator."""
super().__init__()
self.translation = dict()
self.dictEnglish = dict()
def install(self, translation):
"""Handle install for the translator."""
self.translation = translation
for key, value in translation.items():
@@ -51,6 +56,7 @@ class Translator:
self.dictEnglish.update(dict(list((key, key) for key in translation.keys())))
def translate(self, source, locale):
"""Return the translate value used by the translator."""
try:
return self.translation[self.dictEnglish[source]][locale]
except Exception:
@@ -64,10 +70,12 @@ translator = Translator()
def installTranslation(translation):
"""Handle install translation for the application."""
translator.install(translation)
def gettext(source, locale=None):
"""Return text."""
if locale is None:
if APP() is None:
# Normally should not reach here if performing a translation
+56
View File
@@ -15,6 +15,8 @@
# 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 Qt support for GUI editor xxx."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -44,7 +46,9 @@ __all__ = [
class GuiEditorItemTextInput(GuiEditorItemWidgetContainer):
"""Represent GUI editor item text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemTextInput."""
title = kwargs.pop('title', '')
translatable = kwargs.pop('translatable', True)
parent = kwargs.pop('parent', None)
@@ -58,17 +62,22 @@ class GuiEditorItemTextInput(GuiEditorItemWidgetContainer):
self._input = QLineEdit(parent=parent)
def text(self) -> str:
"""Return the text value."""
return self._input.text()
def setText(self, text: str):
"""Set text."""
self._input.setText(text)
def widgets(self):
"""Return the widgets owned by this editor item."""
return self._title, self._input
class GuiEditorItemTextSpinBox(GuiEditorItemWidgetContainer):
"""Represent GUI editor item text spin box."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemTextSpinBox."""
title = kwargs.pop('title', '')
translatable = kwargs.pop('translatable', True)
parent = kwargs.pop('parent', None)
@@ -82,20 +91,26 @@ class GuiEditorItemTextSpinBox(GuiEditorItemWidgetContainer):
self._input = AppQSpinBox(parent=parent)
def value(self) -> int:
"""Return the value value."""
return self._input.value()
def setValue(self, value: int):
"""Set value."""
self._input.setValue(value)
def setRange(self, minRange: int, maxRange: int):
"""Set range."""
self._input.setRange(minRange, maxRange)
def widgets(self):
"""Return the widgets owned by this editor item."""
return self._title, self._input
class GuiEditorItemTextComboBox(GuiEditorItemWidgetContainer):
"""Represent GUI editor item text combo box."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemTextComboBox."""
title = kwargs.pop('title', '')
translatable = kwargs.pop('translatable', True)
parent = kwargs.pop('parent', None)
@@ -109,23 +124,30 @@ class GuiEditorItemTextComboBox(GuiEditorItemWidgetContainer):
self._input = AppQComboBox(parent=parent, translatable=False)
def text(self) -> str:
"""Return the text value."""
return self._input.currentText()
def setText(self, text: str):
"""Set text."""
self._input.setCurrentText(text)
def addItems(self, texts: Sequence[str]):
"""Add items."""
self._input.addItems(texts)
def connectActivated(self, func: Callable):
"""Connect activated."""
self._input.activated.connect(func)
def widgets(self):
"""Return the widgets owned by this editor item."""
return self._title, self._input
class GuiEditorItemTextCheckBox(GuiEditorItemWidgetContainer):
"""Represent GUI editor item text check box."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemTextCheckBox."""
title = kwargs.pop('title', '')
translatable = kwargs.pop('translatable', True)
parent = kwargs.pop('parent', None)
@@ -135,20 +157,26 @@ class GuiEditorItemTextCheckBox(GuiEditorItemWidgetContainer):
self._input = AppQCheckBox(_(title), translatable=translatable, parent=parent)
def isChecked(self) -> bool:
"""Return whether checked."""
return self._input.isChecked()
def setChecked(self, checked: bool):
"""Set checked."""
self._input.setChecked(checked)
def widgets(self):
"""Return the widgets owned by this editor item."""
return (self._input,)
class GuiEditorItemBasicRemark(GuiEditorItemTextInput):
"""Represent GUI editor item basic remark."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemBasicRemark."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldRemark = config.getExtras('remark')
newRemark = self.text()
@@ -162,14 +190,18 @@ class GuiEditorItemBasicRemark(GuiEditorItemTextInput):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
self.setText(config.getExtras('remark'))
class GuiEditorItemProxyHttp(GuiEditorItemTextInput):
"""Represent GUI editor item proxy HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemProxyHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldHttp = config.httpProxy()
newHttp = self.text()
@@ -194,6 +226,7 @@ class GuiEditorItemProxyHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.httpProxy())
except Exception:
@@ -203,10 +236,13 @@ class GuiEditorItemProxyHttp(GuiEditorItemTextInput):
class GuiEditorItemProxySocks(GuiEditorItemTextInput):
"""Represent GUI editor item proxy SOCKS."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorItemProxySocks."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldSocks = config.socksProxy()
newSocks = self.text()
@@ -231,6 +267,7 @@ class GuiEditorItemProxySocks(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.socksProxy())
except Exception:
@@ -240,15 +277,19 @@ class GuiEditorItemProxySocks(GuiEditorItemTextInput):
class GuiEditorWidget(GuiEditorItemFactory):
"""Provide the GUI editor widget."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorWidget."""
super().__init__(*args, **kwargs)
self._containers = self.containerSequence()
def containerSequence(self) -> Sequence[GuiEditorItemWidgetContainer]:
"""Return the editor item containers in display order."""
raise NotImplementedError
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
modified = False
for container in self._containers:
@@ -262,6 +303,7 @@ class GuiEditorWidget(GuiEditorItemFactory):
return modified
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
for container in self._containers:
try:
container.factoryToInput(config)
@@ -272,12 +314,15 @@ class GuiEditorWidget(GuiEditorItemFactory):
class GuiEditorWidgetQWidget(GuiEditorWidget, QWidget):
"""Provide the GUI editor Qt widget."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorWidgetQWidget."""
super().__init__(*args, **kwargs)
self.setupLayout()
def setupLayout(self):
"""Set up layout."""
layout = QFormLayout()
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
@@ -289,7 +334,9 @@ class GuiEditorWidgetQWidget(GuiEditorWidget, QWidget):
class GuiEditorWidgetQGroupBox(GuiEditorWidget, AppQGroupBox):
"""Group the GUI editor widget q editor controls."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorWidgetQGroupBox."""
super().__init__(*args, **kwargs)
page = QWidget()
@@ -305,6 +352,7 @@ class GuiEditorWidgetQGroupBox(GuiEditorWidget, AppQGroupBox):
self.setLayout(vboxLayout)
def setupPageLayout(self):
"""Set up page layout."""
layout = QFormLayout()
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
@@ -316,7 +364,9 @@ class GuiEditorWidgetQGroupBox(GuiEditorWidget, AppQGroupBox):
class GuiEditorWidgetQDialog(GuiEditorItemFactory, AppQDialog):
"""Present the GUI editor widget Qt dialog."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiEditorWidgetQDialog."""
tabText, tabTranslatable, style = (
kwargs.pop('tabText', ''),
kwargs.pop('tabTranslatable', False),
@@ -357,9 +407,11 @@ class GuiEditorWidgetQDialog(GuiEditorItemFactory, AppQDialog):
@functools.lru_cache(None)
def groupBoxSequence(self) -> Sequence[GuiEditorWidgetQGroupBox]:
"""Return the configuration group boxes in display order."""
raise NotImplementedError
def setGroupBoxStyle(self, style: str):
"""Set group box style."""
if style == 'grid':
for index, groupBox in enumerate(self.groupBoxSequence()):
self.tabCentralWidgetLayout.addWidget(groupBox, index // 2, index % 2)
@@ -371,13 +423,16 @@ class GuiEditorWidgetQDialog(GuiEditorItemFactory, AppQDialog):
self.tabCentralWidgetLayout.addWidget(groupBox, index, 0)
def setTabText(self, text: str):
"""Set tab text."""
self.tabWidget.setTabText(0, text)
def closeEvent(self, event):
"""Handle closure of the GUI editor widget Qt dialog."""
self.accepted.disconnect()
self.rejected.disconnect()
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
modified = False
for groupBox in self.groupBoxSequence():
@@ -386,5 +441,6 @@ class GuiEditorWidgetQDialog(GuiEditorItemFactory, AppQDialog):
return modified
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
for groupBox in self.groupBoxSequence():
groupBox.factoryToInput(config)
+12
View File
@@ -15,6 +15,8 @@
# 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 Qt support for network connectivity manager."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -32,10 +34,12 @@ logger = logging.getLogger(__name__)
class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
"""Coordinate network connectivity operations."""
MIN_JOB_INTERVAL = 2500
MAX_JOB_INTERVAL = 2000000000
def __init__(self, parent=None, **kwargs):
"""Initialize the NetworkConnectivityManager."""
actionMessage = kwargs.pop('actionMessage', 'test network connectivity')
super().__init__(parent, actionMessage=actionMessage)
@@ -49,6 +53,7 @@ class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
self.jobArrangeTimer.timeout.connect(lambda: self.startSingleTest())
def recalculateJobInterval(self, jobStatus: bool) -> int:
"""Return the recalculate job interval value used by the network connectivity manager."""
assert isinstance(jobStatus, bool)
if self.jobStatus is jobStatus:
@@ -65,15 +70,18 @@ class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
return self.jobInterval
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
self.jobTimeoutTimer.stop()
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=True))
def failureCallback(self, networkReply, **kwargs):
"""Handle a failed network operation."""
self.jobTimeoutTimer.stop()
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=False))
def startSingleTest(self):
# Use custom network connectivity test URL if possible
"""Start single test."""
settings = AppSettings.get('CustomNetworkConnectivityTestURL')
if isinstance(settings, str):
@@ -84,6 +92,7 @@ class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
networkReply = self.webGET(url)
def abort(_networkReply):
"""Cancel the active network connectivity manager operation."""
if isinstance(_networkReply, QNetworkReply):
_networkReply.abort()
@@ -91,10 +100,12 @@ class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
self.jobTimeoutTimer.start(NetworkConnectivityManager.MIN_JOB_INTERVAL - 500)
def stopTest(self):
"""Stop test."""
self.jobArrangeTimer.stop()
self.jobTimeoutTimer.stop()
def connectedCallback(self):
"""Update the network connectivity manager for a connected state."""
if AppSettings.isStateON_('PowerSaveMode'):
# Power optimization
logger.info('no job for network connectivity manager in power save mode')
@@ -106,4 +117,5 @@ class NetworkConnectivityManager(Mixins.ConnectionAware, WebGETManager):
self.jobArrangeTimer.start(self.jobInterval)
def disconnectedCallback(self):
"""Update the network connectivity manager for a disconnected state."""
self.stopTest()
+27
View File
@@ -15,6 +15,8 @@
# 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 Qt support for qt GUI."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -41,13 +43,16 @@ __all__ = [
class AppQIcon(QIcon):
"""Represent app q icon."""
def __init__(self, iconFileName: str):
"""Initialize the AppQIcon."""
super().__init__(iconFileName)
self.iconFileName = iconFileName
def iconFn(prefix, name):
"""Return the icon fn value used by the application."""
if name.startswith('rocket-takeoff'):
# Colorful. Use default
return AppQIcon(f':/Icons/bootstrap/{name}')
@@ -60,6 +65,7 @@ bootstrapIconWhite = functools.partial(iconFn, 'bootstrap/white')
def setIconAsMask(icon):
"""Set icon as mask."""
if hasattr(icon, 'setIsMask'):
icon.setIsMask(True)
@@ -68,11 +74,13 @@ def setIconAsMask(icon):
@functools.lru_cache(None)
def bootstrapIconMask(name):
"""Return the bootstrap icon mask value used by the application."""
return setIconAsMask(bootstrapIconWhite(name))
@functools.lru_cache(None)
def bootstrapIconWithOpacity(name, opacity, isMask=False):
"""Return the bootstrap icon with opacity value used by the application."""
sourceIcon = bootstrapIconWhite(name)
icon = AppQIcon('')
@@ -103,6 +111,7 @@ def bootstrapIconWithOpacity(name, opacity, isMask=False):
class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
"""Handle the app q action."""
def __init__(
self,
text,
@@ -118,6 +127,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
isTrayAction=False,
**kwargs,
):
"""Initialize the AppQAction."""
super().__init__(text=text, **kwargs)
# Do not use QProtection because it's been managed somewhere else!!!
@@ -164,6 +174,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
@QtCore.Slot(bool)
def triggerSignal(paramChecked):
"""Handle trigger signal for the app q action."""
logger.info(f'action is \'{self.textEnglish}\'. Checked is {paramChecked}')
if callable(self.callback):
@@ -174,6 +185,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
self.triggered.connect(triggerSignal)
def addAction(self, action):
"""Add action."""
if self._menu is not None:
self._menu.addAction(action)
@@ -181,6 +193,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
self._actionGroup.addAction(action)
def removeAction(self, action):
"""Remove action."""
if self._menu is not None:
self._menu.removeAction(action)
@@ -189,17 +202,21 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
@property
def textEnglish(self):
"""Return the text english value."""
return _(self.text(), 'EN')
def __str__(self):
"""Return the display text for the app q action."""
return self.__class__.__name__
def textCompare(self, compare):
"""Return the text compare value used by the app q action."""
return self.textEnglish == compare
@staticmethod
@functools.lru_cache(None)
def getIconFileName(fileName):
"""Return icon file name."""
try:
return fileName.split('/')[-1]
except Exception:
@@ -208,6 +225,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
return ''
def setIconByTheme(self, theme):
"""Set icon by theme."""
if not self.iconFileName:
return
@@ -238,6 +256,7 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
super().setIcon(bootstrapIcon(self.iconFileName))
def setIcon(self, icon: AppQIcon):
"""Set icon."""
self.iconFileName = self.getIconFileName(icon.iconFileName)
if not self.iconFileName:
@@ -247,10 +266,13 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
self.setIconByTheme(APP().theme())
def themeChangedCallback(self, theme):
"""Update the app q action for a theme change."""
self.setIconByTheme(theme)
def retranslate(self):
"""Refresh translated text for the app q action."""
def recursiveTranslate(action, memo):
"""Handle recursive translate for the app q action."""
if action not in memo and not action.isSeparator() and action.translatable:
action.setText(_(action.text()))
action.setStatusTip(_(action.statusTip()))
@@ -268,11 +290,14 @@ class AppQAction(Mixins.QTranslatable, Mixins.ThemeAware, QAction):
def triggeredCallback(self, checked):
# Not a mandatory re-implementation in child class
"""Handle activation of the action."""
pass
class AppQActionGroup(QActionGroup):
"""Represent app q action group."""
def __init__(self, parent, *actions):
"""Initialize the AppQActionGroup."""
super().__init__(parent)
for action in actions:
@@ -280,5 +305,7 @@ class AppQActionGroup(QActionGroup):
class AppQSeperator(QAction):
"""Represent app q seperator."""
def __init__(self):
"""Initialize the AppQSeperator."""
super().__init__()
+5
View File
@@ -15,6 +15,8 @@
# 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 Qt support for qt network."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -27,10 +29,13 @@ __all__ = ['AppQNetworkAccessManager']
class AppQNetworkAccessManager(QNetworkAccessManager):
"""Coordinate app q network access operations."""
def __init__(self, parent=None):
"""Initialize the AppQNetworkAccessManager."""
super().__init__(parent)
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
"""Configure HTTP proxy."""
if httpProxy is None:
useProxy = False
else:
+121
View File
@@ -15,6 +15,8 @@
# 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 Qt support for qt widgets."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -64,6 +66,7 @@ __all__ = [
def moveToCenter(widget, parent=None):
"""Move to center."""
geometry = widget.frameGeometry()
if parent is not None:
@@ -81,24 +84,32 @@ def moveToCenter(widget, parent=None):
class AppQCheckBox(Mixins.QTranslatable, QCheckBox):
"""Represent app q check box."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQCheckBox."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q check box."""
self.setText(_(self.text()))
class AppQComboBox(Mixins.QTranslatable, QComboBox):
"""Represent app q combo box."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQComboBox."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q combo box."""
for index in range(self.count()):
self.setItemText(index, _(self.itemText(index)))
class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
"""Present the app Qt dialog."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQDialog."""
super().__init__(*args, **kwargs)
self._firstShowCall = True
@@ -109,19 +120,23 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
self.setWindowIcon(AppHue.currentWindowIcon())
def setWidthAndHeight(self):
"""Apply the default size for the app Qt dialog."""
pass
def exec(self):
"""Show and execute the app Qt dialog modally."""
self.show()
return super().exec()
def open(self):
"""Open the app Qt dialog asynchronously."""
self.show()
return super().open()
def show(self):
"""Show and position the app Qt dialog."""
super().show()
if PLATFORM == 'Darwin':
@@ -133,40 +148,52 @@ class AppQDialog(Mixins.QTranslatable, Mixins.ConnectionAware, QDialog):
moveToCenter(self)
def retranslate(self):
"""Refresh translated text for the app Qt dialog."""
self.setWindowTitle(_(self.windowTitle()))
def disconnectedCallback(self):
"""Update the app Qt dialog for a disconnected state."""
self.setWindowIcon(AppHue.disconnectedWindowIcon())
def connectedCallback(self):
"""Update the app Qt dialog for a connected state."""
self.setWindowIcon(AppHue.connectedWindowIcon())
class AppQDialogButtonBox(Mixins.QTranslatable, QDialogButtonBox):
"""Represent app Qt dialog button box."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQDialogButtonBox."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app Qt dialog button box."""
for button in self.buttons():
button.setText(_(button.text()))
class AppQGroupBox(Mixins.QTranslatable, QGroupBox):
"""Group the app q editor controls."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQGroupBox."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q group box."""
self.setTitle(_(self.title()))
class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
"""Represent app q header view."""
def sectionSizeSettingsEmpty(self):
"""Return the section size settings empty value used by the app q header view."""
return (
self.legacySectionSizeSettingsName == ''
and self.sectionSizeSettingsName == ''
)
def __init__(self, *args, **kwargs):
"""Initialize the AppQHeaderView."""
self.legacySectionSizeSettingsName = kwargs.pop(
'legacySectionSizeSettingsName', ''
)
@@ -193,6 +220,7 @@ class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
# self.sectionResized.connect(self.handleSectionResized)
def restoreSectionSize(self):
"""Restore section size."""
if self.sectionSizeSettingsEmpty():
return
@@ -237,6 +265,7 @@ class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
def setCustomSectionResizeMode(self):
# Horizontal header resize mode
"""Set custom section resize mode."""
for index in range(self.columnCount):
if index < self.columnCount - 1:
self.setSectionResizeMode(index, AppQHeaderView.ResizeMode.Interactive)
@@ -245,16 +274,19 @@ class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
@staticmethod
def getStyleSheet(color):
"""Return style sheet."""
return f'QHeaderView::section:hover {{ background-color: {color}; }}'
def disconnectedCallback(self):
# self.setStyleSheet(self.getStyleSheet(AppHue.disconnectedColor()))
"""Update the app q header view for a disconnected state."""
pass
def connectedCallback(self):
# self.setStyleSheet(self.getStyleSheet(AppHue.connectedColor()))
"""Update the app q header view for a connected state."""
pass
# Legacy method. Not used
@@ -267,6 +299,7 @@ class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
# self.sectionSizeTable[str(index)] = newSize
def cleanup(self):
"""Release resources owned by the app q header view."""
if self.sectionSizeSettingsEmpty():
return
@@ -279,28 +312,37 @@ class AppQHeaderView(Mixins.CleanupOnExit, Mixins.ConnectionAware, QHeaderView):
class AppQLabel(Mixins.QTranslatable, QLabel):
"""Represent app q label."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQLabel."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q label."""
self.setText(_(self.text()))
class AppQLineEdit(Mixins.QTranslatable, QLineEdit):
"""Represent app q line edit."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQLineEdit."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q line edit."""
self.setPlaceholderText(_(self.placeholderText()))
class AppQListWidget(Mixins.ConnectionAware, QListWidget):
"""Provide the app Qt list widget."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQListWidget."""
super().__init__(*args, **kwargs)
# self.setSelectionColor(AppHue.disconnectedColor())
def setSelectionColor(self, color):
"""Set selection color."""
self.setStyleSheet(
f'QListWidget::item:selected {{'
f' background: {color};'
@@ -313,16 +355,19 @@ class AppQListWidget(Mixins.ConnectionAware, QListWidget):
@property
def selectedIndex(self):
"""Return the selected index value."""
return sorted(list(set(index.row() for index in self.selectedIndexes())))
def disconnectedCallback(self):
# self.setSelectionColor(AppHue.disconnectedColor())
"""Update the app Qt list widget for a disconnected state."""
pass
def connectedCallback(self):
# self.setSelectionColor(AppHue.connectedColor())
"""Update the app Qt list widget for a connected state."""
pass
@@ -332,7 +377,9 @@ class AppQMainWindow(
Mixins.CleanupOnExit,
QMainWindow,
):
"""Present the app q main window."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQMainWindow."""
super().__init__(*args, **kwargs)
self._firstShowCall = True
@@ -346,9 +393,11 @@ class AppQMainWindow(
self.setWidthAndHeight()
def setWidthAndHeight(self):
"""Apply the default size for the app q main window."""
pass
def show(self):
"""Show and position the app q main window."""
super().show()
if PLATFORM == 'Darwin':
@@ -366,20 +415,26 @@ class AppQMainWindow(
self.raise_()
def retranslate(self):
"""Refresh translated text for the app q main window."""
self.setWindowTitle(_(self.windowTitle()))
def disconnectedCallback(self):
"""Update the app q main window for a disconnected state."""
self.setWindowIcon(AppHue.disconnectedWindowIcon())
def connectedCallback(self):
"""Update the app q main window for a connected state."""
self.setWindowIcon(AppHue.connectedWindowIcon())
def cleanup(self):
"""Release resources owned by the app q main window."""
pass
class AppQMenu(Mixins.QTranslatable, QMenu):
"""Represent app q menu."""
def __init__(self, *actions, **kwargs):
"""Initialize the AppQMenu."""
super().__init__(**kwargs)
# In some old version PySide6, the self.actions() method
@@ -399,38 +454,47 @@ class AppQMenu(Mixins.QTranslatable, QMenu):
pass
def retranslate(self):
"""Refresh translated text for the app q menu."""
self.setTitle(_(self.title()))
class AppQMenuBar(QMenuBar):
"""Represent app q menu bar."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQMenuBar."""
super().__init__(*args, **kwargs)
class AppQMessageBox(Mixins.QTranslatable, Mixins.ConnectionAware, QMessageBox):
"""Represent app q message box."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQMessageBox."""
super().__init__(*args, **kwargs)
self.setWindowIcon(AppHue.currentWindowIcon())
def moveToCenter(self):
"""Move to center."""
moveToCenter(self, self.parentWidget())
return self
def exec(self):
"""Show and execute the app q message box modally."""
self.show()
self.moveToCenter()
return super().exec()
def open(self):
"""Open the app q message box asynchronously."""
self.show()
self.moveToCenter()
return super().open()
def retranslate(self):
"""Refresh translated text for the app q message box."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(_(self.text()))
@@ -450,14 +514,18 @@ class AppQMessageBox(Mixins.QTranslatable, Mixins.ConnectionAware, QMessageBox):
self.moveToCenter()
def disconnectedCallback(self):
"""Update the app q message box for a disconnected state."""
self.setWindowIcon(AppHue.disconnectedWindowIcon())
def connectedCallback(self):
"""Update the app q message box for a connected state."""
self.setWindowIcon(AppHue.connectedWindowIcon())
class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
"""Represent app q push button."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQPushButton."""
icon = kwargs.pop('icon', None)
super().__init__(*args, **kwargs)
@@ -470,6 +538,7 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
@staticmethod
@functools.lru_cache(None)
def getIconFileName(fileName):
"""Return icon file name."""
try:
return fileName.split('/')[-1]
except Exception:
@@ -478,6 +547,7 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
return ''
def setIconByTheme(self, theme):
"""Set icon by theme."""
if not self.iconFileName:
return
@@ -503,6 +573,7 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
super().setIcon(bootstrapIcon(self.iconFileName))
def setIcon(self, icon: AppQIcon):
"""Set icon."""
self.iconFileName = self.getIconFileName(icon.iconFileName)
if not self.iconFileName:
@@ -512,17 +583,22 @@ class AppQPushButton(Mixins.QTranslatable, Mixins.ThemeAware, QPushButton):
self.setIconByTheme(APP().theme())
def themeChangedCallback(self, theme):
"""Update the app q push button for a theme change."""
self.setIconByTheme(theme)
def retranslate(self):
"""Refresh translated text for the app q push button."""
self.setText(_(self.text()))
class AppQSpinBox(QSpinBox):
"""Represent app q spin box."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQSpinBox."""
super().__init__(*args, **kwargs)
def resizeHints(self):
"""Handle resize hints for the app q spin box."""
self.setMinimumWidth(
max(
self.sizeHint().width(),
@@ -531,40 +607,50 @@ class AppQSpinBox(QSpinBox):
)
def setRange(self, *args, **kwargs):
"""Set range."""
super().setRange(*args, **kwargs)
self.resizeHints()
class AppQTableView(Mixins.ConnectionAware, QTableView):
"""Represent app Qt table view."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQTableView."""
super().__init__(*args, **kwargs)
self.setWordWrap(False)
self.setAlternatingRowColors(True)
def setDefaultRowHeight(self, height: int):
"""Set default row height."""
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
self.verticalHeader().setDefaultSectionSize(height)
@staticmethod
def getStyleSheet(color):
"""Return style sheet."""
return f'QTableView {{ selection-background-color: {color}; }}'
def setSelectionColor(self, color):
# self.setStyleSheet(self.getStyleSheet(color))
"""Set selection color."""
pass
def disconnectedCallback(self):
"""Update the app Qt table view for a disconnected state."""
self.setSelectionColor(AppHue.disconnectedColor())
def connectedCallback(self):
"""Update the app Qt table view for a connected state."""
self.setSelectionColor(AppHue.connectedColor())
class AppQTableWidget(Mixins.ConnectionAware, QTableWidget):
"""Provide the app Qt table widget."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQTableWidget."""
super().__init__(*args, **kwargs)
self.setWordWrap(False)
@@ -572,18 +658,22 @@ class AppQTableWidget(Mixins.ConnectionAware, QTableWidget):
@property
def selectedIndex(self):
"""Return the selected index value."""
return sorted(list(set(index.row() for index in self.selectedIndexes())))
@staticmethod
def getStyleSheet(color):
"""Return style sheet."""
return f'QTableWidget {{ selection-background-color: {color}; }}'
def setSelectionColor(self, color):
# self.setStyleSheet(self.getStyleSheet(color))
"""Set selection color."""
pass
def activateItemByIndex(self, index, activate):
"""Activate item by index."""
if activate:
for column in range(self.columnCount()):
item = self.item(int(index), column)
@@ -612,6 +702,7 @@ class AppQTableWidget(Mixins.ConnectionAware, QTableWidget):
item.setForeground(QBrush())
def selectMultipleRows(self, indexes: list[int], clearCurrentSelection: bool):
"""Select multiple rows."""
if clearCurrentSelection:
self.selectionModel().clearSelection()
@@ -628,23 +719,30 @@ class AppQTableWidget(Mixins.ConnectionAware, QTableWidget):
)
def disconnectedCallback(self):
"""Update the app Qt table widget for a disconnected state."""
self.setSelectionColor(AppHue.disconnectedColor())
def connectedCallback(self):
"""Update the app Qt table widget for a connected state."""
self.setSelectionColor(AppHue.connectedColor())
class AppQTabWidget(Mixins.QTranslatable, QTabWidget):
"""Provide the app q tab widget."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQTabWidget."""
super().__init__(*args, **kwargs)
def retranslate(self):
"""Refresh translated text for the app q tab widget."""
for index in range(self.count()):
self.setTabText(index, _(self.tabText(index)))
class AppQToolBar(Mixins.QTranslatable, QToolBar):
"""Represent app q tool bar."""
def __init__(self, *actions, **kwargs):
"""Initialize the AppQToolBar."""
super().__init__(**kwargs)
self._actions = []
@@ -666,8 +764,10 @@ class AppQToolBar(Mixins.QTranslatable, QToolBar):
@QtCore.Slot(AppQAction)
def showMenuBelow(self, action: AppQAction):
"""Show menu below."""
def toolBarWidgetForAction() -> Union[QWidget | None]:
# Walk through the toolbar to find the widget for the action
"""Return the tool bar widget for action value used by the app q tool bar."""
for child in self.children():
if hasattr(child, 'defaultAction'):
# PySide6.QtWidgets.QMenu.defaultAction
@@ -688,14 +788,18 @@ class AppQToolBar(Mixins.QTranslatable, QToolBar):
@staticmethod
def getStyleSheet():
"""Return style sheet."""
return f'QToolBar {{ spacing: 5px; }}'
def retranslate(self):
"""Refresh translated text for the app q tool bar."""
self.setWindowTitle(_(self.windowTitle()))
class MBoxQuestionDelete(AppQMessageBox):
"""Represent m box question delete."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxQuestionDelete."""
super().__init__(*args, **kwargs)
self.isMulti = False
@@ -707,12 +811,14 @@ class MBoxQuestionDelete(AppQMessageBox):
)
def customText(self) -> str:
"""Return the user-facing message text for the m box question delete."""
if self.isMulti:
return _('Delete these items?')
else:
return _('Delete this item?') + f'\n\n{self.possibleRemark}'
def retranslate(self):
"""Refresh translated text for the m box question delete."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -722,7 +828,9 @@ class MBoxQuestionDelete(AppQMessageBox):
class MBoxNewChangesNextTime(AppQMessageBox):
"""Represent m box new changes next time."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxNewChangesNextTime."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_(APPLICATION_NAME))
@@ -734,11 +842,13 @@ class MBoxNewChangesNextTime(AppQMessageBox):
@staticmethod
def customText() -> str:
"""Return the user-facing message text for the m box new changes next time."""
return (
_('New changes will take effect next time') + '\n\n' + _('Reconnect now?')
)
def retranslate(self):
"""Refresh translated text for the m box new changes next time."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -749,8 +859,10 @@ class MBoxNewChangesNextTime(AppQMessageBox):
def showMBoxNewChangesNextTime(**kwargs):
"""Show m box new changes next time."""
@QtCore.Slot(int)
def handleResultCode(code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQMessageBox.StandardButton.Yes):
APP().systemTray.ConnectAction.doReconnect()
else:
@@ -782,7 +894,9 @@ def showMBoxNewChangesNextTime(**kwargs):
class MBoxDirectRulesNotAllowed(AppQMessageBox):
"""Represent m box direct rules not allowed."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxDirectRulesNotAllowed."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Unable to connect'))
@@ -794,6 +908,7 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
@staticmethod
def customText() -> str:
"""Return the user-facing message text for the m box direct rules not allowed."""
return (
_('Routing option with direct rules is not allowed in TUN mode')
+ '\n\n'
@@ -801,6 +916,7 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
)
def retranslate(self):
"""Refresh translated text for the m box direct rules not allowed."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -810,8 +926,10 @@ class MBoxDirectRulesNotAllowed(AppQMessageBox):
def showMBoxDirectRulesNotAllowed(**kwargs):
"""Show m box direct rules not allowed."""
@QtCore.Slot(int)
def handleResultCode(code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQMessageBox.StandardButton.Yes):
APP().systemTray.RoutingAction.getGlobalAction().trigger()
APP().systemTray.ConnectAction.trigger()
@@ -832,7 +950,9 @@ def showMBoxDirectRulesNotAllowed(**kwargs):
class MBoxUnrecognizedConfig(AppQMessageBox):
"""Represent m box unrecognized config."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxUnrecognizedConfig."""
super().__init__(*args, **kwargs)
self.setIcon(AppQMessageBox.Icon.Critical)
@@ -840,6 +960,7 @@ class MBoxUnrecognizedConfig(AppQMessageBox):
def showMBoxUnrecognizedConfig(**kwargs):
"""Show m box unrecognized config."""
mbox = MBoxUnrecognizedConfig(**kwargs)
if isinstance(mbox.parent(), QMainWindow):
+36
View File
@@ -15,6 +15,8 @@
# 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 Qt support for text editor."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -38,10 +40,13 @@ __all__ = [
class SupportPointSizeSettings(Mixins.CleanupOnExit):
"""Store and validate support point size settings."""
def pointSizeSettingsEmpty(self):
"""Return the point size settings empty value used by the support point size settings."""
return self.pointSizeSettingsName == ''
def __init__(self, *args, **kwargs):
"""Initialize the SupportPointSizeSettings."""
self.pointSizeSettingsName = kwargs.pop('pointSizeSettingsName', '')
super().__init__(*args, **kwargs)
@@ -51,17 +56,22 @@ class SupportPointSizeSettings(Mixins.CleanupOnExit):
self.restorePointSize()
def restorePointSize(self):
"""Restore point size."""
raise NotImplementedError
def cleanup(self):
"""Release resources owned by the support point size settings."""
raise NotImplementedError
class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
"""Represent app q plain text edit."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQPlainTextEdit."""
super().__init__(*args, **kwargs)
def restorePointSize(self):
"""Restore point size."""
if self.pointSizeSettingsEmpty():
return
@@ -79,6 +89,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
@staticmethod
@functools.lru_cache(128)
def getIndent(line):
"""Return indent."""
indent = ''
for char in line:
@@ -90,6 +101,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
return indent
def getPrevAndNextChar(self, cursor):
"""Return prev and next char."""
plainText = self.toPlainText()
cursor.movePosition(QTextCursor.MoveOperation.Left)
@@ -114,6 +126,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
return prevChar + nextChar
def smartIndent(self, event):
"""Handle smart indent for the app q plain text edit."""
cursor = self.textCursor()
indent = self.getIndent(cursor.block().text())
@@ -126,6 +139,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
self.setTextCursor(cursor)
def smartSymbolPair(self, event, pair):
"""Handle smart symbol pair for the app q plain text edit."""
plainText = self.toPlainText()
cursor = self.textCursor()
@@ -148,6 +162,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
self.setTextCursor(cursor)
def smartBackspace(self, event):
"""Handle smart backspace for the app q plain text edit."""
cursor = self.textCursor()
chPair = self.getPrevAndNextChar(cursor)
@@ -158,6 +173,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
super().keyPressEvent(event)
def hideTabAndSpaces(self):
"""Hide tab and spaces."""
textOption = QTextOption()
self.document().setDefaultTextOption(textOption)
@@ -166,6 +182,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def showTabAndSpaces(self):
"""Show tab and spaces."""
textOption = QTextOption()
textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces)
@@ -175,6 +192,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def keyPressEvent(self, event):
"""Handle a key press for the app q plain text edit."""
if (
event.key() == QtCore.Qt.Key.Key_Return
or event.key() == QtCore.Qt.Key.Key_Enter
@@ -192,6 +210,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
super().keyPressEvent(event)
def wheelEvent(self, event):
"""Handle the wheel event."""
if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
delta = event.angleDelta().y()
@@ -203,6 +222,7 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
super().wheelEvent(event)
def cleanup(self):
"""Release resources owned by the app q plain text edit."""
if self.pointSizeSettingsEmpty():
return
@@ -210,10 +230,13 @@ class AppQPlainTextEdit(SupportPointSizeSettings, QPlainTextEdit):
class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
"""Represent app q text browser."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQTextBrowser."""
super().__init__(*args, **kwargs)
def appendLine(self, line: str):
"""Append line."""
hScrollBar = self.horizontalScrollBar()
vScrollBar = self.verticalScrollBar()
scrollEnds = vScrollBar.maximum() - vScrollBar.value() <= 10
@@ -226,6 +249,7 @@ class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
hScrollBar.setValue(0) # scroll to the left
def restorePointSize(self):
"""Restore point size."""
if self.pointSizeSettingsEmpty():
return
@@ -241,6 +265,7 @@ class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
pass
def wheelEvent(self, event):
"""Handle the wheel event."""
if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
delta = event.angleDelta().y()
@@ -252,6 +277,7 @@ class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
super().wheelEvent(event)
def cleanup(self):
"""Release resources owned by the app q text browser."""
if self.pointSizeSettingsEmpty():
return
@@ -259,7 +285,9 @@ class AppQTextBrowser(SupportPointSizeSettings, QTextBrowser):
class DraculaTextEditor(AppQPlainTextEdit):
"""Represent dracula text editor."""
def __init__(self, *args, **kwargs):
"""Initialize the DraculaTextEditor."""
fontFamily = kwargs.pop('fontFamily', '')
super().__init__(*args, **kwargs)
@@ -276,6 +304,7 @@ class DraculaTextEditor(AppQPlainTextEdit):
@QtCore.Slot(bool)
def handleModificationChanged(changed):
"""Handle modification changed."""
if changed:
self.document().setModified(False)
@@ -284,6 +313,7 @@ class DraculaTextEditor(AppQPlainTextEdit):
@QtCore.Slot()
def handleCursorPositionChanged():
"""Handle cursor position changed."""
if callable(self._cursorPositionChangedCb):
self._cursorPositionChangedCb(self.textCursor())
@@ -291,21 +321,27 @@ class DraculaTextEditor(AppQPlainTextEdit):
self.cursorPositionChanged.connect(handleCursorPositionChanged)
def registerCursorPositionChangedCb(self, callback: Callable[[QTextCursor], None]):
"""Handle register cursor position changed cb for the dracula text editor."""
self._cursorPositionChangedCb = callback
def registerModificationChangedCb(self, callback: Callable[[], None]):
"""Handle register modification changed cb for the dracula text editor."""
self._modificationChangedCb = callback
class DraculaJSONTextEditor(DraculaTextEditor):
"""Represent dracula JSON text editor."""
def __init__(self, *args, **kwargs):
"""Initialize the DraculaJSONTextEditor."""
super().__init__(*args, **kwargs)
self._syntaxHighlighter = DraculaJSONSyntaxHighlighter(self.document())
class DraculaTextBrowser(AppQTextBrowser):
"""Represent dracula text browser."""
def __init__(self, *args, **kwargs):
"""Initialize the DraculaTextBrowser."""
fontFamily = kwargs.pop('fontFamily', '')
super().__init__(*args, **kwargs)
+17
View File
@@ -15,6 +15,8 @@
# 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 Qt support for text editor theme."""
from __future__ import annotations
from Furious.Core import *
@@ -30,7 +32,9 @@ __all__ = [
class EditorHighlightRules:
"""Represent editor highlight rules."""
def __init__(self, regex, color, isBold=False, isItalic=False, isJSONKey=False):
"""Initialize the EditorHighlightRules."""
self.regex = QtCore.QRegularExpression(regex)
self.color = QColor(color)
@@ -47,20 +51,26 @@ class EditorHighlightRules:
class EditorTheme:
"""Represent editor theme."""
def __init__(self, *args, **kwargs):
"""Initialize the EditorTheme."""
super().__init__(*args, **kwargs)
@staticmethod
def getStyleSheet(*args, **kwargs):
"""Return style sheet."""
raise NotImplementedError
class DraculaEditorTheme(EditorTheme):
"""Represent dracula editor theme."""
def __init__(self, *args, **kwargs):
"""Initialize the DraculaEditorTheme."""
super().__init__(*args, **kwargs)
@staticmethod
def getStyleSheet(widgetName, fontFamily):
"""Return style sheet."""
return (
f'{widgetName} {{'
f' background-color: #282A36;'
@@ -71,12 +81,15 @@ class DraculaEditorTheme(EditorTheme):
class AppQSyntaxHighlighter(QSyntaxHighlighter):
"""Apply syntax highlighting for app q syntax text."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQSyntaxHighlighter."""
super().__init__(*args, **kwargs)
self.highlightRules = list()
def highlightBlock(self, text):
"""Handle highlight block for the app q syntax highlighter."""
for highlightRule in self.highlightRules:
iterator = highlightRule.regex.globalMatch(text)
@@ -110,7 +123,9 @@ class AppQSyntaxHighlighter(QSyntaxHighlighter):
class DraculaJSONSyntaxHighlighter(AppQSyntaxHighlighter):
"""Apply syntax highlighting for dracula JSON syntax text."""
def __init__(self, *args, **kwargs):
"""Initialize the DraculaJSONSyntaxHighlighter."""
super().__init__(*args, **kwargs)
self.highlightRules = [
@@ -133,6 +148,7 @@ class DraculaJSONSyntaxHighlighter(AppQSyntaxHighlighter):
class DraculaLoggerSyntaxHighlighter(AppQSyntaxHighlighter):
# https://ihateregex.io/expr/ip/
"""Apply syntax highlighting for dracula logger syntax text."""
IPV4_REGEX = (
r'(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)'
r'(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}(?::\d{1,5})?\b'
@@ -159,6 +175,7 @@ class DraculaLoggerSyntaxHighlighter(AppQSyntaxHighlighter):
)
def __init__(self, *args, **kwargs):
"""Initialize the DraculaLoggerSyntaxHighlighter."""
super().__init__(*args, **kwargs)
self.highlightRules = [
+13
View File
@@ -15,6 +15,8 @@
# 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 Qt support for updates manager."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ logger = logging.getLogger(__name__)
class MBoxQuestionUpdate(AppQMessageBox):
"""Represent m box question update."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxQuestionUpdate."""
super().__init__(*args, **kwargs)
self.version = '0.0.0'
@@ -47,9 +51,11 @@ class MBoxQuestionUpdate(AppQMessageBox):
)
def customText(self):
"""Return the user-facing message text for the m box question update."""
return _('New version available') + f': {self.version}'
def retranslate(self):
"""Refresh translated text for the m box question update."""
self.setText(self.customText())
self.setWindowTitle(_(self.windowTitle()))
self.setInformativeText(_(self.informativeText()))
@@ -60,18 +66,21 @@ class MBoxQuestionUpdate(AppQMessageBox):
class UpdatesManager(WebGETManager):
"""Coordinate updates operations."""
API_URL = (
f'https://api.github.com/repos/'
f'{APPLICATION_REPO_OWNER_NAME}/{APPLICATION_REPO_NAME}/releases/latest'
)
def __init__(self, parent=None, **kwargs):
"""Initialize the UpdatesManager."""
actionMessage = kwargs.pop('actionMessage', 'check for updates')
super().__init__(parent, actionMessage=actionMessage)
@staticmethod
def showErrorMessageBox(parent=None):
"""Show error message box."""
mbox = AppQMessageBox(parent=parent, icon=AppQMessageBox.Icon.Critical)
mbox.setWindowTitle(_(APPLICATION_NAME))
mbox.setText(_('Check for updates failed'))
@@ -80,6 +89,7 @@ class UpdatesManager(WebGETManager):
mbox.open()
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
parent = kwargs.pop('parent', None)
showMessageBox = kwargs.pop('showMessageBox', True)
hasNewVersionCallback = kwargs.pop('hasNewVersionCallback', None)
@@ -101,6 +111,7 @@ class UpdatesManager(WebGETManager):
if versionToValue(newVersion) > versionToValue(APPLICATION_VERSION):
def handleResultCode(code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -140,6 +151,7 @@ class UpdatesManager(WebGETManager):
mbox.open()
def failureCallback(self, networkReply, **kwargs):
"""Handle a failed network operation."""
showMessageBox = kwargs.pop('showMessageBox', True)
if showMessageBox:
@@ -151,6 +163,7 @@ class UpdatesManager(WebGETManager):
hasNewVersionCallback: Callable[[str], None] = None,
**kwargs,
):
"""Check for updates."""
self.webGET(
self.API_URL,
showMessageBox=showMessageBox,
+14
View File
@@ -15,6 +15,8 @@
# 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 Qt support for web get manager."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -34,7 +36,9 @@ logger = logging.getLogger(__name__)
class WebGETManager(AppQNetworkAccessManager):
"""Coordinate web get operations."""
def __init__(self, parent=None, actionMessage='web GET', **kwargs):
"""Initialize the WebGETManager."""
super().__init__(parent)
self.actionMessage = actionMessage
@@ -43,19 +47,25 @@ class WebGETManager(AppQNetworkAccessManager):
self.mustCalled = False
def successCallback(self, networkReply: QNetworkReply, **kwargs):
"""Handle a successful network operation."""
pass
def hasDataCallback(self, networkReply: QNetworkReply, **kwargs):
"""Handle newly available network response data."""
pass
def failureCallback(self, networkReply: QNetworkReply, **kwargs):
"""Handle a failed network operation."""
pass
def mustCall(self, **kwargs):
"""Perform the required completion hook."""
pass
def must(self, **kwargs):
"""Run the required completion hook according to its call policy."""
def call():
"""Invoke the registered completion callback."""
try:
self.mustCall(**kwargs)
except Exception as ex:
@@ -71,9 +81,11 @@ class WebGETManager(AppQNetworkAccessManager):
call()
def handleReadyReadByNetworkReply(self, networkReply: QNetworkReply, **kwargs):
"""Handle ready read by network reply."""
self.hasDataCallback(networkReply, **kwargs)
def handleFinishedByNetworkReply(self, networkReply: QNetworkReply, **kwargs):
"""Handle finished by network reply."""
try:
if not isinstance(networkReply, QNetworkReply):
# Some PySide6 version does not have networkReply as
@@ -102,6 +114,7 @@ class WebGETManager(AppQNetworkAccessManager):
self.must(**kwargs)
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
"""Configure HTTP proxy."""
useProxy = super().configureHttpProxy(httpProxy)
if useProxy:
@@ -112,6 +125,7 @@ class WebGETManager(AppQNetworkAccessManager):
return useProxy
def webGET(self, request: Union[QNetworkRequest, str], **kwargs) -> QNetworkReply:
"""Return the web get value used by the web get manager."""
if isinstance(request, QNetworkRequest):
networkReply = self.get(request)
else:
+23
View File
@@ -15,6 +15,8 @@
# 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 Qt support for Xray asset download manager."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,27 +37,33 @@ logger = logging.getLogger(__name__)
class SHA256Worker(QtCore.QObject, QtCore.QRunnable):
"""Run SHA-256 work in the background."""
finished = QtCore.Signal(str)
def __init__(self, string=b''):
# Explictly called __init__
"""Initialize the SHA256Worker."""
QtCore.QObject.__init__(self)
QtCore.QRunnable.__init__(self)
self.string = string
def run(self):
"""Run the SHA-256 worker task."""
self.finished.emit(hashlib.sha256(self.string).hexdigest())
class XrayAssetSHA256DownloadManager(WebGETManager):
"""Coordinate Xray asset SHA-256 download operations."""
def __init__(self, parent=None, **kwargs):
"""Initialize the XrayAssetSHA256DownloadManager."""
actionMessage = kwargs.pop('actionMessage', 'download sha256')
super().__init__(parent, actionMessage=actionMessage)
@staticmethod
def fileContent(filepath, mode='rb') -> AnyStr:
"""Return the file content value used by the Xray asset SHA-256 download manager."""
try:
with open(filepath, mode) as file:
return file.read()
@@ -65,6 +73,7 @@ class XrayAssetSHA256DownloadManager(WebGETManager):
return b''
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
filepath = kwargs.pop('filepath', '')
downloadCallback = kwargs.pop('downloadCallback', None)
@@ -84,6 +93,7 @@ class XrayAssetSHA256DownloadManager(WebGETManager):
basename = os.path.basename(filepath)
def handleFinished(_digest, _value=''):
"""Handle finished."""
logger.debug(
f'computed digest is \'{_digest}\' while repo digest is \'{_value}\''
)
@@ -104,6 +114,7 @@ class XrayAssetSHA256DownloadManager(WebGETManager):
AppThreadPool().start(worker)
def download(self, url, filepath, downloadCallback: Callable[[], None]):
"""Download the Xray asset SHA-256 download manager."""
self.webGET(
url,
filepath=str(filepath),
@@ -112,12 +123,15 @@ class XrayAssetSHA256DownloadManager(WebGETManager):
class XrayAssetAssetsDownloadManager(WebGETManager):
"""Coordinate Xray asset assets download operations."""
def __init__(self, parent=None, **kwargs):
"""Initialize the XrayAssetAssetsDownloadManager."""
actionMessage = kwargs.pop('actionMessage', 'download assets')
super().__init__(parent, actionMessage=actionMessage)
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
filepath = kwargs.pop('filepath', '')
saveFile = QtCore.QSaveFile(filepath)
@@ -139,11 +153,14 @@ class XrayAssetAssetsDownloadManager(WebGETManager):
logger.info(f'save file to \'{filepath}\' success')
def download(self, url, filepath):
"""Download the Xray asset assets download manager."""
self.webGET(url, filepath=str(filepath))
class XrayAssetPairDownloadHelper:
"""Represent Xray asset pair download helper."""
def __init__(self, *args, **kwargs):
"""Initialize the XrayAssetPairDownloadHelper."""
sha256ActionMessage = kwargs.pop('sha256ActionMessage', 'download sha256')
assetsActionMessage = kwargs.pop('assetsActionMessage', 'download assets')
@@ -157,6 +174,7 @@ class XrayAssetPairDownloadHelper:
)
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
"""Configure HTTP proxy."""
return all(
[
self.sha256Downloader.configureHttpProxy(httpProxy),
@@ -165,6 +183,7 @@ class XrayAssetPairDownloadHelper:
)
def download(self, sha256URL, assetsURL, filepath):
"""Download the Xray asset pair download helper."""
self.sha256Downloader.download(
url=sha256URL,
filepath=filepath,
@@ -175,7 +194,9 @@ class XrayAssetPairDownloadHelper:
class XrayAssetDownloadManager:
"""Coordinate Xray asset download operations."""
def __init__(self, *args, **kwargs):
"""Initialize the XrayAssetDownloadManager."""
super().__init__(*args, **kwargs)
self.downloadHelperGeosite = XrayAssetPairDownloadHelper(
@@ -188,6 +209,7 @@ class XrayAssetDownloadManager:
)
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
"""Configure HTTP proxy."""
return all(
[
self.downloadHelperGeosite.configureHttpProxy(httpProxy),
@@ -196,6 +218,7 @@ class XrayAssetDownloadManager:
)
def download(self):
"""Download the Xray asset download manager."""
self.downloadHelperGeosite.download(
sha256URL=URL_GEOSITE_SHA256,
assetsURL=URL_GEOSITE,
+2
View File
@@ -15,6 +15,8 @@
# 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 reusable Qt helpers and application-aware widgets."""
from __future__ import annotations
from .DNSResolver import *
+24
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for connect."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -39,6 +41,7 @@ registerAppSettings('Connect', isBinary=True)
def validateProxyServer(server) -> bool:
"""Validate proxy server."""
try:
host, port = parseHostPort(server)
@@ -53,7 +56,9 @@ def validateProxyServer(server) -> bool:
class ConnectAction(AppQAction):
"""Handle the connect action."""
def __init__(self, **kwargs):
"""Initialize the ConnectAction."""
super().__init__(
_('Connect'),
icon=bootstrapIcon('unlock-fill.svg'),
@@ -72,6 +77,7 @@ class ConnectAction(AppQAction):
self.assetDownloadManager = XrayAssetDownloadManager()
def reset(self):
"""Restore the connect action to its initial state."""
self.hideProgressBar(True)
self.setText(_('Connect'))
self.setIcon(bootstrapIcon('unlock-fill.svg'))
@@ -83,6 +89,7 @@ class ConnectAction(AppQAction):
self.setDisabledAction(False)
def showProgressBar(self):
"""Show progress bar."""
if AppSettings.isStateON_('ShowProgressBarWhenConnecting'):
self.progressBar.setValue(0)
# Update the progress bar every 50ms
@@ -92,6 +99,7 @@ class ConnectAction(AppQAction):
return self
def hideProgressBar(self, done: bool):
"""Hide progress bar."""
if done:
self.progressBar.setValue(100)
@@ -101,6 +109,7 @@ class ConnectAction(AppQAction):
return self
def setDisabledAction(self, value):
"""Set disabled action."""
self.setDisabled(value)
APP().systemTray.RoutingAction.setDisabled(value)
@@ -112,12 +121,15 @@ class ConnectAction(AppQAction):
TUNModeAction.setDisabled(value)
def isConnected(self) -> bool:
"""Return whether connected."""
return self.textCompare('Disconnect')
def isConnecting(self):
"""Return whether connecting."""
return self.textCompare('Connecting')
def doConnecting(self):
"""Handle do connecting for the connect action."""
self.setText(_('Connecting'))
self.setIcon(bootstrapIcon('lock-fill.svg'))
# Do not accept new action
@@ -125,6 +137,7 @@ class ConnectAction(AppQAction):
self.showProgressBar()
def doConnected(self):
"""Handle do connected for the connect action."""
self.hideProgressBar(True)
# Connected
self.setText(_('Disconnect'))
@@ -137,6 +150,7 @@ class ConnectAction(AppQAction):
self.setDisabledAction(False)
def doDisconnect(self):
"""Handle do disconnect for the connect action."""
SystemProxy.off()
self.actionTimer.stop()
@@ -155,16 +169,19 @@ class ConnectAction(AppQAction):
Mixins.ConnectionAware.callDisconnectedCallback()
def doDisconnectWithTrayMessage(self, message: str):
"""Handle do disconnect with tray message for the connect action."""
self.doDisconnect()
APP().systemTray.showMessage(message)
def doReconnect(self, message=''):
"""Handle do reconnect for the connect action."""
self.doDisconnectWithTrayMessage(message)
self.trigger()
def doConnect(self):
# Connect action
"""Return the do connect value used by the connect action."""
assert self.textCompare('Connect')
if not Storage.UserServers():
@@ -212,6 +229,7 @@ class ConnectAction(AppQAction):
@forceToLocalhostIfPossible()
def getHttpProxy() -> str:
"""Return HTTP proxy."""
return config.httpProxy()
httpProxy = getHttpProxy()
@@ -304,7 +322,9 @@ class ConnectAction(AppQAction):
@callOnceOnly
def doConnectedCallOnceOnly(self):
"""Handle do connected call once only for the connect action."""
def newVersionCallback(newVersion):
"""Handle the new version callback."""
APP().systemTray.showMessage(
f'{APPLICATION_NAME} {newVersion} ' + _('is available to download')
)
@@ -333,6 +353,7 @@ class ConnectAction(AppQAction):
logger.info('skipped auto assets update due to settings')
def callActionFromQueue(self):
"""Call action from queue."""
try:
action = self.actionQueue.get_nowait()
except queue.Empty:
@@ -348,7 +369,9 @@ class ConnectAction(AppQAction):
action()
def coreExitCallback(self, core: CoreProcessFactory, exitcode: int):
"""Handle the core exit callback."""
def putItem(item):
"""Handle put item for the connect action."""
try:
self.actionQueue.put_nowait(item)
except Exception:
@@ -401,6 +424,7 @@ class ConnectAction(AppQAction):
return None
def triggeredCallback(self, checked):
"""Handle activation of the action."""
if checked:
self.doConnect()
else:
+5
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for edit configuration."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -25,7 +27,9 @@ __all__ = ['EditConfigurationAction']
class EditConfigurationAction(AppQAction):
"""Handle the edit configuration action."""
def __init__(self, **kwargs):
"""Initialize the EditConfigurationAction."""
super().__init__(
_('Edit Configuration...'),
icon=bootstrapIcon('pencil-square.svg'),
@@ -33,4 +37,5 @@ class EditConfigurationAction(AppQAction):
)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
APP().mainWindow.show()
+5
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for exit."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -25,7 +27,9 @@ __all__ = ['ExitAction']
class ExitAction(AppQAction):
"""Handle the exit action."""
def __init__(self, **kwargs):
"""Initialize the ExitAction."""
super().__init__(
_('Exit'),
icon=bootstrapIcon('box-arrow-in-left.svg'),
@@ -33,4 +37,5 @@ class ExitAction(AppQAction):
)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
APP().exit()
+43
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for import."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -49,6 +51,7 @@ logger = logging.getLogger(__name__)
def showMBoxImportError(clipboard: str):
"""Show m box import error."""
mbox = MBoxImportError(icon=AppQMessageBox.Icon.Critical)
if len(clipboard) > 1000:
@@ -64,6 +67,7 @@ def showMBoxImportError(clipboard: str):
def importURIFromClipboard(clipboard: str):
"""Import URI from clipboard."""
factory = configFactoryFromAny(clipboard)
if not factory.isValid():
@@ -80,6 +84,7 @@ def importURIFromClipboard(clipboard: str):
def importURIs(*uris, failureCallback: Union[Callable[[], None], None] = None):
"""Import ur is."""
if len(uris) > 1:
dialog = ImportURIsProgressDialog(
uris,
@@ -124,6 +129,7 @@ def importURIs(*uris, failureCallback: Union[Callable[[], None], None] = None):
class ImportURIsProgressDialog(AppQDialog):
"""Present progress and cancellation controls for import ur is."""
ActiveDialogs = list()
def __init__(
@@ -132,6 +138,7 @@ class ImportURIsProgressDialog(AppQDialog):
failureCallback: Union[Callable[[], None], None] = None,
parent=None,
):
"""Initialize the ImportURIsProgressDialog."""
super().__init__(parent)
self.uris = uris
@@ -175,9 +182,11 @@ class ImportURIsProgressDialog(AppQDialog):
self.updateStatus()
def setWidthAndHeight(self):
"""Apply the default size for the import ur is progress dialog."""
self.resize(420, 150)
def open(self):
"""Open the import ur is progress dialog asynchronously."""
ImportURIsProgressDialog.ActiveDialogs.append(self)
result = super().open()
@@ -189,14 +198,17 @@ class ImportURIsProgressDialog(AppQDialog):
return result
def reject(self):
"""Reject the current import ur is progress dialog values."""
self.cancel()
def cancel(self):
"""Cancel the import ur is progress dialog operation."""
self.canceled = True
self.cancelButton.setEnabled(False)
self.updateStatus()
def updateStatus(self):
"""Update status."""
total = len(self.uris)
processed = min(self.currentIndex, total)
@@ -212,6 +224,7 @@ class ImportURIsProgressDialog(AppQDialog):
@staticmethod
def limitedRemark(remark: str) -> str:
"""Return the limited remark value used by the import ur is progress dialog."""
remark = str(remark).strip()
if len(remark) <= 120:
@@ -220,6 +233,7 @@ class ImportURIsProgressDialog(AppQDialog):
return remark[:117] + '...'
def importNext(self):
"""Import next."""
if self.canceled or self.currentIndex >= len(self.uris):
self.finishImport()
@@ -246,6 +260,7 @@ class ImportURIsProgressDialog(AppQDialog):
QtCore.QTimer.singleShot(0, self.importNext)
def finishImport(self):
"""Handle finish import for the import ur is progress dialog."""
if self.finishedImport:
return
@@ -281,18 +296,22 @@ class ImportURIsProgressDialog(AppQDialog):
mbox.open()
def retranslate(self):
"""Refresh translated text for the import ur is progress dialog."""
self.setWindowTitle(_(self.windowTitle()))
self.cancelButton.setText(_(self.cancelButton.text()))
self.updateStatus()
class MBoxImportError(AppQMessageBox):
"""Represent m box import error."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxImportError."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Import'))
def retranslate(self):
"""Refresh translated text for the m box import error."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(_(self.text()))
@@ -302,7 +321,9 @@ class MBoxImportError(AppQMessageBox):
class MBoxImportMultiSuccess(AppQMessageBox):
"""Represent m box import multi success."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxImportMultiSuccess."""
super().__init__(*args, **kwargs)
self.imported = list()
@@ -312,6 +333,7 @@ class MBoxImportMultiSuccess(AppQMessageBox):
self.setIcon(AppQMessageBox.Icon.Information)
def customText(self):
"""Return the user-facing message text for the m box import multi success."""
text = (
_('Import share link success')
+ '\n\n'
@@ -332,6 +354,7 @@ class MBoxImportMultiSuccess(AppQMessageBox):
return _('Import share link success') + f'\n\n...'
def retranslate(self):
"""Refresh translated text for the m box import multi success."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -341,19 +364,23 @@ class MBoxImportMultiSuccess(AppQMessageBox):
class MBoxImportSuccess(AppQMessageBox):
"""Represent m box import success."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxImportSuccess."""
super().__init__(*args, **kwargs)
self.remark = ''
self.setWindowTitle(_('Import'))
def customText(self):
"""Return the user-facing message text for the m box import success."""
if self.remark:
return _('Import success') + f': {self.remark}'
else:
return _('Import success')
def retranslate(self):
"""Refresh translated text for the m box import success."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -363,7 +390,9 @@ class MBoxImportSuccess(AppQMessageBox):
class ImportFromFileAction(AppQAction):
"""Handle the import from file action."""
def __init__(self, **kwargs):
"""Initialize the ImportFromFileAction."""
super().__init__(
_('Import From File...'),
icon=bootstrapIcon('folder2.svg'),
@@ -371,6 +400,7 @@ class ImportFromFileAction(AppQAction):
)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
filename, selectedFilter = QFileDialog.getOpenFileName(
None,
_('Import File'),
@@ -415,10 +445,13 @@ class ImportFromFileAction(AppQAction):
class ImportURIFromClipboardAction(AppQAction):
"""Handle the import URI from clipboard action."""
def __init__(self, **kwargs):
"""Initialize the ImportURIFromClipboardAction."""
super().__init__(_('Import Share Link From Clipboard'), **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
clipboard = QApplication.clipboard().text().strip()
try:
@@ -435,17 +468,22 @@ class ImportURIFromClipboardAction(AppQAction):
class ImportJSONFromClipboardAction(AppQAction):
"""Handle the import JSON from clipboard action."""
def __init__(self, **kwargs):
"""Initialize the ImportJSONFromClipboardAction."""
super().__init__(_('Import JSON Configuration From Clipboard'), **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
clipboard = QApplication.clipboard().text().strip()
importURIFromClipboard(clipboard)
class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
"""Handle the import QR code on the screen action."""
def __init__(self, **kwargs):
"""Initialize the ImportQRCodeOnTheScreenAction."""
super().__init__(
_('Scan QR Code On The Screen'),
icon=bootstrapIcon('qr-code-scan.svg'),
@@ -462,6 +500,7 @@ class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
self.sct = None
def _importFromQRCode(self):
"""Handle import from QR code for the import QR code on the screen action."""
if self.sct is None:
# Nothing to do
return
@@ -487,6 +526,7 @@ class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
importURIs(*uris)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
try:
self._importFromQRCode()
except Exception as ex:
@@ -495,6 +535,7 @@ class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
logger.error(f'error while importing QR code on the screen: {ex}')
def cleanup(self):
"""Release resources owned by the import QR code on the screen action."""
try:
if self.sct is not None:
self.sct.close()
@@ -505,7 +546,9 @@ class ImportQRCodeOnTheScreenAction(Mixins.CleanupOnExit, AppQAction):
class ImportAction(AppQAction):
"""Handle the import action."""
def __init__(self, **kwargs):
"""Initialize the ImportAction."""
super().__init__(
_('Import'),
icon=bootstrapIcon('lightning-charge.svg'),
+8
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for language."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -38,10 +40,13 @@ registerAppSettings(
class LanguageChildAction(AppQAction):
"""Handle the language child action."""
def __init__(self, *args, **kwargs):
"""Initialize the LanguageChildAction."""
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
abbr = LANGUAGE_TO_ABBR[self.text()]
if AppSettings.get('Language') != abbr:
@@ -53,11 +58,14 @@ class LanguageChildAction(AppQAction):
def retranslate(self):
# Nothing to do
"""Refresh translated text for the language child action."""
pass
class LanguageAction(AppQAction):
"""Handle the language action."""
def __init__(self, **kwargs):
"""Initialize the LanguageAction."""
super().__init__(
_('Language'),
icon=bootstrapIcon('globe2.svg'),
+10
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for routing."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,12 +37,15 @@ _TRANSLATABLE_BUILTIN_ROUTING = [
class RoutingChildAction(AppQAction):
"""Handle the routing child action."""
def __init__(self, *args, **kwargs):
"""Initialize the RoutingChildAction."""
self.routingValue = kwargs.pop('routingValue', None)
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
textEnglish = self.routingValue or self.textEnglish
if AppSettings.get('Routing') != textEnglish:
@@ -51,7 +56,9 @@ class RoutingChildAction(AppQAction):
class RoutingAction(AppQAction):
"""Handle the routing action."""
def __init__(self, **kwargs):
"""Initialize the RoutingAction."""
if AppSettings.get('Routing') == 'Bypass':
# Update value for backward compatibility
AppSettings.set('Routing', AppBuiltinRouting.BypassMainlandChina.value)
@@ -66,6 +73,7 @@ class RoutingAction(AppQAction):
self.rebuildMenu()
def routingActions(self):
"""Return the routing actions value used by the routing action."""
actions = list(
RoutingChildAction(
_(routing.value),
@@ -95,6 +103,7 @@ class RoutingAction(AppQAction):
return actions
def rebuildMenu(self):
"""Handle rebuild menu for the routing action."""
self._menu.clear()
self._menu._actions.clear()
self._actionGroup = AppQActionGroup(self)
@@ -110,4 +119,5 @@ class RoutingAction(AppQAction):
def getGlobalAction(self):
# 2nd action
"""Return global action."""
return self._menu.actions()[1]
+11
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for settings."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -53,7 +55,9 @@ _TRANSLATABLE_TUN_MODE = [
class TUNModeAction(AppQAction):
"""Handle the TUN mode action."""
def __init__(self, **kwargs):
"""Initialize the TUNModeAction."""
if PLATFORM == 'Linux':
super().__init__(_('TUN Mode'), **kwargs)
else:
@@ -67,6 +71,7 @@ class TUNModeAction(AppQAction):
self.setDisabled(True)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
if PLATFORM != 'Linux':
assert SystemRuntime.isAdmin()
@@ -79,10 +84,13 @@ class TUNModeAction(AppQAction):
class SettingsChildAction(AppQAction):
"""Handle the settings child action."""
def __init__(self, *args, **kwargs):
"""Initialize the SettingsChildAction."""
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
if self.textCompare('Dark Mode'):
# Settings turn on/off order matters here
if checked:
@@ -170,7 +178,9 @@ class SettingsChildAction(AppQAction):
class SettingsAction(AppQAction):
"""Handle the settings action."""
def __init__(self, **kwargs):
"""Initialize the SettingsAction."""
if SystemRuntime.flatpakID():
tunActions = []
else:
@@ -263,4 +273,5 @@ class SettingsAction(AppQAction):
def getTUNModeAction(self) -> AppQAction:
# 1st action
"""Return TUN mode action."""
return self._menu.actions()[0]
+7
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Implement tray actions for system proxy."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -29,10 +31,13 @@ registerAppSettings(
class SystemProxyChildAction(AppQAction):
"""Handle the system proxy child action."""
def __init__(self, *args, **kwargs):
"""Initialize the SystemProxyChildAction."""
super().__init__(*args, **kwargs)
def triggeredCallback(self, checked):
"""Handle activation of the action."""
if self.textCompare('Automatically Configure System Proxy'):
AppSettings.set('SystemProxyMode', AppBuiltinProxyMode.Auto.value)
elif self.textCompare('Do Not Change System Proxy'):
@@ -40,7 +45,9 @@ class SystemProxyChildAction(AppQAction):
class SystemProxyAction(AppQAction):
"""Handle the system proxy action."""
def __init__(self, **kwargs):
"""Initialize the SystemProxyAction."""
super().__init__(
_('System Proxy'),
icon=bootstrapIcon('hdd-network.svg'),
+2
View File
@@ -15,6 +15,8 @@
# 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 system-tray actions used by the application."""
from __future__ import annotations
from .Connect import *
+8
View File
@@ -15,6 +15,8 @@
# 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 process utilities for app main process."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -43,7 +45,9 @@ else:
class AppMainProcess(ProcessContext.Process):
"""Represent app main process."""
def __init__(self, func: Callable[[], ApplicationFactory], **kwargs):
"""Initialize the AppMainProcess."""
super().__init__(**kwargs)
self.startupTime = str(datetime.datetime.now()).replace(':', '')
@@ -54,6 +58,7 @@ class AppMainProcess(ProcessContext.Process):
self.application = None
def exceptHook(self, exceptionType, exceptionValue, tb):
"""Handle except hook for the app main process."""
if logger.level < logging.CRITICAL:
traceback.print_exception(exceptionType, exceptionValue, tb)
@@ -72,6 +77,7 @@ class AppMainProcess(ProcessContext.Process):
sys.exit(exitcode)
def saveCrashLog(self, exceptionType, exceptionValue, tb):
"""Save crash log."""
try:
os.mkdir(CRASH_LOG_DIR)
except FileExistsError:
@@ -104,11 +110,13 @@ class AppMainProcess(ProcessContext.Process):
pass
def handler(self, signum, frame):
"""Handle r."""
logger.info(f'received signal {signal.Signals(signum).name}. Exit application')
self.application.exit()
def run(self):
"""Run the app main process task."""
sys.excepthook = self.exceptHook
self.application = self.func()
+2
View File
@@ -15,6 +15,8 @@
# 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 process utilities."""
from __future__ import annotations
from .AppMainProcess import *
+2
View File
@@ -15,6 +15,8 @@
# 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."""
from __future__ import annotations
__version__ = '0.7.0'
+43
View File
@@ -15,6 +15,8 @@
# 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 widgets for application."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -48,36 +50,46 @@ registerAppSettings('LogViewerWidgetPointSizeTun_')
class SystemTrayUnavailable(Exception):
"""Represent system tray unavailable."""
pass
class AppLogHandler(logging.Handler):
"""Represent app log handler."""
def __init__(self, emitCallback):
"""Initialize the AppLogHandler."""
super().__init__()
self.emitCallback = emitCallback
def emit(self, record):
"""Emit the current app log handler event or message."""
if callable(self.emitCallback):
self.emitCallback(self.format(record))
class ApplicationExitHelper(QApplication):
"""Represent application exit helper."""
def __init__(self, argv):
"""Initialize the ApplicationExitHelper."""
super().__init__(argv)
# Exiting flag
self._exiting = False
def setExitingFlag(self, value: bool):
"""Set exiting flag."""
self._exiting = value
def isExiting(self) -> bool:
"""Return whether exiting."""
return self._exiting is True
class SingletonApplication(ApplicationExitHelper):
"""Represent singleton application."""
def __init__(self, argv):
"""Initialize the SingletonApplication."""
super().__init__(argv)
self.serverName = LOCAL_SERVER_NAME
@@ -86,6 +98,7 @@ class SingletonApplication(ApplicationExitHelper):
self.server = QLocalServer(self)
def hasRunningApp(self) -> bool:
"""Return whether running app."""
self.socket.connectToServer(self.serverName)
if self.socket.waitForConnected(1000):
@@ -134,18 +147,23 @@ class SingletonApplication(ApplicationExitHelper):
@QtCore.Slot()
def handleNewConnection(self):
"""Handle new connection."""
raise NotImplementedError
class ApplicationThemeDetector(QtCore.QObject):
"""Represent application theme detector."""
themeChanged = QtCore.Signal(str)
def __init__(self, *args, **kwargs):
"""Initialize the ApplicationThemeDetector."""
super().__init__(*args, **kwargs)
class Application(ApplicationFactory, SingletonApplication):
"""Represent application."""
def __init__(self, argv):
"""Initialize the Application."""
super().__init__(argv)
self.setApplicationName(APPLICATION_NAME)
@@ -187,11 +205,13 @@ class Application(ApplicationFactory, SingletonApplication):
@callRateLimited(maxCallPerSecond=2)
@QtCore.Slot()
def handleNewConnection(self):
"""Handle new connection."""
socket = self.server.nextPendingConnection()
socket.readyRead.connect(functools.partial(self.handleNewData, socket))
@QtCore.Slot(QLocalSocket)
def handleNewData(self, socket: QLocalSocket):
"""Handle new data."""
data = socket.readAll().data()
if isinstance(data, bytes):
@@ -216,6 +236,7 @@ class Application(ApplicationFactory, SingletonApplication):
pass
def configureLogging(self):
"""Configure logging."""
self.logViewerWindowSelf = LogViewerWindow(
tabTitle=_('Furious Log'),
fontFamily=self.customFontName,
@@ -245,6 +266,7 @@ class Application(ApplicationFactory, SingletonApplication):
logging.raiseExceptions = False
def addCustomFont(self):
"""Add custom font."""
fontFile = str(DATA_DIR / 'font' / 'CascadiaMono')
fontName = 'Cascadia Mono'
@@ -258,6 +280,7 @@ class Application(ApplicationFactory, SingletonApplication):
self.customFontLoadMsg = f'custom font {fontName} load failed'
def configureApplicationFont(self):
"""Configure application font."""
font = self.font()
font.setPointSize(AppStyleSheet.FontPointSize)
@@ -266,6 +289,7 @@ class Application(ApplicationFactory, SingletonApplication):
@staticmethod
def addEnviron():
# Xray environment variables
"""Add environ."""
os.environ['XRAY_LOCATION_ASSET'] = str(XRAY_ASSET_DIR)
if SystemRuntime.flatpakID():
@@ -276,23 +300,27 @@ class Application(ApplicationFactory, SingletonApplication):
def addStorage(self):
# Protected storage access
"""Add storage."""
self._userActivatedItemIndex = Storage.UserActivatedItemIndex()
self._userServers = Storage.UserServers()
self._userSubs = Storage.UserSubs()
self._userTUNSettings = Storage.UserTUNSettings()
def isSystemTrayConnected(self):
"""Return whether system tray connected."""
if isinstance(self.systemTray, SystemTrayIcon):
return self.systemTray.ConnectAction.isConnected()
else:
return False
def isDarkMode(self):
"""Return whether dark mode."""
backgroudColor = self.palette().color(QPalette.ColorRole.Window)
return backgroudColor.lightness() < 128
def systemTheme(self) -> str:
"""Return the system theme value used by the application."""
try:
theme = darkdetect.theme()
@@ -314,23 +342,28 @@ class Application(ApplicationFactory, SingletonApplication):
# else:
# return self.isDarkMode()
"""Return whether dark mode enabled."""
return AppSettings.isStateON_('DarkMode')
def theme(self):
"""Return the theme value used by the application."""
if self.isDarkModeEnabled():
return AppStyleSheet.Dark
return self.systemTheme()
def applyStyleSheetForTheme(self, theme):
"""Handle apply style sheet for theme for the application."""
self.setStyleSheet(AppStyleSheet.forTheme(theme))
def switchToDarkMode(self):
"""Handle switch to dark mode for the application."""
self.applyStyleSheetForTheme(AppStyleSheet.Dark)
Mixins.ThemeAware.callThemeChangedCallbackUnchecked(AppStyleSheet.Dark)
def switchToAutoMode(self):
"""Handle switch to auto mode for the application."""
theme = self.systemTheme()
self.applyStyleSheetForTheme(theme)
@@ -339,6 +372,7 @@ class Application(ApplicationFactory, SingletonApplication):
@QtCore.Slot(str)
def handleSystemThemeChanged(self, theme):
"""Handle system theme changed."""
if theme not in [AppStyleSheet.Dark, AppStyleSheet.Light]:
theme = self.systemTheme()
@@ -351,6 +385,7 @@ class Application(ApplicationFactory, SingletonApplication):
@callOnceOnly
@QtCore.Slot()
def cleanup():
"""Release resources owned by the application."""
Mixins.CleanupOnExit.cleanupAll()
if AppSettings.get('SystemProxyMode') == AppBuiltinProxyMode.Auto.value:
@@ -364,6 +399,7 @@ class Application(ApplicationFactory, SingletonApplication):
@staticmethod
def setDockIconVisible(visible: bool):
"""Set dock icon visible."""
if PLATFORM != 'Darwin':
return
@@ -375,6 +411,7 @@ class Application(ApplicationFactory, SingletonApplication):
def eventFilter(self, watched, event):
# Show Dock icon on macOS when window is shown
# and hide only when window is closed (not minimized)
"""Return the event filter value used by the application."""
if PLATFORM == 'Darwin' and watched is self.mainWindow:
if event.type() == QtCore.QEvent.Type.Show:
self.setDockIconVisible(True)
@@ -386,6 +423,7 @@ class Application(ApplicationFactory, SingletonApplication):
return super().eventFilter(watched, event)
def installDockIconVisibilityFeature(self, remove=False):
"""Handle install dock icon visibility feature for the application."""
if remove:
self.mainWindow.removeEventFilter(self)
self.setDockIconVisible(True)
@@ -397,6 +435,7 @@ class Application(ApplicationFactory, SingletonApplication):
self.setDockIconVisible(False)
def exit(self, exitcode=0):
"""Shut down and exit the application."""
self.setExitingFlag(True)
self.threadPool.clear()
@@ -408,6 +447,7 @@ class Application(ApplicationFactory, SingletonApplication):
super().exit(exitcode)
def run(self):
"""Run the application task."""
try:
if self.hasRunningApp():
# See: https://github.com/python/cpython/issues/79908
@@ -468,6 +508,7 @@ class Application(ApplicationFactory, SingletonApplication):
@QtCore.Slot()
def handleTimeout():
"""Handle timeout."""
currentTheme = self.systemTheme()
if self.currentTheme != currentTheme:
@@ -483,6 +524,7 @@ class Application(ApplicationFactory, SingletonApplication):
logger.info('theme detect method uses listener implementation')
def listener(*args, **kwargs):
"""Handle listener for the application."""
try:
darkdetect.listener(*args, **kwargs)
except NotImplementedError:
@@ -524,6 +566,7 @@ class Application(ApplicationFactory, SingletonApplication):
self.installDockIconVisibilityFeature()
def onApplicationStateChange(state):
"""Handle on application state change for the application."""
if state == QtCore.Qt.ApplicationState.ApplicationActive:
if (
not self.mainWindow.isVisible()
+18
View File
@@ -15,6 +15,8 @@
# 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 widgets for connect progress bar."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -28,7 +30,9 @@ __all__ = ['ConnectProgressBar']
class AutoUpdateProgressBar(Mixins.ConnectionAware, QProgressBar):
"""Represent auto update progress bar."""
def __init__(self, **kwargs):
"""Initialize the AutoUpdateProgressBar."""
super().__init__(**kwargs)
self.setRange(0, 100)
@@ -36,6 +40,7 @@ class AutoUpdateProgressBar(Mixins.ConnectionAware, QProgressBar):
@QtCore.Slot()
def update():
# Update the progress bar value
"""Update the auto update progress bar."""
if self.value() < 90:
self.setValue(self.value() + 1)
@@ -49,13 +54,16 @@ class AutoUpdateProgressBar(Mixins.ConnectionAware, QProgressBar):
self.setStyleSheet(self.getStyleSheet())
def start(self, msec: int):
"""Start the auto update progress bar."""
self.timer.start(msec)
def stop(self):
"""Stop the auto update progress bar."""
self.timer.stop()
@staticmethod
def getStyleSheet():
"""Return style sheet."""
return (
f'QProgressBar {{'
f' border-radius: 2px;'
@@ -73,14 +81,18 @@ class AutoUpdateProgressBar(Mixins.ConnectionAware, QProgressBar):
)
def disconnectedCallback(self):
"""Update the auto update progress bar for a disconnected state."""
pass
def connectedCallback(self):
"""Update the auto update progress bar for a connected state."""
pass
class ConnectProgressBar(Mixins.QTranslatable, Mixins.ConnectionAware, QWidget):
"""Provide the connect progress bar widget."""
def __init__(self, parent=None):
"""Initialize the ConnectProgressBar."""
super().__init__(parent)
self.setWindowTitle(_(APPLICATION_NAME))
@@ -97,19 +109,25 @@ class ConnectProgressBar(Mixins.QTranslatable, Mixins.ConnectionAware, QWidget):
self.setLayout(self._layout)
def setValue(self, value: int):
"""Set value."""
self._widget.setValue(value)
def start(self, msec: int):
"""Start the connect progress bar."""
self._widget.start(msec)
def stop(self):
"""Stop the connect progress bar."""
self._widget.stop()
def disconnectedCallback(self):
"""Update the connect progress bar for a disconnected state."""
self.setWindowIcon(AppHue.disconnectedWindowIcon())
def connectedCallback(self):
"""Update the connect progress bar for a connected state."""
self.setWindowIcon(AppHue.connectedWindowIcon())
def retranslate(self):
"""Refresh translated text for the connect progress bar."""
self.setWindowTitle(_(self.windowTitle()))
@@ -15,6 +15,8 @@
# 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 widgets for GUI customize network test."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -36,7 +38,9 @@ registerAppSettings('CustomNetworkConnectivityTestURL')
class GuiCustomizeNetworkTestDialog(AppQDialog):
"""Present the GUI customize network test dialog."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiCustomizeNetworkTestDialog."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Customize Network Test URL'))
@@ -100,9 +104,11 @@ class GuiCustomizeNetworkTestDialog(AppQDialog):
self.finished.connect(functools.partial(self.handleResultCode))
def setWidthAndHeight(self):
"""Apply the default size for the GUI customize network test dialog."""
self.resize(656, 180)
def handleResultCode(self, code):
"""Handle result code."""
for widget, info, settingsName, reset in [
(
self.speedTestURLEdit,
@@ -15,6 +15,8 @@
# 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 widgets for GUI customize proxy bypass."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,7 +37,9 @@ registerAppSettings('CustomProxyBypass')
class GuiCustomizeProxyBypassDialog(AppQDialog):
"""Present the GUI customize proxy bypass dialog."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiCustomizeProxyBypassDialog."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Customize Proxy Bypass Address'))
@@ -79,14 +83,17 @@ class GuiCustomizeProxyBypassDialog(AppQDialog):
self.finished.connect(functools.partial(self.handleResultCode))
def setWidthAndHeight(self):
"""Apply the default size for the GUI customize proxy bypass dialog."""
self.resize(656, 125)
def handleResultCode(self, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
oldValue = AppSettings.get('CustomProxyBypass')
newValue = self.proxyBypassEdit.text()
def writeNewSettings(value):
"""Handle write new settings for the GUI customize proxy bypass dialog."""
logger.info(f'setting custom proxy bypass address to \'{value}\'')
AppSettings.set('CustomProxyBypass', value)
@@ -116,4 +123,5 @@ class GuiCustomizeProxyBypassDialog(AppQDialog):
@QtCore.Slot()
def handleResetButtonClicked(self):
"""Handle reset button clicked."""
self.proxyBypassEdit.setText(PROXY_SERVER_BYPASS)
+46
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI hysteria1."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,7 +37,9 @@ logger = logging.getLogger(__name__)
class GuiHy1ItemTextInput(GuiEditorItemTextInput):
"""Represent GUI hy1 item text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemTextInput."""
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
@@ -43,6 +47,7 @@ class GuiHy1ItemTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldValue = config.get(self.key, '')
newValue = self.text()
@@ -63,6 +68,7 @@ class GuiHy1ItemTextInput(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.get(self.key, ''))
except Exception:
@@ -72,12 +78,15 @@ class GuiHy1ItemTextInput(GuiEditorItemTextInput):
class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
"""Represent GUI hy1 item basic protocol."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemBasicProtocol."""
super().__init__(*args, **kwargs)
self.addItems(['', 'udp', 'wechat-video', 'faketcp'])
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldProtocol = config.get('protocol', '')
newProtocol = self.text()
@@ -94,6 +103,7 @@ class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.get('protocol', 'udp'))
except Exception:
@@ -103,13 +113,16 @@ class GuiHy1ItemBasicProtocol(GuiEditorItemTextComboBox):
class GuiHy1ItemSpeedUpMbps(GuiEditorItemTextSpinBox):
"""Represent GUI hy1 item speed up mbps."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemSpeedUpMbps."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 1048576)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldUpMbps = config.get('up_mbps')
newUpMbps = self.value()
@@ -126,6 +139,7 @@ class GuiHy1ItemSpeedUpMbps(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setValue(config.get('up_mbps'))
except Exception:
@@ -135,13 +149,16 @@ class GuiHy1ItemSpeedUpMbps(GuiEditorItemTextSpinBox):
class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox):
"""Represent GUI hy1 item speed down mbps."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemSpeedDownMbps."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 1048576)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldDownMbps = config.get('down_mbps')
newDownMbps = self.value()
@@ -158,6 +175,7 @@ class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setValue(config.get('down_mbps'))
except Exception:
@@ -167,10 +185,13 @@ class GuiHy1ItemSpeedDownMbps(GuiEditorItemTextSpinBox):
class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox):
"""Represent GUI hy1 item TLS insecure."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ItemTLSInsecure."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
try:
oldChecked = config.get('insecure')
except Exception:
@@ -202,6 +223,7 @@ class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
checked = config['insecure']
except Exception:
@@ -216,13 +238,16 @@ class GuiHy1ItemTLSInsecure(GuiEditorItemTextCheckBox):
class GuiHy1ProjectWebsiteURL(AppQLabel):
"""Represent GUI hy1 project website URL."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy1ProjectWebsiteURL."""
super().__init__(*args, **kwargs)
self.setWebsiteURL()
self.linkActivated.connect(self.handleLinkActivated)
def setWebsiteURL(self):
"""Set website URL."""
self.setText(
'<html><head/><body><p>'
'<a href=\"https://v1.hysteria.network/\">'
@@ -233,20 +258,25 @@ class GuiHy1ProjectWebsiteURL(AppQLabel):
@staticmethod
def handleLinkActivated(link: str):
"""Handle link activated."""
if QDesktopServices.openUrl(QtCore.QUrl(link)):
logger.info(f'open link \'{link}\' success')
else:
logger.error(f'open link \'{link}\' failed')
def retranslate(self):
"""Refresh translated text for the GUI hy1 project website URL."""
self.setWebsiteURL()
class GuiHy1GroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI hy1 group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiHy1GroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiHy1ItemTextInput(title=_('Server'), key='server'),
@@ -257,10 +287,13 @@ class GuiHy1GroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiHy1GroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI hy1 group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiHy1GroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -268,10 +301,13 @@ class GuiHy1GroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiHy1GroupBoxSpeed(GuiEditorWidgetQGroupBox):
"""Represent GUI hy1 group box speed."""
def __init__(self, **kwargs):
"""Initialize the GuiHy1GroupBoxSpeed."""
super().__init__(_('Speed'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy1ItemSpeedUpMbps(title='up_mbps', translatable=False),
GuiHy1ItemSpeedDownMbps(title='down_mbps', translatable=False),
@@ -279,12 +315,15 @@ class GuiHy1GroupBoxSpeed(GuiEditorWidgetQGroupBox):
class GuiHy1GroupBoxTLS(GuiEditorWidgetQGroupBox):
"""Represent GUI hy1 group box TLS."""
def __init__(self, **kwargs):
"""Initialize the GuiHy1GroupBoxTLS."""
super().__init__('TLS', **kwargs)
self.translatable = False
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy1ItemTextInput(title='sni', translatable=False, key='server_name'),
GuiHy1ItemTextInput(title='alpn', translatable=False, key='alpn'),
@@ -294,7 +333,9 @@ class GuiHy1GroupBoxTLS(GuiEditorWidgetQGroupBox):
class GuiHy1GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
"""Represent GUI hy1 group box other."""
def __init__(self, **kwargs):
"""Initialize the GuiHy1GroupBoxOther."""
super().__init__(_('Other'), **kwargs)
self._website = GuiHy1ProjectWebsiteURL()
@@ -305,20 +346,25 @@ class GuiHy1GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
self.setLayout(layout)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
pass
class GuiHysteria1(GuiEditorWidgetQDialog):
"""Represent GUI hysteria1."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHysteria1."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Hysteria1.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiHy1GroupBoxBasic(),
GuiHy1GroupBoxProxy(),
+79
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI hysteria2."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,10 +39,13 @@ HY2_OBFS_TYPES = ['', 'salamander', 'gecko']
class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
"""Represent GUI hy2 item basic server."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemBasicServer."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldServer = config.get('server', '')
newServer = self.text()
@@ -57,6 +62,7 @@ class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.get('server', ''))
except Exception:
@@ -66,10 +72,13 @@ class GuiHy2ItemBasicServer(GuiEditorItemTextInput):
class GuiHy2ItemBasicAuth(GuiEditorItemTextInput):
"""Represent GUI hy2 item basic auth."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemBasicAuth."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldAuth = config.get('auth')
newAuth = self.text()
@@ -94,6 +103,7 @@ class GuiHy2ItemBasicAuth(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
self.setText(config.get('auth', ''))
except Exception:
@@ -103,9 +113,11 @@ class GuiHy2ItemBasicAuth(GuiEditorItemTextInput):
class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
"""Represent GUI hy2 item basic congestion combo box."""
CONGESTION_KEYS = ['type', 'bbrProfile']
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemBasicCongestionComboBox."""
key = kwargs.pop('key', '')
assert key in self.CONGESTION_KEYS
@@ -125,6 +137,7 @@ class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
raise
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
newValue = self.text()
if newValue == '':
@@ -179,6 +192,7 @@ class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
value = config['congestion'][self.key]
except Exception:
@@ -190,12 +204,15 @@ class GuiHy2ItemBasicCongestionComboBox(GuiEditorItemTextComboBox):
class GuiHy2ItemObfsType(GuiEditorItemTextComboBox):
"""Represent GUI hy2 item obfs type."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemObfsType."""
super().__init__(*args, **kwargs)
self.addItems(HY2_OBFS_TYPES)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
try:
oldObfsType = config['obfs']['type']
except Exception:
@@ -229,6 +246,7 @@ class GuiHy2ItemObfsType(GuiEditorItemTextComboBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
obfsType = config['obfs']['type']
except Exception:
@@ -243,12 +261,15 @@ class GuiHy2ItemObfsType(GuiEditorItemTextComboBox):
class GuiHy2ItemObfsPassword(GuiEditorItemTextInput):
"""Represent GUI hy2 item obfs password."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemObfsPassword."""
self.obfsType = kwargs.pop('obfsType', 'salamander')
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
newObfsPassword = self.text()
try:
@@ -288,6 +309,7 @@ class GuiHy2ItemObfsPassword(GuiEditorItemTextInput):
return modified
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
obfsType = config['obfs']['type']
except Exception:
@@ -311,7 +333,9 @@ class GuiHy2ItemObfsPassword(GuiEditorItemTextInput):
class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
"""Represent GUI hy2 item obfs packet size."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemObfsPacketSize."""
self.obfsType, self.key, self.default = (
kwargs.pop('obfsType', 'gecko'),
kwargs.pop('key', ''),
@@ -324,6 +348,7 @@ class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
self.setValue(self.default)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
try:
obfsType = config['obfs']['type']
except Exception:
@@ -351,6 +376,7 @@ class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
obfsType = config['obfs']['type']
except Exception:
@@ -377,16 +403,20 @@ class GuiHy2ItemObfsPacketSize(GuiEditorItemTextSpinBox):
class GuiHy2PageObfsXXX(GuiEditorWidgetQWidget):
"""Represent GUI hy2 page obfs xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2PageObfsXXX."""
super().__init__(*args, **kwargs)
def setObfsTypeText(self, text: str):
"""Set obfs type text."""
obfsType = self._containers[0]
if isinstance(obfsType, GuiEditorItemTextComboBox):
obfsType.setText(text)
def connectActivated(self, func):
"""Connect activated."""
obfsType = self._containers[0]
if isinstance(obfsType, GuiEditorItemTextComboBox):
@@ -394,14 +424,18 @@ class GuiHy2PageObfsXXX(GuiEditorWidgetQWidget):
class GuiHy2PageObfsEmpty(GuiHy2PageObfsXXX):
"""Represent GUI hy2 page obfs empty."""
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
]
class GuiHy2PageObfsSalamander(GuiHy2PageObfsXXX):
"""Represent GUI hy2 page obfs salamander."""
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
GuiHy2ItemObfsPassword(
@@ -413,7 +447,9 @@ class GuiHy2PageObfsSalamander(GuiHy2PageObfsXXX):
class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
"""Represent GUI hy2 page obfs gecko."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2PageObfsGecko."""
super().__init__(*args, **kwargs)
self.minPacketSizeItem = self._containers[2]
@@ -425,6 +461,7 @@ class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
self.handleMinPacketSizeChanged(self.minPacketSizeItem.value())
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy2ItemObfsType(title='obfs-type', translatable=False),
GuiHy2ItemObfsPassword(
@@ -447,6 +484,7 @@ class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
]
def handleMinPacketSizeChanged(self, value: int):
"""Handle min packet size changed."""
self.maxPacketSizeItem._input.setMinimum(value)
if self.maxPacketSizeItem.value() < value:
@@ -454,7 +492,9 @@ class GuiHy2PageObfsGecko(GuiHy2PageObfsXXX):
class GuiHy2ObfsPageStackedWidget(QStackedWidget):
"""Provide the GUI hy2 obfs page stacked widget."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ObfsPageStackedWidget."""
super().__init__(*args, **kwargs)
self._pages = [
@@ -467,15 +507,19 @@ class GuiHy2ObfsPageStackedWidget(QStackedWidget):
self.addWidget(page)
def page(self, index: int) -> GuiHy2PageObfsXXX:
"""Return the page value."""
return self._pages[index]
def connectActivated(self, func):
"""Connect activated."""
for page in self._pages:
page.connectActivated(func)
class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
"""Represent GUI hy2 item TLS text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemTLSTextInput."""
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
@@ -483,6 +527,7 @@ class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
newValue = self.text()
if newValue == '':
@@ -539,6 +584,7 @@ class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
value = config['tls'][self.key]
except Exception:
@@ -550,10 +596,13 @@ class GuiHy2ItemTLSTextInput(GuiEditorItemTextInput):
class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox):
"""Represent GUI hy2 item TLS insecure."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ItemTLSInsecure."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
try:
oldChecked = config['tls']['insecure']
except Exception:
@@ -585,6 +634,7 @@ class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
checked = config['tls']['insecure']
except Exception:
@@ -599,10 +649,13 @@ class GuiHy2ItemTLSInsecure(GuiEditorItemTextCheckBox):
class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI hy2 group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiHy2GroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiHy2ItemBasicServer(title=_('Server')),
@@ -617,10 +670,13 @@ class GuiHy2GroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiHy2GroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI hy2 group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiHy2GroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -628,7 +684,9 @@ class GuiHy2GroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
"""Represent GUI hy2 group box obfs."""
def __init__(self, **kwargs):
"""Initialize the GuiHy2GroupBoxObfs."""
translatable = kwargs.pop('translatable', False)
super().__init__('obfs', **kwargs, translatable=translatable)
@@ -644,15 +702,19 @@ class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
self.setLayout(layout)
def currentIndex(self) -> int:
"""Return the current index value."""
return self._widget.currentIndex()
def setCurrentIndex(self, index: int):
"""Set current index."""
self._widget.setCurrentIndex(index)
def page(self, index: int) -> GuiHy2PageObfsXXX:
"""Return the page value."""
return self._widget.page(index)
def handleActivated(self, index: int):
"""Handle activated."""
page = self.page(index)
page.factoryToInput(self._config)
page.setObfsTypeText(HY2_OBFS_TYPES[index])
@@ -660,6 +722,7 @@ class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
self.setCurrentIndex(index)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
oldObfs = config.get('obfs')
oldObfsType = ''
@@ -694,6 +757,7 @@ class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
return modified
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
self._config = config
try:
@@ -714,12 +778,15 @@ class GuiHy2GroupBoxObfs(GuiEditorItemFactory, AppQGroupBox):
class GuiHy2GroupBoxTLS(GuiEditorWidgetQGroupBox):
"""Represent GUI hy2 group box TLS."""
def __init__(self, **kwargs):
"""Initialize the GuiHy2GroupBoxTLS."""
super().__init__('TLS', **kwargs)
self.translatable = False
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiHy2ItemTLSTextInput(title='sni', key='sni', translatable=False),
GuiHy2ItemTLSTextInput(
@@ -731,13 +798,16 @@ class GuiHy2GroupBoxTLS(GuiEditorWidgetQGroupBox):
class GuiHy2ProjectWebsiteURL(AppQLabel):
"""Represent GUI hy2 project website URL."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHy2ProjectWebsiteURL."""
super().__init__(*args, **kwargs)
self.setWebsiteURL()
self.linkActivated.connect(self.handleLinkActivated)
def setWebsiteURL(self):
"""Set website URL."""
self.setText(
'<html><head/><body><p>'
'<a href=\"https://v2.hysteria.network/\">'
@@ -748,17 +818,21 @@ class GuiHy2ProjectWebsiteURL(AppQLabel):
@staticmethod
def handleLinkActivated(link: str):
"""Handle link activated."""
if QDesktopServices.openUrl(QtCore.QUrl(link)):
logger.info(f'open link \'{link}\' success')
else:
logger.error(f'open link \'{link}\' failed')
def retranslate(self):
"""Refresh translated text for the GUI hy2 project website URL."""
self.setWebsiteURL()
class GuiHy2GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
"""Represent GUI hy2 group box other."""
def __init__(self, **kwargs):
"""Initialize the GuiHy2GroupBoxOther."""
super().__init__(_('Other'), **kwargs)
self._website = GuiHy2ProjectWebsiteURL()
@@ -769,20 +843,25 @@ class GuiHy2GroupBoxOther(GuiEditorItemFactory, AppQGroupBox):
self.setLayout(layout)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
pass
class GuiHysteria2(GuiEditorWidgetQDialog):
"""Represent GUI hysteria2."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiHysteria2."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Hysteria2.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiHy2GroupBoxBasic(),
GuiHy2GroupBoxProxy(),
+23
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI shadowsocks."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ getProxyOutboundServer = functools.partial(
class GuiSSItemTextInput(GuiEditorItemTextInput):
"""Represent GUI ss item text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSSItemTextInput."""
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
@@ -45,6 +49,7 @@ class GuiSSItemTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldValue = proxyOutboundServer.get(self.key, '')
@@ -67,6 +72,7 @@ class GuiSSItemTextInput(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -78,13 +84,16 @@ class GuiSSItemTextInput(GuiEditorItemTextInput):
class GuiSSItemBasicPort(GuiEditorItemTextSpinBox):
"""Represent GUI ss item basic port."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSSItemBasicPort."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
@@ -103,6 +112,7 @@ class GuiSSItemBasicPort(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -114,7 +124,9 @@ class GuiSSItemBasicPort(GuiEditorItemTextSpinBox):
class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
"""Represent GUI ss item basic method."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSSItemBasicMethod."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -135,6 +147,7 @@ class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldMethod = proxyOutboundServer.get('method', '')
@@ -153,6 +166,7 @@ class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -164,10 +178,13 @@ class GuiSSItemBasicMethod(GuiEditorItemTextComboBox):
class GuiSSGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI ss group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiSSGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiSSItemTextInput(title=_('Address'), key='address'),
@@ -178,10 +195,13 @@ class GuiSSGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiSSGroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI ss group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiSSGroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -189,13 +209,16 @@ class GuiSSGroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiShadowsocks(GuiEditorWidgetQDialog):
"""Represent GUI shadowsocks."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiShadowsocks."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Shadowsocks.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiSSGroupBoxBasic(),
GuiSSGroupBoxProxy(),
+19
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI SOCKS."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ getProxyOutboundServer = functools.partial(
class GuiSocksItemTextInput(GuiEditorItemTextInput):
"""Represent GUI SOCKS item text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSocksItemTextInput."""
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
@@ -45,6 +49,7 @@ class GuiSocksItemTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldValue = proxyOutboundServer.get(self.key, '')
@@ -66,6 +71,7 @@ class GuiSocksItemTextInput(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -77,12 +83,15 @@ class GuiSocksItemTextInput(GuiEditorItemTextInput):
class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
"""Represent GUI SOCKS item basic port."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSocksItemBasicPort."""
super().__init__(*args, **kwargs)
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
@@ -101,6 +110,7 @@ class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -112,10 +122,13 @@ class GuiSocksItemBasicPort(GuiEditorItemTextSpinBox):
class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI SOCKS group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiSocksGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiSocksItemTextInput(title=_('Address'), key='address'),
@@ -126,10 +139,13 @@ class GuiSocksGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiSocksGroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI SOCKS group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiSocksGroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -137,13 +153,16 @@ class GuiSocksGroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiSocks(GuiEditorWidgetQDialog):
"""Represent GUI SOCKS."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiSocks."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Socks.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiSocksGroupBoxBasic(),
GuiSocksGroupBoxProxy(),
+37
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI TUN settings."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,12 +37,15 @@ logger = logging.getLogger(__name__)
class GuiTUNSettingsItemXXX(GuiEditorItemTextInput):
"""Represent GUI TUN settings item xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTUNSettingsItemXXX."""
self.key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
oldItem = config.get(self.key, '')
newItem = self.text()
@@ -54,11 +59,14 @@ class GuiTUNSettingsItemXXX(GuiEditorItemTextInput):
return False
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
self.setText(config.get(self.key, ''))
class GuiTUNSettingsItemSpinBoxBufferSizeXXX(GuiEditorItemTextSpinBox):
"""Represent GUI TUN settings item spin box buffer size xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTUNSettingsItemSpinBoxBufferSizeXXX."""
self.key = kwargs.pop('key', '')
self.default = kwargs.pop('default', 1)
self.start = kwargs.pop('start', 1)
@@ -70,6 +78,7 @@ class GuiTUNSettingsItemSpinBoxBufferSizeXXX(GuiEditorItemTextSpinBox):
self.setRange(self.start, self.end)
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
oldValue = config.get(self.key, self.default)
newValue = self.value()
@@ -86,6 +95,7 @@ class GuiTUNSettingsItemSpinBoxBufferSizeXXX(GuiEditorItemTextSpinBox):
return False
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
oldValue = config.get(self.key, self.default)
if not isinstance(oldValue, int):
@@ -95,13 +105,16 @@ class GuiTUNSettingsItemSpinBoxBufferSizeXXX(GuiEditorItemTextSpinBox):
class GuiTUNSettingsItemCheckBoxXXX(GuiEditorItemTextCheckBox):
"""Represent GUI TUN settings item check box xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTUNSettingsItemCheckBoxXXX."""
self.key = kwargs.pop('key', '')
self.default = kwargs.pop('default', 'False')
super().__init__(*args, **kwargs)
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
oldItem = config.get(self.key, self.default)
newItem = self.isChecked()
@@ -119,17 +132,21 @@ class GuiTUNSettingsItemCheckBoxXXX(GuiEditorItemTextCheckBox):
return False
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
self.setChecked(config.get(self.key, self.default) == 'True')
class AppQLabelHelpPage(AppQLabel):
"""Represent app q label help page."""
def __init__(self, *args, **kwargs):
"""Initialize the AppQLabelHelpPage."""
super().__init__(*args, **kwargs)
self.setWebsiteURL()
self.linkActivated.connect(self.handleLinkActivated)
def setWebsiteURL(self):
"""Set website URL."""
self.setText(
f'<html><head/><body><p>'
f'<a href=\"{APPLICATION_ABOUT_PAGE}/wiki/TUN-Mode\">'
@@ -140,30 +157,38 @@ class AppQLabelHelpPage(AppQLabel):
@staticmethod
def handleLinkActivated(link: str):
"""Handle link activated."""
if QDesktopServices.openUrl(QtCore.QUrl(link)):
logger.info(f'open link \'{link}\' success')
else:
logger.error(f'open link \'{link}\' failed')
def retranslate(self):
"""Refresh translated text for the app q label help page."""
self.setWebsiteURL()
class GuiTUNSettingsItemHelpPage(GuiEditorItemWidgetContainer):
"""Provide the TUN settings item help configuration editor page."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTUNSettingsItemHelpPage."""
super().__init__(*args, **kwargs)
self._page = AppQLabelHelpPage()
def widgets(self):
"""Return the widgets owned by this editor item."""
return [self._page]
class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI TUN settings group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiTUNSettingsGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def setupPageLayout(self):
"""Set up page layout."""
layout = QFormLayout()
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
@@ -173,6 +198,7 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
pairsLayout.setColumnStretch(3, 1)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
pairsLayout.addWidget(label, row, column)
@@ -191,6 +217,7 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
return layout
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiTUNSettingsItemXXX(
title=_('Primary Adapter Interface Name'),
@@ -224,10 +251,13 @@ class GuiTUNSettingsGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiTUNSettingsGroupBoxMemory(GuiEditorWidgetQGroupBox):
"""Represent GUI TUN settings group box memory."""
def __init__(self, **kwargs):
"""Initialize the GuiTUNSettingsGroupBoxMemory."""
super().__init__(_('Memory Optimization'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiTUNSettingsItemSpinBoxBufferSizeXXX(
title=_('TCP Send Buffer Size (MiB)'),
@@ -252,7 +282,9 @@ class GuiTUNSettingsGroupBoxMemory(GuiEditorWidgetQGroupBox):
class GuiTUNSettings(GuiEditorWidgetQDialog):
"""Store and validate GUI TUN settings."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTUNSettings."""
tabTranslatable = kwargs.pop('tabTranslatable', True)
style = kwargs.pop('style', 'portrait')
@@ -279,6 +311,7 @@ class GuiTUNSettings(GuiEditorWidgetQDialog):
self.rejected.connect(functools.partial(self.handleRejected))
def handleAccepted(self, config: dict):
"""Handle accepted."""
modified = self.inputToFactory(config)
if modified and SystemRuntime.isTUNMode():
@@ -288,10 +321,12 @@ class GuiTUNSettings(GuiEditorWidgetQDialog):
self.rejected.disconnect()
def handleRejected(self):
"""Handle rejected."""
self.accepted.disconnect()
self.rejected.disconnect()
def inputToFactory(self, config: dict) -> bool:
"""Apply the current editor value to the configuration."""
modified = False
for groupBox in self.groupBoxSequence():
@@ -300,11 +335,13 @@ class GuiTUNSettings(GuiEditorWidgetQDialog):
return modified
def factoryToInput(self, config: dict):
"""Load the configuration value into the editor."""
for groupBox in self.groupBoxSequence():
groupBox.factoryToInput(config)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiTUNSettingsGroupBoxBasic(),
GuiTUNSettingsGroupBoxMemory(),
+19
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI trojan."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ getProxyOutboundServer = functools.partial(
class GuiTrojanItemTextInput(GuiEditorItemTextInput):
"""Represent GUI trojan item text input."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTrojanItemTextInput."""
key = kwargs.pop('key', '')
super().__init__(*args, **kwargs)
@@ -45,6 +49,7 @@ class GuiTrojanItemTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldValue = proxyOutboundServer.get(self.key, '')
@@ -67,6 +72,7 @@ class GuiTrojanItemTextInput(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -78,13 +84,16 @@ class GuiTrojanItemTextInput(GuiEditorItemTextInput):
class GuiTrojanItemBasicPort(GuiEditorItemTextSpinBox):
"""Represent GUI trojan item basic port."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTrojanItemBasicPort."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
@@ -103,6 +112,7 @@ class GuiTrojanItemBasicPort(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -114,10 +124,13 @@ class GuiTrojanItemBasicPort(GuiEditorItemTextSpinBox):
class GuiTrojanGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI trojan group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiTrojanGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiTrojanItemTextInput(title=_('Address'), key='address'),
@@ -127,10 +140,13 @@ class GuiTrojanGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiTrojanGroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI trojan group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiTrojanGroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -138,13 +154,16 @@ class GuiTrojanGroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiTrojan(GuiEditorWidgetQDialog):
"""Represent GUI trojan."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiTrojan."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.Trojan.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiTrojanGroupBoxBasic(),
GuiTrojanGroupBoxProxy(),
+37
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI VLESS."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -46,10 +48,13 @@ getProxyOutboundUser = functools.partial(
class GuiVLESSItemBasicAddress(GuiEditorItemTextInput):
"""Represent GUI VLESS item basic address."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicAddress."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldAddress = proxyOutboundServer.get('address', '')
@@ -72,6 +77,7 @@ class GuiVLESSItemBasicAddress(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -83,13 +89,16 @@ class GuiVLESSItemBasicAddress(GuiEditorItemTextInput):
class GuiVLESSItemBasicPort(GuiEditorItemTextSpinBox):
"""Represent GUI VLESS item basic port."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicPort."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
@@ -108,6 +117,7 @@ class GuiVLESSItemBasicPort(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -119,7 +129,9 @@ class GuiVLESSItemBasicPort(GuiEditorItemTextSpinBox):
class GuiVLESSItemBasicId(GuiEditorItemTextInput):
"""Represent GUI VLESS item basic id."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicId."""
super().__init__(*args, **kwargs)
self.generateButton = AppQPushButton(_('Generate'))
@@ -133,12 +145,15 @@ class GuiVLESSItemBasicId(GuiEditorItemTextInput):
layout.addWidget(self.generateButton)
def generateUUID(self):
"""Handle generate uuid for the GUI VLESS item basic id."""
self.setText(str(uuid.uuid4()))
def widgets(self):
"""Return the widgets owned by this editor item."""
return self._title, self._widget
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldId = proxyOutboundUser.get('id', '')
@@ -161,6 +176,7 @@ class GuiVLESSItemBasicId(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -172,10 +188,13 @@ class GuiVLESSItemBasicId(GuiEditorItemTextInput):
class GuiVLESSItemBasicEncryption(GuiEditorItemTextInput):
"""Represent GUI VLESS item basic encryption."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicEncryption."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldEncryption = proxyOutboundUser.get('encryption', '')
@@ -198,6 +217,7 @@ class GuiVLESSItemBasicEncryption(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -209,7 +229,9 @@ class GuiVLESSItemBasicEncryption(GuiEditorItemTextInput):
class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
"""Represent GUI VLESS item basic flow."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESSItemBasicFlow."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -222,6 +244,7 @@ class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldFlow = proxyOutboundUser.get('flow', '')
@@ -240,6 +263,7 @@ class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -251,15 +275,19 @@ class GuiVLESSItemBasicFlow(GuiEditorItemTextComboBox):
class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI VLESS group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiVLESSGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def setupPageLayout(self):
"""Set up page layout."""
layout = QGridLayout()
layout.setColumnStretch(1, 1)
layout.setColumnStretch(3, 1)
def keepSpinBoxCompact(widget: QWidget):
"""Handle keep spin box compact for the GUI VLESS group box basic."""
if isinstance(widget, QSpinBox):
widget.setSizePolicy(
QSizePolicy.Policy.Fixed,
@@ -267,6 +295,7 @@ class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
@@ -275,6 +304,7 @@ class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
@@ -294,6 +324,7 @@ class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
return layout
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiVLESSItemBasicAddress(title=_('Address')),
@@ -305,10 +336,13 @@ class GuiVLESSGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiVLESSGroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI VLESS group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiVLESSGroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -316,13 +350,16 @@ class GuiVLESSGroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiVLESS(GuiEditorWidgetQDialog):
"""Represent GUI VLESS."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVLESS."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.VLESS.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiVLESSGroupBoxBasic(),
GuiVLESSGroupBoxProxy(),
+38
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI v mess."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -46,10 +48,13 @@ getProxyOutboundUser = functools.partial(
class GuiVMessItemBasicAddress(GuiEditorItemTextInput):
"""Represent GUI v mess item basic address."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicAddress."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldAddress = proxyOutboundServer.get('address', '')
@@ -72,6 +77,7 @@ class GuiVMessItemBasicAddress(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -83,13 +89,16 @@ class GuiVMessItemBasicAddress(GuiEditorItemTextInput):
class GuiVMessItemBasicPort(GuiEditorItemTextSpinBox):
"""Represent GUI v mess item basic port."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicPort."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundServer = getProxyOutboundServer(config)
oldPort = proxyOutboundServer.get('port')
@@ -108,6 +117,7 @@ class GuiVMessItemBasicPort(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundServer = getProxyOutboundServer(config)
@@ -119,7 +129,9 @@ class GuiVMessItemBasicPort(GuiEditorItemTextSpinBox):
class GuiVMessItemBasicId(GuiEditorItemTextInput):
"""Represent GUI v mess item basic id."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicId."""
super().__init__(*args, **kwargs)
self.generateButton = AppQPushButton(_('Generate'))
@@ -133,12 +145,15 @@ class GuiVMessItemBasicId(GuiEditorItemTextInput):
layout.addWidget(self.generateButton)
def generateUUID(self):
"""Handle generate uuid for the GUI v mess item basic id."""
self.setText(str(uuid.uuid4()))
def widgets(self):
"""Return the widgets owned by this editor item."""
return self._title, self._widget
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldId = proxyOutboundUser.get('id', '')
@@ -161,6 +176,7 @@ class GuiVMessItemBasicId(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -172,13 +188,16 @@ class GuiVMessItemBasicId(GuiEditorItemTextInput):
class GuiVMessItemBasicAlterId(GuiEditorItemTextSpinBox):
"""Represent GUI v mess item basic alter id."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicAlterId."""
super().__init__(*args, **kwargs)
# Range
self.setRange(0, 65535)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldAlterId = proxyOutboundUser.get('alterId')
@@ -197,6 +216,7 @@ class GuiVMessItemBasicAlterId(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -208,7 +228,9 @@ class GuiVMessItemBasicAlterId(GuiEditorItemTextSpinBox):
class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
"""Represent GUI v mess item basic security."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMessItemBasicSecurity."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -223,6 +245,7 @@ class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
proxyOutboundUser = getProxyOutboundUser(config)
oldSecurity = proxyOutboundUser.get('security', '')
@@ -241,6 +264,7 @@ class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
proxyOutboundUser = getProxyOutboundUser(config)
@@ -252,14 +276,18 @@ class GuiVMessItemBasicSecurity(GuiEditorItemTextComboBox):
class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
"""Represent GUI v mess group box basic."""
def __init__(self, **kwargs):
"""Initialize the GuiVMessGroupBoxBasic."""
super().__init__(_('Basic Configuration'), **kwargs)
def setupPageLayout(self):
"""Set up page layout."""
layout = QGridLayout()
layout.setColumnStretch(4, 1)
def keepSpinBoxCompact(widget: QWidget):
"""Handle keep spin box compact for the GUI v mess group box basic."""
if isinstance(widget, QSpinBox):
widget.setSizePolicy(
QSizePolicy.Policy.Fixed,
@@ -267,6 +295,7 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
)
def keepComboBoxCompact(widget: QWidget):
"""Handle keep combo box compact for the GUI v mess group box basic."""
if isinstance(widget, QComboBox):
widget.setSizePolicy(
QSizePolicy.Policy.Fixed,
@@ -274,6 +303,7 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
@@ -282,6 +312,7 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
keepSpinBoxCompact(inputWidget)
@@ -302,6 +333,7 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
return layout
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemBasicRemark(title=_('Remark')),
GuiVMessItemBasicAddress(title=_('Address')),
@@ -313,10 +345,13 @@ class GuiVMessGroupBoxBasic(GuiEditorWidgetQGroupBox):
class GuiVMessGroupBoxProxy(GuiEditorWidgetQGroupBox):
"""Represent GUI v mess group box proxy."""
def __init__(self, **kwargs):
"""Initialize the GuiVMessGroupBoxProxy."""
super().__init__(_('Proxy'), **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiEditorItemProxyHttp(title='http', translatable=False),
GuiEditorItemProxySocks(title='socks', translatable=False),
@@ -324,13 +359,16 @@ class GuiVMessGroupBoxProxy(GuiEditorWidgetQGroupBox):
class GuiVMess(GuiEditorWidgetQDialog):
"""Represent GUI v mess."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVMess."""
super().__init__(*args, **kwargs)
self.setTabText(Protocol.VMess.value)
@functools.lru_cache(None)
def groupBoxSequence(self):
"""Return the configuration group boxes in display order."""
return [
GuiVMessGroupBoxBasic(),
GuiVMessGroupBoxProxy(),
+87
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI vtls."""
from __future__ import annotations
from Furious.Interface import *
@@ -37,12 +39,15 @@ STREAM_SECURITY = [
class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
"""Represent GUI vtls item security."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemSecurity."""
super().__init__(*args, **kwargs)
self.addItems(STREAM_SECURITY)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
try:
@@ -55,6 +60,7 @@ class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
newSecurity = self.text()
def setNewSecurity():
"""Set new security."""
if newSecurity == '':
streamSettings.pop('security', None)
else:
@@ -82,6 +88,7 @@ class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
streamSettings = ConfigXray.getProxyOutboundStream(config)
@@ -93,8 +100,10 @@ class GuiVTLSItemSecurity(GuiEditorItemTextComboBox):
class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
"""Represent GUI vtls item xxx server name."""
def __init__(self, *args, **kwargs):
# Mandatory
"""Initialize the GuiVTLSItemXXXServerName."""
securityKey = kwargs.pop('securityKey')
super().__init__(*args, **kwargs)
@@ -102,6 +111,7 @@ class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
self.securityKey = securityKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get(self.securityKey), dict):
@@ -119,6 +129,7 @@ class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
newServerName = self.text()
def setNewServerName():
"""Set new server name."""
if newServerName == '':
xxxObject.pop('serverName', None)
else:
@@ -137,6 +148,7 @@ class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
@@ -148,22 +160,28 @@ class GuiVTLSItemXXXServerName(GuiEditorItemTextInput):
class GuiVTLSItemTLSServerName(GuiVTLSItemXXXServerName):
"""Represent GUI vtls item TLS server name."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemTLSServerName."""
securityKey = kwargs.pop('securityKey', 'tlsSettings')
super().__init__(*args, **kwargs, securityKey=securityKey)
class GuiVTLSItemRealityServerName(GuiVTLSItemXXXServerName):
"""Represent GUI vtls item reality server name."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealityServerName."""
securityKey = kwargs.pop('securityKey', 'realitySettings')
super().__init__(*args, **kwargs, securityKey=securityKey)
class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
"""Represent GUI vtls item xxx fingerprint."""
def __init__(self, *args, **kwargs):
# Mandatory
"""Initialize the GuiVTLSItemXXXFingerprint."""
securityKey = kwargs.pop('securityKey')
super().__init__(*args, **kwargs)
@@ -171,6 +189,7 @@ class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
self.securityKey = securityKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get(self.securityKey), dict):
@@ -188,6 +207,7 @@ class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
newFingerprint = self.text()
def setNewFingerprint():
"""Set new fingerprint."""
if newFingerprint == '':
xxxObject.pop('fingerprint', None)
else:
@@ -206,6 +226,7 @@ class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.securityKey]
@@ -217,24 +238,31 @@ class GuiVTLSItemXXXFingerprint(GuiEditorItemTextInput):
class GuiVTLSItemTLSFingerprint(GuiVTLSItemXXXFingerprint):
"""Represent GUI vtls item TLS fingerprint."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemTLSFingerprint."""
securityKey = kwargs.pop('securityKey', 'tlsSettings')
super().__init__(*args, **kwargs, securityKey=securityKey)
class GuiVTLSItemRealityFingerprint(GuiVTLSItemXXXFingerprint):
"""Represent GUI vtls item reality fingerprint."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealityFingerprint."""
securityKey = kwargs.pop('securityKey', 'realitySettings')
super().__init__(*args, **kwargs, securityKey=securityKey)
class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
"""Represent GUI vtls item TLS alpn."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemTLSAlpn."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('tlsSettings'), dict):
@@ -252,6 +280,7 @@ class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
newAlpn = self.text()
def setNewAlpn():
"""Set new alpn."""
if newAlpn == '':
tlsObject.pop('alpn', None)
else:
@@ -270,6 +299,7 @@ class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
@@ -281,8 +311,10 @@ class GuiVTLSItemTLSAlpn(GuiEditorItemTextInput):
class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
"""Represent GUI vtls item tlsxxx text input."""
def __init__(self, *args, **kwargs):
# Mandatory
"""Initialize the GuiVTLSItemTLSXXXTextInput."""
key = kwargs.pop('key')
super().__init__(*args, **kwargs)
@@ -290,6 +322,7 @@ class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
self.key = key
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('tlsSettings'), dict):
@@ -301,6 +334,7 @@ class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
newValue = self.text()
def setNewTLSXXXValue():
"""Set new tlsxxx value."""
if newValue == '':
tlsObject.pop(self.key, None)
else:
@@ -319,6 +353,7 @@ class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
@@ -330,10 +365,13 @@ class GuiVTLSItemTLSXXXTextInput(GuiEditorItemTextInput):
class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox):
"""Represent GUI vtls item TLS allow insecure."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemTLSAllowInsecure."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('tlsSettings'), dict):
@@ -366,6 +404,7 @@ class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
tlsObject = ConfigXray.getProxyOutboundStream(config)['tlsSettings']
@@ -377,8 +416,10 @@ class GuiVTLSItemTLSAllowInsecure(GuiEditorItemTextCheckBox):
class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
"""Represent GUI vtls item reality xxx."""
def __init__(self, *args, **kwargs):
# Mandatory
"""Initialize the GuiVTLSItemRealityXXX."""
realityKey = kwargs.pop('realityKey')
super().__init__(*args, **kwargs)
@@ -386,6 +427,7 @@ class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
self.realityKey = realityKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('realitySettings'), dict):
@@ -403,6 +445,7 @@ class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
newPublicKey = self.text()
def setNewPublicKey():
"""Set new public key."""
if newPublicKey == '':
realityObject.pop(self.realityKey, None)
else:
@@ -421,6 +464,7 @@ class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
realityObject = ConfigXray.getProxyOutboundStream(config)['realitySettings']
@@ -432,44 +476,56 @@ class GuiVTLSItemRealityXXX(GuiEditorItemTextInput):
class GuiVTLSItemRealityPublicKey(GuiVTLSItemRealityXXX):
"""Represent GUI vtls item reality public key."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealityPublicKey."""
realityKey = kwargs.pop('realityKey', 'publicKey')
super().__init__(*args, **kwargs, realityKey=realityKey)
class GuiVTLSItemRealityShortId(GuiVTLSItemRealityXXX):
"""Represent GUI vtls item reality short id."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealityShortId."""
realityKey = kwargs.pop('realityKey', 'shortId')
super().__init__(*args, **kwargs, realityKey=realityKey)
class GuiVTLSItemRealityMldsa65Verify(GuiVTLSItemRealityXXX):
"""Represent GUI vtls item reality mldsa65 verify."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealityMldsa65Verify."""
realityKey = kwargs.pop('realityKey', 'mldsa65Verify')
super().__init__(*args, **kwargs, realityKey=realityKey)
class GuiVTLSItemRealitySpiderX(GuiVTLSItemRealityXXX):
"""Represent GUI vtls item reality spider x."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSItemRealitySpiderX."""
realityKey = kwargs.pop('realityKey', 'spiderX')
super().__init__(*args, **kwargs, realityKey=realityKey)
class GuiVTLSPageXXX(GuiEditorWidgetQWidget):
"""Represent GUI vtls page xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageXXX."""
super().__init__(*args, **kwargs)
def setSecurityText(self, text: str):
"""Set security text."""
security = self._containers[0]
if isinstance(security, GuiEditorItemTextComboBox):
security.setText(text)
def connectActivated(self, func: Callable):
"""Connect activated."""
security = self._containers[0]
if isinstance(security, GuiEditorItemTextComboBox):
@@ -477,30 +533,39 @@ class GuiVTLSPageXXX(GuiEditorWidgetQWidget):
class GuiVTLSPageEmpty(GuiVTLSPageXXX):
"""Represent GUI vtls page empty."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageEmpty."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTLSItemSecurity(title='TLS', translatable=False),
]
class GuiVTLSPageNone(GuiVTLSPageXXX):
"""Represent GUI vtls page none."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageNone."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTLSItemSecurity(title='TLS', translatable=False),
]
class GuiVTLSPageTLS(GuiVTLSPageXXX):
"""Represent GUI vtls page TLS."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageTLS."""
super().__init__(*args, **kwargs)
def setupLayout(self):
"""Set up layout."""
layout = QFormLayout()
layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
@@ -510,18 +575,21 @@ class GuiVTLSPageTLS(GuiVTLSPageXXX):
basicLayout.setColumnStretch(3, 1)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
basicLayout.addWidget(label, row, column)
basicLayout.addWidget(inputWidget, row, column + 1)
def addBasicFullRow(index: int, row: int):
"""Add basic full row."""
label, inputWidget = self._containers[index].widgets()
basicLayout.addWidget(label, row, 0)
basicLayout.addWidget(inputWidget, row, 1, 1, 3)
def addFullRow(index: int):
"""Add full row."""
widgets = self._containers[index].widgets()
if len(widgets) == 1:
@@ -546,6 +614,7 @@ class GuiVTLSPageTLS(GuiVTLSPageXXX):
self.setLayout(layout)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTLSItemSecurity(title='TLS', translatable=False),
GuiVTLSItemTLSServerName(title='SNI', translatable=False),
@@ -571,22 +640,27 @@ class GuiVTLSPageTLS(GuiVTLSPageXXX):
class GuiVTLSPageReality(GuiVTLSPageXXX):
"""Represent GUI vtls page reality."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageReality."""
super().__init__(*args, **kwargs)
def setupLayout(self):
"""Set up layout."""
layout = QGridLayout()
layout.setColumnStretch(1, 1)
layout.setColumnStretch(3, 1)
layout.setHorizontalSpacing(12)
def addPair(index: int, row: int, column: int):
"""Add pair."""
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, row, column)
layout.addWidget(inputWidget, row, column + 1)
def addFullRow(index: int, row: int):
"""Add full row."""
label, inputWidget = self._containers[index].widgets()
layout.addWidget(label, row, 0)
@@ -603,6 +677,7 @@ class GuiVTLSPageReality(GuiVTLSPageXXX):
self.setLayout(layout)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTLSItemSecurity(title='TLS', translatable=False),
GuiVTLSItemRealityServerName(title='SNI', translatable=False),
@@ -615,7 +690,9 @@ class GuiVTLSPageReality(GuiVTLSPageXXX):
class GuiVTLSPageStackedWidget(QStackedWidget):
"""Provide the GUI vtls page stacked widget."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTLSPageStackedWidget."""
super().__init__(*args, **kwargs)
# Corresponds to stream security
@@ -630,15 +707,19 @@ class GuiVTLSPageStackedWidget(QStackedWidget):
self.addWidget(page)
def page(self, index: int) -> GuiVTLSPageXXX:
"""Return the page value."""
return self._pages[index]
def connectActivated(self, func: Callable):
"""Connect activated."""
for page in self._pages:
page.connectActivated(func)
class GuiVTLSQGroupBox(GuiEditorItemFactory, AppQGroupBox):
"""Group the GUI vtlsq editor controls."""
def __init__(self, **kwargs):
"""Initialize the GuiVTLSQGroupBox."""
translatable = kwargs.pop('translatable', False)
super().__init__('TLS', **kwargs, translatable=translatable)
@@ -654,15 +735,19 @@ class GuiVTLSQGroupBox(GuiEditorItemFactory, AppQGroupBox):
self.setLayout(layout)
def currentIndex(self) -> int:
"""Return the current index value."""
return self._widget.currentIndex()
def setCurrentIndex(self, index: int):
"""Set current index."""
self._widget.setCurrentIndex(index)
def page(self, index: int) -> GuiVTLSPageXXX:
"""Return the page value."""
return self._widget.page(index)
def handleActivated(self, index: int):
"""Handle activated."""
page = self.page(index)
page.factoryToInput(self._config)
page.setSecurityText(STREAM_SECURITY[index])
@@ -670,10 +755,12 @@ class GuiVTLSQGroupBox(GuiEditorItemFactory, AppQGroupBox):
self.setCurrentIndex(index)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
return self.page(self.currentIndex()).inputToFactory(config)
def factoryToInput(self, config: ConfigFactory):
# Shallow copy
"""Load the configuration value into the editor."""
self._config = config
streamSettings = ConfigXray.getProxyOutboundStream(config)
+186
View File
@@ -15,6 +15,8 @@
# 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 widgets for GUI v transport."""
from __future__ import annotations
from Furious.Interface import *
@@ -44,12 +46,15 @@ STREAM_NETWORK = [
class GuiVTransportItemNetwork(GuiEditorItemTextComboBox):
"""Represent GUI v transport item network."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemNetwork."""
super().__init__(*args, **kwargs)
self.addItems(STREAM_NETWORK)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
try:
@@ -62,6 +67,7 @@ class GuiVTransportItemNetwork(GuiEditorItemTextComboBox):
newNetwork = self.text()
def setNewNetwork():
"""Set new network."""
streamSettings['network'] = newNetwork
for network in STREAM_NETWORK:
@@ -89,6 +95,7 @@ class GuiVTransportItemNetwork(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
streamSettings = ConfigXray.getProxyOutboundStream(config)
@@ -100,10 +107,13 @@ class GuiVTransportItemNetwork(GuiEditorItemTextComboBox):
class GuiVTransportItemFinalMask(GuiEditorItemTextInput):
"""Represent GUI v transport item final mask."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemFinalMask."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
oldFinalMaskObject = streamSettings.get('finalmask')
@@ -137,6 +147,7 @@ class GuiVTransportItemFinalMask(GuiEditorItemTextInput):
return False
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
finalMaskObject = ConfigXray.getProxyOutboundStream(config)['finalmask']
@@ -148,7 +159,9 @@ class GuiVTransportItemFinalMask(GuiEditorItemTextInput):
class GuiVTransportItemTypeXXX(GuiEditorItemTextComboBox):
"""Represent GUI v transport item type xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemTypeXXX."""
networkKey = kwargs.pop('networkKey', '')
super().__init__(*args, **kwargs)
@@ -156,6 +169,7 @@ class GuiVTransportItemTypeXXX(GuiEditorItemTextComboBox):
self.networkKey = networkKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get(self.networkKey), dict):
@@ -173,6 +187,7 @@ class GuiVTransportItemTypeXXX(GuiEditorItemTextComboBox):
newType = self.text()
def setNewType():
"""Set new type."""
if not isinstance(xxxObject.get('header'), dict):
xxxObject['header'] = {}
@@ -194,6 +209,7 @@ class GuiVTransportItemTypeXXX(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xxxObject = ConfigXray.getProxyOutboundStream(config)[self.networkKey]
@@ -205,7 +221,9 @@ class GuiVTransportItemTypeXXX(GuiEditorItemTextComboBox):
class GuiVTransportItemTypeTcpOrRaw(GuiVTransportItemTypeXXX):
"""Represent GUI v transport item type TCP or raw."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemTypeTcpOrRaw."""
networkKey = kwargs.pop('networkKey', 'tcpSettings')
super().__init__(*args, **kwargs, networkKey=networkKey)
@@ -220,7 +238,9 @@ class GuiVTransportItemTypeTcpOrRaw(GuiVTransportItemTypeXXX):
class GuiVTransportItemHostTcpOrRaw(GuiEditorItemTextInput):
"""Represent GUI v transport item host TCP or raw."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostTcpOrRaw."""
networkKey = kwargs.pop('networkKey', 'tcpSettings')
super().__init__(*args, **kwargs)
@@ -228,6 +248,7 @@ class GuiVTransportItemHostTcpOrRaw(GuiEditorItemTextInput):
self.networkKey = networkKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get(self.networkKey), dict):
@@ -245,6 +266,7 @@ class GuiVTransportItemHostTcpOrRaw(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if not isinstance(tcpObject.get('header'), dict):
tcpObject['header'] = {}
@@ -272,6 +294,7 @@ class GuiVTransportItemHostTcpOrRaw(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
tcpObject = ConfigXray.getProxyOutboundStream(config)[self.networkKey]
@@ -283,7 +306,9 @@ class GuiVTransportItemHostTcpOrRaw(GuiEditorItemTextInput):
class GuiVTransportItemPathTcpOrRaw(GuiEditorItemTextInput):
"""Represent GUI v transport item path TCP or raw."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathTcpOrRaw."""
networkKey = kwargs.pop('networkKey', 'tcpSettings')
super().__init__(*args, **kwargs)
@@ -291,6 +316,7 @@ class GuiVTransportItemPathTcpOrRaw(GuiEditorItemTextInput):
self.networkKey = networkKey
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get(self.networkKey), dict):
@@ -308,6 +334,7 @@ class GuiVTransportItemPathTcpOrRaw(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if not isinstance(tcpObject.get('header'), dict):
tcpObject['header'] = {}
@@ -332,6 +359,7 @@ class GuiVTransportItemPathTcpOrRaw(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
tcpObject = ConfigXray.getProxyOutboundStream(config)[self.networkKey]
@@ -343,7 +371,9 @@ class GuiVTransportItemPathTcpOrRaw(GuiEditorItemTextInput):
class GuiVTransportItemTypeKcp(GuiVTransportItemTypeXXX):
"""Represent GUI v transport item type kcp."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemTypeKcp."""
networkKey = kwargs.pop('networkKey', 'kcpSettings')
super().__init__(*args, **kwargs, networkKey=networkKey)
@@ -363,10 +393,13 @@ class GuiVTransportItemTypeKcp(GuiVTransportItemTypeXXX):
class GuiVTransportItemSeedKcp(GuiEditorItemTextInput):
"""Represent GUI v transport item seed kcp."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemSeedKcp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('kcpSettings'), dict):
@@ -384,6 +417,7 @@ class GuiVTransportItemSeedKcp(GuiEditorItemTextInput):
newSeed = self.text()
def setNewSeed():
"""Set new seed."""
if newSeed == '':
kcpObject.pop('seed', None)
else:
@@ -402,6 +436,7 @@ class GuiVTransportItemSeedKcp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
kcpObject = ConfigXray.getProxyOutboundStream(config)['kcpSettings']
@@ -413,10 +448,13 @@ class GuiVTransportItemSeedKcp(GuiEditorItemTextInput):
class GuiVTransportItemHostWs(GuiEditorItemTextInput):
"""Represent GUI v transport item host ws."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostWs."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('wsSettings'), dict):
@@ -434,6 +472,7 @@ class GuiVTransportItemHostWs(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if not isinstance(wsObject.get('headers'), dict):
wsObject['headers'] = {}
@@ -455,6 +494,7 @@ class GuiVTransportItemHostWs(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
wsObject = ConfigXray.getProxyOutboundStream(config)['wsSettings']
@@ -466,10 +506,13 @@ class GuiVTransportItemHostWs(GuiEditorItemTextInput):
class GuiVTransportItemPathWs(GuiEditorItemTextInput):
"""Represent GUI v transport item path ws."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathWs."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('wsSettings'), dict):
@@ -487,6 +530,7 @@ class GuiVTransportItemPathWs(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if newPath == '':
wsObject.pop('path', None)
else:
@@ -505,6 +549,7 @@ class GuiVTransportItemPathWs(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
wsObject = ConfigXray.getProxyOutboundStream(config)['wsSettings']
@@ -516,10 +561,13 @@ class GuiVTransportItemPathWs(GuiEditorItemTextInput):
class GuiVTransportItemHostHttpUpgrade(GuiEditorItemTextInput):
"""Represent GUI v transport item host HTTP upgrade."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostHttpUpgrade."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('httpupgradeSettings'), dict):
@@ -537,6 +585,7 @@ class GuiVTransportItemHostHttpUpgrade(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if newHost == '':
httpUpgradeObject.pop('host', None)
else:
@@ -555,6 +604,7 @@ class GuiVTransportItemHostHttpUpgrade(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
httpUpgradeObject = ConfigXray.getProxyOutboundStream(config)[
'httpupgradeSettings'
@@ -568,10 +618,13 @@ class GuiVTransportItemHostHttpUpgrade(GuiEditorItemTextInput):
class GuiVTransportItemPathHttpUpgrade(GuiEditorItemTextInput):
"""Represent GUI v transport item path HTTP upgrade."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathHttpUpgrade."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('httpupgradeSettings'), dict):
@@ -589,6 +642,7 @@ class GuiVTransportItemPathHttpUpgrade(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if newPath == '':
httpUpgradeObject.pop('path', None)
else:
@@ -607,6 +661,7 @@ class GuiVTransportItemPathHttpUpgrade(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
httpUpgradeObject = ConfigXray.getProxyOutboundStream(config)[
'httpupgradeSettings'
@@ -620,10 +675,13 @@ class GuiVTransportItemPathHttpUpgrade(GuiEditorItemTextInput):
class GuiVTransportItemHostSplitHttp(GuiEditorItemTextInput):
"""Represent GUI v transport item host split HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostSplitHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('splithttpSettings'), dict):
@@ -641,6 +699,7 @@ class GuiVTransportItemHostSplitHttp(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if newHost == '':
splitHttpObject.pop('host', None)
else:
@@ -659,6 +718,7 @@ class GuiVTransportItemHostSplitHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
splitHttpObject = ConfigXray.getProxyOutboundStream(config)[
'splithttpSettings'
@@ -672,10 +732,13 @@ class GuiVTransportItemHostSplitHttp(GuiEditorItemTextInput):
class GuiVTransportItemPathSplitHttp(GuiEditorItemTextInput):
"""Represent GUI v transport item path split HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathSplitHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('splithttpSettings'), dict):
@@ -693,6 +756,7 @@ class GuiVTransportItemPathSplitHttp(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if newPath == '':
splitHttpObject.pop('path', None)
else:
@@ -711,6 +775,7 @@ class GuiVTransportItemPathSplitHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
splitHttpObject = ConfigXray.getProxyOutboundStream(config)[
'splithttpSettings'
@@ -724,10 +789,13 @@ class GuiVTransportItemPathSplitHttp(GuiEditorItemTextInput):
class GuiVTransportItemHostXHttp(GuiEditorItemTextInput):
"""Represent GUI v transport item host x HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostXHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('xhttpSettings'), dict):
@@ -745,6 +813,7 @@ class GuiVTransportItemHostXHttp(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if newHost == '':
xhttpObject.pop('host', None)
else:
@@ -763,6 +832,7 @@ class GuiVTransportItemHostXHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xhttpObject = ConfigXray.getProxyOutboundStream(config)['xhttpSettings']
@@ -774,10 +844,13 @@ class GuiVTransportItemHostXHttp(GuiEditorItemTextInput):
class GuiVTransportItemPathXHttp(GuiEditorItemTextInput):
"""Represent GUI v transport item path x HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathXHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('xhttpSettings'), dict):
@@ -795,6 +868,7 @@ class GuiVTransportItemPathXHttp(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if newPath == '':
xhttpObject.pop('path', None)
else:
@@ -813,6 +887,7 @@ class GuiVTransportItemPathXHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xhttpObject = ConfigXray.getProxyOutboundStream(config)['xhttpSettings']
@@ -824,7 +899,9 @@ class GuiVTransportItemPathXHttp(GuiEditorItemTextInput):
class GuiVTransportItemModeXHttp(GuiEditorItemTextComboBox):
"""Represent GUI v transport item mode x HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemModeXHttp."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -837,6 +914,7 @@ class GuiVTransportItemModeXHttp(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('xhttpSettings'), dict):
@@ -854,6 +932,7 @@ class GuiVTransportItemModeXHttp(GuiEditorItemTextComboBox):
newMode = self.text()
def setNewMode():
"""Set new mode."""
if newMode == '':
xhttpObject.pop('mode', None)
else:
@@ -872,6 +951,7 @@ class GuiVTransportItemModeXHttp(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xhttpObject = ConfigXray.getProxyOutboundStream(config)['xhttpSettings']
@@ -883,10 +963,13 @@ class GuiVTransportItemModeXHttp(GuiEditorItemTextComboBox):
class GuiVTransportItemExtraXHttp(GuiEditorItemTextInput):
"""Represent GUI v transport item extra x HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemExtraXHttp."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('xhttpSettings'), dict):
@@ -907,6 +990,7 @@ class GuiVTransportItemExtraXHttp(GuiEditorItemTextInput):
newExtra = self.text()
def setNewExtra():
"""Set new extra."""
if newExtra == '':
xhttpObject.pop('extra', None)
else:
@@ -930,6 +1014,7 @@ class GuiVTransportItemExtraXHttp(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
xhttpObject = ConfigXray.getProxyOutboundStream(config)['xhttpSettings']
@@ -944,10 +1029,13 @@ class GuiVTransportItemExtraXHttp(GuiEditorItemTextInput):
class GuiVTransportItemHostH2(GuiEditorItemTextInput):
"""Represent GUI v transport item host h2."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemHostH2."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('httpSettings'), dict):
@@ -965,6 +1053,7 @@ class GuiVTransportItemHostH2(GuiEditorItemTextInput):
newHost = self.text()
def setNewHost():
"""Set new host."""
if newHost == '':
httpObject.pop('host', None)
else:
@@ -983,6 +1072,7 @@ class GuiVTransportItemHostH2(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
httpObject = ConfigXray.getProxyOutboundStream(config)['httpSettings']
@@ -994,10 +1084,13 @@ class GuiVTransportItemHostH2(GuiEditorItemTextInput):
class GuiVTransportItemPathH2(GuiEditorItemTextInput):
"""Represent GUI v transport item path h2."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPathH2."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('httpSettings'), dict):
@@ -1015,6 +1108,7 @@ class GuiVTransportItemPathH2(GuiEditorItemTextInput):
newPath = self.text()
def setNewPath():
"""Set new path."""
if newPath == '':
httpObject.pop('path', None)
else:
@@ -1033,6 +1127,7 @@ class GuiVTransportItemPathH2(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
httpObject = ConfigXray.getProxyOutboundStream(config)['httpSettings']
@@ -1044,7 +1139,9 @@ class GuiVTransportItemPathH2(GuiEditorItemTextInput):
class GuiVTransportItemTypeQuic(GuiVTransportItemTypeXXX):
"""Represent GUI v transport item type quic."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemTypeQuic."""
networkKey = kwargs.pop('networkKey', 'quicSettings')
super().__init__(*args, **kwargs, networkKey=networkKey)
@@ -1063,7 +1160,9 @@ class GuiVTransportItemTypeQuic(GuiVTransportItemTypeXXX):
class GuiVTransportItemSecurityQuic(GuiEditorItemTextComboBox):
"""Represent GUI v transport item security quic."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemSecurityQuic."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -1076,6 +1175,7 @@ class GuiVTransportItemSecurityQuic(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('quicSettings'), dict):
@@ -1093,6 +1193,7 @@ class GuiVTransportItemSecurityQuic(GuiEditorItemTextComboBox):
newSecurity = self.text()
def setNewSecurity():
"""Set new security."""
quicObject['security'] = newSecurity
if isinstance(oldSecurity, str):
@@ -1108,6 +1209,7 @@ class GuiVTransportItemSecurityQuic(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
quicObject = ConfigXray.getProxyOutboundStream(config)['quicSettings']
@@ -1119,10 +1221,13 @@ class GuiVTransportItemSecurityQuic(GuiEditorItemTextComboBox):
class GuiVTransportItemKeyQuic(GuiEditorItemTextInput):
"""Represent GUI v transport item key quic."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemKeyQuic."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('quicSettings'), dict):
@@ -1140,6 +1245,7 @@ class GuiVTransportItemKeyQuic(GuiEditorItemTextInput):
newKey = self.text()
def setNewKey():
"""Set new key."""
if newKey == '':
quicObject.pop('key', None)
else:
@@ -1158,6 +1264,7 @@ class GuiVTransportItemKeyQuic(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
quicObject = ConfigXray.getProxyOutboundStream(config)['quicSettings']
@@ -1169,7 +1276,9 @@ class GuiVTransportItemKeyQuic(GuiEditorItemTextInput):
class GuiVTransportItemModeGRPC(GuiEditorItemTextComboBox):
"""Represent GUI v transport item mode grpc."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemModeGRPC."""
super().__init__(*args, **kwargs)
self.addItems(
@@ -1181,6 +1290,7 @@ class GuiVTransportItemModeGRPC(GuiEditorItemTextComboBox):
)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('grpcSettings'), dict):
@@ -1203,6 +1313,7 @@ class GuiVTransportItemModeGRPC(GuiEditorItemTextComboBox):
newMode = self.text()
def setNewMode():
"""Set new mode."""
if newMode == 'multi':
grpcObject['multiMode'] = True
else:
@@ -1221,6 +1332,7 @@ class GuiVTransportItemModeGRPC(GuiEditorItemTextComboBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
grpcObject = ConfigXray.getProxyOutboundStream(config)['grpcSettings']
@@ -1239,10 +1351,13 @@ class GuiVTransportItemModeGRPC(GuiEditorItemTextComboBox):
class GuiVTransportItemAuthorityGRPC(GuiEditorItemTextInput):
"""Represent GUI v transport item authority grpc."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemAuthorityGRPC."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('grpcSettings'), dict):
@@ -1260,6 +1375,7 @@ class GuiVTransportItemAuthorityGRPC(GuiEditorItemTextInput):
newAuthority = self.text()
def setNewAuthority():
"""Set new authority."""
if newAuthority == '':
grpcObject.pop('authority', None)
else:
@@ -1278,6 +1394,7 @@ class GuiVTransportItemAuthorityGRPC(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
grpcObject = ConfigXray.getProxyOutboundStream(config)['grpcSettings']
@@ -1289,10 +1406,13 @@ class GuiVTransportItemAuthorityGRPC(GuiEditorItemTextInput):
class GuiVTransportItemServiceNameGRPC(GuiEditorItemTextInput):
"""Represent GUI v transport item service name grpc."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemServiceNameGRPC."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('grpcSettings'), dict):
@@ -1310,6 +1430,7 @@ class GuiVTransportItemServiceNameGRPC(GuiEditorItemTextInput):
newServiceName = self.text()
def setNewServiceName():
"""Set new service name."""
if newServiceName == '':
grpcObject.pop('serviceName', None)
else:
@@ -1328,6 +1449,7 @@ class GuiVTransportItemServiceNameGRPC(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
grpcObject = ConfigXray.getProxyOutboundStream(config)['grpcSettings']
@@ -1339,13 +1461,16 @@ class GuiVTransportItemServiceNameGRPC(GuiEditorItemTextInput):
class GuiVTransportItemVersionHysteria(GuiEditorItemTextSpinBox):
"""Represent GUI v transport item version hysteria."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemVersionHysteria."""
super().__init__(*args, **kwargs)
# Range. 0 means invalid
self.setRange(0, 2)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('hysteriaSettings'), dict):
@@ -1369,6 +1494,7 @@ class GuiVTransportItemVersionHysteria(GuiEditorItemTextSpinBox):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
hysteriaObject = ConfigXray.getProxyOutboundStream(config)[
'hysteriaSettings'
@@ -1382,10 +1508,13 @@ class GuiVTransportItemVersionHysteria(GuiEditorItemTextSpinBox):
class GuiVTransportItemAuthHysteria(GuiEditorItemTextInput):
"""Represent GUI v transport item auth hysteria."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemAuthHysteria."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('hysteriaSettings'), dict):
@@ -1403,6 +1532,7 @@ class GuiVTransportItemAuthHysteria(GuiEditorItemTextInput):
newAuth = self.text()
def setNewAuth():
"""Set new auth."""
if newAuth == '':
hysteriaObject.pop('auth', None)
else:
@@ -1421,6 +1551,7 @@ class GuiVTransportItemAuthHysteria(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
hysteriaObject = ConfigXray.getProxyOutboundStream(config)[
'hysteriaSettings'
@@ -1434,10 +1565,13 @@ class GuiVTransportItemAuthHysteria(GuiEditorItemTextInput):
class GuiVTransportItemPasswordHysteria(GuiEditorItemTextInput):
"""Represent GUI v transport item password hysteria."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportItemPasswordHysteria."""
super().__init__(*args, **kwargs)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
streamSettings = ConfigXray.getProxyOutboundStream(config)
if not isinstance(streamSettings.get('finalmask'), dict):
@@ -1460,6 +1594,7 @@ class GuiVTransportItemPasswordHysteria(GuiEditorItemTextInput):
newPassword = self.text()
def setNewPassword():
"""Set new password."""
if newPassword == '':
finalmaskObject.pop('salamander', None)
else:
@@ -1478,6 +1613,7 @@ class GuiVTransportItemPasswordHysteria(GuiEditorItemTextInput):
return True
def factoryToInput(self, config: ConfigFactory):
"""Load the configuration value into the editor."""
try:
salamander = ConfigXray.getProxyOutboundStream(config)['finalmask'][
'salamander'
@@ -1491,16 +1627,20 @@ class GuiVTransportItemPasswordHysteria(GuiEditorItemTextInput):
class GuiVTransportPageXXX(GuiEditorWidgetQWidget):
"""Represent GUI v transport page xxx."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageXXX."""
super().__init__(*args, **kwargs)
def setNetworkText(self, text: str):
"""Set network text."""
network = self._containers[0]
if isinstance(network, GuiEditorItemTextComboBox):
network.setText(text)
def connectActivated(self, func: Callable):
"""Connect activated."""
network = self._containers[0]
if isinstance(network, GuiEditorItemTextComboBox):
@@ -1508,10 +1648,13 @@ class GuiVTransportPageXXX(GuiEditorWidgetQWidget):
class GuiVTransportPageTcp(GuiVTransportPageXXX):
"""Represent GUI v transport page TCP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageTcp."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1528,10 +1671,13 @@ class GuiVTransportPageTcp(GuiVTransportPageXXX):
class GuiVTransportPageRaw(GuiVTransportPageXXX):
"""Represent GUI v transport page raw."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageRaw."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1548,10 +1694,13 @@ class GuiVTransportPageRaw(GuiVTransportPageXXX):
class GuiVTransportPageKcp(GuiVTransportPageXXX):
"""Represent GUI v transport page kcp."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageKcp."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1561,10 +1710,13 @@ class GuiVTransportPageKcp(GuiVTransportPageXXX):
class GuiVTransportPageWs(GuiVTransportPageXXX):
"""Represent GUI v transport page ws."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageWs."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1574,10 +1726,13 @@ class GuiVTransportPageWs(GuiVTransportPageXXX):
class GuiVTransportPageHttpUpgrade(GuiVTransportPageXXX):
"""Represent GUI v transport page HTTP upgrade."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageHttpUpgrade."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1587,10 +1742,13 @@ class GuiVTransportPageHttpUpgrade(GuiVTransportPageXXX):
class GuiVTransportPageSplitHttp(GuiVTransportPageXXX):
"""Represent GUI v transport page split HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageSplitHttp."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1600,10 +1758,13 @@ class GuiVTransportPageSplitHttp(GuiVTransportPageXXX):
class GuiVTransportPageXHttp(GuiVTransportPageXXX):
"""Represent GUI v transport page x HTTP."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageXHttp."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1615,10 +1776,13 @@ class GuiVTransportPageXHttp(GuiVTransportPageXXX):
class GuiVTransportPageH2(GuiVTransportPageXXX):
"""Represent GUI v transport page h2."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageH2."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1628,10 +1792,13 @@ class GuiVTransportPageH2(GuiVTransportPageXXX):
class GuiVTransportPageQuic(GuiVTransportPageXXX):
"""Represent GUI v transport page quic."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageQuic."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1642,10 +1809,13 @@ class GuiVTransportPageQuic(GuiVTransportPageXXX):
class GuiVTransportPageGRPC(GuiVTransportPageXXX):
"""Represent GUI v transport page grpc."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageGRPC."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1658,10 +1828,13 @@ class GuiVTransportPageGRPC(GuiVTransportPageXXX):
class GuiVTransportPageHysteria(GuiVTransportPageXXX):
"""Represent GUI v transport page hysteria."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageHysteria."""
super().__init__(*args, **kwargs)
def containerSequence(self):
"""Return the editor item containers in display order."""
return [
GuiVTransportItemNetwork(title='Network', translatable=False),
GuiVTransportItemFinalMask(title='Finalmask', translatable=False),
@@ -1672,7 +1845,9 @@ class GuiVTransportPageHysteria(GuiVTransportPageXXX):
class GuiVTransportPageStackedWidget(QStackedWidget):
"""Provide the GUI v transport page stacked widget."""
def __init__(self, *args, **kwargs):
"""Initialize the GuiVTransportPageStackedWidget."""
super().__init__(*args, **kwargs)
# Corresponds to stream network
@@ -1696,15 +1871,19 @@ class GuiVTransportPageStackedWidget(QStackedWidget):
self.addWidget(page)
def page(self, index: int) -> GuiVTransportPageXXX:
"""Return the page value."""
return self._pages[index]
def connectActivated(self, func: Callable):
"""Connect activated."""
for page in self._pages:
page.connectActivated(func)
class GuiVTransportQGroupBox(GuiEditorItemFactory, AppQGroupBox):
"""Group the GUI v transport q editor controls."""
def __init__(self, **kwargs):
"""Initialize the GuiVTransportQGroupBox."""
super().__init__(_('Transport'), **kwargs)
self._config = ConfigFactory()
@@ -1718,15 +1897,19 @@ class GuiVTransportQGroupBox(GuiEditorItemFactory, AppQGroupBox):
self.setLayout(layout)
def currentIndex(self) -> int:
"""Return the current index value."""
return self._widget.currentIndex()
def setCurrentIndex(self, index: int):
"""Set current index."""
self._widget.setCurrentIndex(index)
def page(self, index: int) -> GuiVTransportPageXXX:
"""Return the page value."""
return self._widget.page(index)
def handleActivated(self, index: int):
"""Handle activated."""
page = self.page(index)
page.factoryToInput(self._config)
page.setNetworkText(STREAM_NETWORK[index])
@@ -1734,10 +1917,12 @@ class GuiVTransportQGroupBox(GuiEditorItemFactory, AppQGroupBox):
self.setCurrentIndex(index)
def inputToFactory(self, config: ConfigFactory) -> bool:
"""Apply the current editor value to the configuration."""
return self.page(self.currentIndex()).inputToFactory(config)
def factoryToInput(self, config: ConfigFactory):
# Shallow copy
"""Load the configuration value into the editor."""
self._config = config
streamSettings = ConfigXray.getProxyOutboundStream(config)
@@ -1747,6 +1932,7 @@ class GuiVTransportQGroupBox(GuiEditorItemFactory, AppQGroupBox):
return
def alterNetwork(sourceNetwork, targetNetwork):
"""Handle alter network for the GUI v transport q group box."""
nonlocal network
if network == sourceNetwork:
+5
View File
@@ -15,6 +15,8 @@
# 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 widgets for indent spin box."""
from __future__ import annotations
from Furious.Qt import *
@@ -27,7 +29,9 @@ __all__ = ['IndentSpinBox']
class IndentSpinBox(AppQDialog):
"""Represent indent spin box."""
def __init__(self, parent=None):
"""Initialize the IndentSpinBox."""
super().__init__(parent)
self.setWindowTitle(_('Set Indent'))
@@ -54,4 +58,5 @@ class IndentSpinBox(AppQDialog):
self.setLayout(layout)
def value(self):
"""Return the value value."""
return self.indentSpin.value()
+20
View File
@@ -15,6 +15,8 @@
# 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 widgets for system tray icon."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -45,7 +47,9 @@ class SystemTrayIcon(
Mixins.ThemeAware,
QSystemTrayIcon,
):
"""Represent system tray icon."""
def __init__(self, *args, **kwargs):
"""Initialize the SystemTrayIcon."""
super().__init__(*args, **kwargs)
actions = [
@@ -112,6 +116,7 @@ class SystemTrayIcon(
self.activated.connect(self.handleActivated)
def rebuildDynamicMenus(self):
"""Handle rebuild dynamic menus for the system tray icon."""
for action in self._actions:
if hasattr(action, 'rebuildMenu'):
action.rebuildMenu()
@@ -126,6 +131,7 @@ class SystemTrayIcon(
menu.addAction(childAction)
def bootstrap(self):
"""Handle bootstrap for the system tray icon."""
if AppSettings.isStateON_('StartupOnBoot'):
# Rrefresh startup application location
StartupOnBoot.on_()
@@ -134,11 +140,14 @@ class SystemTrayIcon(
self.ConnectAction.trigger()
def showMessage(self, message: str, *args, **kwargs):
"""Show message."""
if message:
super().showMessage(_(APPLICATION_NAME), message, *args, **kwargs)
def setMonochromeIconByTheme(self, theme):
"""Set monochrome icon by theme."""
def switchMonochrome():
"""Handle switch monochrome for the system tray icon."""
if theme == 'Dark':
self.setIcon(bootstrapIconWhite('monochrome-rocket-takeoff.svg'))
else:
@@ -166,18 +175,21 @@ class SystemTrayIcon(
switchMonochrome()
def setMonochromeIcon(self):
"""Set monochrome icon."""
if APP().isSystemTrayConnected():
self.setConnectedMonochromeIcon()
else:
self.setDisconnectedMonochromeIcon()
def setConnectedMonochromeIcon(self):
"""Set connected monochrome icon."""
if PLATFORM == 'Darwin':
self.setIcon(bootstrapIconMask('monochrome-rocket-takeoff.svg'))
else:
self.setMonochromeIconByTheme(APP().theme())
def setDisconnectedMonochromeIcon(self):
"""Set disconnected monochrome icon."""
if PLATFORM == 'Darwin':
icon = bootstrapIconWithOpacity(
'monochrome-rocket-takeoff.svg',
@@ -189,6 +201,7 @@ class SystemTrayIcon(
self.setMonochromeIconByTheme(APP().theme())
def setDisconnectedIcon(self):
"""Set disconnected icon."""
if AppSettings.isStateON_('UseMonochromeTrayIcon'):
self.setDisconnectedMonochromeIcon()
@@ -205,6 +218,7 @@ class SystemTrayIcon(
self.setIcon(bootstrapIcon('rocket-takeoff-disconnected.svg'))
def setConnectedIcon(self):
"""Set connected icon."""
if AppSettings.isStateON_('UseMonochromeTrayIcon'):
self.setConnectedMonochromeIcon()
@@ -225,10 +239,12 @@ class SystemTrayIcon(
@QtCore.Slot(QSystemTrayIcon.ActivationReason)
def handleActivated(self, reason):
"""Handle activated."""
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
APP().mainWindow.show()
def setCustomToolTip(self):
"""Set custom tool tip."""
if SystemRuntime.isAdmin():
self.setToolTip(
f'{_(APPLICATION_NAME)} {APPLICATION_VERSION} ({_(ADMINISTRATOR_NAME)})'
@@ -237,12 +253,15 @@ class SystemTrayIcon(
self.setToolTip(f'{_(APPLICATION_NAME)} {APPLICATION_VERSION}')
def disconnectedCallback(self):
"""Update the system tray icon for a disconnected state."""
self.setDisconnectedIcon()
def connectedCallback(self):
"""Update the system tray icon for a connected state."""
self.setConnectedIcon()
def themeChangedCallback(self, theme):
"""Update the system tray icon for a theme change."""
if AppSettings.isStateON_('UseMonochromeTrayIcon'):
# 'theme' is not used. Instead, always query current theme
self.setMonochromeIcon()
@@ -255,4 +274,5 @@ class SystemTrayIcon(
self.setDisconnectedIcon()
def retranslate(self):
"""Refresh translated text for the system tray icon."""
self.setCustomToolTip()
File diff suppressed because it is too large Load Diff
+41
View File
@@ -15,6 +15,8 @@
# 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 widgets for user subs Qt table view."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -41,68 +43,85 @@ registerAppSettings('UserSubsHeaderViewState')
class UserSubsQTableViewHorizontalHeader(AppQHeaderView):
"""Provide the user subs Qt table view horizontal table header."""
def __init__(self, *args, **kwargs):
"""Initialize the UserSubsQTableViewHorizontalHeader."""
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
class UserSubsQTableViewVerticalHeader(AppQHeaderView):
"""Provide the user subs Qt table view vertical table header."""
def __init__(self, *args, **kwargs):
"""Initialize the UserSubsQTableViewVerticalHeader."""
super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs)
class UserSubsQTableViewHeaders:
"""Describe and render user subs Qt table view table columns."""
def __init__(self, name: str, func: Callable[[dict], str] = None):
"""Initialize the UserSubsQTableViewHeaders."""
self.name = name
self.func = func
def __call__(self, item: dict) -> str:
"""Invoke the user subs Qt table view headers as a callable."""
if callable(self.func):
return self.func(item)
else:
return ''
def __eq__(self, other):
"""Compare the user subs Qt table view headers with another value."""
return str(self) == str(other)
def __str__(self):
"""Return the display text for the user subs Qt table view headers."""
return self.name
class UserSubsAppQComboBox(AppQComboBox):
"""Represent user subs app q combo box."""
def __init__(self, *args, **kwargs):
"""Initialize the UserSubsAppQComboBox."""
super().__init__(*args, **kwargs)
def retranslate(self):
# Do not emit 'currentTextChanged' when retranslate
"""Refresh translated text for the user subs app q combo box."""
with Mixins.QBlockSignalContext(self):
super().retranslate()
class UserSubsTableModel(QtCore.QAbstractTableModel):
"""Expose user subs table data through a Qt item model."""
def __init__(
self,
headers: list[UserSubsQTableViewHeaders],
itemKey: list[str],
parent=None,
):
"""Initialize the UserSubsTableModel."""
super().__init__(parent)
self.headers = headers
self.itemKey = itemKey
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of rows exposed by the model."""
if parent.isValid():
return 0
return len(Storage.UserSubs())
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of columns exposed by the model."""
if parent.isValid():
return 0
return len(self.headers)
def flags(self, index):
"""Return the Qt item flags for a model index."""
if not index.isValid():
return QtCore.Qt.ItemFlag.NoItemFlags
@@ -114,6 +133,7 @@ class UserSubsTableModel(QtCore.QAbstractTableModel):
return flags
def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
"""Return the data managed by the user subs table model."""
if not index.isValid():
return None
@@ -147,6 +167,7 @@ class UserSubsTableModel(QtCore.QAbstractTableModel):
return None
def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool:
"""Update model data for the requested role."""
if role != QtCore.Qt.ItemDataRole.EditRole or not index.isValid():
return False
@@ -172,6 +193,7 @@ class UserSubsTableModel(QtCore.QAbstractTableModel):
orientation: QtCore.Qt.Orientation,
role=QtCore.Qt.ItemDataRole.DisplayRole,
):
"""Return display data for a table header section."""
if role != QtCore.Qt.ItemDataRole.DisplayRole:
return None
@@ -185,13 +207,16 @@ class UserSubsTableModel(QtCore.QAbstractTableModel):
@staticmethod
def uniqueByRow(row: int) -> str:
"""Return the unique by row value used by the user subs table model."""
return list(Storage.UserSubs().keys())[row]
@classmethod
def subsObjectByRow(cls, row: int) -> dict:
"""Return the subs object by row value used by the user subs table model."""
return Storage.UserSubs()[cls.uniqueByRow(row)]
def emitRowChanged(self, row: int, column: Union[int, None] = None):
"""Handle emit row changed for the user subs table model."""
if row < 0 or row >= self.rowCount():
return
@@ -205,6 +230,7 @@ class UserSubsTableModel(QtCore.QAbstractTableModel):
self.dataChanged.emit(left, right, [])
def emitAllChanged(self):
"""Handle emit all changed for the user subs table model."""
if self.rowCount() == 0 or self.columnCount() == 0:
return
@@ -241,6 +267,7 @@ _TRANSLATABLE_HEADERS = [
class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
"""Represent user subs Qt table view."""
RowHeight = 42
AutoUpdateOptions = {
@@ -283,6 +310,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
ItemKey = ['remark', 'webURL', 'autoupdate', 'proxy']
def __init__(self, *args, **kwargs):
"""Initialize the UserSubsQTableView."""
self.deleteUniqueCallback = kwargs.pop('deleteUniqueCallback', None)
super().__init__(*args, **kwargs)
@@ -337,15 +365,18 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
@property
def selectedIndex(self):
"""Return the selected index value."""
return sorted(
list(set(index.row() for index in self.selectionModel().selectedRows()))
)
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
"""Handle custom context menu requested."""
self.contextMenu.exec(self.viewport().mapToGlobal(point))
def deleteSelectedItem(self):
"""Delete selected item."""
indexes = self.selectedIndex
if len(indexes) == 0:
@@ -353,6 +384,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
return
def handleResultCode(_indexes, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -421,6 +453,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
mbox.open()
def handleAutoUpdateComboBoxCurrentTextChanged(self, text: str, row: int):
"""Handle auto update combo box current text changed."""
textEnglish = _(text, 'EN')
unique = list(Storage.UserSubs().keys())[row]
@@ -452,6 +485,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
)
def getHttpProxy(_subsob):
"""Return HTTP proxy."""
return self.ProxyOptions[_subsob.get('proxy', '')]()
if self.timerConnected[row]:
@@ -496,6 +530,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
return textEnglish
def handleProxyComboBoxCurrentTextChanged(self, text: str, row: int):
"""Handle proxy combo box current text changed."""
textEnglish = _(text, 'EN')
unique = list(Storage.UserSubs().keys())[row]
@@ -526,6 +561,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
return textEnglish
def flushItem(self, row, column, item):
"""Refresh item."""
if row < 0 or row >= self.sourceModel.rowCount():
return
@@ -581,16 +617,19 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
self.sourceModel.emitRowChanged(row, column)
def flushRow(self, row, item):
"""Refresh row."""
for column in list(range(self.sourceModel.columnCount())):
self.flushItem(row, column, item)
def flushAll(self):
"""Refresh all."""
self.sourceModel.emitAllChanged()
for index, key in enumerate(Storage.UserSubs()):
self.flushRow(index, Storage.UserSubs()[key])
def appendNewItem(self, **kwargs):
"""Append new item."""
unique, remark, webURL, autoupdate, proxy = (
kwargs.pop('unique', ''),
kwargs.pop('remark', ''),
@@ -631,6 +670,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
def updateSubsByUnique(
unique: str, httpProxy: Union[str, Callable, None], **kwargs
):
"""Update subs by unique."""
showMessageBox = kwargs.pop('showMessageBox', False)
if callable(httpProxy):
@@ -653,6 +693,7 @@ class UserSubsQTableView(Mixins.QTranslatable, AppQTableView):
)
def retranslate(self):
"""Refresh translated text for the user subs Qt table view."""
self.sourceModel.headerDataChanged.emit(
QtCore.Qt.Orientation.Horizontal,
0,
+1
View File
@@ -54,6 +54,7 @@ class WaitingSpinner(QWidget):
speed: float = math.pi / 2,
color: QColor = QColor(0, 0, 0),
) -> None:
"""Initialize the WaitingSpinner."""
super().__init__(parent)
self._center_on_parent: bool = center_on_parent
@@ -15,6 +15,8 @@
# 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 widgets for Xray asset viewer Qt list widget."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ logger = logging.getLogger(__name__)
class MBoxAssetExists(AppQMessageBox):
"""Represent m box asset exists."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxAssetExists."""
super().__init__(*args, **kwargs)
self.setStandardButtons(
@@ -45,6 +49,7 @@ class MBoxAssetExists(AppQMessageBox):
)
def retranslate(self):
"""Refresh translated text for the m box asset exists."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(_(self.text()))
@@ -54,7 +59,9 @@ class MBoxAssetExists(AppQMessageBox):
class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
"""Provide the Xray asset viewer Qt list widget."""
def __init__(self, *args, **kwargs):
"""Initialize the XrayAssetViewerQListWidget."""
super().__init__(*args, **kwargs)
self.setAlternatingRowColors(True)
@@ -89,9 +96,11 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
"""Handle custom context menu requested."""
self.contextMenu.exec(self.viewport().mapToGlobal(point))
def flushItemByTheme(self, theme: str):
"""Refresh item by theme."""
self.clear()
maxlen = max(
@@ -141,6 +150,7 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
self.addItem(item)
def flushItem(self):
"""Refresh item."""
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
assert self.initialTheme is not None
@@ -150,7 +160,9 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
self.flushItemByTheme(APP().theme())
def appendNewItem(self, filename: str):
"""Append new item."""
def append(_filename):
"""Append the Xray asset viewer Qt list widget."""
try:
shutil.copy(_filename, XRAY_ASSET_DIR)
except shutil.SameFileError:
@@ -181,6 +193,7 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
if os.path.isfile(XRAY_ASSET_DIR / basename):
def handleResultCode(_filename, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -201,6 +214,7 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
append(filename)
def deleteSelectedItem(self):
"""Delete selected item."""
indexes = self.selectedIndex
if len(indexes) == 0:
@@ -208,6 +222,7 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
return
def handleResultCode(_indexes, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -238,12 +253,14 @@ class XrayAssetViewerQListWidget(Mixins.ThemeAware, AppQListWidget):
mbox.open()
def keyPressEvent(self, event):
"""Handle a key press for the Xray asset viewer Qt list widget."""
if event.key() == QtCore.Qt.Key.Key_Delete:
self.deleteSelectedItem()
else:
super().keyPressEvent(event)
def themeChangedCallback(self, theme):
"""Update the Xray asset viewer Qt list widget for a theme change."""
if PLATFORM == 'Linux' and SystemRuntime.ubuntuRelease() == '20.04':
# Ubuntu 20.04 system dark theme does not
# change menu color. Do nothing
+2
View File
@@ -15,6 +15,8 @@
# 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 reusable application widgets and configuration editors."""
from __future__ import annotations
from .Application import *
+42
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for app main window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -51,10 +53,13 @@ registerAppSettings('AppMainWindowState')
class AppNetworkConnectivityManager(NetworkConnectivityManager):
"""Coordinate app network connectivity operations."""
def __init__(self, parent=None):
"""Initialize the AppNetworkConnectivityManager."""
super().__init__(parent)
def successCallback(self, networkReply, **kwargs):
"""Handle a successful network operation."""
parent = self.parent()
if isinstance(parent, AppMainWindow):
@@ -63,6 +68,7 @@ class AppNetworkConnectivityManager(NetworkConnectivityManager):
super().successCallback(networkReply, **kwargs)
def failureCallback(self, networkReply, **kwargs):
"""Handle a failed network operation."""
parent = self.parent()
if isinstance(parent, AppMainWindow):
@@ -71,6 +77,7 @@ class AppNetworkConnectivityManager(NetworkConnectivityManager):
super().failureCallback(networkReply, **kwargs)
def startSingleTest(self):
"""Start single test."""
if not APP().isSystemTrayConnected():
parent = self.parent()
@@ -92,6 +99,7 @@ class AppNetworkConnectivityManager(NetworkConnectivityManager):
super().startSingleTest()
def disconnectedCallback(self):
"""Update the app network connectivity manager for a disconnected state."""
parent = self.parent()
if isinstance(parent, AppMainWindow):
@@ -101,6 +109,7 @@ class AppNetworkConnectivityManager(NetworkConnectivityManager):
class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
"""Provide the network state badge widget."""
DefaultIconFileName = 'reception-4.svg'
StateIconFileName = {
'success': 'reception-4.svg',
@@ -109,6 +118,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
IconSize = QtCore.QSize(16, 16)
def __init__(self, parent=None):
"""Initialize the NetworkStateBadge."""
super().__init__(parent)
self.currentIconFileName = self.DefaultIconFileName
@@ -132,6 +142,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.setIconByTheme(APP().theme())
def setIconByTheme(self, theme: str, iconFileName: Union[str, None] = None):
"""Set icon by theme."""
iconFileName = iconFileName or self.currentIconFileName
if theme == AppStyleSheet.Dark:
@@ -142,6 +153,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.iconLabel.setPixmap(icon.pixmap(self.IconSize))
def statusText(self):
"""Return the status text value used by the network state badge."""
if self.currentState == 'success':
return f'{_("Network OK")} - {self.currentRemark}'
@@ -151,6 +163,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
return ''
def statusToolTip(self):
"""Return the status tool tip value used by the network state badge."""
if self.currentState == 'success':
return self.currentRemark
@@ -160,6 +173,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
return ''
def updateStatusText(self):
"""Update status text."""
text = self.statusText()
tooltip = self.statusToolTip()
@@ -169,6 +183,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.textLabel.setToolTip(tooltip)
def setStatus(self, state: str, remark: str, errorString: str = ''):
"""Set status."""
self.currentState = state
self.currentRemark = remark
self.currentErrorString = errorString
@@ -182,6 +197,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.refreshStyle()
def clearStatus(self):
"""Clear status."""
self.currentState = ''
self.currentRemark = ''
self.currentErrorString = ''
@@ -194,6 +210,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.refreshStyle()
def refreshStyle(self):
"""Refresh style."""
for widget in [self, self.iconLabel, self.textLabel]:
widget.style().unpolish(widget)
widget.style().polish(widget)
@@ -202,14 +219,18 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget):
self.setVisible(bool(self.textLabel.text()))
def themeChangedCallback(self, theme: str):
"""Update the network state badge for a theme change."""
self.setIconByTheme(theme)
def retranslate(self):
"""Refresh translated text for the network state badge."""
self.updateStatusText()
class SearchButton(AppQPushButton):
"""Represent search button."""
def __init__(self, *args, **kwargs):
"""Initialize the SearchButton."""
super().__init__(*args, **kwargs)
self.setText(self.customText())
@@ -217,19 +238,23 @@ class SearchButton(AppQPushButton):
@staticmethod
def customText():
"""Return the user-facing message text for the search button."""
return ' ' * 2 + _('Search')
def retranslate(self):
"""Refresh translated text for the search button."""
self.setText(self.customText())
class AppMainWindow(AppQMainWindow):
"""Present the app main window."""
DEFAULT_WINDOW_SIZE_DARWIN = QtCore.QSize(1500, 780)
DEFAULT_WINDOW_SIZE = (
QtCore.QSize(1800, 960) if PLATFORM != 'Darwin' else DEFAULT_WINDOW_SIZE_DARWIN
)
def __init__(self, *args, **kwargs):
"""Initialize the AppMainWindow."""
super().__init__(*args, **kwargs)
if SystemRuntime.isAdmin():
@@ -610,27 +635,35 @@ class AppMainWindow(AppQMainWindow):
@QtCore.Slot(str)
def handleUserServersSearchTextChanged(self, text: str):
"""Handle user servers search text changed."""
if not text:
self.userServersQTableWidget.clearSearch()
def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
"""Update subs by unique."""
self.userServersQTableWidget.updateSubsByUnique(unique, httpProxy, **kwargs)
def appendNewItemByFactory(self, factory: ConfigFactory):
"""Append new item by factory."""
self.userServersQTableWidget.appendNewItemByFactory(factory)
def flushRow(self, row: int, item: ConfigFactory):
"""Refresh row."""
self.userServersQTableWidget.flushRow(row, item)
def showTabAndSpaces(self):
"""Show tab and spaces."""
self.userServersQTableWidget.showTabAndSpaces()
def hideTabAndSpaces(self):
"""Hide tab and spaces."""
self.userServersQTableWidget.hideTabAndSpaces()
def getGuiTUNSettings(self, **kwargs):
"""Return GUI TUN settings."""
@functools.lru_cache(None)
def cachedGuiTUNSettings():
"""Return the cached GUI TUN settings value used by the app main window."""
parent = kwargs.pop('parent', self)
return GuiTUNSettings(parent=parent, **kwargs)
@@ -641,13 +674,16 @@ class AppMainWindow(AppQMainWindow):
return guiTUNSettings
def checkForUpdates(self, **kwargs):
"""Check for updates."""
self.updatesManager.configureHttpProxy(Storage.Extras.UserHttpProxy())
self.updatesManager.checkForUpdates(**kwargs)
def resetNetworkState(self):
"""Reset network state."""
self.networkState.clearStatus()
def setNetworkState(self, success: bool, **kwargs):
"""Set network state."""
remark = Storage.Extras.UserServerRemark()
if not remark:
@@ -673,6 +709,7 @@ class AppMainWindow(AppQMainWindow):
@staticmethod
def openApplicationFolder():
"""Open application folder."""
appFolder = os.path.dirname(APP().applicationFilePath())
if QDesktopServices.openUrl(QtCore.QUrl(appFolder)):
@@ -682,6 +719,7 @@ class AppMainWindow(AppQMainWindow):
@staticmethod
def restartAsAdmin():
"""Handle restart as admin for the app main window."""
if not SystemRuntime.isScriptMode():
if not SystemRuntime.isAdmin():
process = QtCore.QProcess()
@@ -718,12 +756,14 @@ class AppMainWindow(AppQMainWindow):
@staticmethod
def openAboutPage():
"""Open about page."""
if QDesktopServices.openUrl(QtCore.QUrl(APPLICATION_ABOUT_PAGE)):
logger.info('open about page success')
else:
logger.error('open about page failed')
def setWidthAndHeight(self):
"""Apply the default size for the app main window."""
if AppSettings.get('AppMainWindowGeometry') is None:
# Migrate legacy settings
try:
@@ -773,11 +813,13 @@ class AppMainWindow(AppQMainWindow):
)
def retranslate(self):
"""Refresh translated text for the app main window."""
if SystemRuntime.isAdmin():
self.setWindowTitle(f'{_(APPLICATION_NAME)} ({_(ADMINISTRATOR_NAME)})')
else:
self.setWindowTitle(f'{_(APPLICATION_NAME)}')
def cleanup(self):
"""Release resources owned by the app main window."""
AppSettings.set('AppMainWindowGeometry', self.saveGeometry())
AppSettings.set('AppMainWindowState', self.saveState())
+12
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for log viewer window."""
from __future__ import annotations
from Furious.Qt import *
@@ -27,18 +29,22 @@ __all__ = ['LogViewerWindow']
class MBoxSaveError(AppQMessageBox):
"""Represent m box save error."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxSaveError."""
super().__init__(*args, **kwargs)
self.saveError = ''
def customText(self):
"""Return the user-facing message text for the m box save error."""
if self.saveError:
return _('Unable to save log') + f'\n\n{self.saveError}'
else:
return _('Unable to save log')
def retranslate(self):
"""Refresh translated text for the m box save error."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -48,6 +54,7 @@ class MBoxSaveError(AppQMessageBox):
def saveAsFile(content: str):
"""Save as file."""
filename, selectedFilter = QFileDialog.getSaveFileName(
None, _('Save File'), filter=_('Text files (*.txt);;All files (*)')
)
@@ -69,7 +76,9 @@ def saveAsFile(content: str):
class LogViewerWindow(AppQMainWindow):
"""Present the log viewer window."""
def __init__(self, *args, **kwargs):
"""Initialize the LogViewerWindow."""
tabTitle = kwargs.pop('tabTitle', '')
fontFamily = kwargs.pop('fontFamily', '')
pointSizeSettingsName = kwargs.pop('pointSizeSettingsName', '')
@@ -152,10 +161,13 @@ class LogViewerWindow(AppQMainWindow):
self.menuBar().addMenu(self._viewMenu)
def plainText(self) -> str:
"""Return the plain text value used by the log viewer window."""
return self.textBrowser.toPlainText()
def appendLine(self, line: str):
"""Append line."""
self.textBrowser.appendLine(line)
def clear(self):
"""Remove all data from the log viewer window."""
self.textBrowser.clear()
+7
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for QR code window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -33,7 +35,9 @@ __all__ = ['QRCodeWindow']
class QRCodeWindow(AppQMainWindow):
"""Present the QR code window."""
def __init__(self, *args, **kwargs):
"""Initialize the QRCodeWindow."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_(APPLICATION_NAME))
@@ -46,9 +50,11 @@ class QRCodeWindow(AppQMainWindow):
self.setCentralWidget(self.tabWidget)
def tabCount(self) -> int:
"""Return the tab count value used by the QR code window."""
return self.tabWidget.count()
def initTabByIndex(self, indexes):
"""Handle init tab by index for the QR code window."""
self.tabWidget.clear()
for index in indexes:
@@ -79,6 +85,7 @@ class QRCodeWindow(AppQMainWindow):
@QtCore.Slot(int)
def handleTabCloseRequested(self, index):
"""Handle tab close requested."""
self.tabWidget.removeTab(index)
if self.tabWidget.count() == 0:
+24
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for text editor window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -35,7 +37,9 @@ registerAppSettings('ServerWidgetPointSize')
class MBoxQuestionSave(AppQMessageBox):
"""Represent m box question save."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxQuestionSave."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Save Changes'))
@@ -51,18 +55,22 @@ class MBoxQuestionSave(AppQMessageBox):
class MBoxJSONDecodeError(AppQMessageBox):
"""Represent m box JSON decode error."""
def __init__(self, *args, **kwargs):
"""Initialize the MBoxJSONDecodeError."""
super().__init__(*args, **kwargs)
self.error = ''
def customText(self):
"""Return the user-facing message text for the m box JSON decode error."""
return (
_('Please check if the configuration is in valid JSON format')
+ f'\n\n{self.error}'
)
def retranslate(self):
"""Refresh translated text for the m box JSON decode error."""
self.setWindowTitle(_(self.windowTitle()))
self.setText(self.customText())
@@ -72,7 +80,9 @@ class MBoxJSONDecodeError(AppQMessageBox):
class TextEditorWindow(AppQMainWindow):
"""Present the text editor window."""
def __init__(self, *args, **kwargs):
"""Initialize the TextEditorWindow."""
super().__init__(*args, **kwargs)
self.customWindowTitle = ''
@@ -90,9 +100,11 @@ class TextEditorWindow(AppQMainWindow):
self.statusBar().addPermanentWidget(self.lineColumnLabel)
def modificationCallback():
"""Handle the modification callback."""
self.markAsModified()
def cursorChangedCallback(cursor: QTextCursor):
"""Handle the cursor changed callback."""
self.lineColumnLabel.setText(
f'{cursor.blockNumber() + 1}:{cursor.columnNumber() + 1} {cursor.position()}'
)
@@ -233,14 +245,17 @@ class TextEditorWindow(AppQMainWindow):
self.menuBar().addMenu(self.viewMenu)
def markAsModified(self):
"""Handle mark as modified for the text editor window."""
self.modified = True
self.setWindowTitle(self.customWindowTitle + self.modifiedMark)
def markAsSaved(self):
"""Handle mark as saved for the text editor window."""
self.modified = False
self.setWindowTitle(self.customWindowTitle)
def setPlainText(self, text: str, blockSignals: bool):
"""Set plain text."""
if blockSignals:
with Mixins.QBlockSignalContext(self.jsonEditor):
self.jsonEditor.setPlainText(text)
@@ -248,6 +263,7 @@ class TextEditorWindow(AppQMainWindow):
self.jsonEditor.setPlainText(text)
def save(self, showChangesMethod='open') -> bool:
"""Save the text editor window."""
index = self.currentIndex
if index < 0:
@@ -296,6 +312,7 @@ class TextEditorWindow(AppQMainWindow):
return True
def saveAsFile(self):
"""Save as file."""
filename, selectedFilter = QFileDialog.getSaveFileName(
None, _('Save File'), filter=_('Text files (*.json);;All files (*)')
)
@@ -317,7 +334,9 @@ class TextEditorWindow(AppQMainWindow):
mbox.open()
def setIndent(self):
"""Set indent."""
def handleResultCode(_indentSpinBox, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
plain = self.jsonEditor.toPlainText()
@@ -353,6 +372,7 @@ class TextEditorWindow(AppQMainWindow):
indentSpinBox.open()
def showTabAndSpaces(self):
"""Show tab and spaces."""
textOption = QTextOption()
textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces)
@@ -362,6 +382,7 @@ class TextEditorWindow(AppQMainWindow):
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def hideTabAndSpaces(self):
"""Hide tab and spaces."""
textOption = QTextOption()
self.jsonEditor.document().setDefaultTextOption(textOption)
@@ -370,11 +391,13 @@ class TextEditorWindow(AppQMainWindow):
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
def closeEvent(self, event: QtCore.QEvent):
"""Handle closure of the text editor window."""
if self.modified:
mbox = MBoxQuestionSave(icon=AppQMessageBox.Icon.Question, parent=self)
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
def handleButtonClicked(button):
"""Handle button clicked."""
if button == mbox.button0:
# Save
if self.save(showChangesMethod='exec'):
@@ -403,4 +426,5 @@ class TextEditorWindow(AppQMainWindow):
def retranslate(self):
# Do nothing
"""Refresh translated text for the text editor window."""
pass
+95
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for user routing window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -48,6 +50,7 @@ DOMAIN_STRATEGIES, ROUTING_STATES = (
def valuesToList(text: str) -> list[str]:
"""Return the values to list value used by the application."""
values = list()
for line in text.splitlines():
@@ -61,6 +64,7 @@ def valuesToList(text: str) -> list[str]:
def listToText(value) -> str:
"""Return the list to text value used by the application."""
if isinstance(value, list):
return '\n'.join(str(item) for item in value)
@@ -68,6 +72,7 @@ def listToText(value) -> str:
def routingStateText(state: str) -> str:
"""Return the routing state text value used by the application."""
if state == 'Enabled':
return _('Enabled')
elif state == 'Disabled':
@@ -77,6 +82,7 @@ def routingStateText(state: str) -> str:
def addRoutingStateItems(combo: QComboBox):
"""Add routing state items."""
combo.clear()
for state in ROUTING_STATES:
@@ -84,6 +90,7 @@ def addRoutingStateItems(combo: QComboBox):
def comboCurrentData(combo: QComboBox, default=''):
"""Return the combo current data value used by the application."""
data = combo.currentData()
if data is None:
@@ -93,6 +100,7 @@ def comboCurrentData(combo: QComboBox, default=''):
def routingObjectFromProfile(routingProfile: dict):
"""Return the routing object from profile value used by the application."""
if not isinstance(routingProfile, dict):
routingProfile = dict()
@@ -116,7 +124,9 @@ def routingObjectFromProfile(routingProfile: dict):
class RoutingPreviewDialog(AppQDialog):
"""Present the routing preview dialog."""
def __init__(self, routingProfile: dict, parent=None):
"""Initialize the RoutingPreviewDialog."""
super().__init__(parent)
self.setWindowTitle(_('Preview Routing'))
@@ -151,11 +161,14 @@ class RoutingPreviewDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing preview dialog."""
self.setFixedSize(400, int(400 * GOLDEN_RATIO))
class RoutingTextEditDialog(AppQDialog):
"""Present the routing text edit dialog."""
def __init__(self, text='', parent=None):
"""Initialize the RoutingTextEditDialog."""
super().__init__(parent)
self.setWindowTitle(_('Edit Text'))
@@ -179,22 +192,28 @@ class RoutingTextEditDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing text edit dialog."""
self.setFixedSize(760, 470)
def text(self):
"""Return the text value."""
return self.textEdit.toPlainText()
class RoutingTextEdit(Mixins.QTranslatable, QTextEdit):
"""Represent routing text edit."""
def __init__(self, text='', parent=None):
"""Initialize the RoutingTextEdit."""
super().__init__(text, parent)
self.setToolTip(_('Double-click to enlarge'))
def mouseDoubleClickEvent(self, event):
"""Handle the mouse double click event."""
dialog = RoutingTextEditDialog(self.toPlainText(), parent=self)
def handleResultCode(code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
self.setPlainText(dialog.text())
@@ -204,19 +223,23 @@ class RoutingTextEdit(Mixins.QTranslatable, QTextEdit):
event.accept()
def retranslate(self):
"""Refresh translated text for the routing text edit."""
self.setToolTip(_(self.toolTip()))
class RoutingDocumentationURL(AppQLabel):
"""Represent routing documentation URL."""
URL = 'https://xtls.github.io/config/routing.html'
def __init__(self, *args, **kwargs):
"""Initialize the RoutingDocumentationURL."""
super().__init__(*args, **kwargs)
self.setWebsiteURL()
self.linkActivated.connect(self.handleLinkActivated)
def setWebsiteURL(self):
"""Set website URL."""
self.setText(
'<html><head/><body><p>'
f'<a href="{self.URL}">'
@@ -227,37 +250,44 @@ class RoutingDocumentationURL(AppQLabel):
@staticmethod
def handleLinkActivated(link: str):
"""Handle link activated."""
if QDesktopServices.openUrl(QtCore.QUrl(link)):
logger.info(f'open link \'{link}\' success')
else:
logger.error(f'open link \'{link}\' failed')
def retranslate(self):
"""Refresh translated text for the routing documentation URL."""
self.setWebsiteURL()
class RoutingProfilesModel(QtCore.QAbstractTableModel):
"""Expose routing profiles data through a Qt item model."""
Headers = ['Remark', 'Domain Strategy', 'State']
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of rows exposed by the model."""
if parent.isValid():
return 0
return len(Storage.UserRoutings())
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of columns exposed by the model."""
if parent.isValid():
return 0
return len(self.Headers)
def flags(self, index):
"""Return the Qt item flags for a model index."""
if not index.isValid():
return QtCore.Qt.ItemFlag.NoItemFlags
return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable
def headerData(self, section, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole):
"""Return display data for a table header section."""
if role != QtCore.Qt.ItemDataRole.DisplayRole:
return None
@@ -272,12 +302,15 @@ class RoutingProfilesModel(QtCore.QAbstractTableModel):
return section + 1
def routingUniqueByRow(self, row: int):
"""Return the routing unique by row value used by the routing profiles model."""
return list(Storage.UserRoutings().keys())[row]
def routingByRow(self, row: int):
"""Return the routing by row value used by the routing profiles model."""
return Storage.UserRoutings()[self.routingUniqueByRow(row)]
def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
"""Return the data managed by the routing profiles model."""
if not index.isValid():
return None
@@ -304,9 +337,11 @@ class RoutingProfilesModel(QtCore.QAbstractTableModel):
return None
def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole):
"""Update model data for the requested role."""
return False
def emitAllChanged(self):
"""Handle emit all changed for the routing profiles model."""
if self.rowCount() == 0:
return
@@ -317,11 +352,13 @@ class RoutingProfilesModel(QtCore.QAbstractTableModel):
class RoutingRuleEditDialog(AppQDialog):
"""Present the routing rule edit dialog."""
MatchInputHeight = 72
ShortInputWidth = 240
@staticmethod
def textEdit(text=''):
"""Return the text edit value used by the routing rule edit dialog."""
widget = RoutingTextEdit(text)
widget.setFixedHeight(RoutingRuleEditDialog.MatchInputHeight)
@@ -329,12 +366,14 @@ class RoutingRuleEditDialog(AppQDialog):
@staticmethod
def lineEdit(text=''):
"""Return the line edit value used by the routing rule edit dialog."""
widget = QLineEdit(text)
widget.setFixedHeight(RoutingRuleEditDialog.MatchInputHeight)
return widget
def __init__(self, rule: dict, parent=None):
"""Initialize the RoutingRuleEditDialog."""
super().__init__(parent)
self.rule = dict(rule)
@@ -500,9 +539,11 @@ class RoutingRuleEditDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing rule edit dialog."""
self.resize(int(800 * GOLDEN_RATIO), 800)
def routingRule(self):
"""Return the routing rule value used by the routing rule edit dialog."""
rule = {
'type': 'field',
'outboundTag': self.outboundEdit.text().strip() or 'proxy',
@@ -539,7 +580,9 @@ class RoutingRuleEditDialog(AppQDialog):
class RoutingRemarkEditDialog(AppQDialog):
"""Present the routing remark edit dialog."""
def __init__(self, remark: str, parent=None):
"""Initialize the RoutingRemarkEditDialog."""
super().__init__(parent)
self.setWindowTitle(_('Edit Routing Remark'))
@@ -563,14 +606,18 @@ class RoutingRemarkEditDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing remark edit dialog."""
self.resize(420, 120)
def remark(self):
"""Return the remark value used by the routing remark edit dialog."""
return self.remarkEdit.text().strip()
class RoutingProfileEditDialog(AppQDialog):
"""Present the routing profile edit dialog."""
def __init__(self, parent=None):
"""Initialize the RoutingProfileEditDialog."""
super().__init__(parent)
self.setWindowTitle(_('Add Routing'))
@@ -607,9 +654,11 @@ class RoutingProfileEditDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing profile edit dialog."""
self.resize(460, 160)
def routing(self):
"""Return the routing value used by the routing profile edit dialog."""
return {
'remark': self.remarkEdit.text().strip() or _('New Routing'),
'domainStrategy': self.domainStrategyCombo.currentText(),
@@ -619,12 +668,14 @@ class RoutingProfileEditDialog(AppQDialog):
class RoutingRulesQListWidget(AppQListWidget):
"""Provide the routing rules Qt list widget."""
editRequested, deleteRequested = (
QtCore.Signal(),
QtCore.Signal(),
)
def __init__(self, routing: dict, parent=None):
"""Initialize the RoutingRulesQListWidget."""
super().__init__(parent)
self.routing = routing
@@ -646,6 +697,7 @@ class RoutingRulesQListWidget(AppQListWidget):
self.flushAll()
def rules(self):
"""Return the rules value used by the routing rules Qt list widget."""
rules = self.routing.setdefault('rules', list())
if not isinstance(rules, list):
@@ -654,9 +706,11 @@ class RoutingRulesQListWidget(AppQListWidget):
return rules
def ruleAt(self, index: int):
"""Return the rule at value used by the routing rules Qt list widget."""
return self.rules()[index]
def ruleText(self, rule: dict) -> str:
"""Return the rule text value used by the routing rules Qt list widget."""
name, outbound, domains, ips = (
rule.get('ruleTag', '') or 'Untitled Rule',
rule.get('outboundTag', 'proxy'),
@@ -667,6 +721,7 @@ class RoutingRulesQListWidget(AppQListWidget):
return f'{name} -> {outbound} ({domains} domains, {ips} IPs)'
def selectedRuleText(self):
"""Select ed rule text."""
indexes = self.selectedIndex
if not indexes:
@@ -675,20 +730,24 @@ class RoutingRulesQListWidget(AppQListWidget):
return self.ruleText(self.ruleAt(indexes[0]))
def appendRule(self, rule: dict):
"""Append rule."""
self.rules().append(rule)
self.flushAll()
def setRule(self, index: int, rule: dict):
"""Set rule."""
self.rules()[index] = rule
self.flushAll()
def deleteRules(self, indexes: list[int]):
"""Delete rules."""
for i in range(len(indexes)):
self.rules().pop(indexes[i] - i)
self.flushAll()
def flushAll(self):
"""Refresh all."""
self.clear()
for rule in self.rules():
@@ -696,11 +755,14 @@ class RoutingRulesQListWidget(AppQListWidget):
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
"""Handle custom context menu requested."""
self.contextMenu.exec(self.viewport().mapToGlobal(point))
class RoutingRulesDialog(AppQDialog):
"""Present the routing rules dialog."""
def __init__(self, routing: dict, parent=None):
"""Initialize the RoutingRulesDialog."""
super().__init__(parent)
self.routing = routing
@@ -744,13 +806,16 @@ class RoutingRulesDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the routing rules dialog."""
self.setFixedSize(760, 470)
def addRule(self):
"""Add rule."""
rule = {'type': 'field', 'outboundTag': 'proxy', 'ruleTag': 'New Rule'}
dialog = RoutingRuleEditDialog(rule, parent=None)
def handleResultCode(code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
self.listWidget.appendRule(dialog.routingRule())
@@ -758,6 +823,7 @@ class RoutingRulesDialog(AppQDialog):
dialog.open()
def editRule(self):
"""Handle edit rule for the routing rules dialog."""
indexes = self.listWidget.selectedIndex
if len(indexes) != 1:
@@ -767,6 +833,7 @@ class RoutingRulesDialog(AppQDialog):
dialog = RoutingRuleEditDialog(self.listWidget.ruleAt(index), parent=None)
def handleResultCode(_index, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
self.listWidget.setRule(_index, dialog.routingRule())
@@ -774,12 +841,14 @@ class RoutingRulesDialog(AppQDialog):
dialog.open()
def deleteRule(self):
"""Delete rule."""
indexes = self.listWidget.selectedIndex
if len(indexes) == 0:
return
def handleResultCode(_indexes, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -809,14 +878,18 @@ class RoutingRulesDialog(AppQDialog):
class UserRoutingQTableViewHorizontalHeader(AppQHeaderView):
"""Provide the user routing Qt table view horizontal table header."""
def __init__(self, *args, **kwargs):
"""Initialize the UserRoutingQTableViewHorizontalHeader."""
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
"""Represent user routing table view."""
RowHeight = 42
def __init__(self, parent=None):
"""Initialize the UserRoutingTableView."""
super().__init__(parent)
self.sourceModel = RoutingProfilesModel(parent=self)
@@ -862,6 +935,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.flushAll()
def configureHeader(self):
"""Configure header."""
header = self.horizontalHeader()
header.setSectionsMovable(True)
header.setFirstSectionMovable(True)
@@ -870,15 +944,18 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
@property
def selectedIndex(self):
"""Return the selected index value."""
return sorted(
list(set(index.row() for index in self.selectionModel().selectedRows()))
)
def routingUniqueByRow(self, row):
"""Return the routing unique by row value used by the user routing table view."""
return self.sourceModel.routingUniqueByRow(row)
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
"""Handle custom context menu requested."""
indexes = self.selectedIndex
isUniqueFlag = len(indexes) == 1
@@ -889,6 +966,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.contextMenu.exec(self.viewport().mapToGlobal(point))
def flushItem(self, row: int, column: int):
"""Refresh item."""
index = self.sourceModel.index(row, column)
if column == 1:
@@ -924,16 +1002,19 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.setIndexWidget(index, combo)
def flushRow(self, row: int):
"""Refresh row."""
for column in range(self.sourceModel.columnCount()):
self.flushItem(row, column)
def flushAll(self):
"""Refresh all."""
self.sourceModel.emitAllChanged()
for row in range(self.sourceModel.rowCount()):
self.flushRow(row)
def retranslate(self):
"""Refresh translated text for the user routing table view."""
self.sourceModel.headerDataChanged.emit(
QtCore.Qt.Orientation.Horizontal,
0,
@@ -942,6 +1023,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.flushAll()
def setDomainStrategy(self, row: int, text: str):
"""Set domain strategy."""
if row < 0 or row >= self.sourceModel.rowCount():
return
@@ -949,6 +1031,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.sourceModel.emitAllChanged()
def setEnabled(self, row: int, state: str):
"""Set enabled."""
if row < 0 or row >= self.sourceModel.rowCount():
return
@@ -956,9 +1039,11 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
self.sourceModel.emitAllChanged()
def appendNewItem(self):
"""Append new item."""
dialog = RoutingProfileEditDialog(parent=self)
def handleResultCode(code):
"""Handle result code."""
if code != PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
return
@@ -976,6 +1061,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
dialog.open()
def deleteSelectedItem(self):
"""Delete selected item."""
indexes = self.selectedIndex
if not indexes:
@@ -983,6 +1069,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
return
def handleResultCode(_indexes, code):
"""Handle result code."""
if code != PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
@@ -1027,6 +1114,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
mbox.open()
def renameSelectedItem(self):
"""Handle rename selected item for the user routing table view."""
indexes = self.selectedIndex
if not indexes:
@@ -1042,6 +1130,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
dialog = RoutingRemarkEditDialog(routing.get('remark', ''), parent=self)
def handleResultCode(code):
"""Handle result code."""
if code != PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
return
@@ -1055,6 +1144,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
dialog.open()
def previewSelectedItem(self):
"""Handle preview selected item for the user routing table view."""
indexes = self.selectedIndex
if len(indexes) != 1:
@@ -1066,6 +1156,7 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
dialog.open()
def editSelectedRules(self):
"""Handle edit selected rules for the user routing table view."""
indexes = self.selectedIndex
if not indexes:
@@ -1084,9 +1175,11 @@ class UserRoutingTableView(Mixins.QTranslatable, AppQTableView):
class UserRoutingWindow(AppQMainWindow):
"""Present the user routing window."""
DEFAULT_WINDOW_SIZE = QtCore.QSize(980, 560)
def __init__(self, *args, **kwargs):
"""Initialize the UserRoutingWindow."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Edit Routing'))
@@ -1116,6 +1209,7 @@ class UserRoutingWindow(AppQMainWindow):
self.setCentralWidget(centralWidget)
def setWidthAndHeight(self):
"""Apply the default size for the user routing window."""
if AppSettings.get('UserRoutingWindowGeometry') is None:
self.resize(UserRoutingWindow.DEFAULT_WINDOW_SIZE)
else:
@@ -1137,5 +1231,6 @@ class UserRoutingWindow(AppQMainWindow):
pass
def cleanup(self):
"""Release resources owned by the user routing window."""
AppSettings.set('UserRoutingWindowGeometry', self.saveGeometry())
AppSettings.set('UserRoutingWindowState', self.saveState())
+15
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for user subs window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -37,7 +39,9 @@ registerAppSettings('UserSubsWindowState')
class AddSubsDialog(AppQDialog):
"""Present the add subs dialog."""
def __init__(self, *args, **kwargs):
"""Initialize the AddSubsDialog."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Add Subscription'))
@@ -67,19 +71,24 @@ class AddSubsDialog(AppQDialog):
self.setLayout(layout)
def setWidthAndHeight(self):
"""Apply the default size for the add subs dialog."""
self.resize(456, 150)
def subsRemark(self):
"""Return the subs remark value used by the add subs dialog."""
return self.remarkEdit.text()
def subsWebURL(self):
"""Return the subs web URL value used by the add subs dialog."""
return self.webURLEdit.text()
class UserSubsWindow(AppQMainWindow):
"""Present the user subs window."""
DEFAULT_WINDOW_SIZE = QtCore.QSize(1120, 600)
def __init__(self, *args, **kwargs):
"""Initialize the UserSubsWindow."""
callback = kwargs.pop('deleteUniqueCallback', None)
super().__init__(*args, **kwargs)
@@ -116,7 +125,9 @@ class UserSubsWindow(AppQMainWindow):
self.setCentralWidget(self.fakeCentralWidget)
def addSubs(self):
"""Add subs."""
def handleResultCode(_addSubsDialog, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted):
remark = _addSubsDialog.subsRemark()
webURL = _addSubsDialog.subsWebURL()
@@ -142,15 +153,18 @@ class UserSubsWindow(AppQMainWindow):
addSubsDialog.open()
def deleteSelectedItem(self):
"""Delete selected item."""
self.userSubsQTableWidget.deleteSelectedItem()
def keyPressEvent(self, event):
"""Handle a key press for the user subs window."""
if event.key() == QtCore.Qt.Key.Key_Delete:
self.deleteSelectedItem()
else:
super().keyPressEvent(event)
def setWidthAndHeight(self):
"""Apply the default size for the user subs window."""
if AppSettings.get('UserSubsWindowGeometry') is None:
# Migrate legacy settings
try:
@@ -185,5 +199,6 @@ class UserSubsWindow(AppQMainWindow):
pass
def cleanup(self):
"""Release resources owned by the user subs window."""
AppSettings.set('UserSubsWindowGeometry', self.saveGeometry())
AppSettings.set('UserSubsWindowState', self.saveState())
+8
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the application window for Xray asset viewer window."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -33,7 +35,9 @@ logger = logging.getLogger(__name__)
class XrayAssetViewerWindow(AppQMainWindow):
"""Present the Xray asset viewer window."""
def __init__(self, *args, **kwargs):
"""Initialize the XrayAssetViewerWindow."""
super().__init__(*args, **kwargs)
self.setWindowTitle(_('Xray-core Asset File'))
@@ -83,19 +87,23 @@ class XrayAssetViewerWindow(AppQMainWindow):
self.menuBar().addMenu(self.fileMenu)
def setWidthAndHeight(self):
"""Apply the default size for the Xray asset viewer window."""
self.resize(360, 360 * GOLDEN_RATIO)
def flushItem(self):
"""Refresh item."""
self.xrayAssetViewerWidget.flushItem()
@staticmethod
def openAssetDirectory():
"""Open asset directory."""
if QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(XRAY_ASSET_DIR)):
logger.info(f'open Xray-core asset dir success')
else:
logger.error(f'open Xray-core asset dir failed')
def appendNewItem(self):
"""Append new item."""
filename, selectedFilter = QFileDialog.getOpenFileName(
None, _('Import File'), filter=_('All files (*)')
)
+2
View File
@@ -15,6 +15,8 @@
# 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 application window classes."""
from __future__ import annotations
from .AppMainWindow import *
+2
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Initialize the Furious application package."""
from __future__ import annotations
from .Frozenlib import AppResources
+6
View File
@@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide the Furious GUI application entry point."""
from __future__ import annotations
from Furious.Frozenlib import *
@@ -39,6 +41,7 @@ logger = logging.getLogger(__name__)
def runClearSettings():
"""Run clear settings."""
app = QtCore.QCoreApplication(sys.argv)
app.setApplicationName(APPLICATION_NAME)
app.setApplicationVersion(APPLICATION_VERSION)
@@ -55,6 +58,7 @@ def runClearSettings():
def runAppMain():
"""Run app main."""
process = AppMainProcess(functools.partial(Application, sys.argv))
process.start()
@@ -130,6 +134,7 @@ def runAppMain():
button1 = mbox.addButton(_('OK'), AppQMessageBox.ButtonRole.RejectRole)
def handleButtonClicked(button):
"""Handle button clicked."""
if button == button0:
# Open
if QDesktopServices.openUrl(
@@ -157,6 +162,7 @@ def runAppMain():
def main():
"""Run the module command-line entry point."""
try:
if len(sys.argv) > 1 and sys.argv[1] == AppBuiltinCommand.Clear.value:
runClearSettings()