mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-13 01:38:33 +03:00
Prepare the 0.5.0 release workload
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
@@ -119,7 +119,7 @@ else:
|
||||
ARTIFACT_NAME = ''
|
||||
|
||||
|
||||
def downloadXrayAssets(url, filename):
|
||||
def downloadXrayAssets(url, filepath):
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
@@ -130,9 +130,6 @@ def downloadXrayAssets(url, filename):
|
||||
if not os.path.exists(XRAY_ASSET_DIR):
|
||||
os.makedirs(XRAY_ASSET_DIR)
|
||||
|
||||
# Full path where the file will be saved
|
||||
filepath = os.path.join(XRAY_ASSET_DIR, filename)
|
||||
|
||||
# Send an HTTP GET request to the URL
|
||||
response = requests.get(url)
|
||||
response.raise_for_status() # Check if the request was successful
|
||||
@@ -213,18 +210,10 @@ def cleanup():
|
||||
|
||||
|
||||
def download():
|
||||
# URLs of geosite and geoip assets
|
||||
url_geosite = 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat'
|
||||
url_geoip = 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat'
|
||||
|
||||
# Xray assets names
|
||||
filename_geosite = 'geosite.dat'
|
||||
filename_geoip = 'geoip.dat'
|
||||
|
||||
return all(
|
||||
[
|
||||
downloadXrayAssets(url_geosite, filename_geosite),
|
||||
downloadXrayAssets(url_geoip, filename_geoip),
|
||||
downloadXrayAssets(URL_GEOSITE, XRAY_ASSET_PATH_GEOSITE),
|
||||
downloadXrayAssets(URL_GEOIP, XRAY_ASSET_PATH_GEOIP),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
+172
-41
@@ -78,6 +78,30 @@ def fixLogObjectPath(config: ConfigurationFactory, attr: str, value: str, log=Tr
|
||||
)
|
||||
|
||||
|
||||
def getUserTUNSettings(*args, **kwargs):
|
||||
return AS_UserTUNSettings().get(*args, **kwargs)
|
||||
|
||||
|
||||
userPrimaryAdapterInterfaceName = functools.partial(
|
||||
getUserTUNSettings, 'primaryAdapterInterfaceName', ''
|
||||
)
|
||||
userPrimaryAdapterInterfaceIP = functools.partial(
|
||||
getUserTUNSettings, 'primaryAdapterInterfaceIP', ''
|
||||
)
|
||||
userDefaultPrimaryGatewayIP = functools.partial(
|
||||
getUserTUNSettings, 'defaultPrimaryGatewayIP', ''
|
||||
)
|
||||
userTunAdapterInterfaceDNS = functools.partial(
|
||||
getUserTUNSettings, 'tunAdapterInterfaceDNS', ''
|
||||
)
|
||||
userBypassTUNAdapterInterfaceIP = functools.partial(
|
||||
getUserTUNSettings, 'bypassTUNAdapterInterfaceIP', ''
|
||||
)
|
||||
userDisablePrimaryAdapterInterfaceDNS = functools.partial(
|
||||
getUserTUNSettings, 'disablePrimaryAdapterInterfaceDNS', 'True'
|
||||
)
|
||||
|
||||
|
||||
class CoreManager(SupportExitCleanup):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -116,12 +140,12 @@ class CoreManager(SupportExitCleanup):
|
||||
fixLogObjectPath(configcopy, attr, logRedirectValue, log)
|
||||
|
||||
if routing == 'Bypass Mainland China':
|
||||
# VPN Mode handling
|
||||
if not proxyModeOnly and isVPNMode():
|
||||
# TUN Mode handling
|
||||
if not proxyModeOnly and isTUNMode():
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_('Unable to connect'))
|
||||
mbox.setText(
|
||||
_('Routing option with direct rules is not allowed in VPN mode')
|
||||
_('Routing option with direct rules is not allowed in TUN mode')
|
||||
)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
@@ -218,12 +242,12 @@ class CoreManager(SupportExitCleanup):
|
||||
success = coreProcess.start(configcopy, **kwargs)
|
||||
elif isinstance(configcopy, ConfigurationHysteria1):
|
||||
if routing == 'Bypass Mainland China':
|
||||
# VPN Mode handling
|
||||
if not proxyModeOnly and isVPNMode():
|
||||
# TUN Mode handling
|
||||
if not proxyModeOnly and isTUNMode():
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_('Unable to connect'))
|
||||
mbox.setText(
|
||||
_('Routing option with direct rules is not allowed in VPN mode')
|
||||
_('Routing option with direct rules is not allowed in TUN mode')
|
||||
)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
@@ -290,9 +314,9 @@ class CoreManager(SupportExitCleanup):
|
||||
|
||||
return success
|
||||
|
||||
# VPN Mode handling
|
||||
if not proxyModeOnly and isVPNMode():
|
||||
# Currently VPN Mode is only supported on Windows and macOS
|
||||
# TUN Mode handling
|
||||
if not proxyModeOnly and isTUNMode():
|
||||
# Currently TUN Mode is only supported on Windows and macOS
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
if PLATFORM == 'Windows':
|
||||
# cleanup first
|
||||
@@ -300,27 +324,47 @@ class CoreManager(SupportExitCleanup):
|
||||
'0.0.0.0', APPLICATION_TUN_GATEWAY_ADDRESS
|
||||
)
|
||||
|
||||
defaultGateway = SystemRoutingTable.getDefaultGateway()
|
||||
# Handle user defined settings
|
||||
userGateway, userInterfaceIP = (
|
||||
userDefaultPrimaryGatewayIP(),
|
||||
userPrimaryAdapterInterfaceIP(),
|
||||
)
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
# Need this?
|
||||
defaultGateway = list(
|
||||
# Filter TUN Gateway
|
||||
filter(
|
||||
lambda x: x != APPLICATION_TUN_GATEWAY_ADDRESS,
|
||||
defaultGateway,
|
||||
)
|
||||
if userGateway and userInterfaceIP:
|
||||
logger.info(
|
||||
f'got user defined TUN settings. '
|
||||
f'\'DefaultPrimaryGatewayIP\': {userGateway}. '
|
||||
f'\'PrimaryAdapterInterfaceIP\': {userInterfaceIP}'
|
||||
)
|
||||
|
||||
if len(defaultGateway) != 1:
|
||||
logger.error(f'bad default gateway: {defaultGateway}')
|
||||
|
||||
return False
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
gateway, interfaceIP = defaultGateway[0]
|
||||
gateway, interfaceIP = userGateway, userInterfaceIP
|
||||
else:
|
||||
gateway, interfaceIP = defaultGateway[0], None
|
||||
logger.info(
|
||||
f'automatically fetching TUN settings: '
|
||||
f'\'DefaultPrimaryGatewayIP\' and \'PrimaryAdapterInterfaceIP\''
|
||||
)
|
||||
|
||||
defaultGateway = SystemRoutingTable.getDefaultGateway()
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
# Need this?
|
||||
defaultGateway = list(
|
||||
# Filter TUN Gateway
|
||||
filter(
|
||||
lambda x: x != APPLICATION_TUN_GATEWAY_ADDRESS,
|
||||
defaultGateway,
|
||||
)
|
||||
)
|
||||
|
||||
if len(defaultGateway) != 1:
|
||||
logger.error(f'bad default gateway: {defaultGateway}')
|
||||
|
||||
return False
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
gateway, interfaceIP = defaultGateway[0]
|
||||
else:
|
||||
gateway, interfaceIP = defaultGateway[0], None
|
||||
|
||||
tunProcess = Tun2socks(
|
||||
exitCallback=exitCallback, msgCallback=msgCallbackTun_
|
||||
@@ -336,24 +380,61 @@ class CoreManager(SupportExitCleanup):
|
||||
):
|
||||
return False
|
||||
|
||||
address = configcopy.itemAddress
|
||||
# Handle user defined settings
|
||||
bypassTUN = userBypassTUNAdapterInterfaceIP()
|
||||
|
||||
if not isValidIPAddress(address):
|
||||
error, resolved = DNSResolver.resolve(
|
||||
address, *parseHostPort(configcopy.httpProxyEndpoint())
|
||||
)
|
||||
if bypassTUN:
|
||||
try:
|
||||
bypassSplit = bypassTUN.split(',')
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
if error:
|
||||
logger.error(f'DNS resolution failed: {address}')
|
||||
logger.error(
|
||||
f'error when processing ' f'user TUN bypass Settings: {ex}'
|
||||
)
|
||||
|
||||
SystemRoutingTable.Relations.clear()
|
||||
|
||||
return False
|
||||
else:
|
||||
for address in resolved:
|
||||
SystemRoutingTable.Relations.append([address, gateway])
|
||||
for bypass in bypassSplit:
|
||||
if isValidIPAddress(bypass):
|
||||
logger.info(f'processing user TUN bypass IP: {bypass}')
|
||||
|
||||
SystemRoutingTable.Relations.append([bypass, gateway])
|
||||
else:
|
||||
logger.error(
|
||||
f'invalid IP address when processing '
|
||||
f'user TUN bypass Settings: {bypass}'
|
||||
)
|
||||
|
||||
SystemRoutingTable.Relations.clear()
|
||||
|
||||
return False
|
||||
else:
|
||||
SystemRoutingTable.Relations.append([address, gateway])
|
||||
logger.info(
|
||||
f'automatically fetching TUN settings: '
|
||||
f'\'BypassTUNAdapterInterfaceIP\''
|
||||
)
|
||||
|
||||
address = configcopy.itemAddress
|
||||
|
||||
if not isValidIPAddress(address):
|
||||
DNSResolver.configureHttpProxy(configcopy.httpProxyEndpoint())
|
||||
|
||||
error, resolved = DNSResolver.resolve(address)
|
||||
|
||||
if error:
|
||||
logger.error(f'DNS resolution failed: {address}')
|
||||
|
||||
SystemRoutingTable.Relations.clear()
|
||||
|
||||
return False
|
||||
else:
|
||||
for address in resolved:
|
||||
SystemRoutingTable.Relations.append([address, gateway])
|
||||
else:
|
||||
SystemRoutingTable.Relations.append([address, gateway])
|
||||
|
||||
if PLATFORM == 'Windows':
|
||||
foundDevice = False
|
||||
@@ -392,7 +473,25 @@ class CoreManager(SupportExitCleanup):
|
||||
|
||||
return False
|
||||
|
||||
alias = SystemRoutingTable.WIN32GetInterfaceAliasByIP(interfaceIP)
|
||||
# Handle user defined settings
|
||||
userInterfaceName = userPrimaryAdapterInterfaceName()
|
||||
|
||||
if userInterfaceName:
|
||||
logger.info(
|
||||
f'got user defined TUN settings. '
|
||||
f'\'PrimaryAdapterInterfaceName\': {userInterfaceName}'
|
||||
)
|
||||
|
||||
alias = userInterfaceName
|
||||
else:
|
||||
logger.info(
|
||||
f'automatically fetching TUN settings: '
|
||||
f'\'PrimaryAdapterInterfaceName\''
|
||||
)
|
||||
|
||||
alias = SystemRoutingTable.WIN32GetInterfaceAliasByIP(
|
||||
interfaceIP
|
||||
)
|
||||
|
||||
if alias:
|
||||
|
||||
@@ -402,14 +501,35 @@ class CoreManager(SupportExitCleanup):
|
||||
|
||||
tunProcess.cleanup = functools.partial(_windowsCleanup, alias)
|
||||
|
||||
SystemRoutingTable.WIN32SetInterfaceDNS(
|
||||
alias, '127.0.0.1', False
|
||||
# Handle user defined settings
|
||||
userDisableInterfaceDNS = (
|
||||
userDisablePrimaryAdapterInterfaceDNS()
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'DisablePrimaryInterfaceDNS: {userDisableInterfaceDNS}'
|
||||
)
|
||||
|
||||
if userDisableInterfaceDNS != 'False':
|
||||
SystemRoutingTable.WIN32SetInterfaceDNS(
|
||||
alias, '127.0.0.1', False
|
||||
)
|
||||
|
||||
# Handle user defined settings
|
||||
userTunInterfaceDNS = userTunAdapterInterfaceDNS()
|
||||
|
||||
if userTunInterfaceDNS == '':
|
||||
userTunInterfaceDNS = APPLICATION_TUN_INTERFACE_DNS_ADDRESS
|
||||
else:
|
||||
logger.info(
|
||||
f'got user defined TUN settings. '
|
||||
f'TunAdapterInterfaceDNS: {userTunInterfaceDNS}'
|
||||
)
|
||||
|
||||
SystemRoutingTable.addRelations()
|
||||
SystemRoutingTable.WIN32SetInterfaceDNS(
|
||||
APPLICATION_TUN_DEVICE_NAME,
|
||||
APPLICATION_TUN_INTERFACE_DNS_ADDRESS,
|
||||
userTunInterfaceDNS,
|
||||
False,
|
||||
)
|
||||
SystemRoutingTable.setDeviceGateway(
|
||||
@@ -436,10 +556,21 @@ class CoreManager(SupportExitCleanup):
|
||||
|
||||
tunProcess.cleanup = functools.partial(_darwinCleanup, servers)
|
||||
|
||||
# Handle user defined settings
|
||||
userTunInterfaceDNS = userTunAdapterInterfaceDNS()
|
||||
|
||||
if userTunInterfaceDNS == '':
|
||||
userTunInterfaceDNS = APPLICATION_TUN_INTERFACE_DNS_ADDRESS
|
||||
else:
|
||||
logger.info(
|
||||
f'got user defined TUN settings. '
|
||||
f'TunAdapterInterfaceDNS: {userTunInterfaceDNS}'
|
||||
)
|
||||
|
||||
for service, dnsserver in servers:
|
||||
SystemRoutingTable.DarwinSetDNSServers(
|
||||
service,
|
||||
APPLICATION_TUN_INTERFACE_DNS_ADDRESS,
|
||||
userTunInterfaceDNS,
|
||||
)
|
||||
|
||||
SystemRoutingTable.setDeviceGateway(
|
||||
|
||||
Vendored
+287
-13
@@ -422,7 +422,8 @@ TRANSLATION = {
|
||||
"Furious.Widget.GuiTrojan",
|
||||
"Furious.Widget.GuiVLESS",
|
||||
"Furious.Widget.GuiVMess",
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
"Furious.Widget.UserServersQTableWidget",
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Примечание",
|
||||
"ZH": "别名",
|
||||
@@ -1015,28 +1016,28 @@ TRANSLATION = {
|
||||
"ZH": "路由",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"VPN Mode": {
|
||||
"TUN Mode": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Settings"
|
||||
],
|
||||
"RU": "VPN режим",
|
||||
"ZH": "VPN模式",
|
||||
"RU": "TUN режим",
|
||||
"ZH": "TUN模式",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"VPN Mode Disabled (Administrator)": {
|
||||
"TUN Mode Disabled (Administrator)": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Settings"
|
||||
],
|
||||
"RU": "VPN режим отключен (Administrator)",
|
||||
"ZH": "VPN模式已禁用(管理员)",
|
||||
"RU": "TUN режим отключен (Administrator)",
|
||||
"ZH": "TUN模式已禁用(管理员)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"VPN Mode Disabled (root)": {
|
||||
"TUN Mode Disabled (root)": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Settings"
|
||||
],
|
||||
"RU": "VPN режим отключен (root)",
|
||||
"ZH": "VPN模式已禁用(root)",
|
||||
"RU": "TUN режим отключен (root)",
|
||||
"ZH": "TUN模式已禁用(root)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Settings": {
|
||||
@@ -1213,6 +1214,7 @@ TRANSLATION = {
|
||||
"Furious.Widget.GuiHysteria2",
|
||||
"Furious.Widget.GuiShadowsocks",
|
||||
"Furious.Widget.GuiTrojan",
|
||||
"Furious.Widget.GuiTUNSettings",
|
||||
"Furious.Widget.GuiVLESS",
|
||||
"Furious.Widget.GuiVMess"
|
||||
],
|
||||
@@ -1341,12 +1343,12 @@ TRANSLATION = {
|
||||
"ZH": "添加Hysteria2服务器...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Routing option with direct rules is not allowed in VPN mode": {
|
||||
"Routing option with direct rules is not allowed in TUN mode": {
|
||||
"source": [
|
||||
"Furious.Core.CoreManager"
|
||||
],
|
||||
"RU": "Опция маршрутизации с прямыми правилами не разрешена в режиме VPN",
|
||||
"ZH": "VPN模式下不允许使用直接规则的路由选项",
|
||||
"RU": "Опция маршрутизации с прямыми правилами не разрешена в режиме TUN",
|
||||
"ZH": "TUN模式下不允许使用直接规则的路由选项",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Select and press Enter to activate configuration and connect": {
|
||||
@@ -1375,5 +1377,277 @@ TRANSLATION = {
|
||||
"RU": "OK",
|
||||
"ZH": "OK",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Advanced...": {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
],
|
||||
"RU": "Расширенные...",
|
||||
"ZH": "高级...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"is available to download": {
|
||||
"source": [
|
||||
"Furious.TrayActions.Connect"
|
||||
],
|
||||
"RU": "доступен для скачивания",
|
||||
"ZH": "可供下载",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Update subscription completed": {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
],
|
||||
"RU": "Подписка на обновление завершена",
|
||||
"ZH": "更新订阅完成",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Update subscription failed": {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
],
|
||||
"RU": "Обновить подписку не удалось",
|
||||
"ZH": "更新订阅失败",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Configuration has been updated": {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
],
|
||||
"RU": "Конфигурация обновлена",
|
||||
"ZH": "配置已更新",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Configuration update failed": {
|
||||
"source": [
|
||||
"Furious.Widget.UserServersQTableWidget"
|
||||
],
|
||||
"RU": "Обновление конфигурации не удалось",
|
||||
"ZH": "配置更新失败",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Auto Update": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Авто обновление",
|
||||
"ZH": "自动更新",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Auto Update Use Proxy": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Авто обновление Использовать прокси",
|
||||
"ZH": "自动更新使用代理",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Never": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Никогда",
|
||||
"ZH": "从不",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 5 mins": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 5 минут",
|
||||
"ZH": "每5分钟",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 10 mins": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 10 минут",
|
||||
"ZH": "每10分钟",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 15 mins": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 15 минут",
|
||||
"ZH": "每15分钟",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 30 mins": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 30 минут",
|
||||
"ZH": "每30分钟",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 45 mins": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 45 минут",
|
||||
"ZH": "每45分钟",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 1 hour": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждый 1 час",
|
||||
"ZH": "每1小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 2 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 2 часа",
|
||||
"ZH": "每2小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 3 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 3 часа",
|
||||
"ZH": "每3小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 6 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 6 часа",
|
||||
"ZH": "每6小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 8 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 8 часа",
|
||||
"ZH": "每8小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 10 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 10 часа",
|
||||
"ZH": "每10小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 12 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 12 часа",
|
||||
"ZH": "每12小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Every 24 hours": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Каждые 24 часа",
|
||||
"ZH": "每24小时",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Use current proxy": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Использовать текущий прокси-сервер",
|
||||
"ZH": "使用当前代理",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Force proxy": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Обязательно через прокси-сервер",
|
||||
"ZH": "强制代理",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"No proxy": {
|
||||
"source": [
|
||||
"Furious.Widget.UserSubsQTableWidget"
|
||||
],
|
||||
"RU": "Не использовать прокси",
|
||||
"ZH": "无代理",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Customize TUN Settings": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "Настройте параметры TUN",
|
||||
"ZH": "自定义TUN设置",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Customize TUN Settings...": {
|
||||
"source": [
|
||||
"Furious.Window.AppMainWindow"
|
||||
],
|
||||
"RU": "Настройте параметры TUN...",
|
||||
"ZH": "自定义TUN设置...",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Go to help page": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "Перейти на страницу помощи",
|
||||
"ZH": "转到帮助页面",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Primary Adapter Interface Name": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "Имя основного сетевого адаптера",
|
||||
"ZH": "主适配器接口名称",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Primary Adapter Interface IP": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "IP-адрес основного сетевого адаптера",
|
||||
"ZH": "主适配器接口IP",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Default Primary Gateway IP": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "IP-адрес основного шлюза по умолчанию",
|
||||
"ZH": "默认主网关IP",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"TUN Adapter Interface DNS": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "DNS-адрес интерфейса адаптера TUN",
|
||||
"ZH": "TUN适配器接口DNS",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Bypass TUN adapter interface IP (separated by commas)": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "Обход IP-адреса интерфейса адаптера TUN (через запятую)",
|
||||
"ZH": "绕过TUN适配器接口IP(以逗号分割)",
|
||||
"isReviewed": "True"
|
||||
},
|
||||
"Disable Primary Adapter Interface DNS (Mitigating DNS leaks on Windows)": {
|
||||
"source": [
|
||||
"Furious.Widget.GuiTUNSettings"
|
||||
],
|
||||
"RU": "Отключить DNS основного сетевого адаптера (для предотвращения утечек DNS в Windows)",
|
||||
"ZH": "禁用主适配器接口DNS(在Windows上缓解DNS泄露)",
|
||||
"isReviewed": "True"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ class ConfigurationFactory(UserServersTableItem, dict, ABC):
|
||||
:param config: The input configuration. Can be a string or dict
|
||||
"""
|
||||
|
||||
self._index = kwargs.pop('index', 0)
|
||||
|
||||
# Extra attributes
|
||||
self.kwargs = kwargs
|
||||
|
||||
@@ -96,6 +98,12 @@ class ConfigurationFactory(UserServersTableItem, dict, ABC):
|
||||
def setExtras(self, item, value):
|
||||
self.kwargs[item] = value
|
||||
|
||||
def index(self) -> int:
|
||||
return self._index
|
||||
|
||||
def setIndex(self, index):
|
||||
self._index = index
|
||||
|
||||
@property
|
||||
def itemRemark(self) -> str:
|
||||
return self.getExtras('remark')
|
||||
|
||||
@@ -27,15 +27,7 @@ import ujson
|
||||
__all__ = ['CoreFactory']
|
||||
|
||||
|
||||
class CoreFactorySuper:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
ExitCallbackType = Callable[[CoreFactorySuper, int], None]
|
||||
|
||||
|
||||
class CoreFactory(CoreFactorySuper, ABC):
|
||||
class CoreFactory(ABC):
|
||||
class ExitCode:
|
||||
ConfigurationError = 23
|
||||
# Windows: 4294967295. Darwin, Linux: 255 (-1)
|
||||
@@ -43,21 +35,14 @@ class CoreFactory(CoreFactorySuper, ABC):
|
||||
# Windows shutting down
|
||||
SystemShuttingDown = 0x40010004
|
||||
|
||||
def __init__(self, exitCallback: ExitCallbackType = None):
|
||||
def __init__(
|
||||
self, exitCallback: Union[Callable[[CoreFactory, int], None], None] = None
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self._exitCallback = exitCallback
|
||||
self._jsonConfig = ''
|
||||
|
||||
@property
|
||||
def exitCallback(self) -> ExitCallbackType:
|
||||
return self._exitCallback
|
||||
|
||||
def registerExitCallback(self, exitCallback: ExitCallbackType) -> CoreFactory:
|
||||
self._exitCallback = exitCallback
|
||||
|
||||
return self
|
||||
|
||||
def callExitCallback(self, exitcode: int):
|
||||
if callable(self._exitCallback):
|
||||
self._exitCallback(self, exitcode)
|
||||
@@ -67,14 +52,6 @@ class CoreFactory(CoreFactorySuper, ABC):
|
||||
|
||||
return self
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def configJSONString(self) -> str:
|
||||
if isinstance(self._jsonConfig, str):
|
||||
return self._jsonConfig
|
||||
@@ -105,6 +82,14 @@ class CoreFactory(CoreFactorySuper, ABC):
|
||||
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def name() -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def version() -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def start(self, *args, **kwargs) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface.ConfigurationFactory import ConfigurationFactory
|
||||
|
||||
from typing import Any
|
||||
|
||||
__all__ = ['GuiEditorItemFactory', 'GuiEditorItemWidgetContainer']
|
||||
@@ -28,10 +26,10 @@ class GuiEditorItemFactory:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: ConfigurationFactory) -> Any:
|
||||
def inputToFactory(self, *args, **kwargs) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
def factoryToInput(self, config: ConfigurationFactory) -> Any:
|
||||
def factoryToInput(self, *args, **kwargs) -> Any:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Sequence
|
||||
|
||||
__all__ = ['ItemUpdateProtocol']
|
||||
|
||||
|
||||
class ItemUpdateProtocol:
|
||||
def __init__(self, sequence: Sequence, currentIndex: int, currentItem: Any):
|
||||
self.sequence = sequence
|
||||
self.currentIndex = currentIndex
|
||||
self.currentItem = currentItem
|
||||
|
||||
def currentItemDeleted(self, *args, **kwargs) -> bool:
|
||||
return self.currentIndex < 0 or self.currentIndex >= len(self.sequence)
|
||||
|
||||
def updateImpl(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def updateResult(self):
|
||||
if self.currentItemDeleted():
|
||||
# Deleted. Do nothing
|
||||
return
|
||||
|
||||
if id(self.sequence[self.currentIndex]) == id(self.currentItem):
|
||||
self.updateImpl()
|
||||
else:
|
||||
# Linear find and update
|
||||
for index, item in enumerate(self.sequence):
|
||||
if id(item) == id(self.currentItem):
|
||||
# Found. Update index
|
||||
self.currentIndex = index
|
||||
self.updateImpl()
|
||||
|
||||
# Updated
|
||||
break
|
||||
|
||||
# Not found. Do nothing
|
||||
@@ -18,7 +18,6 @@
|
||||
from .Encoder import *
|
||||
from .GuiEditorItemFactory import *
|
||||
from .UserServersTableItem import *
|
||||
from .ItemUpdateProtocol import *
|
||||
from .Storage import *
|
||||
from .Application import *
|
||||
from .ConfigurationFactory import *
|
||||
|
||||
@@ -19,19 +19,20 @@ from __future__ import annotations
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from typing import Any
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = [
|
||||
'Translatable',
|
||||
'SupportConnectedCallback',
|
||||
'SupportThemeChangedCallback',
|
||||
'SupportExitCleanup',
|
||||
'SupportImplicitReference',
|
||||
'FastItemDeletionSearch',
|
||||
'GarbageCollector',
|
||||
]
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Translatable:
|
||||
ObjectsPool = list()
|
||||
@@ -48,11 +49,11 @@ class Translatable:
|
||||
|
||||
@staticmethod
|
||||
def retranslateAll():
|
||||
for ob in Translatable.ObjectsPool:
|
||||
assert isinstance(ob, Translatable)
|
||||
for object_ in Translatable.ObjectsPool:
|
||||
assert isinstance(object_, Translatable)
|
||||
|
||||
if ob.translatable:
|
||||
ob.retranslate()
|
||||
if object_.translatable:
|
||||
object_.retranslate()
|
||||
|
||||
|
||||
class SupportConnectedCallback:
|
||||
@@ -71,17 +72,17 @@ class SupportConnectedCallback:
|
||||
|
||||
@staticmethod
|
||||
def callConnectedCallback():
|
||||
for ob in SupportConnectedCallback.ObjectsPool:
|
||||
assert isinstance(ob, SupportConnectedCallback)
|
||||
for object_ in SupportConnectedCallback.ObjectsPool:
|
||||
assert isinstance(object_, SupportConnectedCallback)
|
||||
|
||||
ob.connectedCallback()
|
||||
object_.connectedCallback()
|
||||
|
||||
@staticmethod
|
||||
def callDisconnectedCallback():
|
||||
for ob in SupportConnectedCallback.ObjectsPool:
|
||||
assert isinstance(ob, SupportConnectedCallback)
|
||||
for object_ in SupportConnectedCallback.ObjectsPool:
|
||||
assert isinstance(object_, SupportConnectedCallback)
|
||||
|
||||
ob.disconnectedCallback()
|
||||
object_.disconnectedCallback()
|
||||
|
||||
|
||||
class SupportThemeChangedCallback:
|
||||
@@ -97,10 +98,10 @@ class SupportThemeChangedCallback:
|
||||
|
||||
@staticmethod
|
||||
def callThemeChangedCallbackUnchecked(theme: str):
|
||||
for ob in SupportThemeChangedCallback.ObjectsPool:
|
||||
assert isinstance(ob, SupportThemeChangedCallback)
|
||||
for object_ in SupportThemeChangedCallback.ObjectsPool:
|
||||
assert isinstance(object_, SupportThemeChangedCallback)
|
||||
|
||||
ob.themeChangedCallback(theme)
|
||||
object_.themeChangedCallback(theme)
|
||||
|
||||
@staticmethod
|
||||
def callThemeChangedCallback(theme: str):
|
||||
@@ -138,40 +139,37 @@ class SupportExitCleanup:
|
||||
|
||||
@staticmethod
|
||||
def cleanupAll():
|
||||
for ob in SupportExitCleanup.ObjectsPool:
|
||||
assert isinstance(ob, SupportExitCleanup)
|
||||
for object_ in SupportExitCleanup.ObjectsPool:
|
||||
assert isinstance(object_, SupportExitCleanup)
|
||||
|
||||
if ob.uniqueCleanup:
|
||||
obtype = str(type(ob))
|
||||
if object_.uniqueCleanup:
|
||||
obtype = str(type(object_))
|
||||
|
||||
if not SupportExitCleanup.VisitedType.get(obtype, False):
|
||||
ob.cleanup()
|
||||
object_.cleanup()
|
||||
|
||||
SupportExitCleanup.VisitedType[obtype] = True
|
||||
else:
|
||||
pass
|
||||
else:
|
||||
ob.cleanup()
|
||||
object_.cleanup()
|
||||
|
||||
|
||||
class SupportImplicitReference:
|
||||
class GarbageCollector:
|
||||
ObjectsPool = list()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
SupportImplicitReference.ObjectsPool.append(self)
|
||||
|
||||
|
||||
class FastItemDeletionSearch:
|
||||
DeletedItem = list()
|
||||
DeletedId = dict()
|
||||
# ObjectsId is used to speed up searching
|
||||
ObjectsId = dict()
|
||||
|
||||
@staticmethod
|
||||
def moveToTrash(item):
|
||||
FastItemDeletionSearch.DeletedItem.append(item)
|
||||
FastItemDeletionSearch.DeletedId[id(item)] = True
|
||||
def track(object_: Any):
|
||||
GarbageCollector.ObjectsPool.append(object_)
|
||||
GarbageCollector.ObjectsId[id(object_)] = True
|
||||
|
||||
@staticmethod
|
||||
def isInTrash(item) -> bool:
|
||||
return id(item) in FastItemDeletionSearch.DeletedId
|
||||
def isTracked(object_: Any) -> bool:
|
||||
return id(object_) in GarbageCollector.ObjectsId
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
GarbageCollector.ObjectsPool.clear()
|
||||
GarbageCollector.ObjectsId.clear()
|
||||
|
||||
@@ -15,36 +15,38 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.PyFramework import *
|
||||
|
||||
__all__ = ['QBlockSignals', 'QTranslatable']
|
||||
from PySide6 import QtCore
|
||||
|
||||
__all__ = ['QBlockSignals', 'QTranslatable', 'QGarbageCollector']
|
||||
|
||||
|
||||
class QProtection:
|
||||
def __init__(self, qobject):
|
||||
def __init__(self, qobject: QtCore.QObject):
|
||||
self.qobject = qobject
|
||||
self.hasattr = hasattr(self.qobject, 'setDisabled')
|
||||
|
||||
def __enter__(self):
|
||||
if self.hasattr:
|
||||
if hasattr(self.qobject, 'setDisabled'):
|
||||
self.qobject.setDisabled(True)
|
||||
|
||||
def __exit__(self, exceptionType, exceptionValue, tb):
|
||||
if self.hasattr:
|
||||
if hasattr(self.qobject, 'setDisabled'):
|
||||
self.qobject.setDisabled(False)
|
||||
|
||||
|
||||
class QBlockSignals:
|
||||
def __init__(self, qobject):
|
||||
def __init__(self, qobject: QtCore.QObject):
|
||||
self.qobject = qobject
|
||||
self.hasattr = hasattr(self.qobject, 'blockSignals')
|
||||
|
||||
def __enter__(self):
|
||||
if self.hasattr:
|
||||
if hasattr(self.qobject, 'blockSignals'):
|
||||
self.qobject.blockSignals(True)
|
||||
|
||||
def __exit__(self, exceptionType, exceptionValue, tb):
|
||||
if self.hasattr:
|
||||
if hasattr(self.qobject, 'blockSignals'):
|
||||
self.qobject.blockSignals(False)
|
||||
|
||||
|
||||
@@ -64,7 +66,22 @@ class QTranslatable(Translatable):
|
||||
|
||||
if ob.translatable:
|
||||
if ob.useQProtection:
|
||||
assert isinstance(ob, QtCore.QObject)
|
||||
|
||||
with QProtection(ob):
|
||||
ob.retranslate()
|
||||
else:
|
||||
ob.retranslate()
|
||||
|
||||
|
||||
class QGarbageCollector(GarbageCollector):
|
||||
Timer = QtCore.QTimer()
|
||||
|
||||
@staticmethod
|
||||
def isEnabled():
|
||||
return QGarbageCollector.Timer.isActive()
|
||||
|
||||
@staticmethod
|
||||
def collect(timeout: int):
|
||||
QGarbageCollector.Timer.timeout.connect(QGarbageCollector.clear)
|
||||
QGarbageCollector.Timer.start(timeout)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.QtFramework.WebGETManager import *
|
||||
from Furious.Utility import *
|
||||
from Furious.Library import *
|
||||
|
||||
@@ -32,8 +33,11 @@ __all__ = ['DNSResolver']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DNSResolver:
|
||||
Manager = QNetworkAccessManager()
|
||||
class _DNSResolver(WebGETManager):
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
actionMessage = kwargs.pop('actionMessage', 'DNS resolution')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
@staticmethod
|
||||
def request(address) -> QNetworkRequest:
|
||||
@@ -44,22 +48,23 @@ class DNSResolver:
|
||||
|
||||
return request
|
||||
|
||||
@staticmethod
|
||||
def handleFinishedByNetworkReply(networkReply, domain, resultMap):
|
||||
assert isinstance(networkReply, QNetworkReply)
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
domain = kwargs.pop('domain', '')
|
||||
resultMap = kwargs.pop('resultMap', {})
|
||||
|
||||
if networkReply.error() != QNetworkReply.NetworkError.NoError:
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
try:
|
||||
replyObject = UJSONEncoder.decode(data)
|
||||
except Exception as ex:
|
||||
logger.error(
|
||||
f'DNS resolution for \'{domain}\' failed. {networkReply.errorString()}'
|
||||
f'bad network reply while resolving DNS for \'{domain}\'. {ex}'
|
||||
)
|
||||
|
||||
resultMap['error'] = True
|
||||
else:
|
||||
logger.info(f'DNS resolution for \'{domain}\' success')
|
||||
|
||||
# Unchecked?
|
||||
replyObject = UJSONEncoder.decode(networkReply.readAll().data())
|
||||
|
||||
for record in replyObject['Answer']:
|
||||
address = record['data']
|
||||
|
||||
@@ -70,46 +75,31 @@ class DNSResolver:
|
||||
else:
|
||||
resultMap['depth'] += 1
|
||||
|
||||
newNetworkReply = DNSResolver.Manager.get(
|
||||
DNSResolver.request(address)
|
||||
)
|
||||
newNetworkReply.finished.connect(
|
||||
functools.partial(
|
||||
DNSResolver.handleFinishedByNetworkReply,
|
||||
newNetworkReply,
|
||||
address,
|
||||
resultMap,
|
||||
)
|
||||
newNetworkReply = self.webGET(
|
||||
self.request(address),
|
||||
logActionMessage=False,
|
||||
domain=address,
|
||||
resultMap=resultMap,
|
||||
)
|
||||
|
||||
resultMap['reference'].append(newNetworkReply)
|
||||
|
||||
resultMap['depth'] -= 1
|
||||
|
||||
@staticmethod
|
||||
def resolve(domain, proxyHost=None, proxyPort=None) -> Tuple[bool, list[str]]:
|
||||
if proxyHost is None or proxyPort is None:
|
||||
DNSResolver.Manager.setProxy(QNetworkProxy.ProxyType.NoProxy)
|
||||
else:
|
||||
try:
|
||||
DNSResolver.Manager.setProxy(
|
||||
QNetworkProxy(
|
||||
QNetworkProxy.ProxyType.HttpProxy, proxyHost, int(proxyPort)
|
||||
)
|
||||
)
|
||||
def failureCallback(self, networkReply: QNetworkReply, **kwargs):
|
||||
domain = kwargs.pop('domain', '')
|
||||
resultMap = kwargs.pop('resultMap', {})
|
||||
|
||||
logger.info(f'DNS resolution uses proxy server {proxyHost}:{proxyPort}')
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
logger.error(
|
||||
f'DNS resolution for \'{domain}\' failed. {networkReply.errorString()}'
|
||||
)
|
||||
|
||||
logger.error(
|
||||
f'invalid proxy server {proxyHost}:{proxyPort}. {ex}. '
|
||||
f'DNS resolution uses no proxy'
|
||||
)
|
||||
|
||||
DNSResolver.Manager.setProxy(QNetworkProxy.ProxyType.NoProxy)
|
||||
resultMap['error'] = True
|
||||
resultMap['depth'] -= 1
|
||||
|
||||
def resolve(self, domain, timeout=30000) -> Tuple[bool, list[str]]:
|
||||
resultMap = {
|
||||
'domain': domain,
|
||||
'depth': 0,
|
||||
'error': False,
|
||||
'reference': [],
|
||||
@@ -118,26 +108,28 @@ class DNSResolver:
|
||||
|
||||
resultMap['depth'] += 1
|
||||
|
||||
networkReply = DNSResolver.Manager.get(DNSResolver.request(domain))
|
||||
networkReply.finished.connect(
|
||||
functools.partial(
|
||||
DNSResolver.handleFinishedByNetworkReply,
|
||||
networkReply,
|
||||
domain,
|
||||
resultMap,
|
||||
)
|
||||
networkReply = self.webGET(
|
||||
self.request(domain),
|
||||
logActionMessage=False,
|
||||
domain=domain,
|
||||
resultMap=resultMap,
|
||||
)
|
||||
|
||||
resultMap['reference'].append(networkReply)
|
||||
|
||||
DNSResolver.wait(resultMap)
|
||||
self.wait(resultMap, timeout=timeout)
|
||||
|
||||
return resultMap['error'], list(resultMap['result'].keys())
|
||||
|
||||
@staticmethod
|
||||
def wait(resultMap, startCounter=0, timeout=30000, step=100):
|
||||
domain = resultMap.get('domain', '')
|
||||
|
||||
if not domain:
|
||||
return
|
||||
|
||||
if resultMap['depth'] != 0:
|
||||
logger.info('DNS resolution in progress. Wait')
|
||||
logger.info(f'DNS resolution for \'{domain}\' in progress. Wait')
|
||||
else:
|
||||
return
|
||||
|
||||
@@ -147,7 +139,9 @@ class DNSResolver:
|
||||
startCounter += step
|
||||
|
||||
if resultMap['depth'] != 0:
|
||||
logger.error('DNS resolution timeout')
|
||||
logger.error(
|
||||
f'DNS resolution for \'{domain}\' reached timeout {timeout // 1000}s'
|
||||
)
|
||||
|
||||
for networkReply in resultMap['reference']:
|
||||
if (
|
||||
@@ -155,3 +149,6 @@ class DNSResolver:
|
||||
and not networkReply.isFinished()
|
||||
):
|
||||
networkReply.abort()
|
||||
|
||||
|
||||
DNSResolver = _DNSResolver()
|
||||
|
||||
@@ -95,7 +95,7 @@ class GuiEditorItemTextComboBox(GuiEditorItemWidgetContainer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._title = AppQLabel(_(title), translatable=translatable, parent=parent)
|
||||
self._input = QComboBox(parent=parent)
|
||||
self._input = AppQComboBox(parent=parent, translatable=False)
|
||||
|
||||
def text(self) -> str:
|
||||
return self._input.currentText()
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.PyFramework import *
|
||||
from Furious.QtFramework.QtNetwork import *
|
||||
from Furious.QtFramework.WebGETManager import *
|
||||
from Furious.Utility import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -30,12 +32,14 @@ __all__ = ['NetworkStateManager']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkStateManager(SupportConnectedCallback, AppQNetworkAccessManager):
|
||||
class NetworkStateManager(SupportConnectedCallback, WebGETManager):
|
||||
MIN_JOB_INTERVAL = 2500
|
||||
MAX_JOB_INTERVAL = 2000000000
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
actionMessage = kwargs.pop('actionMessage', 'test network state')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
self.jobStatus = False
|
||||
self.jobInterval = NetworkStateManager.MIN_JOB_INTERVAL
|
||||
@@ -45,58 +49,32 @@ class NetworkStateManager(SupportConnectedCallback, AppQNetworkAccessManager):
|
||||
|
||||
self.jobArrangeTimer.timeout.connect(lambda: self.startSingleTest())
|
||||
|
||||
def successCallback(self):
|
||||
raise NotImplementedError
|
||||
def recalculateJobInterval(self, jobStatus: bool) -> int:
|
||||
assert isinstance(jobStatus, bool)
|
||||
|
||||
def errorCallback(self, errorString: str):
|
||||
raise NotImplementedError
|
||||
|
||||
def handleFinishedByNetworkReply(self, networkReply):
|
||||
assert isinstance(networkReply, QNetworkReply)
|
||||
|
||||
if networkReply.error() != QNetworkReply.NetworkError.NoError:
|
||||
self.jobTimeoutTimer.stop()
|
||||
|
||||
errorString = networkReply.errorString()
|
||||
|
||||
logger.error(f'connection test failed. {errorString}')
|
||||
|
||||
self.errorCallback(errorString)
|
||||
|
||||
if self.jobStatus is False:
|
||||
self.jobInterval *= 2
|
||||
else:
|
||||
self.jobInterval = NetworkStateManager.MIN_JOB_INTERVAL
|
||||
|
||||
self.jobStatus = False
|
||||
if self.jobStatus is jobStatus:
|
||||
self.jobInterval *= 2
|
||||
else:
|
||||
self.jobTimeoutTimer.stop()
|
||||
self.jobInterval = NetworkStateManager.MIN_JOB_INTERVAL
|
||||
|
||||
logger.info(f'connection test success')
|
||||
|
||||
self.successCallback()
|
||||
|
||||
if self.jobStatus is True:
|
||||
self.jobInterval *= 2
|
||||
else:
|
||||
self.jobInterval = NetworkStateManager.MIN_JOB_INTERVAL
|
||||
|
||||
self.jobStatus = True
|
||||
self.jobStatus = jobStatus
|
||||
|
||||
if self.jobInterval >= NetworkStateManager.MAX_JOB_INTERVAL:
|
||||
# Limited
|
||||
self.jobInterval = NetworkStateManager.MAX_JOB_INTERVAL
|
||||
|
||||
self.jobArrangeTimer.start(self.jobInterval)
|
||||
return self.jobInterval
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
self.jobTimeoutTimer.stop()
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=True))
|
||||
|
||||
def failureCallback(self, networkReply, **kwargs):
|
||||
self.jobTimeoutTimer.stop()
|
||||
self.jobArrangeTimer.start(self.recalculateJobInterval(jobStatus=False))
|
||||
|
||||
def startSingleTest(self):
|
||||
networkReply = self.get(QNetworkRequest(QtCore.QUrl(NETWORK_STATE_TEST_URL)))
|
||||
networkReply.finished.connect(
|
||||
functools.partial(
|
||||
self.handleFinishedByNetworkReply,
|
||||
networkReply,
|
||||
)
|
||||
)
|
||||
networkReply = self.webGET(NETWORK_STATE_TEST_URL)
|
||||
|
||||
def abort(_networkReply):
|
||||
if isinstance(_networkReply, QNetworkReply):
|
||||
@@ -107,17 +85,18 @@ class NetworkStateManager(SupportConnectedCallback, AppQNetworkAccessManager):
|
||||
|
||||
def stopTest(self):
|
||||
self.jobArrangeTimer.stop()
|
||||
self.jobTimeoutTimer.stop()
|
||||
|
||||
def connectedCallback(self):
|
||||
if AppSettings.isStateON_('PowerSaveMode'):
|
||||
# Power optimization
|
||||
logger.info('no job for network state manager in power save mode')
|
||||
|
||||
self.jobArrangeTimer.stop()
|
||||
self.stopTest()
|
||||
else:
|
||||
self.jobInterval = NetworkStateManager.MIN_JOB_INTERVAL
|
||||
|
||||
self.jobArrangeTimer.start(self.jobInterval)
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.jobArrangeTimer.stop()
|
||||
self.stopTest()
|
||||
|
||||
@@ -34,6 +34,7 @@ import functools
|
||||
__all__ = [
|
||||
'moveToCenter',
|
||||
'AppQCheckBox',
|
||||
'AppQComboBox',
|
||||
'AppQDialog',
|
||||
'AppQDialogButtonBox',
|
||||
'AppQGroupBox',
|
||||
@@ -77,6 +78,15 @@ class AppQCheckBox(QTranslatable, QCheckBox):
|
||||
self.setText(_(self.text()))
|
||||
|
||||
|
||||
class AppQComboBox(QTranslatable, QComboBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def retranslate(self):
|
||||
for index in range(self.count()):
|
||||
self.setItemText(index, _(self.itemText(index)))
|
||||
|
||||
|
||||
class AppQDialog(QTranslatable, SupportConnectedCallback, QDialog):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -176,7 +186,7 @@ class AppQHeaderView(SupportExitCleanup, SupportConnectedCallback, QHeaderView):
|
||||
self.sectionSizeTable[str(column)] = self.defaultSectionSize()
|
||||
|
||||
with QBlockSignals(self):
|
||||
for key, value in self.sectionSizeTable.items():
|
||||
for key, value in reversed(self.sectionSizeTable.items()):
|
||||
self.resizeSection(int(key), value)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
@@ -293,15 +303,12 @@ class AppQMainWindow(
|
||||
|
||||
moveToCenter(self)
|
||||
|
||||
APP().processEvents()
|
||||
|
||||
if PLATFORM == 'Darwin':
|
||||
self.activateWindow()
|
||||
self.raise_()
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
self.hide()
|
||||
|
||||
def retranslate(self):
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.PyFramework import *
|
||||
from Furious.QtFramework.TextEditorTheme import (
|
||||
DraculaEditorTheme,
|
||||
DraculaJSONSyntaxHighlighter,
|
||||
)
|
||||
from Furious.QtFramework.TextEditorTheme import *
|
||||
from Furious.Utility import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
@@ -314,6 +311,8 @@ class DraculaTextBrowser(AppQTextBrowser):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._syntaxHighlighter = DraculaLoggerSyntaxHighlighter(self.document())
|
||||
|
||||
# Theme
|
||||
self.setStyleSheet(
|
||||
DraculaEditorTheme.getStyleSheet(
|
||||
|
||||
@@ -15,14 +15,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Core import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
|
||||
__all__ = ['DraculaEditorTheme', 'DraculaJSONSyntaxHighlighter']
|
||||
__all__ = [
|
||||
'DraculaEditorTheme',
|
||||
'DraculaJSONSyntaxHighlighter',
|
||||
'DraculaLoggerSyntaxHighlighter',
|
||||
]
|
||||
|
||||
|
||||
class EditorHighlightRules:
|
||||
def __init__(self, regex, color, isBold=False, isJSONKey=False):
|
||||
def __init__(self, regex, color, isBold=False, isItalic=False, isJSONKey=False):
|
||||
self.regex = QtCore.QRegularExpression(regex)
|
||||
self.color = QColor(color)
|
||||
|
||||
@@ -32,6 +40,9 @@ class EditorHighlightRules:
|
||||
if isBold:
|
||||
self.rules.setFontWeight(QFont.Weight.Bold)
|
||||
|
||||
if isItalic:
|
||||
self.rules.setFontItalic(True)
|
||||
|
||||
self.isJSONKey = isJSONKey
|
||||
|
||||
|
||||
@@ -59,7 +70,46 @@ class DraculaEditorTheme(EditorTheme):
|
||||
)
|
||||
|
||||
|
||||
class DraculaJSONSyntaxHighlighter(QSyntaxHighlighter):
|
||||
class AppQSyntaxHighlighter(QSyntaxHighlighter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.highlightRules = list()
|
||||
|
||||
def highlightBlock(self, text):
|
||||
for highlightRule in self.highlightRules:
|
||||
iterator = highlightRule.regex.globalMatch(text)
|
||||
|
||||
while iterator.hasNext():
|
||||
match = iterator.next()
|
||||
|
||||
if highlightRule.isJSONKey:
|
||||
# JSON keys. Ignore trailing :
|
||||
capturedLength = match.capturedLength() - 1
|
||||
else:
|
||||
capturedLength = match.capturedLength()
|
||||
|
||||
capturedStart = match.capturedStart()
|
||||
captured = text[capturedStart : capturedStart + capturedLength]
|
||||
shouldHighlight = True
|
||||
|
||||
for version in [
|
||||
XrayCore.version(),
|
||||
Hysteria1.version(),
|
||||
Hysteria2.version(),
|
||||
Tun2socks.version(),
|
||||
]:
|
||||
if captured == version:
|
||||
# These x.y.z.u version values are not IPv4 addresses. Do not highlight
|
||||
shouldHighlight = False
|
||||
|
||||
break
|
||||
|
||||
if shouldHighlight:
|
||||
self.setFormat(capturedStart, capturedLength, highlightRule.rules)
|
||||
|
||||
|
||||
class DraculaJSONSyntaxHighlighter(AppQSyntaxHighlighter):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -77,22 +127,66 @@ class DraculaJSONSyntaxHighlighter(QSyntaxHighlighter):
|
||||
r'"([^"\\]*(\\.[^"\\]*)*)"\s*:', '#50FA7B', isJSONKey=True
|
||||
),
|
||||
# Comments(only for hints on display). Grey
|
||||
EditorHighlightRules(r'^#.*', '#6272a4'),
|
||||
EditorHighlightRules(r'^#.*', '#6272A4'),
|
||||
]
|
||||
|
||||
def highlightBlock(self, text):
|
||||
for highlightRule in self.highlightRules:
|
||||
iterator = highlightRule.regex.globalMatch(text)
|
||||
|
||||
while iterator.hasNext():
|
||||
match = iterator.next()
|
||||
class DraculaLoggerSyntaxHighlighter(AppQSyntaxHighlighter):
|
||||
# https://ihateregex.io/expr/ip/
|
||||
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'
|
||||
)
|
||||
# https://vernon.mauery.com/content/2008/04/21/ipv6-regex/
|
||||
IPV6_REGEX = (
|
||||
r'(A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,6}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,5}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,4}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,3}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,5}(:[0-9a-f]{1,4}){1,2}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,6}(:[0-9a-f]{1,4}){1,1}Z)|'
|
||||
r'(A(([0-9a-f]{1,4}:){1,7}|:):Z)|'
|
||||
r'(A:(:[0-9a-f]{1,4}){1,7}Z)|'
|
||||
r'(A((([0-9a-f]{1,4}:){6})(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3})Z)|'
|
||||
r'(A(([0-9a-f]{1,4}:){5}[0-9a-f]{1,4}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3})Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){5}:[0-9a-f]{1,4}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,1}(:[0-9a-f]{1,4}){1,4}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,2}(:[0-9a-f]{1,4}){1,3}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,3}(:[0-9a-f]{1,4}){1,2}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A([0-9a-f]{1,4}:){1,4}(:[0-9a-f]{1,4}){1,1}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A(([0-9a-f]{1,4}:){1,5}|:):(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)|'
|
||||
r'(A:(:[0-9a-f]{1,4}){1,5}:(25[0-5]|2[0-4]d|[0-1]?d?d)(.(25[0-5]|2[0-4]d|[0-1]?d?d)){3}Z)(?::\d{1,5})?'
|
||||
)
|
||||
|
||||
if highlightRule.isJSONKey:
|
||||
# JSON keys. Ignore trailing :
|
||||
capturedLength = match.capturedLength() - 1
|
||||
else:
|
||||
capturedLength = match.capturedLength()
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setFormat(
|
||||
match.capturedStart(), capturedLength, highlightRule.rules
|
||||
)
|
||||
self.highlightRules = [
|
||||
# IP addresses (IPv4 & IPv6)
|
||||
EditorHighlightRules(
|
||||
# https://ihateregex.io/expr/ip/
|
||||
DraculaLoggerSyntaxHighlighter.IPV4_REGEX
|
||||
+ r'|'
|
||||
+ DraculaLoggerSyntaxHighlighter.IPV6_REGEX,
|
||||
'#B4FFFF',
|
||||
),
|
||||
# URLs
|
||||
EditorHighlightRules(r'(https?:)?//[^\s,\'"\]\)\(<>]+', '#FFDD88'),
|
||||
# Timestamps
|
||||
EditorHighlightRules(
|
||||
r'\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}\]', '#7F7F7F'
|
||||
),
|
||||
# Logger name: [X.Y.Z]
|
||||
EditorHighlightRules(
|
||||
r'\[[A-Za-z0-9_]+\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+\]', '#FFB46E'
|
||||
),
|
||||
# Log levels
|
||||
EditorHighlightRules(r'\[INFO\]', '#A0C882', isBold=True),
|
||||
EditorHighlightRules(r'\[DEBUG\]', '#82B4FF', isBold=True),
|
||||
EditorHighlightRules(r'\[WARNING\]', '#FFD75A', isBold=True),
|
||||
EditorHighlightRules(r'\[ERROR\]', '#FF7878', isBold=True),
|
||||
EditorHighlightRules(r'\[CRITICAL\]', '#FF5050', isBold=True),
|
||||
# Quoted strings (single or double)
|
||||
EditorHighlightRules(r"'[^']*'", '#A0FFA0'),
|
||||
EditorHighlightRules(r'"[^"]*"', '#A0FFA0'),
|
||||
]
|
||||
|
||||
@@ -20,6 +20,7 @@ from __future__ import annotations
|
||||
from Furious.QtFramework.QtNetwork import *
|
||||
from Furious.QtFramework.QtWidgets import *
|
||||
from Furious.QtFramework.DynamicTranslate import gettext as _
|
||||
from Furious.QtFramework.WebGETManager import *
|
||||
from Furious.Utility import *
|
||||
from Furious.Library import *
|
||||
|
||||
@@ -27,10 +28,9 @@ from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtNetwork import *
|
||||
|
||||
from typing import Union
|
||||
from typing import Union, Callable
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import functools
|
||||
|
||||
__all__ = ['UpdatesManager']
|
||||
@@ -61,34 +61,36 @@ class QuestionUpdateMBox(AppQMessageBox):
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class UpdatesManager(AppQNetworkAccessManager):
|
||||
class UpdatesManager(WebGETManager):
|
||||
API_URL = (
|
||||
f'https://api.github.com/repos/'
|
||||
f'{APPLICATION_REPO_OWNER_NAME}/{APPLICATION_REPO_NAME}/releases/latest'
|
||||
)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
actionMessage = kwargs.pop('actionMessage', 'check for updates')
|
||||
|
||||
def handleFinishedByNetworkReply(self, networkReply):
|
||||
assert isinstance(networkReply, QNetworkReply)
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
if networkReply.error() != QNetworkReply.NetworkError.NoError:
|
||||
logger.error(f'check for updates failed. {networkReply.errorString()}')
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
showMessageBox = kwargs.pop('showMessageBox', True)
|
||||
hasNewVersionCallback = kwargs.pop('hasNewVersionCallback', None)
|
||||
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||||
mbox.setText(_('Check for updates failed'))
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
try:
|
||||
info = UJSONEncoder.decode(data)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'bad network reply while checking for updates. {ex}')
|
||||
|
||||
if showMessageBox:
|
||||
self.showErrorMessageBox()
|
||||
else:
|
||||
logger.info('check for updates success')
|
||||
newVersion = info['tag_name']
|
||||
|
||||
# Unchecked?
|
||||
info = UJSONEncoder.decode(networkReply.readAll().data())
|
||||
|
||||
if versionToValue(info['tag_name']) > versionToValue(APPLICATION_VERSION):
|
||||
if versionToValue(newVersion) > versionToValue(APPLICATION_VERSION):
|
||||
|
||||
def handleResultCode(code):
|
||||
if code == PySide6LegacyEnumValueWrapper(
|
||||
@@ -102,40 +104,49 @@ class UpdatesManager(AppQNetworkAccessManager):
|
||||
# Do nothing
|
||||
pass
|
||||
|
||||
mbox = QuestionUpdateMBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.version = info['tag_name']
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.setInformativeText(_('Go to download page?'))
|
||||
mbox.finished.connect(functools.partial(handleResultCode))
|
||||
if callable(hasNewVersionCallback):
|
||||
hasNewVersionCallback(newVersion)
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_questionUpdateMBox', mbox)
|
||||
if showMessageBox:
|
||||
mbox = QuestionUpdateMBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.version = newVersion
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.setInformativeText(_('Go to download page?'))
|
||||
mbox.finished.connect(functools.partial(handleResultCode))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||||
mbox.setText(_(f'{APPLICATION_NAME} is already the latest version'))
|
||||
if showMessageBox:
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||||
mbox.setText(_(f'{APPLICATION_NAME} is already the latest version'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
useProxy = super().configureHttpProxy(httpProxy)
|
||||
@staticmethod
|
||||
def showErrorMessageBox():
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||||
mbox.setText(_('Check for updates failed'))
|
||||
|
||||
if useProxy:
|
||||
logger.info(f'check for updates uses proxy server {httpProxy}')
|
||||
else:
|
||||
logger.info(f'check for updates uses no proxy')
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
return useProxy
|
||||
def failureCallback(self, networkReply, **kwargs):
|
||||
showMessageBox = kwargs.pop('showMessageBox', True)
|
||||
|
||||
def checkForUpdates(self):
|
||||
networkReply = self.get(QNetworkRequest(QtCore.QUrl(self.API_URL)))
|
||||
networkReply.finished.connect(
|
||||
functools.partial(
|
||||
self.handleFinishedByNetworkReply,
|
||||
networkReply,
|
||||
)
|
||||
if showMessageBox:
|
||||
self.showErrorMessageBox()
|
||||
|
||||
def checkForUpdates(
|
||||
self,
|
||||
showMessageBox=True,
|
||||
hasNewVersionCallback: Callable[[str], None] = None,
|
||||
):
|
||||
self.webGET(
|
||||
self.API_URL,
|
||||
showMessageBox=showMessageBox,
|
||||
hasNewVersionCallback=hasNewVersionCallback,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.QtFramework.QtNetwork import *
|
||||
from Furious.QtFramework.QtWidgets import *
|
||||
from Furious.QtFramework.DynamicTranslate import gettext as _
|
||||
from Furious.Utility import *
|
||||
from Furious.Library import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtNetwork import *
|
||||
|
||||
from typing import AnyStr, Union, Callable
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
__all__ = ['WebGETManager']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WebGETManager(AppQNetworkAccessManager):
|
||||
def __init__(self, parent=None, actionMessage='web GET'):
|
||||
super().__init__(parent)
|
||||
|
||||
self.actionMessage = actionMessage
|
||||
|
||||
def successCallback(self, networkReply: QNetworkReply, **kwargs):
|
||||
pass
|
||||
|
||||
def hasDataCallback(self, networkReply: QNetworkReply, **kwargs):
|
||||
pass
|
||||
|
||||
def failureCallback(self, networkReply: QNetworkReply, **kwargs):
|
||||
pass
|
||||
|
||||
def handleReadyReadByNetworkReply(self, networkReply: QNetworkReply, **kwargs):
|
||||
self.hasDataCallback(networkReply, **kwargs)
|
||||
|
||||
def handleFinishedByNetworkReply(self, networkReply: QNetworkReply, **kwargs):
|
||||
if not isinstance(networkReply, QNetworkReply):
|
||||
# Some PySide6 version does not have networkReply as
|
||||
# QNetworkReply instance, so assertion is not used here
|
||||
logger.error(f'QNetworkReply error in PySide6 {PYSIDE6_VERSION} version')
|
||||
|
||||
return
|
||||
|
||||
logActionMessage = kwargs.pop('logActionMessage', True)
|
||||
|
||||
if networkReply.error() != QNetworkReply.NetworkError.NoError:
|
||||
if logActionMessage:
|
||||
logger.error(
|
||||
f'{self.actionMessage} failed. {networkReply.errorString()}'
|
||||
)
|
||||
|
||||
self.failureCallback(networkReply, **kwargs)
|
||||
else:
|
||||
if logActionMessage:
|
||||
logger.info(f'{self.actionMessage} success')
|
||||
|
||||
self.successCallback(networkReply, **kwargs)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
useProxy = super().configureHttpProxy(httpProxy)
|
||||
|
||||
if useProxy:
|
||||
logger.info(f'{self.actionMessage} uses proxy server {httpProxy}')
|
||||
else:
|
||||
logger.info(f'{self.actionMessage} uses no proxy')
|
||||
|
||||
return useProxy
|
||||
|
||||
def webGET(self, request: Union[QNetworkRequest, str], **kwargs) -> QNetworkReply:
|
||||
if isinstance(request, QNetworkRequest):
|
||||
networkReply = self.get(request)
|
||||
else:
|
||||
networkReply = self.get(QNetworkRequest(QtCore.QUrl(request)))
|
||||
|
||||
networkReply.readyRead.connect(
|
||||
functools.partial(
|
||||
self.handleReadyReadByNetworkReply,
|
||||
networkReply,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
networkReply.finished.connect(
|
||||
functools.partial(
|
||||
self.handleFinishedByNetworkReply,
|
||||
networkReply,
|
||||
**kwargs,
|
||||
)
|
||||
)
|
||||
|
||||
return networkReply
|
||||
@@ -0,0 +1,210 @@
|
||||
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.QtFramework.QtNetwork import *
|
||||
from Furious.QtFramework.QtWidgets import *
|
||||
from Furious.QtFramework.DynamicTranslate import gettext as _
|
||||
from Furious.QtFramework.WebGETManager import *
|
||||
from Furious.Utility import *
|
||||
from Furious.Library import *
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtNetwork import *
|
||||
|
||||
from typing import AnyStr, Union, Callable
|
||||
|
||||
import os
|
||||
import logging
|
||||
import hashlib
|
||||
import functools
|
||||
|
||||
__all__ = ['XrayAssetDownloadManager']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SHA256Worker(QtCore.QObject, QtCore.QRunnable):
|
||||
finished = QtCore.Signal(str)
|
||||
|
||||
def __init__(self, string=b''):
|
||||
# Explictly called __init__
|
||||
QtCore.QObject.__init__(self)
|
||||
QtCore.QRunnable.__init__(self)
|
||||
|
||||
self.string = string
|
||||
|
||||
def run(self):
|
||||
self.finished.emit(hashlib.sha256(self.string).hexdigest())
|
||||
|
||||
|
||||
class XrayAssetSHA256DownloadManager(WebGETManager):
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
actionMessage = kwargs.pop('actionMessage', 'download sha256')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
@staticmethod
|
||||
def fileContent(filepath, mode='rb') -> AnyStr:
|
||||
try:
|
||||
with open(filepath, mode) as file:
|
||||
return file.read()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return b''
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
downloadCallback = kwargs.pop('downloadCallback', None)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if isinstance(data, bytes):
|
||||
datastr = data.decode('utf-8', 'replace')
|
||||
else:
|
||||
datastr = str(data)
|
||||
|
||||
logger.debug(f'data: {data}')
|
||||
|
||||
# 6068a73edfa08b63080b8d362bd4c5b069689a3548f413f43cfa58227a201571 geosite.dat
|
||||
# 3a12beaa33c81b6751b45833a0a942b9462420d91182e7fa1d768b418e604049 geoip.dat
|
||||
value = datastr.split()[0]
|
||||
|
||||
basename = os.path.basename(filepath)
|
||||
|
||||
def handleFinished(_digest, _value=''):
|
||||
logger.debug(
|
||||
f'computed digest is \'{_digest}\' while repo digest is \'{_value}\''
|
||||
)
|
||||
|
||||
if _digest != _value:
|
||||
logger.info(f'digest not equal for {basename}. Start downloading asset')
|
||||
|
||||
if callable(downloadCallback):
|
||||
downloadCallback()
|
||||
else:
|
||||
logger.info(f'digest equal for {basename}. Nothing to do')
|
||||
|
||||
worker = SHA256Worker(self.fileContent(filepath))
|
||||
|
||||
worker.setAutoDelete(True)
|
||||
worker.finished.connect(functools.partial(handleFinished, _value=value))
|
||||
|
||||
AppThreadPool().start(worker)
|
||||
|
||||
def download(self, url, filepath, downloadCallback: Callable[[], None]):
|
||||
self.webGET(
|
||||
url,
|
||||
filepath=str(filepath),
|
||||
downloadCallback=downloadCallback,
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetAssetsDownloadManager(WebGETManager):
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
actionMessage = kwargs.pop('actionMessage', 'download assets')
|
||||
|
||||
super().__init__(parent, actionMessage=actionMessage)
|
||||
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
filepath = kwargs.pop('filepath', '')
|
||||
|
||||
saveFile = QtCore.QSaveFile(filepath)
|
||||
|
||||
data = networkReply.readAll().data()
|
||||
|
||||
if not saveFile.open(QtCore.QSaveFile.OpenModeFlag.WriteOnly):
|
||||
logger.error(f'failed to open \'{filepath}\' for writing')
|
||||
else:
|
||||
saveFile.write(data)
|
||||
|
||||
if not saveFile.commit():
|
||||
logger.error(f'save file to \'{filepath}\' failed')
|
||||
else:
|
||||
logger.info(f'save file to \'{filepath}\' success')
|
||||
|
||||
def download(self, url, filepath):
|
||||
self.webGET(url, filepath=str(filepath))
|
||||
|
||||
|
||||
class XrayAssetPairDownloadHelper:
|
||||
def __init__(self, *args, **kwargs):
|
||||
sha256ActionMessage = kwargs.pop('sha256ActionMessage', 'download sha256')
|
||||
assetsActionMessage = kwargs.pop('assetsActionMessage', 'download assets')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.sha256Downloader = XrayAssetSHA256DownloadManager(
|
||||
actionMessage=sha256ActionMessage
|
||||
)
|
||||
self.assetsDownloader = XrayAssetAssetsDownloadManager(
|
||||
actionMessage=assetsActionMessage
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.sha256Downloader.configureHttpProxy(httpProxy),
|
||||
self.assetsDownloader.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self, sha256URL, assetsURL, filepath):
|
||||
self.sha256Downloader.download(
|
||||
url=sha256URL,
|
||||
filepath=filepath,
|
||||
downloadCallback=functools.partial(
|
||||
self.assetsDownloader.download, assetsURL, filepath
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class XrayAssetDownloadManager:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.downloadHelperGeosite = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geosite sha256',
|
||||
assetsActionMessage='download geosite assets',
|
||||
)
|
||||
self.downloadHelperGeoip = XrayAssetPairDownloadHelper(
|
||||
sha256ActionMessage='download geoip sha256',
|
||||
assetsActionMessage='download geoip assets',
|
||||
)
|
||||
|
||||
def configureHttpProxy(self, httpProxy: Union[str, None]) -> bool:
|
||||
return all(
|
||||
[
|
||||
self.downloadHelperGeosite.configureHttpProxy(httpProxy),
|
||||
self.downloadHelperGeoip.configureHttpProxy(httpProxy),
|
||||
]
|
||||
)
|
||||
|
||||
def download(self):
|
||||
self.downloadHelperGeosite.download(
|
||||
sha256URL=URL_GEOSITE_SHA256,
|
||||
assetsURL=URL_GEOSITE,
|
||||
filepath=XRAY_ASSET_PATH_GEOSITE,
|
||||
)
|
||||
self.downloadHelperGeoip.download(
|
||||
sha256URL=URL_GEOIP_SHA256,
|
||||
assetsURL=URL_GEOIP,
|
||||
filepath=XRAY_ASSET_PATH_GEOIP,
|
||||
)
|
||||
@@ -28,3 +28,5 @@ from .NetworkStateManager import *
|
||||
from .TextEditor import *
|
||||
from .TextEditorTheme import *
|
||||
from .GuiEditorXXX import *
|
||||
from .WebGETManager import *
|
||||
from .XrayAssetDownloadManager import *
|
||||
|
||||
@@ -27,6 +27,12 @@ __all__ = ['UserServers']
|
||||
registerAppSettings('Configuration')
|
||||
|
||||
|
||||
class UserServer:
|
||||
remark: str
|
||||
config: str
|
||||
subsId: str
|
||||
|
||||
|
||||
class UserServers(SupportExitCleanup, StorageFactory):
|
||||
# remark, config, subsId. (subsId corresponds to unique in user subscription)
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -44,8 +50,8 @@ class UserServers(SupportExitCleanup, StorageFactory):
|
||||
|
||||
self._data = restore()
|
||||
self._list = list(
|
||||
constructFromAny(model.pop('config', ''), **model)
|
||||
for model in self._data['model']
|
||||
constructFromAny(model.pop('config', ''), index=index, **model)
|
||||
for index, model in enumerate(self._data['model'])
|
||||
)
|
||||
|
||||
def sync(self):
|
||||
|
||||
@@ -27,8 +27,17 @@ __all__ = ['UserSubs']
|
||||
registerAppSettings('CustomSubscription')
|
||||
|
||||
|
||||
class UserSubEntry:
|
||||
remark: str
|
||||
webURL: str
|
||||
|
||||
|
||||
class UserSub:
|
||||
unique: dict[str, dict]
|
||||
|
||||
|
||||
class UserSubs(SupportExitCleanup, StorageFactory):
|
||||
# unique: remark, webURL
|
||||
# unique: { remark, webURL }
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.PyFramework import *
|
||||
from Furious.Utility import *
|
||||
from Furious.Library import *
|
||||
|
||||
__all__ = ['UserTUNSettings']
|
||||
|
||||
registerAppSettings('CustomTUNSettings')
|
||||
|
||||
|
||||
class UserTUNSettings(SupportExitCleanup, StorageFactory):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def restore():
|
||||
try:
|
||||
return UJSONEncoder.decode(
|
||||
PyBase64Encoder.decode(AppSettings.get('CustomTUNSettings'))
|
||||
)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return {}
|
||||
|
||||
self._data = restore()
|
||||
|
||||
def sync(self):
|
||||
AppSettings.set(
|
||||
'CustomTUNSettings',
|
||||
PyBase64Encoder.encode(
|
||||
UJSONEncoder.encode(self._data).encode(),
|
||||
),
|
||||
)
|
||||
|
||||
def data(self) -> dict[str, str]:
|
||||
# Shallow copy
|
||||
return self._data
|
||||
|
||||
def cleanup(self):
|
||||
self.sync()
|
||||
@@ -17,3 +17,4 @@
|
||||
|
||||
from .UserServers import *
|
||||
from .UserSubs import *
|
||||
from .UserTUNSettings import *
|
||||
|
||||
@@ -69,6 +69,9 @@ class ConnectAction(AppQAction):
|
||||
self.actionTimer = QtCore.QTimer()
|
||||
self.actionTimer.timeout.connect(lambda: self.callActionFromQueue())
|
||||
|
||||
self.updatesManager = UpdatesManager()
|
||||
self.assetDownloadManager = XrayAssetDownloadManager()
|
||||
|
||||
def reset(self):
|
||||
self.hideProgressBar(True)
|
||||
self.setText(_('Connect'))
|
||||
@@ -93,7 +96,7 @@ class ConnectAction(AppQAction):
|
||||
if done:
|
||||
self.progressBar.setValue(100)
|
||||
|
||||
self.progressBar.hide()
|
||||
self.progressBar.close()
|
||||
self.progressBar.stop()
|
||||
|
||||
return self
|
||||
@@ -105,10 +108,10 @@ class ConnectAction(AppQAction):
|
||||
APP().systemTray.SystemProxyAction.setDisabled(value)
|
||||
|
||||
if isAdministrator():
|
||||
VPNModeAction = APP().systemTray.SettingsAction.getVPNModeAction()
|
||||
TUNModeAction = APP().systemTray.SettingsAction.getTUNModeAction()
|
||||
|
||||
if VPNModeAction is not None:
|
||||
VPNModeAction.setDisabled(value)
|
||||
if TUNModeAction is not None:
|
||||
TUNModeAction.setDisabled(value)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self.textCompare('Disconnect')
|
||||
@@ -202,69 +205,93 @@ class ConnectAction(AppQAction):
|
||||
AppSettings.turnOFF('Connect')
|
||||
|
||||
self.setChecked(False)
|
||||
else:
|
||||
assert isinstance(config, ConfigurationFactory)
|
||||
|
||||
return
|
||||
if not validateProxyServer(config.httpProxyEndpoint()):
|
||||
# Proxy server is not valid. Do not connect
|
||||
|
||||
assert isinstance(config, ConfigurationFactory)
|
||||
AppSettings.turnOFF('Connect')
|
||||
|
||||
if not validateProxyServer(config.httpProxyEndpoint()):
|
||||
AppSettings.turnOFF('Connect')
|
||||
self.setChecked(False)
|
||||
|
||||
self.setChecked(False)
|
||||
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_('Unable to connect'))
|
||||
mbox.setText(
|
||||
_(
|
||||
f'{APPLICATION_NAME} cannot find any valid http proxy endpoint in the configuration'
|
||||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Critical)
|
||||
mbox.setWindowTitle(_('Unable to connect'))
|
||||
mbox.setText(
|
||||
_(
|
||||
f'{APPLICATION_NAME} cannot find any valid http proxy endpoint in the configuration'
|
||||
)
|
||||
)
|
||||
mbox.setInformativeText(_('Please complete your server configuration'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
return
|
||||
|
||||
self.doConnecting()
|
||||
|
||||
# Clear previous log
|
||||
loggerCore().clear()
|
||||
loggerTun_().clear()
|
||||
|
||||
success = self.coreManager.start(
|
||||
config,
|
||||
routing=AppSettings.get('Routing'),
|
||||
exitCallback=self.coreExitCallback,
|
||||
msgCallbackCore=lambda line: loggerCore().appendLine(line),
|
||||
msgCallbackTun_=lambda line: loggerTun_().appendLine(line),
|
||||
)
|
||||
mbox.setInformativeText(_('Please complete your server configuration'))
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
if self.actionQueue.empty():
|
||||
if success:
|
||||
SystemProxy.set(config.httpProxyEndpoint(), PROXY_SERVER_BYPASS)
|
||||
|
||||
return
|
||||
self.doConnected()
|
||||
|
||||
self.doConnecting()
|
||||
APP().systemTray.showMessage(
|
||||
f'{config.coreName()}: ' + _('Connected')
|
||||
)
|
||||
|
||||
# Clear previous log
|
||||
loggerCore().clear()
|
||||
loggerTun_().clear()
|
||||
if AppSettings.isStateON_('PowerSaveMode'):
|
||||
# Power optimization
|
||||
logger.info(f'no action queue in power save mode')
|
||||
|
||||
success = self.coreManager.start(
|
||||
config,
|
||||
routing=AppSettings.get('Routing'),
|
||||
exitCallback=self.coreExitCallback,
|
||||
msgCallbackCore=lambda line: loggerCore().appendLine(line),
|
||||
msgCallbackTun_=lambda line: loggerTun_().appendLine(line),
|
||||
self.actionTimer.stop()
|
||||
else:
|
||||
self.actionTimer.start(CORE_CHECK_ALIVE_INTERVAL)
|
||||
|
||||
self.doConnectedCallOnceOnly()
|
||||
else:
|
||||
logger.error('failed to start core manager')
|
||||
|
||||
self.coreManager.stopAll()
|
||||
self.doDisconnectWithTrayMessage(
|
||||
f'{config.coreName()}: ' + _('Unknown error')
|
||||
)
|
||||
else:
|
||||
while not self.actionQueue.empty():
|
||||
self.callActionFromQueue()
|
||||
|
||||
@callOnceOnly
|
||||
def doConnectedCallOnceOnly(self):
|
||||
def newVersionCallback(newVersion):
|
||||
APP().systemTray.showMessage(
|
||||
f'{APPLICATION_NAME} {newVersion} ' + _('is available to download')
|
||||
)
|
||||
|
||||
connectedHttpProxy = connectedHttpProxyEndpoint()
|
||||
|
||||
# Check for updates
|
||||
self.updatesManager.configureHttpProxy(connectedHttpProxy)
|
||||
self.updatesManager.checkForUpdates(
|
||||
showMessageBox=False,
|
||||
hasNewVersionCallback=newVersionCallback,
|
||||
)
|
||||
|
||||
if self.actionQueue.empty():
|
||||
if success:
|
||||
SystemProxy.set(config.httpProxyEndpoint(), PROXY_SERVER_BYPASS)
|
||||
|
||||
self.doConnected()
|
||||
|
||||
APP().systemTray.showMessage(f'{config.coreName()}: ' + _('Connected'))
|
||||
|
||||
if AppSettings.isStateON_('PowerSaveMode'):
|
||||
# Power optimization
|
||||
logger.info(f'no action queue in power save mode')
|
||||
|
||||
self.actionTimer.stop()
|
||||
else:
|
||||
self.actionTimer.start(CORE_CHECK_ALIVE_INTERVAL)
|
||||
else:
|
||||
logger.error('failed to start core manager')
|
||||
|
||||
self.coreManager.stopAll()
|
||||
self.doDisconnectWithTrayMessage(
|
||||
f'{config.coreName()}: ' + _('Unknown error')
|
||||
)
|
||||
else:
|
||||
while not self.actionQueue.empty():
|
||||
self.callActionFromQueue()
|
||||
# Automatically update assets
|
||||
self.assetDownloadManager.configureHttpProxy(connectedHttpProxy)
|
||||
self.assetDownloadManager.download()
|
||||
|
||||
def callActionFromQueue(self):
|
||||
try:
|
||||
|
||||
@@ -96,7 +96,7 @@ class ImportMultiSuccessMBox(AppQMessageBox):
|
||||
def customText(self):
|
||||
text = (
|
||||
_('Import share link success')
|
||||
+ f'\n\n'
|
||||
+ '\n\n'
|
||||
+ '\n'.join(
|
||||
list(
|
||||
f'{index + 1} - {remark}. '
|
||||
|
||||
@@ -36,18 +36,18 @@ registerAppSettings(
|
||||
registerAppSettings('ShowTabAndSpacesInEditor', isBinary=True)
|
||||
|
||||
# Administrator, root
|
||||
TRANSLATABLE_VPN_MODE = [
|
||||
_('VPN Mode Disabled (Administrator)'),
|
||||
_('VPN Mode Disabled (root)'),
|
||||
TRANSLATABLE_TUN_MODE = [
|
||||
_('TUN Mode Disabled (Administrator)'),
|
||||
_('TUN Mode Disabled (root)'),
|
||||
]
|
||||
|
||||
|
||||
class VPNModeAction(AppQAction):
|
||||
class TUNModeAction(AppQAction):
|
||||
def __init__(self, **kwargs):
|
||||
if isAdministrator():
|
||||
super().__init__(_('VPN Mode'), **kwargs)
|
||||
super().__init__(_('TUN Mode'), **kwargs)
|
||||
else:
|
||||
super().__init__(_(f'VPN Mode Disabled ({ADMINISTRATOR_NAME})'), **kwargs)
|
||||
super().__init__(_(f'TUN Mode Disabled ({ADMINISTRATOR_NAME})'), **kwargs)
|
||||
|
||||
self.setDisabled(True)
|
||||
|
||||
@@ -136,10 +136,16 @@ class SettingsAction(AppQAction):
|
||||
def __init__(self, **kwargs):
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
extraActions = [
|
||||
VPNModeAction(
|
||||
TUNModeAction(
|
||||
checkable=True,
|
||||
checked=AppSettings.isStateON_('VPNMode'),
|
||||
),
|
||||
AppQAction(
|
||||
_('Customize TUN Settings...'),
|
||||
icon=bootstrapIcon('diagram-3.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: APP().mainWindow.getGuiTUNSettings().open(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
]
|
||||
else:
|
||||
@@ -187,7 +193,7 @@ class SettingsAction(AppQAction):
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def getVPNModeAction(self) -> Union[AppQAction, None]:
|
||||
def getTUNModeAction(self) -> Union[AppQAction, None]:
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
# 1st action
|
||||
return self._menu.actions()[0]
|
||||
|
||||
@@ -20,9 +20,18 @@ from __future__ import annotations
|
||||
from Furious.Utility.Constants import *
|
||||
from Furious.Utility.AppSettings import *
|
||||
|
||||
from typing import Union
|
||||
|
||||
import functools
|
||||
|
||||
__all___ = ['AS_UserActivatedItemIndex', 'AS_UserServers', 'AS_UserSubscription']
|
||||
__all___ = [
|
||||
'AS_UserActivatedItemIndex',
|
||||
'AS_UserServers',
|
||||
'AS_UserSubscription',
|
||||
'AS_UserTUNSettings',
|
||||
'connectedHttpProxyEndpoint',
|
||||
'connectedServerRemark',
|
||||
]
|
||||
|
||||
|
||||
def _activatedItemIndex() -> int:
|
||||
@@ -52,6 +61,52 @@ def _userSubscriptionData() -> dict[str, dict]:
|
||||
return {}
|
||||
|
||||
|
||||
def _userTUNSettingsData() -> dict[str, str]:
|
||||
try:
|
||||
return APP().userTUNSettings.data()
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
AS_UserActivatedItemIndex = functools.partial(_activatedItemIndex)
|
||||
AS_UserServers = functools.partial(_userServersData)
|
||||
AS_UserSubscription = functools.partial(_userSubscriptionData)
|
||||
AS_UserTUNSettings = functools.partial(_userTUNSettingsData)
|
||||
|
||||
|
||||
def connectedHttpProxyEndpoint() -> Union[str, None]:
|
||||
try:
|
||||
if APP().isSystemTrayConnected():
|
||||
index = AS_UserActivatedItemIndex()
|
||||
|
||||
if index >= 0:
|
||||
return AS_UserServers()[index].httpProxyEndpoint()
|
||||
else:
|
||||
# Should not reach here
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def connectedServerRemark() -> str:
|
||||
try:
|
||||
if APP().isSystemTrayConnected():
|
||||
index = AS_UserActivatedItemIndex()
|
||||
|
||||
if index >= 0:
|
||||
return f'{index + 1} - ' + AS_UserServers()[index].getExtras('remark')
|
||||
else:
|
||||
# Should not reach here
|
||||
return ''
|
||||
else:
|
||||
return ''
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return ''
|
||||
|
||||
@@ -59,6 +59,16 @@ XRAY_ASSET_DIR = DATA_DIR / 'xray'
|
||||
CRASH_LOG_DIR = PACKAGE_DIR / 'CrashLog'
|
||||
GEN_TRANSLATION_FILE = PACKAGE_DIR / 'Externals' / 'GenTranslation.py'
|
||||
|
||||
URL_GEOSITE = 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat'
|
||||
URL_GEOIP = (
|
||||
'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat'
|
||||
)
|
||||
URL_GEOSITE_SHA256 = 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat.sha256sum'
|
||||
URL_GEOIP_SHA256 = 'https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat.sha256sum'
|
||||
|
||||
XRAY_ASSET_PATH_GEOSITE = XRAY_ASSET_DIR / 'geosite.dat'
|
||||
XRAY_ASSET_PATH_GEOIP = XRAY_ASSET_DIR / 'geoip.dat'
|
||||
|
||||
PROXY_SERVER_BYPASS = (
|
||||
'localhost;*.local;127.*;10.*;172.16.*;172.17.*;'
|
||||
'172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;'
|
||||
@@ -92,6 +102,9 @@ APPLICATION_TUN_INTERFACE_DNS_ADDRESS = '1.1.1.1'
|
||||
ADMINISTRATOR_NAME = 'Administrator' if PLATFORM == 'Windows' else 'root'
|
||||
|
||||
NETWORK_STATE_TEST_URL = 'http://cp.cloudflare.com'
|
||||
NETWORK_SPEED_TEST_URL = (
|
||||
'http://speed.cloudflare.com/__down?during=download&bytes=104857600'
|
||||
)
|
||||
|
||||
UNICODE_LARGE_RED_CIRCLE = u'\U0001F534'
|
||||
UNICODE_LARGE_GREEN_CIRCLE = u'\U0001F7E2'
|
||||
|
||||
@@ -288,7 +288,7 @@ class _SystemProxy:
|
||||
|
||||
return False
|
||||
|
||||
# Note: try to resolve non-Windows proxy daemon issue by
|
||||
# Note: try to solve non-Windows proxy daemon issue by
|
||||
# turning on StartupOnBoot by default. This should be
|
||||
# friendly for most of the users
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ class SystemRoutingTable:
|
||||
result = runExternalCommand(
|
||||
'netsh interface ip set address'.split()
|
||||
+ [
|
||||
deviceName,
|
||||
f'\"{deviceName}\"',
|
||||
'static',
|
||||
deviceIP,
|
||||
'255.255.255.0',
|
||||
|
||||
@@ -31,7 +31,7 @@ __all__ = [
|
||||
'getPythonVersion',
|
||||
'getUbuntuRelease',
|
||||
'isAdministrator',
|
||||
'isVPNMode',
|
||||
'isTUNMode',
|
||||
'isScriptMode',
|
||||
'isPythonw',
|
||||
'isWindows7',
|
||||
@@ -79,7 +79,7 @@ def isAdministrator() -> bool:
|
||||
return os.geteuid() == 0
|
||||
|
||||
|
||||
def isVPNMode() -> bool:
|
||||
def isTUNMode() -> bool:
|
||||
return isAdministrator() and AppSettings.isStateON_('VPNMode')
|
||||
|
||||
|
||||
|
||||
@@ -30,8 +30,10 @@ import subprocess
|
||||
import urllib.parse
|
||||
|
||||
__all__ = [
|
||||
'AppThreadPool',
|
||||
'Protocol',
|
||||
'BinarySettings',
|
||||
'callOnceOnly',
|
||||
'protocolRepr',
|
||||
'isValidIPAddress',
|
||||
'parseHostPort',
|
||||
@@ -46,6 +48,13 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def _appThreadPool() -> QtCore.QThreadPool:
|
||||
return APP().threadPool
|
||||
|
||||
|
||||
AppThreadPool = functools.partial(_appThreadPool)
|
||||
|
||||
|
||||
class Protocol:
|
||||
VMess = 'VMess'
|
||||
VLESS = 'VLESS'
|
||||
@@ -62,6 +71,26 @@ class BinarySettings:
|
||||
RANGE = [OFF, ON_]
|
||||
|
||||
|
||||
def callOnceOnly(func):
|
||||
"""
|
||||
Decorator that ensures a function is only called once.
|
||||
Later calls return the cached result from the first invocation.
|
||||
"""
|
||||
result = None
|
||||
called = False
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal result, called
|
||||
|
||||
if not called:
|
||||
result = func(*args, **kwargs)
|
||||
called = True
|
||||
|
||||
return result
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@functools.lru_cache(None)
|
||||
def protocolRepr(protocol: str) -> str:
|
||||
if not isinstance(protocol, str):
|
||||
|
||||
@@ -181,6 +181,7 @@ class Application(ApplicationFactory, SingletonApplication):
|
||||
# Initialize storage
|
||||
self.userServers = UserServers()
|
||||
self.userSubs = UserSubs()
|
||||
self.userTUNSettings = UserTUNSettings()
|
||||
|
||||
# ThreadPool
|
||||
self.threadPool = QtCore.QThreadPool()
|
||||
|
||||
@@ -85,11 +85,6 @@ class ConnectProgressBar(QTranslatable, SupportConnectedCallback, QWidget):
|
||||
f'}}'
|
||||
)
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
self.hide()
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.setWindowIcon(AppHue.disconnectedWindowIcon())
|
||||
self._progressBar.setStyleSheet(self.getStyleSheet(AppHue.disconnectedColor()))
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# Copyright (C) 2024 Loren Eteval <loren.eteval@proton.me>
|
||||
#
|
||||
# This file is part of Furious.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from Furious.Interface import *
|
||||
from Furious.QtFramework import *
|
||||
from Furious.QtFramework import gettext as _
|
||||
from Furious.Utility import *
|
||||
|
||||
from PySide6.QtGui import *
|
||||
|
||||
import logging
|
||||
|
||||
__all__ = ['GuiTUNSettings']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GuiTUNSettingsItemXXX(GuiEditorItemTextInput):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
oldItem = config.get(self.key, '')
|
||||
newItem = self.text()
|
||||
|
||||
if newItem != oldItem:
|
||||
config[self.key] = newItem
|
||||
|
||||
# Modified
|
||||
return True
|
||||
else:
|
||||
# Not modified
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: dict):
|
||||
self.setText(config.get(self.key, ''))
|
||||
|
||||
|
||||
class GuiTUNSettingsItemDisablePrimaryAdapterInterfaceDNS(GuiEditorItemTextCheckBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.key = kwargs.pop('key', '')
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
oldItem = config.get(self.key, 'True')
|
||||
newItem = self.isChecked()
|
||||
|
||||
if oldItem not in ['False', 'True']:
|
||||
# Invalid value. Set to default
|
||||
oldItem = 'True'
|
||||
|
||||
if str(newItem) != oldItem:
|
||||
config[self.key] = str(newItem)
|
||||
|
||||
# Modified
|
||||
return True
|
||||
else:
|
||||
# Not modified
|
||||
return False
|
||||
|
||||
def factoryToInput(self, config: dict):
|
||||
self.setChecked(config.get(self.key, 'True') != 'False')
|
||||
|
||||
|
||||
class AppQLabelHelpPage(AppQLabel):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWebsiteURL()
|
||||
self.linkActivated.connect(self.handleLinkActivated)
|
||||
|
||||
def setWebsiteURL(self):
|
||||
self.setText(
|
||||
'<html><head/><body><p>'
|
||||
'<a href=\"https://github.com/LorenEteval/Furious/wiki/TUN-Mode\">'
|
||||
'<span style=\" text-decoration: underline; color:#007ad6;\">'
|
||||
+ _('Go to help page')
|
||||
+ '</span></a></p></body></html>'
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def handleLinkActivated(link: str):
|
||||
if QDesktopServices.openUrl(QtCore.QUrl(link)):
|
||||
logger.info(f'open link \'{link}\' success')
|
||||
else:
|
||||
logger.error(f'open link \'{link}\' failed')
|
||||
|
||||
def retranslate(self):
|
||||
self.setWebsiteURL()
|
||||
|
||||
|
||||
class GuiTUNSettingsItemHelpPage(GuiEditorItemWidgetContainer):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._page = AppQLabelHelpPage()
|
||||
|
||||
def widgets(self):
|
||||
return [self._page]
|
||||
|
||||
|
||||
class GuiTUNSettingsGroupBox(GuiEditorWidgetQGroupBox):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(_('Basic Configuration'), **kwargs)
|
||||
|
||||
def containerSequence(self):
|
||||
return [
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Primary Adapter Interface Name'),
|
||||
key='primaryAdapterInterfaceName',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Primary Adapter Interface IP'),
|
||||
key='primaryAdapterInterfaceIP',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Default Primary Gateway IP'),
|
||||
key='defaultPrimaryGatewayIP',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('TUN Adapter Interface DNS'),
|
||||
key='tunAdapterInterfaceDNS',
|
||||
),
|
||||
GuiTUNSettingsItemXXX(
|
||||
title=_('Bypass TUN adapter interface IP (separated by commas)'),
|
||||
key='bypassTUNAdapterInterfaceIP',
|
||||
),
|
||||
GuiTUNSettingsItemDisablePrimaryAdapterInterfaceDNS(
|
||||
title=_(
|
||||
'Disable Primary Adapter Interface DNS (Mitigating DNS leaks on Windows)'
|
||||
),
|
||||
key='disablePrimaryAdapterInterfaceDNS',
|
||||
),
|
||||
GuiTUNSettingsItemHelpPage(),
|
||||
]
|
||||
|
||||
|
||||
class GuiTUNSettings(GuiEditorWidgetQDialog):
|
||||
def __init__(self, *args, **kwargs):
|
||||
tabTranslatable = kwargs.pop('tabTranslatable', True)
|
||||
|
||||
super().__init__(*args, tabTranslatable=tabTranslatable, **kwargs)
|
||||
|
||||
self.setTabText(_('Customize TUN Settings'))
|
||||
self.setFixedSize(int(460 * GOLDEN_RATIO), int(460))
|
||||
|
||||
# Shallow copy
|
||||
config = AS_UserTUNSettings()
|
||||
|
||||
try:
|
||||
self.factoryToInput(config)
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'error while converting factory to input: {ex}')
|
||||
|
||||
self.accepted.connect(functools.partial(self.handleAccepted, config))
|
||||
self.rejected.connect(functools.partial(self.handleRejected))
|
||||
|
||||
def handleAccepted(self, config: dict):
|
||||
modified = self.inputToFactory(config)
|
||||
|
||||
if modified and isTUNMode():
|
||||
showNewChangesNextTimeMBox()
|
||||
|
||||
self.accepted.disconnect()
|
||||
self.rejected.disconnect()
|
||||
|
||||
def handleRejected(self):
|
||||
self.accepted.disconnect()
|
||||
self.rejected.disconnect()
|
||||
|
||||
def inputToFactory(self, config: dict) -> bool:
|
||||
modified = False
|
||||
|
||||
for groupBox in self.groupBoxes:
|
||||
modified |= groupBox.inputToFactory(config)
|
||||
|
||||
return modified
|
||||
|
||||
def factoryToInput(self, config: dict):
|
||||
for groupBox in self.groupBoxes:
|
||||
groupBox.factoryToInput(config)
|
||||
|
||||
def groupBoxSequence(self):
|
||||
return [
|
||||
GuiTUNSettingsGroupBox(),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,10 +28,13 @@ from PySide6 import QtCore
|
||||
from PySide6.QtGui import *
|
||||
from PySide6.QtWidgets import *
|
||||
|
||||
from typing import Callable
|
||||
from typing import Union, Callable
|
||||
|
||||
import logging
|
||||
import functools
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['UserSubsQTableWidget']
|
||||
|
||||
registerAppSettings('SubscriptionWidgetSectionSizeTable')
|
||||
@@ -65,10 +68,76 @@ class UserSubsQTableWidgetHeaders:
|
||||
return self.name
|
||||
|
||||
|
||||
class UserSubsAppQComboBox(AppQComboBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def retranslate(self):
|
||||
# Do not emit 'currentTextChanged' when retranslate
|
||||
with QBlockSignals(self):
|
||||
super().retranslate()
|
||||
|
||||
|
||||
# Headers VALUE
|
||||
TRANSLATABLE_HEADERS = [
|
||||
_('Never'),
|
||||
_('Every 5 mins'),
|
||||
_('Every 10 mins'),
|
||||
_('Every 15 mins'),
|
||||
_('Every 30 mins'),
|
||||
_('Every 45 mins'),
|
||||
_('Every 1 hour'),
|
||||
_('Every 2 hours'),
|
||||
_('Every 3 hours'),
|
||||
_('Every 6 hours'),
|
||||
_('Every 8 hours'),
|
||||
_('Every 10 hours'),
|
||||
_('Every 12 hours'),
|
||||
_('Every 24 hours'),
|
||||
_('Use current proxy'),
|
||||
_('Force proxy'),
|
||||
_('No proxy'),
|
||||
_('Remark'),
|
||||
_('Auto Update'),
|
||||
_('Auto Update Use Proxy'),
|
||||
]
|
||||
|
||||
|
||||
class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
AutoUpdateOptions = {
|
||||
'': None,
|
||||
'Never': None,
|
||||
'Every 5 mins': 5 * 60 * 1000,
|
||||
'Every 10 mins': 10 * 60 * 1000,
|
||||
'Every 15 mins': 15 * 60 * 1000,
|
||||
'Every 30 mins': 30 * 60 * 1000,
|
||||
'Every 45 mins': 45 * 60 * 1000,
|
||||
'Every 1 hour': 1 * 60 * 60 * 1000,
|
||||
'Every 2 hours': 2 * 60 * 60 * 1000,
|
||||
'Every 3 hours': 3 * 60 * 60 * 1000,
|
||||
'Every 6 hours': 6 * 60 * 60 * 1000,
|
||||
'Every 8 hours': 8 * 60 * 60 * 1000,
|
||||
'Every 10 hours': 10 * 60 * 60 * 1000,
|
||||
'Every 12 hours': 12 * 60 * 60 * 1000,
|
||||
'Every 24 hours': 24 * 60 * 60 * 1000,
|
||||
}
|
||||
|
||||
ProxyOptions = {
|
||||
'': lambda: None,
|
||||
'Use current proxy': lambda: connectedHttpProxyEndpoint(),
|
||||
'Force proxy': lambda: '127.0.0.1:10809',
|
||||
'No proxy': lambda: None,
|
||||
}
|
||||
|
||||
Headers = [
|
||||
UserSubsQTableWidgetHeaders('Remark', lambda item: item.get('remark', '')),
|
||||
UserSubsQTableWidgetHeaders('URL', lambda item: item.get('webURL', '')),
|
||||
UserSubsQTableWidgetHeaders(
|
||||
'Auto Update', lambda item: item.get('autoupdate', '')
|
||||
),
|
||||
UserSubsQTableWidgetHeaders(
|
||||
'Auto Update Use Proxy', lambda item: item.get('proxy', '')
|
||||
),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -76,6 +145,8 @@ class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.timers = list(QtCore.QTimer() for i in range(len(AS_UserSubscription())))
|
||||
|
||||
# Must set before flush all
|
||||
self.setColumnCount(len(self.Headers))
|
||||
|
||||
@@ -124,7 +195,8 @@ class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
@QtCore.Slot(QTableWidgetItem)
|
||||
def handleItemChanged(self, item: QTableWidgetItem):
|
||||
unique = list(AS_UserSubscription().keys())[item.row()]
|
||||
keyMap = ['remark', 'webURL']
|
||||
# 'autoupdate', 'proxy' is not triggered here
|
||||
keyMap = ['remark', 'webURL', 'autoupdate', 'proxy']
|
||||
|
||||
AS_UserSubscription()[unique][keyMap[item.column()]] = item.text()
|
||||
|
||||
@@ -149,6 +221,25 @@ class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
|
||||
AS_UserSubscription().pop(deleteUnique)
|
||||
|
||||
# Begin timer cleanup
|
||||
qtimer = self.timers[deleteIndex]
|
||||
|
||||
assert isinstance(qtimer, QtCore.QTimer)
|
||||
|
||||
try:
|
||||
qtimer.timeout.disconnect()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
# Disconnect all previous signals if possible
|
||||
pass
|
||||
|
||||
qtimer.stop()
|
||||
# End timer cleanup
|
||||
|
||||
# Remove timer from list
|
||||
self.timers.pop(deleteIndex)
|
||||
|
||||
if callable(self.deleteUniqueCallback):
|
||||
self.deleteUniqueCallback(deleteUnique)
|
||||
else:
|
||||
@@ -170,37 +261,189 @@ class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_questionDeleteMBox', mbox)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
def flushItem(self, row, column, item):
|
||||
header = self.Headers[column]
|
||||
def handleAutoUpdateComboBoxCurrentTextChanged(self, text: str, row: int):
|
||||
textEnglish = _(text, 'EN')
|
||||
|
||||
oldItem = self.item(row, column)
|
||||
newItem = QTableWidgetItem(header(item))
|
||||
unique = list(AS_UserSubscription().keys())[row]
|
||||
subsob = AS_UserSubscription()[unique]
|
||||
|
||||
if oldItem is None:
|
||||
# Item does not exists
|
||||
newItem.setFont(QFont(APP().customFontName))
|
||||
else:
|
||||
# Use existing
|
||||
newItem.setFont(oldItem.font())
|
||||
newItem.setForeground(oldItem.foreground())
|
||||
remark, webURL = subsob['remark'], subsob['webURL']
|
||||
|
||||
if oldItem.textAlignment() != 0:
|
||||
newItem.setTextAlignment(oldItem.textAlignment())
|
||||
try:
|
||||
timems = self.AutoUpdateOptions[textEnglish]
|
||||
except KeyError:
|
||||
# Invalid key. Reset
|
||||
logger.error(f'\'{textEnglish}\' is not in auto update options. Reset')
|
||||
|
||||
# Editable
|
||||
newItem.setFlags(
|
||||
QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemIsSelectable
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable
|
||||
textEnglish = ''
|
||||
|
||||
# Write to subs object
|
||||
subsob['autoupdate'] = textEnglish
|
||||
|
||||
timems = self.AutoUpdateOptions[textEnglish]
|
||||
|
||||
qtimer = self.timers[row]
|
||||
|
||||
assert isinstance(qtimer, QtCore.QTimer)
|
||||
|
||||
if timems is not None:
|
||||
logger.info(
|
||||
f'start auto update job for subscription ({remark}, {webURL}). '
|
||||
f'Interval is {timems // (60 * 1000)} mins'
|
||||
)
|
||||
|
||||
self.setItem(row, column, newItem)
|
||||
def getHttpProxy(_subsob):
|
||||
return self.ProxyOptions[_subsob.get('proxy', '')]()
|
||||
|
||||
try:
|
||||
qtimer.timeout.disconnect()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
# Disconnect all previous signals if possible
|
||||
pass
|
||||
|
||||
qtimer.timeout.connect(
|
||||
functools.partial(
|
||||
self.updateSubsByUnique,
|
||||
unique=unique,
|
||||
httpProxy=functools.partial(getHttpProxy, subsob),
|
||||
)
|
||||
)
|
||||
qtimer.start(timems)
|
||||
else:
|
||||
logger.info(f'stop auto update job for subscription ({remark}, {webURL})')
|
||||
|
||||
try:
|
||||
qtimer.timeout.disconnect()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
# Disconnect all previous signals if possible
|
||||
pass
|
||||
|
||||
qtimer.stop()
|
||||
|
||||
# Write to subs object
|
||||
subsob['autoupdate'] = textEnglish
|
||||
|
||||
# Return potentially fixed value
|
||||
return textEnglish
|
||||
|
||||
def handleProxyComboBoxCurrentTextChanged(self, text: str, row: int):
|
||||
textEnglish = _(text, 'EN')
|
||||
|
||||
unique = list(AS_UserSubscription().keys())[row]
|
||||
subsob = AS_UserSubscription()[unique]
|
||||
|
||||
try:
|
||||
proxyFn = self.ProxyOptions[textEnglish]
|
||||
except KeyError:
|
||||
# Invalid key. Reset
|
||||
logger.error(f'\'{textEnglish}\' is not in proxy options. Reset')
|
||||
|
||||
textEnglish = ''
|
||||
|
||||
# Write to subs object
|
||||
subsob['proxy'] = textEnglish
|
||||
|
||||
proxyFn = self.ProxyOptions[textEnglish]
|
||||
|
||||
assert callable(proxyFn)
|
||||
|
||||
# Write to subs object
|
||||
subsob['proxy'] = textEnglish
|
||||
|
||||
# Return potentially fixed value
|
||||
return textEnglish
|
||||
|
||||
def flushItem(self, row, column, item):
|
||||
if column == self.Headers.index('Auto Update'):
|
||||
header = self.Headers[column]
|
||||
|
||||
oldItem = self.item(row, column)
|
||||
newItem = UserSubsAppQComboBox()
|
||||
newItem.addItems(list(_(key) for key in self.AutoUpdateOptions.keys()))
|
||||
|
||||
if oldItem is None:
|
||||
# Item does not exist
|
||||
newItem.setFont(QFont(APP().customFontName))
|
||||
else:
|
||||
# Use existing
|
||||
newItem.setFont(oldItem.font())
|
||||
|
||||
index = newItem.findText(
|
||||
_(self.handleAutoUpdateComboBoxCurrentTextChanged(header(item), row))
|
||||
)
|
||||
|
||||
if index >= 0:
|
||||
newItem.setCurrentIndex(index)
|
||||
|
||||
newItem.currentTextChanged.connect(
|
||||
functools.partial(
|
||||
self.handleAutoUpdateComboBoxCurrentTextChanged,
|
||||
row=row,
|
||||
)
|
||||
)
|
||||
|
||||
self.setCellWidget(row, column, newItem)
|
||||
elif column == self.Headers.index('Auto Update Use Proxy'):
|
||||
header = self.Headers[column]
|
||||
|
||||
oldItem = self.item(row, column)
|
||||
newItem = UserSubsAppQComboBox()
|
||||
newItem.addItems(list(_(key) for key in self.ProxyOptions.keys()))
|
||||
|
||||
if oldItem is None:
|
||||
# Item does not exist
|
||||
newItem.setFont(QFont(APP().customFontName))
|
||||
else:
|
||||
# Use existing
|
||||
newItem.setFont(oldItem.font())
|
||||
|
||||
index = newItem.findText(
|
||||
_(self.handleProxyComboBoxCurrentTextChanged(header(item), row))
|
||||
)
|
||||
|
||||
if index >= 0:
|
||||
newItem.setCurrentIndex(index)
|
||||
|
||||
newItem.currentTextChanged.connect(
|
||||
functools.partial(
|
||||
self.handleProxyComboBoxCurrentTextChanged,
|
||||
row=row,
|
||||
)
|
||||
)
|
||||
|
||||
self.setCellWidget(row, column, newItem)
|
||||
else:
|
||||
header = self.Headers[column]
|
||||
|
||||
oldItem = self.item(row, column)
|
||||
newItem = QTableWidgetItem(header(item))
|
||||
|
||||
if oldItem is None:
|
||||
# Item does not exist
|
||||
newItem.setFont(QFont(APP().customFontName))
|
||||
else:
|
||||
# Use existing
|
||||
newItem.setFont(oldItem.font())
|
||||
newItem.setForeground(oldItem.foreground())
|
||||
|
||||
if oldItem.textAlignment() != 0:
|
||||
newItem.setTextAlignment(oldItem.textAlignment())
|
||||
|
||||
# Editable
|
||||
newItem.setFlags(
|
||||
QtCore.Qt.ItemFlag.ItemIsEnabled
|
||||
| QtCore.Qt.ItemFlag.ItemIsSelectable
|
||||
| QtCore.Qt.ItemFlag.ItemIsEditable
|
||||
)
|
||||
|
||||
self.setItem(row, column, newItem)
|
||||
|
||||
def flushRow(self, row, item):
|
||||
for column in list(range(self.columnCount())):
|
||||
@@ -217,24 +460,62 @@ class UserSubsQTableWidget(QTranslatable, AppQTableWidget):
|
||||
self.flushRow(index, AS_UserSubscription()[key])
|
||||
|
||||
def appendNewItem(self, **kwargs):
|
||||
unique = kwargs.pop('unique', '')
|
||||
remark = kwargs.pop('remark', '')
|
||||
webURL = kwargs.pop('webURL', '')
|
||||
unique, remark, webURL, autoupdate, proxy = (
|
||||
kwargs.pop('unique', ''),
|
||||
kwargs.pop('remark', ''),
|
||||
kwargs.pop('webURL', ''),
|
||||
# Below values are not filled when adding item via Gui
|
||||
kwargs.pop('autoupdate', ''),
|
||||
kwargs.pop('proxy', ''),
|
||||
)
|
||||
|
||||
# The internal subs object
|
||||
subs = {
|
||||
subsob = {
|
||||
unique: {
|
||||
'remark': remark,
|
||||
'webURL': webURL,
|
||||
'autoupdate': autoupdate,
|
||||
'proxy': proxy,
|
||||
}
|
||||
}
|
||||
|
||||
AS_UserSubscription().update(subs)
|
||||
# 'unique' should be unique in subscription storage,
|
||||
# but protect it anyway.
|
||||
if unique not in AS_UserSubscription():
|
||||
# Add timer
|
||||
self.timers.append(QtCore.QTimer())
|
||||
|
||||
AS_UserSubscription().update(subsob)
|
||||
|
||||
row = self.rowCount()
|
||||
|
||||
self.insertRow(row)
|
||||
self.flushRow(row, subs[unique])
|
||||
self.flushRow(row, subsob[unique])
|
||||
|
||||
@staticmethod
|
||||
def updateSubsByUnique(
|
||||
unique: str, httpProxy: Union[str, Callable, None], **kwargs
|
||||
):
|
||||
showMessageBox = kwargs.pop('showMessageBox', False)
|
||||
|
||||
if callable(httpProxy):
|
||||
try:
|
||||
realHttpProxy = httpProxy()
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(
|
||||
f'error while configuring http proxy: {ex}. '
|
||||
f'Update subscription uses no proxy'
|
||||
)
|
||||
|
||||
realHttpProxy = None
|
||||
else:
|
||||
realHttpProxy = httpProxy
|
||||
|
||||
APP().mainWindow.updateSubsByUnique(
|
||||
unique, realHttpProxy, showMessageBox=showMessageBox, **kwargs
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
self.setHorizontalHeaderLabels(list(_(str(header)) for header in self.Headers))
|
||||
|
||||
@@ -195,9 +195,6 @@ class XrayAssetViewerQListWidget(SupportThemeChangedCallback, AppQListWidget):
|
||||
mbox.setInformativeText(basename)
|
||||
mbox.finished.connect(functools.partial(handleResultCode, filename))
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_assetExistsMBox', mbox)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
@@ -235,9 +232,6 @@ class XrayAssetViewerQListWidget(SupportThemeChangedCallback, AppQListWidget):
|
||||
mbox.setText(mbox.customText())
|
||||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_questionDeleteMBox', mbox)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from .GuiHysteria1 import *
|
||||
from .GuiHysteria2 import *
|
||||
from .GuiShadowsocks import *
|
||||
from .GuiTrojan import *
|
||||
from .GuiTUNSettings import *
|
||||
from .GuiVLESS import *
|
||||
from .GuiVMess import *
|
||||
from .IndentSpinBox import *
|
||||
|
||||
@@ -23,6 +23,7 @@ from Furious.QtFramework import gettext as _
|
||||
from Furious.Library import *
|
||||
from Furious.Utility import *
|
||||
from Furious.Widget.UserServersQTableWidget import *
|
||||
from Furious.Widget.GuiTUNSettings import *
|
||||
from Furious.Window.UserSubsWindow import *
|
||||
from Furious.Window.LogViewerWindow import *
|
||||
from Furious.Window.XrayAssetViewerWindow import *
|
||||
@@ -45,57 +46,25 @@ logger = logging.getLogger(__name__)
|
||||
registerAppSettings('ServerWidgetWindowSize')
|
||||
|
||||
|
||||
def connectedHttpProxyEndpoint() -> Union[str, None]:
|
||||
try:
|
||||
if APP().isSystemTrayConnected():
|
||||
index = AS_UserActivatedItemIndex()
|
||||
|
||||
if index >= 0:
|
||||
return AS_UserServers()[index].httpProxyEndpoint()
|
||||
else:
|
||||
# Should not reach here
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def connectedRemark() -> str:
|
||||
try:
|
||||
if APP().isSystemTrayConnected():
|
||||
index = AS_UserActivatedItemIndex()
|
||||
|
||||
if index >= 0:
|
||||
return f'{index + 1} - ' + AS_UserServers()[index].getExtras('remark')
|
||||
else:
|
||||
# Should not reach here
|
||||
return ''
|
||||
else:
|
||||
return ''
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
class AppNetworkStateManager(NetworkStateManager):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def successCallback(self):
|
||||
def successCallback(self, networkReply, **kwargs):
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, AppMainWindow):
|
||||
parent.setNetworkState(True)
|
||||
|
||||
def errorCallback(self, errorString: str):
|
||||
super().successCallback(networkReply, **kwargs)
|
||||
|
||||
def failureCallback(self, networkReply, **kwargs):
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, AppMainWindow):
|
||||
parent.setNetworkState(False, errorString=errorString)
|
||||
parent.setNetworkState(False, errorString=networkReply.errorString())
|
||||
|
||||
super().failureCallback(networkReply, **kwargs)
|
||||
|
||||
def startSingleTest(self):
|
||||
if not APP().isSystemTrayConnected():
|
||||
@@ -106,15 +75,15 @@ class AppNetworkStateManager(NetworkStateManager):
|
||||
|
||||
self.stopTest()
|
||||
|
||||
httpProxyEndpoint = connectedHttpProxyEndpoint()
|
||||
connectedHttpProxy = connectedHttpProxyEndpoint()
|
||||
|
||||
if httpProxyEndpoint is None:
|
||||
if connectedHttpProxy is None:
|
||||
parent = self.parent()
|
||||
|
||||
if isinstance(parent, AppMainWindow):
|
||||
parent.resetNetworkState()
|
||||
else:
|
||||
self.configureHttpProxy(httpProxyEndpoint)
|
||||
self.configureHttpProxy(connectedHttpProxy)
|
||||
|
||||
super().startSingleTest()
|
||||
|
||||
@@ -124,7 +93,7 @@ class AppNetworkStateManager(NetworkStateManager):
|
||||
if isinstance(parent, AppMainWindow):
|
||||
parent.resetNetworkState()
|
||||
|
||||
self.stopTest()
|
||||
super().disconnectedCallback()
|
||||
|
||||
|
||||
class AppMainWindow(AppQMainWindow):
|
||||
@@ -132,7 +101,8 @@ class AppMainWindow(AppQMainWindow):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_(APPLICATION_NAME))
|
||||
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||
# TODO: Need this?
|
||||
# self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||||
|
||||
self.updatesManager = UpdatesManager()
|
||||
self.networkStateManager = AppNetworkStateManager(parent=self)
|
||||
@@ -263,7 +233,21 @@ class AppMainWindow(AppQMainWindow):
|
||||
),
|
||||
]
|
||||
|
||||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||||
customizeTUNSettingsAction = [
|
||||
AppQAction(
|
||||
_('Customize TUN Settings...'),
|
||||
icon=bootstrapIcon('diagram-3.svg'),
|
||||
checkable=False,
|
||||
callback=lambda: self.getGuiTUNSettings().open(),
|
||||
),
|
||||
AppQSeperator(),
|
||||
]
|
||||
else:
|
||||
customizeTUNSettingsAction = []
|
||||
|
||||
toolsActions = [
|
||||
*customizeTUNSettingsAction,
|
||||
AppQAction(
|
||||
_('Manage Xray-core Asset File...'),
|
||||
callback=lambda: self.xrayAssetViewerWindow.show(),
|
||||
@@ -402,6 +386,9 @@ class AppMainWindow(AppQMainWindow):
|
||||
|
||||
self.setCentralWidget(self._widget)
|
||||
|
||||
def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
|
||||
self.userServersQTableWidget.updateSubsByUnique(unique, httpProxy, **kwargs)
|
||||
|
||||
def appendNewItemByFactory(self, factory: ConfigurationFactory):
|
||||
self.userServersQTableWidget.appendNewItemByFactory(factory)
|
||||
|
||||
@@ -414,15 +401,25 @@ class AppMainWindow(AppQMainWindow):
|
||||
def hideTabAndSpaces(self):
|
||||
self.userServersQTableWidget.hideTabAndSpaces()
|
||||
|
||||
def checkForUpdates(self):
|
||||
def getGuiTUNSettings(self, **kwargs):
|
||||
@functools.lru_cache(None)
|
||||
def cachedGuiTUNSettings():
|
||||
return GuiTUNSettings(self, **kwargs)
|
||||
|
||||
guiTUNSettings = cachedGuiTUNSettings()
|
||||
guiTUNSettings.factoryToInput(AS_UserTUNSettings())
|
||||
|
||||
return guiTUNSettings
|
||||
|
||||
def checkForUpdates(self, **kwargs):
|
||||
self.updatesManager.configureHttpProxy(connectedHttpProxyEndpoint())
|
||||
self.updatesManager.checkForUpdates()
|
||||
self.updatesManager.checkForUpdates(**kwargs)
|
||||
|
||||
def resetNetworkState(self):
|
||||
self.networkState.setText('')
|
||||
|
||||
def setNetworkState(self, success: bool, **kwargs):
|
||||
remark = connectedRemark()
|
||||
remark = connectedServerRemark()
|
||||
|
||||
if success:
|
||||
if remark:
|
||||
|
||||
@@ -99,7 +99,7 @@ class LogViewerWindow(AppQMainWindow):
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Exit'),
|
||||
callback=lambda: self.hide(),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self,
|
||||
|
||||
@@ -33,7 +33,7 @@ import pyqrcode
|
||||
__all__ = ['QRCodeWindow']
|
||||
|
||||
|
||||
class QRCodeWindow(SupportImplicitReference, AppQMainWindow):
|
||||
class QRCodeWindow(AppQMainWindow):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -83,4 +83,4 @@ class QRCodeWindow(SupportImplicitReference, AppQMainWindow):
|
||||
self.tabWidget.removeTab(index)
|
||||
|
||||
if self.tabWidget.count() == 0:
|
||||
self.hide()
|
||||
self.close()
|
||||
|
||||
@@ -124,7 +124,7 @@ class TextEditorWindow(AppQMainWindow):
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Close Window'),
|
||||
callback=lambda: self.questionSave(),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self.menuBar(),
|
||||
@@ -228,38 +228,6 @@ class TextEditorWindow(AppQMainWindow):
|
||||
self.modified = False
|
||||
self.setWindowTitle(self.customWindowTitle)
|
||||
|
||||
def questionSave(self):
|
||||
if self.modified:
|
||||
|
||||
def handleButtonClicked(saveMBox, button):
|
||||
assert isinstance(saveMBox, QuestionSaveMBox)
|
||||
|
||||
if saveMBox.button0 == button:
|
||||
# Save
|
||||
if self.save():
|
||||
saveMBox.close()
|
||||
self.hide()
|
||||
elif saveMBox.button1 == button:
|
||||
# Discard
|
||||
self.markAsSaved()
|
||||
saveMBox.close()
|
||||
self.hide()
|
||||
elif saveMBox.button2 == button:
|
||||
# Cancel. Do nothing
|
||||
pass
|
||||
|
||||
mbox = QuestionSaveMBox(icon=AppQMessageBox.Icon.Question, parent=self)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
mbox.buttonClicked.connect(functools.partial(handleButtonClicked, mbox))
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_questionSaveMBox', mbox)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
mbox.open()
|
||||
else:
|
||||
self.hide()
|
||||
|
||||
def setPlainText(self, text: str, blockSignals: bool):
|
||||
if blockSignals:
|
||||
with QBlockSignals(self.jsonEditor):
|
||||
@@ -297,7 +265,7 @@ class TextEditorWindow(AppQMainWindow):
|
||||
old = AS_UserServers()[index]
|
||||
new = constructFromDict(jsonObject, **old.kwargs)
|
||||
|
||||
FastItemDeletionSearch.moveToTrash(old)
|
||||
QGarbageCollector.track(old)
|
||||
|
||||
AS_UserServers()[index] = new
|
||||
|
||||
@@ -369,9 +337,6 @@ class TextEditorWindow(AppQMainWindow):
|
||||
functools.partial(handleResultCode, indentSpinBox)
|
||||
)
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_IndentSpinBox', indentSpinBox)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
indentSpinBox.open()
|
||||
|
||||
@@ -393,9 +358,36 @@ class TextEditorWindow(AppQMainWindow):
|
||||
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
|
||||
def closeEvent(self, event: QtCore.QEvent):
|
||||
event.ignore()
|
||||
if self.modified:
|
||||
mbox = QuestionSaveMBox(icon=AppQMessageBox.Icon.Question, parent=self)
|
||||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||||
|
||||
self.questionSave()
|
||||
def handleButtonClicked(button):
|
||||
if button == mbox.button0:
|
||||
# Save
|
||||
if self.save():
|
||||
mbox.close()
|
||||
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
elif button == mbox.button1:
|
||||
# Discard
|
||||
self.markAsSaved()
|
||||
|
||||
mbox.close()
|
||||
|
||||
event.accept()
|
||||
elif button == mbox.button2:
|
||||
# Cancel. Do nothing
|
||||
event.ignore()
|
||||
|
||||
mbox.buttonClicked.connect(functools.partial(handleButtonClicked))
|
||||
|
||||
# Show the MessageBox and wait for the user to close it
|
||||
mbox.exec()
|
||||
else:
|
||||
event.accept()
|
||||
|
||||
def retranslate(self):
|
||||
# Do nothing
|
||||
|
||||
@@ -116,7 +116,7 @@ class UserSubsWindow(AppQMainWindow):
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.setGeometry(100, 100, 360 * GOLDEN_RATIO, 360)
|
||||
self.setGeometry(100, 100, 500 * GOLDEN_RATIO, 500)
|
||||
|
||||
def addSubs(self):
|
||||
def handleResultCode(_addSubsDialog, code):
|
||||
@@ -141,9 +141,6 @@ class UserSubsWindow(AppQMainWindow):
|
||||
functools.partial(handleResultCode, addSubsDialog)
|
||||
)
|
||||
|
||||
# dummy ref
|
||||
setattr(self, '_addSubsDialog', addSubsDialog)
|
||||
|
||||
# Show the MessageBox asynchronously
|
||||
addSubsDialog.open()
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ class XrayAssetViewerWindow(AppQMainWindow):
|
||||
AppQSeperator(),
|
||||
AppQAction(
|
||||
_('Exit'),
|
||||
callback=lambda: self.hide(),
|
||||
callback=lambda: self.close(),
|
||||
),
|
||||
title=_('File'),
|
||||
parent=self.menuBar(),
|
||||
|
||||
+33
-23
@@ -36,12 +36,12 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def runAppMain():
|
||||
appMainProcess = AppMainProcess(functools.partial(Application, sys.argv))
|
||||
process = AppMainProcess(functools.partial(Application, sys.argv))
|
||||
|
||||
appMainProcess.start()
|
||||
appMainProcess.join()
|
||||
process.start()
|
||||
process.join()
|
||||
|
||||
exitcode = appMainProcess.exitcode
|
||||
exitcode = process.exitcode
|
||||
|
||||
if exitcode == 0:
|
||||
sys.exit(exitcode)
|
||||
@@ -76,7 +76,7 @@ def runAppMain():
|
||||
)
|
||||
)
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
# Show the MessageBox and wait for the user to close it
|
||||
messageBox.exec()
|
||||
else:
|
||||
if exitcode == ApplicationFactory.ExitCode.AssertionError:
|
||||
@@ -96,34 +96,44 @@ def runAppMain():
|
||||
messageBox.setWindowTitle(_(APPLICATION_NAME))
|
||||
messageBox.setText(text)
|
||||
|
||||
if appMainProcess.fileWritten.value:
|
||||
if process.fileWritten.value:
|
||||
# Crash log saved
|
||||
crashLogFile = str(CRASH_LOG_DIR / appMainProcess.logFileName)
|
||||
crashLogFile = str(CRASH_LOG_DIR / process.logFileName)
|
||||
|
||||
logger.info(f'crash log has been saved to {crashLogFile}')
|
||||
|
||||
messageBox.setInformativeText(
|
||||
_('Crash log has been saved to') + f' {crashLogFile}'
|
||||
)
|
||||
messageBox.addButton(
|
||||
button0 = messageBox.addButton(
|
||||
_('Open crash log'), AppQMessageBox.ButtonRole.AcceptRole
|
||||
)
|
||||
messageBox.addButton(_('OK'), AppQMessageBox.ButtonRole.RejectRole)
|
||||
button1 = messageBox.addButton(
|
||||
_('OK'), AppQMessageBox.ButtonRole.RejectRole
|
||||
)
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
if messageBox.exec() == PySide6LegacyEnumValueWrapper(
|
||||
AppQMessageBox.ButtonRole.AcceptRole
|
||||
):
|
||||
# Open
|
||||
if QDesktopServices.openUrl(QtCore.QUrl.fromLocalFile(crashLogFile)):
|
||||
logger.info(f'open crash log {crashLogFile} success')
|
||||
def handleButtonClicked(button):
|
||||
if button == button0:
|
||||
# Open
|
||||
if QDesktopServices.openUrl(
|
||||
QtCore.QUrl.fromLocalFile(crashLogFile)
|
||||
):
|
||||
logger.info(f'open crash log {crashLogFile} success')
|
||||
else:
|
||||
logger.error(f'open crash log {crashLogFile} failed')
|
||||
else:
|
||||
logger.error(f'open crash log {crashLogFile} failed')
|
||||
else:
|
||||
# OK. Do nothing
|
||||
pass
|
||||
else:
|
||||
# Crash log not saved
|
||||
# OK. Do nothing
|
||||
pass
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
messageBox.buttonClicked.connect(handleButtonClicked)
|
||||
|
||||
# Show the MessageBox and wait for the user to close it
|
||||
messageBox.exec()
|
||||
else:
|
||||
# Crash log wasn't saved
|
||||
logger.info(f'crash log was not saved')
|
||||
|
||||
# Show the MessageBox and wait for the user to close it
|
||||
messageBox.exec()
|
||||
|
||||
sys.exit(exitcode)
|
||||
|
||||
Reference in New Issue
Block a user