mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-14 01:59:26 +03:00
Add docstrings across Python modules
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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''):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 *
|
||||
|
||||
Vendored
+2
@@ -1,3 +1,5 @@
|
||||
"""Define source strings and translations used by the localization workflow."""
|
||||
|
||||
TRANSLATION = {
|
||||
"Delete": {
|
||||
"source": [
|
||||
|
||||
Vendored
+2
@@ -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 *
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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_')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'] = ''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ''
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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__()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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())
|
||||
|
||||
@@ -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 (*)')
|
||||
)
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user