mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-03 04:17:58 +03:00
Add Edit Routing feature
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
+102
-99
@@ -1,11 +1,11 @@
|
||||
from Furious.Core.Core import XrayCore, Hysteria
|
||||
from Furious.Core.Intellisense import Intellisense
|
||||
from Furious.Core.Configuration import Configuration
|
||||
from Furious.Action.Routing import SUPPORTED_ROUTING_DETAIL
|
||||
from Furious.Action.Routing import BUILTIN_ROUTING_TABLE, BUILTIN_ROUTING
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.ConnectingProgressBar import ConnectingProgressBar
|
||||
from Furious.Widget.Widget import MessageBox
|
||||
from Furious.Utility.Constants import APPLICATION_NAME, PROXY_SERVER_BYPASS
|
||||
from Furious.Utility.Constants import APP, APPLICATION_NAME, PROXY_SERVER_BYPASS
|
||||
from Furious.Utility.Utility import (
|
||||
Switch,
|
||||
SupportConnectedCallback,
|
||||
@@ -16,7 +16,6 @@ from Furious.Utility.Translator import gettext as _
|
||||
from Furious.Utility.Proxy import Proxy
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtNetwork import (
|
||||
QNetworkAccessManager,
|
||||
QNetworkReply,
|
||||
@@ -180,7 +179,7 @@ class ConnectAction(Action):
|
||||
self.Hysteria.stop()
|
||||
|
||||
def showConnectingProgressBar(self):
|
||||
if QApplication.instance().ShowProgressBarWhenConnecting == Switch.ON_:
|
||||
if APP().ShowProgressBarWhenConnecting == Switch.ON_:
|
||||
self.connectingProgressBar.progressBar.setValue(0)
|
||||
# Update the progress bar every 50ms
|
||||
self.connectingProgressBar.timer.start(50)
|
||||
@@ -209,7 +208,7 @@ class ConnectAction(Action):
|
||||
def setDisabledAction(self, value):
|
||||
self.setDisabled(value)
|
||||
|
||||
QApplication.instance().tray.RoutingAction.setDisabled(value)
|
||||
APP().tray.RoutingAction.setDisabled(value)
|
||||
|
||||
def setConnectingStatus(self, showProgressBar=True):
|
||||
if showProgressBar:
|
||||
@@ -220,13 +219,13 @@ class ConnectAction(Action):
|
||||
self.setText(_('Connecting'))
|
||||
self.setIcon(bootstrapIcon('lock-fill.svg'))
|
||||
|
||||
QApplication.instance().tray.setPlainIcon()
|
||||
APP().tray.setPlainIcon()
|
||||
|
||||
def setConnectedStatus(self):
|
||||
self.hideConnectingProgressBar(done=True)
|
||||
self.setDisabledAction(False)
|
||||
|
||||
QApplication.instance().tray.setConnectedIcon()
|
||||
APP().tray.setConnectedIcon()
|
||||
|
||||
# Finished. Reset connecting flag
|
||||
self.connectingFlag = False
|
||||
@@ -255,8 +254,8 @@ class ConnectAction(Action):
|
||||
self.setIcon(bootstrapIcon('unlock-fill.svg'))
|
||||
self.setChecked(False)
|
||||
|
||||
QApplication.instance().Connect = Switch.OFF
|
||||
QApplication.instance().tray.setPlainIcon()
|
||||
APP().Connect = Switch.OFF
|
||||
APP().tray.setPlainIcon()
|
||||
|
||||
self.proxyServer = ''
|
||||
|
||||
@@ -276,12 +275,12 @@ class ConnectAction(Action):
|
||||
@property
|
||||
def MainWidget(self):
|
||||
# Handy reference
|
||||
return QApplication.instance().MainWidget
|
||||
return APP().MainWidget
|
||||
|
||||
@property
|
||||
def activatedServer(self):
|
||||
try:
|
||||
activatedIndex = int(QApplication.instance().ActivatedItemIndex)
|
||||
activatedIndex = int(APP().ActivatedItemIndex)
|
||||
|
||||
if activatedIndex < 0:
|
||||
return None
|
||||
@@ -395,10 +394,9 @@ class ConnectAction(Action):
|
||||
if validateProxyServer(proxyServer):
|
||||
self.coreRunning = True
|
||||
|
||||
routing = QApplication.instance().Routing
|
||||
routing = APP().Routing
|
||||
|
||||
logger.info(f'Core {XrayCore.name()} configured')
|
||||
logger.info(f'Routing is {routing}')
|
||||
logger.info(f'core {XrayCore.name()} configured')
|
||||
|
||||
def fixLoggingRelativePath(attr):
|
||||
# Relative path fails if booting on start up
|
||||
@@ -426,53 +424,35 @@ class ConnectAction(Action):
|
||||
fixLoggingRelativePath('access')
|
||||
fixLoggingRelativePath('error')
|
||||
|
||||
if routing == 'Bypass':
|
||||
self.coreJSON['routing'] = {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': (
|
||||
# ads
|
||||
{
|
||||
'type': 'field',
|
||||
'outboundTag': 'block',
|
||||
'domain': ['geosite:category-ads-all'],
|
||||
},
|
||||
# geosite
|
||||
{
|
||||
'type': 'field',
|
||||
'outboundTag': 'direct',
|
||||
'domain': ['geosite:cn'],
|
||||
},
|
||||
# geoip
|
||||
{
|
||||
'type': 'field',
|
||||
'outboundTag': 'direct',
|
||||
'ip': ['geoip:private', 'geoip:cn'],
|
||||
},
|
||||
# Proxy everything
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
),
|
||||
}
|
||||
if routing == 'Global':
|
||||
self.coreJSON['routing'] = {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': (
|
||||
# Proxy everything
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
),
|
||||
}
|
||||
if routing == 'Custom':
|
||||
# Filter Custom
|
||||
if routing in BUILTIN_ROUTING[:-1]:
|
||||
routingObject = BUILTIN_ROUTING_TABLE[routing][XrayCore.name()]
|
||||
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {routingObject}')
|
||||
|
||||
self.coreJSON['routing'] = routingObject
|
||||
elif routing == 'Custom':
|
||||
logger.info(f'routing is {routing}')
|
||||
logger.info(f'RoutingObject: {self.XrayRouting}')
|
||||
|
||||
# Assign user routing
|
||||
self.coreJSON['routing'] = self.XrayRouting
|
||||
else:
|
||||
try:
|
||||
routingWidget = APP().editRoutingWidget
|
||||
|
||||
route = routingWidget.RoutesList[int(routing)]
|
||||
|
||||
logger.info(f'routing is {route["remark"]}')
|
||||
logger.info(f'RoutingObject: {route[XrayCore.name()]}')
|
||||
|
||||
self.coreJSON['routing'] = route[XrayCore.name()]
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
# Fast fail
|
||||
self.coreJSON = {}
|
||||
|
||||
# Refresh configuration modified before. User cannot feel
|
||||
self.coreText = ujson.dumps(
|
||||
@@ -498,33 +478,52 @@ class ConnectAction(Action):
|
||||
if validateProxyServer(proxyServer):
|
||||
self.coreRunning = True
|
||||
|
||||
routing = QApplication.instance().Routing
|
||||
routing = APP().Routing
|
||||
|
||||
logger.info(f'Core {Hysteria.name()} configured')
|
||||
logger.info(f'Routing is {routing}')
|
||||
logger.info(f'core {Hysteria.name()} configured')
|
||||
|
||||
# Filter Global, Custom
|
||||
if routing in BUILTIN_ROUTING[:-2]:
|
||||
logger.info(f'routing is {routing}')
|
||||
|
||||
routingObject = BUILTIN_ROUTING_TABLE[routing][Hysteria.name()]
|
||||
|
||||
if routing == 'Bypass':
|
||||
self.Hysteria.start(
|
||||
self.coreText, Hysteria.rule(), Hysteria.mmdb()
|
||||
self.coreText,
|
||||
Hysteria.rule(routingObject.get('acl')),
|
||||
Hysteria.mmdb(routingObject.get('mmdb')),
|
||||
)
|
||||
if routing == 'Global':
|
||||
elif routing == 'Global':
|
||||
logger.info(f'routing is {routing}')
|
||||
|
||||
self.Hysteria.start(self.coreText, '', '')
|
||||
if routing == 'Custom':
|
||||
if self.coreJSON.get('acl') is not None:
|
||||
# acl specified
|
||||
if self.coreJSON.get('mmdb') is not None:
|
||||
# mmdb specified. Start "as is"
|
||||
self.Hysteria.start(
|
||||
self.coreText,
|
||||
Hysteria.rule(self.coreJSON['acl']),
|
||||
Hysteria.mmdb(self.coreJSON['mmdb']),
|
||||
)
|
||||
else:
|
||||
# mmdb not specified. Route as global
|
||||
self.Hysteria.start(self.coreText, '', '')
|
||||
else:
|
||||
# acl not specified. Route as global
|
||||
self.Hysteria.start(self.coreText, '', '')
|
||||
elif routing == 'Custom':
|
||||
logger.info(f'routing is {routing}')
|
||||
|
||||
self.Hysteria.start(
|
||||
self.coreText,
|
||||
Hysteria.rule(self.coreJSON.get('acl')),
|
||||
Hysteria.mmdb(self.coreJSON.get('mmdb')),
|
||||
)
|
||||
else:
|
||||
try:
|
||||
routingWidget = APP().editRoutingWidget
|
||||
|
||||
route = routingWidget.RoutesList[int(routing)]
|
||||
|
||||
logger.info(f'routing is {route["remark"]}')
|
||||
logger.info(f'RoutingObject: {route[Hysteria.name()]}')
|
||||
|
||||
self.Hysteria.start(
|
||||
self.coreText,
|
||||
Hysteria.rule(route[Hysteria.name()].get('acl')),
|
||||
Hysteria.mmdb(route[Hysteria.name()].get('mmdb')),
|
||||
)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
# Fast fail
|
||||
self.Hysteria.start('', '', '')
|
||||
|
||||
return Hysteria.name()
|
||||
|
||||
@@ -572,24 +571,28 @@ class ConnectAction(Action):
|
||||
else:
|
||||
logger.info(f'{self.coreName}: connection test success. Connected')
|
||||
|
||||
QApplication.instance().Connect = Switch.ON_
|
||||
APP().Connect = Switch.ON_
|
||||
|
||||
# Connected status
|
||||
self.setConnectedStatus()
|
||||
|
||||
if showRoutingChangedMessage:
|
||||
# Routing changed
|
||||
QApplication.instance().tray.showMessage(
|
||||
_('Routing changed: ')
|
||||
+ _(
|
||||
f'{SUPPORTED_ROUTING_DETAIL[QApplication.instance().Routing]}'
|
||||
try:
|
||||
routingWidget = APP().editRoutingWidget
|
||||
|
||||
route = routingWidget.RoutesList[int(APP().Routing)]
|
||||
|
||||
APP().tray.showMessage(
|
||||
_('Routing changed: ') + f'{route["remark"]}'
|
||||
)
|
||||
except ValueError:
|
||||
APP().tray.showMessage(
|
||||
_('Routing changed: ') + _(f'{APP().Routing}')
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Connected
|
||||
QApplication.instance().tray.showMessage(
|
||||
f'{self.coreName}: {_("Connected")}'
|
||||
)
|
||||
APP().tray.showMessage(f'{self.coreName}: {_("Connected")}')
|
||||
|
||||
self.networkReply.finished.connect(finishedCallback)
|
||||
|
||||
@@ -600,11 +603,8 @@ class ConnectAction(Action):
|
||||
# Connecting
|
||||
self.connectingFlag = True
|
||||
|
||||
if (
|
||||
not QApplication.instance().Configuration
|
||||
or len(self.MainWidget.ServerList) == 0
|
||||
):
|
||||
QApplication.instance().Connect = Switch.OFF
|
||||
if not APP().Configuration or len(self.MainWidget.ServerList) == 0:
|
||||
APP().Connect = Switch.OFF
|
||||
|
||||
self.setChecked(False)
|
||||
self.connectingFlag = False
|
||||
@@ -615,7 +615,7 @@ class ConnectAction(Action):
|
||||
myText = self.activatedServer
|
||||
|
||||
if myText is None:
|
||||
QApplication.instance().Connect = Switch.OFF
|
||||
APP().Connect = Switch.OFF
|
||||
|
||||
self.setChecked(False)
|
||||
self.connectingFlag = False
|
||||
@@ -624,7 +624,7 @@ class ConnectAction(Action):
|
||||
return
|
||||
|
||||
if myText == '':
|
||||
QApplication.instance().Connect = Switch.OFF
|
||||
APP().Connect = Switch.OFF
|
||||
|
||||
self.setChecked(False)
|
||||
self.connectingFlag = False
|
||||
@@ -637,7 +637,7 @@ class ConnectAction(Action):
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
QApplication.instance().Connect = Switch.OFF
|
||||
APP().Connect = Switch.OFF
|
||||
|
||||
self.setChecked(False)
|
||||
self.connectingFlag = False
|
||||
@@ -657,6 +657,9 @@ class ConnectAction(Action):
|
||||
self.connectingAction()
|
||||
|
||||
def connectingAction(self, showProgressBar=True, showRoutingChangedMessage=False):
|
||||
# Connecting. Redefined
|
||||
self.connectingFlag = True
|
||||
|
||||
# Connecting status
|
||||
self.setConnectingStatus(showProgressBar)
|
||||
|
||||
@@ -703,7 +706,7 @@ class ConnectAction(Action):
|
||||
|
||||
SupportConnectedCallback.callDisconnectedCallback()
|
||||
|
||||
QApplication.instance().tray.showMessage(reason)
|
||||
APP().tray.showMessage(reason)
|
||||
|
||||
def reconnectAction(self, reason=''):
|
||||
self.disconnectAction(reason)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import bootstrapIcon
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
class EditConfigurationAction(Action):
|
||||
def __init__(self):
|
||||
@@ -13,4 +12,4 @@ class EditConfigurationAction(Action):
|
||||
)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
QApplication.instance().MainWidget.show()
|
||||
APP().MainWidget.show()
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import bootstrapIcon
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
class ExitAction(Action):
|
||||
def __init__(self):
|
||||
@@ -13,4 +12,4 @@ class ExitAction(Action):
|
||||
)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
QApplication.instance().exit()
|
||||
APP().exit()
|
||||
|
||||
@@ -2,6 +2,7 @@ from Furious.Core.Configuration import Configuration
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.Widget import Menu, MessageBox
|
||||
from Furious.Widget.ExportQRCode import ExportQRCode
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import StateContext, bootstrapIcon
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
|
||||
@@ -110,7 +111,7 @@ class ExportJSONResultBox(MessageBox):
|
||||
|
||||
|
||||
def exportLink(selectedIndex):
|
||||
serverList = QApplication.instance().MainWidget.ServerList
|
||||
serverList = APP().MainWidget.ServerList
|
||||
serverLink = []
|
||||
successStr = []
|
||||
failureStr = []
|
||||
@@ -140,7 +141,7 @@ class ExportLinkAction(Action):
|
||||
self.exportLinkResult = ExportLinkResultBox('', '', isQRCodeExport=False)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
selectedIndex = QApplication.instance().MainWidget.selectedIndex
|
||||
selectedIndex = APP().MainWidget.selectedIndex
|
||||
|
||||
if len(selectedIndex) == 0:
|
||||
# Nothing selected. Do nothing
|
||||
@@ -166,7 +167,7 @@ class ExportQRCodeAction(Action):
|
||||
self.exportLinkResult = ExportLinkResultBox('', '', isQRCodeExport=True)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
selectedIndex = QApplication.instance().MainWidget.selectedIndex
|
||||
selectedIndex = APP().MainWidget.selectedIndex
|
||||
|
||||
if len(selectedIndex) == 0:
|
||||
# Nothing selected. Do nothing
|
||||
@@ -194,7 +195,7 @@ class ExportJSONAction(Action):
|
||||
self.exportJSONResult = ExportJSONResultBox()
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
selectedIndex = QApplication.instance().MainWidget.selectedIndex
|
||||
selectedIndex = APP().MainWidget.selectedIndex
|
||||
|
||||
if len(selectedIndex) == 0:
|
||||
# Nothing selected. Do nothing
|
||||
@@ -203,7 +204,7 @@ class ExportJSONAction(Action):
|
||||
try:
|
||||
QApplication.clipboard().setText(
|
||||
'\n'.join(
|
||||
QApplication.instance().MainWidget.ServerList[index]['config']
|
||||
APP().MainWidget.ServerList[index]['config']
|
||||
for index in selectedIndex
|
||||
)
|
||||
)
|
||||
|
||||
+12
-11
@@ -6,11 +6,12 @@ from Furious.Core.Configuration import (
|
||||
)
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.Widget import Menu, MessageBox
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import (
|
||||
Base64Encoder,
|
||||
Protocol,
|
||||
StateContext,
|
||||
Storage,
|
||||
ServerStorage,
|
||||
bootstrapIcon,
|
||||
protocolRepr,
|
||||
)
|
||||
@@ -145,7 +146,7 @@ class ImportLinkAction(Action):
|
||||
|
||||
if choice == MessageBox.ButtonRole.AcceptRole.value:
|
||||
# Go to edit
|
||||
QApplication.instance().MainWidget.show()
|
||||
APP().MainWidget.show()
|
||||
else:
|
||||
# OK. Do nothing
|
||||
pass
|
||||
@@ -186,7 +187,7 @@ class ImportLinkAction(Action):
|
||||
|
||||
remark = urllib.parse.unquote(getOrDefault('ps'))
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
APP().MainWidget.importServer(
|
||||
remark,
|
||||
ujson.dumps(
|
||||
myJSON,
|
||||
@@ -246,7 +247,7 @@ class ImportLinkAction(Action):
|
||||
)
|
||||
)
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
APP().MainWidget.importServer(
|
||||
remark,
|
||||
ujson.dumps(
|
||||
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
|
||||
@@ -297,7 +298,7 @@ class ImportLinkAction(Action):
|
||||
)
|
||||
)
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
APP().MainWidget.importServer(
|
||||
remark,
|
||||
ujson.dumps(
|
||||
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
|
||||
@@ -323,7 +324,7 @@ class ImportLinkAction(Action):
|
||||
ProxyOutboundObjectSS(*re.split(r'[@:]', myData))
|
||||
)
|
||||
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
APP().MainWidget.importServer(
|
||||
'sslegacy',
|
||||
ujson.dumps(
|
||||
myJSON, indent=2, ensure_ascii=False, escape_forward_slashes=False
|
||||
@@ -380,7 +381,7 @@ class ImportLinkAction(Action):
|
||||
|
||||
if result:
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
self.showLinkImportOK(remark)
|
||||
else:
|
||||
@@ -388,7 +389,7 @@ class ImportLinkAction(Action):
|
||||
else:
|
||||
successRemark = []
|
||||
|
||||
rowCount = QApplication.instance().MainWidget.rowCount
|
||||
rowCount = APP().MainWidget.rowCount
|
||||
|
||||
for shareLink in splitByNewLine:
|
||||
remark, result = self.parseShareLink(shareLink)
|
||||
@@ -402,7 +403,7 @@ class ImportLinkAction(Action):
|
||||
return
|
||||
|
||||
# At least one server's been imported. Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
if len(successRemark) == 1:
|
||||
# Fall back to single
|
||||
@@ -445,7 +446,7 @@ class ImportJSONAction(Action):
|
||||
|
||||
if choice == MessageBox.ButtonRole.AcceptRole.value:
|
||||
# Go to edit
|
||||
QApplication.instance().MainWidget.show()
|
||||
APP().MainWidget.show()
|
||||
else:
|
||||
# OK. Do nothing
|
||||
pass
|
||||
@@ -460,7 +461,7 @@ class ImportJSONAction(Action):
|
||||
|
||||
self.showJSONErrorBox()
|
||||
else:
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
APP().MainWidget.importServer(
|
||||
_('Untitled'), self.clipboard, syncStorage=True
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.Widget import Menu
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import bootstrapIcon
|
||||
from Furious.Utility.Translator import (
|
||||
Translatable,
|
||||
@@ -8,8 +9,6 @@ from Furious.Utility.Translator import (
|
||||
LANGUAGE_TO_ABBR,
|
||||
)
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -24,10 +23,10 @@ class LanguageChildAction(Action):
|
||||
def triggeredCallback(self, checked):
|
||||
checkedLanguage = LANGUAGE_TO_ABBR[self.text()]
|
||||
|
||||
if QApplication.instance().Language != checkedLanguage:
|
||||
if APP().Language != checkedLanguage:
|
||||
logger.info(f'set language to \'{self.text()}\'')
|
||||
|
||||
QApplication.instance().Language = checkedLanguage
|
||||
APP().Language = checkedLanguage
|
||||
|
||||
Translatable.retranslateAll()
|
||||
|
||||
@@ -43,8 +42,7 @@ class LanguageAction(Action):
|
||||
# Language representation
|
||||
text,
|
||||
checkable=True,
|
||||
checked=text
|
||||
== ABBR_TO_LANGUAGE[QApplication.instance().Language],
|
||||
checked=text == ABBR_TO_LANGUAGE[APP().Language],
|
||||
)
|
||||
for text in list(LANGUAGE_TO_ABBR.keys())
|
||||
),
|
||||
|
||||
+202
-20
@@ -1,50 +1,232 @@
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Core.Core import XrayCore, Hysteria
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Widget.Widget import Menu
|
||||
from Furious.Utility.Constants import APP, DATA_DIR
|
||||
from Furious.Utility.Utility import Switch, bootstrapIcon
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
SUPPORTED_ROUTING = ('Bypass', 'Global', 'Custom')
|
||||
|
||||
SUPPORTED_ROUTING_DETAIL = {
|
||||
'Bypass': 'Bypass Mainland China',
|
||||
'Global': 'Global',
|
||||
'Custom': 'Custom',
|
||||
BUILTIN_ROUTING_TABLE = {
|
||||
'Bypass Mainland China': {
|
||||
XrayCore.name(): {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': [
|
||||
# ads
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': [
|
||||
'geosite:category-ads-all',
|
||||
],
|
||||
'outboundTag': 'block',
|
||||
},
|
||||
# geosite
|
||||
{
|
||||
'type': 'field',
|
||||
'domain': [
|
||||
'geosite:cn',
|
||||
],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
# geoip
|
||||
{
|
||||
'type': 'field',
|
||||
'ip': [
|
||||
'geoip:private',
|
||||
'geoip:cn',
|
||||
],
|
||||
'outboundTag': 'direct',
|
||||
},
|
||||
# Proxy everything
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
],
|
||||
},
|
||||
Hysteria.name(): {
|
||||
'acl': str(DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl'),
|
||||
'mmdb': str(DATA_DIR / 'hysteria' / 'country.mmdb'),
|
||||
},
|
||||
},
|
||||
'Bypass Iran': {
|
||||
XrayCore.name(): {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"domainMatcher": "hybrid",
|
||||
"rules": [
|
||||
# ads
|
||||
{
|
||||
"type": "field",
|
||||
"domain": [
|
||||
"geosite:category-ads-all",
|
||||
"iran:ads",
|
||||
],
|
||||
"outboundTag": "block",
|
||||
},
|
||||
# Iran sites
|
||||
{
|
||||
"type": "field",
|
||||
"domain": [
|
||||
"iran:ir",
|
||||
"iran:other",
|
||||
],
|
||||
"outboundTag": "direct",
|
||||
},
|
||||
# Iran IP
|
||||
{
|
||||
"type": "field",
|
||||
"ip": [
|
||||
"geoip:private",
|
||||
"geoip:ir",
|
||||
],
|
||||
"outboundTag": "direct",
|
||||
},
|
||||
# Proxy everything
|
||||
{
|
||||
"type": "field",
|
||||
"port": "0-65535",
|
||||
"outboundTag": "proxy",
|
||||
},
|
||||
],
|
||||
},
|
||||
Hysteria.name(): {
|
||||
'acl': str(DATA_DIR / 'hysteria' / 'bypass-Iran.acl'),
|
||||
'mmdb': str(DATA_DIR / 'hysteria' / 'country.mmdb'),
|
||||
},
|
||||
},
|
||||
'Global': {
|
||||
XrayCore.name(): {
|
||||
'domainStrategy': 'IPIfNonMatch',
|
||||
'domainMatcher': 'hybrid',
|
||||
'rules': [
|
||||
# Proxy everything
|
||||
{
|
||||
'type': 'field',
|
||||
'port': '0-65535',
|
||||
'outboundTag': 'proxy',
|
||||
},
|
||||
],
|
||||
},
|
||||
Hysteria.name(): {},
|
||||
},
|
||||
'Custom': {
|
||||
XrayCore.name(): {},
|
||||
Hysteria.name(): {},
|
||||
},
|
||||
}
|
||||
|
||||
BUILTIN_ROUTING = list(BUILTIN_ROUTING_TABLE.keys())
|
||||
|
||||
|
||||
def routingToIndex():
|
||||
currentRouting = APP().Routing
|
||||
|
||||
try:
|
||||
index = int(currentRouting)
|
||||
except ValueError:
|
||||
for i, routing in enumerate(BUILTIN_ROUTING):
|
||||
if routing == currentRouting:
|
||||
return i
|
||||
|
||||
return -1
|
||||
else:
|
||||
return index + len(BUILTIN_ROUTING)
|
||||
|
||||
|
||||
class BuiltinRoutingChildAction(Action):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
textEnglish = self.textEnglish
|
||||
|
||||
if APP().Routing != textEnglish:
|
||||
# De-activate
|
||||
APP().editRoutingWidget.activateItemByIndex(
|
||||
routingToIndex(), activate=False
|
||||
)
|
||||
APP().Routing = textEnglish
|
||||
# Activate
|
||||
APP().editRoutingWidget.activateItemByIndex(routingToIndex(), activate=True)
|
||||
|
||||
if APP().tray.ConnectAction.isConnected():
|
||||
# Connected. Re-configure connection
|
||||
APP().tray.ConnectAction.connectingAction(
|
||||
showProgressBar=True,
|
||||
showRoutingChangedMessage=True,
|
||||
)
|
||||
|
||||
|
||||
class RoutingChildAction(Action):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
for key, value in SUPPORTED_ROUTING_DETAIL.items():
|
||||
if self.textCompare(value):
|
||||
if QApplication.instance().Routing != key:
|
||||
QApplication.instance().Routing = key
|
||||
self.translatable = False
|
||||
|
||||
if QApplication.instance().tray.ConnectAction.isConnected():
|
||||
def triggeredCallback(self, checked):
|
||||
routingAction = APP().tray.RoutingAction
|
||||
|
||||
for index, action in enumerate(
|
||||
# Skip built-in actions.
|
||||
routingAction.menu().actions()[len(BUILTIN_ROUTING) :]
|
||||
):
|
||||
if id(self) == id(action):
|
||||
# Found action
|
||||
|
||||
if APP().Routing != str(index):
|
||||
# De-activate
|
||||
APP().editRoutingWidget.activateItemByIndex(
|
||||
routingToIndex(), activate=False
|
||||
)
|
||||
APP().Routing = str(index)
|
||||
# Activate
|
||||
APP().editRoutingWidget.activateItemByIndex(
|
||||
routingToIndex(), activate=True
|
||||
)
|
||||
|
||||
if APP().tray.ConnectAction.isConnected():
|
||||
# Connected. Re-configure connection
|
||||
QApplication.instance().tray.ConnectAction.connectingAction(
|
||||
showProgressBar=False,
|
||||
APP().tray.ConnectAction.connectingAction(
|
||||
showProgressBar=True,
|
||||
showRoutingChangedMessage=True,
|
||||
)
|
||||
|
||||
# Select routing action done
|
||||
return
|
||||
|
||||
raise Exception('Fatal error occurred')
|
||||
|
||||
@property
|
||||
def textEnglish(self):
|
||||
return self.text()
|
||||
|
||||
|
||||
class RoutingAction(Action):
|
||||
def __init__(self):
|
||||
if APP().Routing == 'Bypass':
|
||||
# Update value for backward compatibility
|
||||
APP().Routing = 'Bypass Mainland China'
|
||||
|
||||
super().__init__(
|
||||
_('Routing'),
|
||||
icon=bootstrapIcon('shuffle.svg'),
|
||||
menu=Menu(
|
||||
*list(
|
||||
RoutingChildAction(
|
||||
_(SUPPORTED_ROUTING_DETAIL[mode]),
|
||||
BuiltinRoutingChildAction(
|
||||
_(routing),
|
||||
checkable=True,
|
||||
checked=mode == QApplication.instance().Routing,
|
||||
checked=APP().Routing == routing,
|
||||
)
|
||||
for mode in SUPPORTED_ROUTING
|
||||
for routing in BUILTIN_ROUTING
|
||||
),
|
||||
*list(
|
||||
RoutingChildAction(
|
||||
route['remark'],
|
||||
checkable=True,
|
||||
checked=APP().Routing == str(index),
|
||||
)
|
||||
for index, route in enumerate(APP().editRoutingWidget.RoutesList)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
+11
-14
@@ -1,11 +1,10 @@
|
||||
from Furious.Gui.Action import Action
|
||||
from Furious.Widget.Widget import Menu
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import bootstrapIcon, Switch
|
||||
from Furious.Utility.Translator import gettext as _
|
||||
from Furious.Utility.StartupOnBoot import StartupOnBoot
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
class SettingsChildAction(Action):
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -16,23 +15,23 @@ class SettingsChildAction(Action):
|
||||
if checked:
|
||||
StartupOnBoot.on_()
|
||||
|
||||
QApplication.instance().StartupOnBoot = Switch.ON_
|
||||
APP().StartupOnBoot = Switch.ON_
|
||||
else:
|
||||
StartupOnBoot.off()
|
||||
|
||||
QApplication.instance().StartupOnBoot = Switch.OFF
|
||||
APP().StartupOnBoot = Switch.OFF
|
||||
if self.textCompare('Show Progress Bar When Connecting'):
|
||||
if checked:
|
||||
QApplication.instance().ShowProgressBarWhenConnecting = Switch.ON_
|
||||
APP().ShowProgressBarWhenConnecting = Switch.ON_
|
||||
else:
|
||||
QApplication.instance().ShowProgressBarWhenConnecting = Switch.OFF
|
||||
APP().ShowProgressBarWhenConnecting = Switch.OFF
|
||||
if self.textCompare('Show Tab And Spaces In Editor'):
|
||||
if checked:
|
||||
QApplication.instance().ShowTabAndSpacesInEditor = Switch.ON_
|
||||
APP().ShowTabAndSpacesInEditor = Switch.ON_
|
||||
else:
|
||||
QApplication.instance().ShowTabAndSpacesInEditor = Switch.OFF
|
||||
APP().ShowTabAndSpacesInEditor = Switch.OFF
|
||||
|
||||
QApplication.instance().MainWidget.showTabAndSpacesIfNecessary()
|
||||
APP().MainWidget.showTabAndSpacesIfNecessary()
|
||||
|
||||
|
||||
class SettingsAction(Action):
|
||||
@@ -44,19 +43,17 @@ class SettingsAction(Action):
|
||||
SettingsChildAction(
|
||||
_('Startup On Boot'),
|
||||
checkable=True,
|
||||
checked=QApplication.instance().StartupOnBoot == Switch.ON_,
|
||||
checked=APP().StartupOnBoot == Switch.ON_,
|
||||
),
|
||||
SettingsChildAction(
|
||||
_('Show Progress Bar When Connecting'),
|
||||
checkable=True,
|
||||
checked=QApplication.instance().ShowProgressBarWhenConnecting
|
||||
== Switch.ON_,
|
||||
checked=APP().ShowProgressBarWhenConnecting == Switch.ON_,
|
||||
),
|
||||
SettingsChildAction(
|
||||
_('Show Tab And Spaces In Editor'),
|
||||
checkable=True,
|
||||
checked=QApplication.instance().ShowTabAndSpacesInEditor
|
||||
== Switch.ON_,
|
||||
checked=APP().ShowTabAndSpacesInEditor == Switch.ON_,
|
||||
),
|
||||
),
|
||||
useActionGroup=False,
|
||||
|
||||
+20
-14
@@ -142,33 +142,39 @@ class Hysteria(Core):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def rule(rulePath=''):
|
||||
if rulePath == '':
|
||||
path = DATA_DIR / 'hysteria' / 'bypass-mainland-China.acl'
|
||||
else:
|
||||
path = getAbsolutePath(rulePath)
|
||||
def rule(rulePath):
|
||||
path = getAbsolutePath(str(rulePath))
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as file:
|
||||
return file.read()
|
||||
except Exception:
|
||||
data = file.read()
|
||||
|
||||
logger.info(f'hysteria rule \'{path}\' load success')
|
||||
|
||||
return data
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'hysteria rule \'{path}\' load failed. {ex}')
|
||||
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def mmdb(mmdbPath=''):
|
||||
if mmdbPath == '':
|
||||
path = DATA_DIR / 'hysteria' / 'country.mmdb'
|
||||
else:
|
||||
path = getAbsolutePath(mmdbPath)
|
||||
def mmdb(mmdbPath):
|
||||
path = getAbsolutePath(str(mmdbPath))
|
||||
|
||||
try:
|
||||
with open(path, 'rb') as file:
|
||||
return file.read()
|
||||
except Exception:
|
||||
data = file.read()
|
||||
|
||||
logger.info(f'hysteria mmdb \'{path}\' load success')
|
||||
|
||||
return data
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
logger.error(f'hysteria mmdb \'{path}\' load failed. {ex}')
|
||||
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
|
||||
+20
-2
@@ -9,7 +9,6 @@ from Furious.Utility.Translator import Translatable, gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QAction, QActionGroup
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import logging
|
||||
import darkdetect
|
||||
@@ -47,6 +46,11 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
|
||||
self._actionGroup = ActionGroup(self, *menu.actions())
|
||||
|
||||
self.setActionGroup(self._actionGroup)
|
||||
else:
|
||||
self._actionGroup = None
|
||||
else:
|
||||
self._menu = None
|
||||
self._actionGroup = None
|
||||
|
||||
self.setCheckable(checkable)
|
||||
self.setChecked(checked)
|
||||
@@ -65,6 +69,20 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
|
||||
|
||||
self.triggered.connect(triggerSignal)
|
||||
|
||||
def addAction(self, action):
|
||||
if self._menu is not None:
|
||||
self._menu.addAction(action)
|
||||
|
||||
if self._actionGroup is not None:
|
||||
self._actionGroup.addAction(action)
|
||||
|
||||
def removeAction(self, action):
|
||||
if self._menu is not None:
|
||||
self._menu.removeAction(action)
|
||||
|
||||
if self._actionGroup is not None:
|
||||
self._actionGroup.removeAction(action)
|
||||
|
||||
@property
|
||||
def textEnglish(self):
|
||||
return _(self.text(), 'EN')
|
||||
@@ -127,7 +145,7 @@ class Action(Translatable, SupportThemeChangedCallback, QAction):
|
||||
|
||||
def retranslate(self):
|
||||
def recursiveTranslate(action, memo):
|
||||
if action not in memo and not action.isSeparator():
|
||||
if action not in memo and not action.isSeparator() and action.translatable:
|
||||
action.setText(_(action.text()))
|
||||
|
||||
memo[action] = True
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from Furious.Version import __version__
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import math
|
||||
import pathlib
|
||||
import platform
|
||||
import functools
|
||||
|
||||
APP = functools.partial(QApplication.instance)
|
||||
|
||||
APPLICATION_NAME = 'Furious'
|
||||
APPLICATION_VERSION = __version__
|
||||
@@ -24,9 +28,9 @@ GOLDEN_RATIO = (math.sqrt(5) - 1) / 2
|
||||
|
||||
SYSTEM_LANGUAGE = QtCore.QLocale().name()[:2].upper()
|
||||
|
||||
ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent
|
||||
DATA_DIR = ROOT_DIR / 'Data'
|
||||
CRASH_LOG_DIR = ROOT_DIR / 'CrashLog'
|
||||
ROOT_DIR = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||
DATA_DIR = ROOT_DIR / APPLICATION_NAME / 'Data'
|
||||
CRASH_LOG_DIR = ROOT_DIR / APPLICATION_NAME / 'CrashLog'
|
||||
|
||||
PROXY_SERVER_BYPASS = (
|
||||
'localhost;*.local;127.*;10.*;172.16.*;172.17.*;'
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
from Furious.Widget.Application import Application
|
||||
from Furious.Utility.Constants import PLATFORM, CRASH_LOG_DIR
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from Furious.Utility.Constants import APP, PLATFORM, CRASH_LOG_DIR
|
||||
|
||||
import os
|
||||
import sys
|
||||
@@ -41,8 +39,8 @@ class Process(ProcessContext.Process):
|
||||
|
||||
self.saveCrashLog(exceptionType, exceptionValue, tb)
|
||||
|
||||
if QApplication.instance() is not None:
|
||||
QApplication.instance().exit(error)
|
||||
if APP() is not None:
|
||||
APP().exit(error)
|
||||
else:
|
||||
sys.exit(error)
|
||||
|
||||
@@ -63,10 +61,10 @@ class Process(ProcessContext.Process):
|
||||
traceback.format_exception(exceptionType, exceptionValue, tb),
|
||||
)
|
||||
|
||||
if QApplication.instance() is None:
|
||||
if APP() is None:
|
||||
crashLog = f'{stackLog}'
|
||||
else:
|
||||
crashLog = f'{QApplication.instance().log()}\n{stackLog}'
|
||||
crashLog = f'{APP().log()}\n{stackLog}'
|
||||
|
||||
with open(CRASH_LOG_DIR / self.logFileName, 'w', encoding='utf-8') as file:
|
||||
file.write(crashLog)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from Furious.Action.Routing import SUPPORTED_ROUTING
|
||||
from Furious.Action.Language import SUPPORTED_LANGUAGE
|
||||
from Furious.Utility.Constants import SYSTEM_LANGUAGE
|
||||
from Furious.Utility.Utility import Switch
|
||||
@@ -53,22 +52,42 @@ SMART_CHOSEN_LANGUAGE = (
|
||||
)
|
||||
|
||||
SUPPORTED_SETTINGS = (
|
||||
# Connected last time or not
|
||||
Settings('Connect', Switch.RANGE),
|
||||
Settings('Routing', SUPPORTED_ROUTING),
|
||||
# User Routing option
|
||||
Settings('Routing'),
|
||||
# User Custom Routing object
|
||||
Settings('CustomRouting'),
|
||||
# User Configuration
|
||||
Settings('Configuration'),
|
||||
# User Activated Server Index
|
||||
Settings('ActivatedItemIndex'),
|
||||
# Main Widget Window Size
|
||||
Settings('MainWidgetWindowSize'),
|
||||
# Edit Routing Widget Window Size
|
||||
Settings('RoutesWidgetWindowSize'),
|
||||
# Edit Routing Widget Editor Window Size
|
||||
Settings('RoutesEditorWindowSize'),
|
||||
# Main Widget Section Size
|
||||
Settings('ServerWidgetSectionSizeTable'),
|
||||
# Edit Routing Widget Section Size
|
||||
Settings('RoutesWidgetSectionSizeTable'),
|
||||
# Main Widget Font Point Size
|
||||
Settings('EditorWidgetPointSize'),
|
||||
# Log Viewer Widget Font Point Size
|
||||
Settings('ViewerWidgetPointSize'),
|
||||
# User selected language
|
||||
Settings('Language', SUPPORTED_LANGUAGE, SMART_CHOSEN_LANGUAGE),
|
||||
# Startup On Boot
|
||||
Settings('StartupOnBoot', Switch.RANGE, Switch.ON_), # On by default
|
||||
# Show Progressbar
|
||||
Settings(
|
||||
# For user experience: On by default
|
||||
'ShowProgressBarWhenConnecting',
|
||||
Switch.RANGE,
|
||||
Switch.ON_,
|
||||
),
|
||||
# Show Tab and spaces
|
||||
Settings('ShowTabAndSpacesInEditor', Switch.RANGE),
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from Furious.Utility.Constants import (
|
||||
)
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -40,6 +40,8 @@ class DraculaTheme(Theme):
|
||||
HighlightRules(r'"[^"\\]*(\\.[^"\\]*)*"', '#F1FA8C'),
|
||||
# JSON keys. Green
|
||||
HighlightRules(r'"([^"\\]*(\\.[^"\\]*)*)"\s*:', '#50FA7B', isJSONKey=True),
|
||||
# Comments(only for hints on display). Grey
|
||||
HighlightRules(r'^#.*', '#6272a4'),
|
||||
]
|
||||
|
||||
def highlightBlock(self, text):
|
||||
|
||||
+112
-11
@@ -1,6 +1,5 @@
|
||||
from Furious.Utility.Constants import APPLICATION_NAME
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from Furious.Core.Core import XrayCore, Hysteria
|
||||
from Furious.Utility.Constants import APP, APPLICATION_NAME
|
||||
|
||||
|
||||
class Translatable:
|
||||
@@ -9,6 +8,8 @@ class Translatable:
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.translatable = True
|
||||
|
||||
Translatable.Object.append(self)
|
||||
|
||||
def retranslate(self):
|
||||
@@ -19,7 +20,8 @@ class Translatable:
|
||||
for ob in Translatable.Object:
|
||||
assert isinstance(ob, Translatable)
|
||||
|
||||
ob.retranslate()
|
||||
if ob.translatable:
|
||||
ob.retranslate()
|
||||
|
||||
|
||||
class Translator:
|
||||
@@ -58,9 +60,9 @@ def installTranslation(translation):
|
||||
|
||||
def gettext(source, locale=None):
|
||||
if locale is None:
|
||||
assert QApplication.instance() is not None
|
||||
assert APP() is not None
|
||||
|
||||
return _trans.translate(source, QApplication.instance().Language)
|
||||
return _trans.translate(source, APP().Language)
|
||||
else:
|
||||
assert locale in list(LANGUAGE_TO_ABBR.values())
|
||||
|
||||
@@ -83,6 +85,8 @@ NO_TRANSLATION = [
|
||||
APPLICATION_NAME,
|
||||
*LANGUAGE_TO_ABBR.keys(),
|
||||
'TLS',
|
||||
XrayCore.name(),
|
||||
Hysteria.name(),
|
||||
]
|
||||
|
||||
NO_TRANSLATION_DICT = {key: True for key in NO_TRANSLATION}
|
||||
@@ -124,6 +128,11 @@ TRANSLATION = {
|
||||
'ZH': '绕过中国大陆',
|
||||
'TW': '繞過中國大陸',
|
||||
},
|
||||
'Bypass Iran': {
|
||||
'ES': 'Evitar Irán',
|
||||
'ZH': '绕过伊朗',
|
||||
'TW': '繞過伊朗',
|
||||
},
|
||||
'Global': {
|
||||
'ES': 'Global',
|
||||
'ZH': '全球',
|
||||
@@ -134,6 +143,11 @@ TRANSLATION = {
|
||||
'ZH': '自定义',
|
||||
'TW': '自定義',
|
||||
},
|
||||
'Edit Routing...': {
|
||||
'ES': 'Editar Enrutamiento...',
|
||||
'ZH': '编辑路由...',
|
||||
'TW': '編輯路由...',
|
||||
},
|
||||
'Import': {
|
||||
'ES': 'Importar',
|
||||
'ZH': '导入',
|
||||
@@ -496,16 +510,16 @@ TRANSLATION = {
|
||||
'ZH': '删除',
|
||||
'TW': '刪除',
|
||||
},
|
||||
'Delete this configuration?': {
|
||||
'ES': '¿Eliminar esta configuración?',
|
||||
'ZH': '删除此配置?',
|
||||
'TW': '刪除此配置?',
|
||||
},
|
||||
'Delete these configuration?': {
|
||||
'ES': '¿Eliminar esta configuración?',
|
||||
'ZH': '删除这些配置?',
|
||||
'TW': '刪除這些配置?',
|
||||
},
|
||||
'Delete this configuration?': {
|
||||
'ES': '¿Eliminar esta configuración?',
|
||||
'ZH': '删除此配置?',
|
||||
'TW': '刪除此配置?',
|
||||
},
|
||||
'Connecting. Please wait.': {
|
||||
'ES': 'Conectando. Espere por favor.',
|
||||
'ZH': '正在连接,请稍候。',
|
||||
@@ -637,6 +651,93 @@ TRANSLATION = {
|
||||
'ZH': '滚动到激活的服务器',
|
||||
'TW': '滾動到激活的服務器',
|
||||
},
|
||||
# Edit Routing widget
|
||||
'Edit Routing': {
|
||||
'ES': 'Editar Enrutamiento',
|
||||
'ZH': '编辑路由',
|
||||
'TW': '編輯路由',
|
||||
},
|
||||
'Type': {
|
||||
'ES': 'Tipo',
|
||||
'ZH': '类型',
|
||||
'TW': '類型',
|
||||
},
|
||||
'Built-in': {
|
||||
'ES': 'Incorporado',
|
||||
'ZH': '内置',
|
||||
'TW': '内置',
|
||||
},
|
||||
'Routing Rules': {
|
||||
'ES': 'Reglas de Enrutamiento',
|
||||
'ZH': '路由规则',
|
||||
'TW': '路由規則',
|
||||
},
|
||||
'User Defined': {
|
||||
'ES': 'Usuario Definido',
|
||||
'ZH': '用户定义',
|
||||
'TW': '用戶定義',
|
||||
},
|
||||
'Add routing': {
|
||||
'ES': 'Agregar enrutamiento',
|
||||
'ZH': '添加路由',
|
||||
'TW': '添加路由',
|
||||
},
|
||||
'Enter routing remark:': {
|
||||
'ES': 'Introducir comentario de enrutamiento:',
|
||||
'ZH': '输入路由名称:',
|
||||
'TW': '輸入路由名稱:',
|
||||
},
|
||||
'Add': {
|
||||
'ES': 'Agregar',
|
||||
'ZH': '添加',
|
||||
'TW': '添加',
|
||||
},
|
||||
'Import Asset File...': {
|
||||
'ES': 'Importar Archivo de Activos...',
|
||||
'ZH': '导入资源文件...',
|
||||
'TW': '導入資源文件...',
|
||||
},
|
||||
'Error saving routing configuration': {
|
||||
'ES': 'Error al guardar la configuración de enrutamiento',
|
||||
'ZH': '保存路由配置时出错',
|
||||
'TW': '保存路由配置時出錯',
|
||||
},
|
||||
# Asset Viewer
|
||||
'Asset File': {
|
||||
'ES': 'Activos',
|
||||
'ZH': '资源文件',
|
||||
'TW': '資源文件',
|
||||
},
|
||||
'Asset file already exists. Overwrite?': {
|
||||
'ES': 'Archivo de activos ya existe. ¿Sobrescribir?',
|
||||
'ZH': '资源文件已存在。是否覆盖?',
|
||||
'TW': '資源文件已存在。是否覆蓋?',
|
||||
},
|
||||
'Error import asset file.': {
|
||||
'ES': 'Error al importar el archivo de activos.',
|
||||
'ZH': '导入资源文件出错。',
|
||||
'TW': '導入資源文件出錯。',
|
||||
},
|
||||
'Import asset file success.': {
|
||||
'ES': 'Importar archivo de activos con éxito.',
|
||||
'ZH': '导入资源文件成功。',
|
||||
'TW': '導入資源文件成功。',
|
||||
},
|
||||
'Delete these asset files?': {
|
||||
'ES': '¿Eliminar estos archivos de activos?',
|
||||
'ZH': '删除这些资源文件?',
|
||||
'TW': '刪除這些資源文件?',
|
||||
},
|
||||
'Delete this asset file?': {
|
||||
'ES': '¿Eliminar este archivo de activos?',
|
||||
'ZH': '删除该资源文件?',
|
||||
'TW': '刪除該資源文件?',
|
||||
},
|
||||
'All files (*)': {
|
||||
'ES': 'Todos los archivos (*)',
|
||||
'ZH': '所有文件 (*)',
|
||||
'TW': '所有文件 (*)',
|
||||
},
|
||||
# Log Viewer
|
||||
'Log Viewer': {
|
||||
'ES': 'Visor de Registro',
|
||||
|
||||
+47
-11
@@ -1,5 +1,5 @@
|
||||
from Furious.Gui.Icon import Icon
|
||||
from Furious.Utility.Constants import PLATFORM, ROOT_DIR
|
||||
from Furious.Utility.Constants import APP, PLATFORM, ROOT_DIR
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
@@ -46,29 +46,27 @@ class StateContext:
|
||||
self._ob.setDisabled(False)
|
||||
|
||||
|
||||
class Storage:
|
||||
class ServerStorage:
|
||||
EMPTY_OBJECT = {'model': []}
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
return copy.deepcopy(Storage.EMPTY_OBJECT)
|
||||
return copy.deepcopy(ServerStorage.EMPTY_OBJECT)
|
||||
|
||||
@staticmethod
|
||||
def sync(ob=None):
|
||||
if ob is None:
|
||||
# Object is up-to-date
|
||||
QApplication.instance().Configuration = Storage.toStorage(
|
||||
QApplication.instance().MainWidget.StorageObj
|
||||
)
|
||||
APP().Configuration = ServerStorage.toStorage(APP().MainWidget.StorageObj)
|
||||
else:
|
||||
# Object is up-to-date
|
||||
QApplication.instance().Configuration = Storage.toStorage(ob)
|
||||
APP().Configuration = ServerStorage.toStorage(ob)
|
||||
|
||||
@staticmethod
|
||||
def toObject(st):
|
||||
if not st:
|
||||
# Storage does not exist, or is empty
|
||||
return Storage.init()
|
||||
# Server storage does not exist, or is empty
|
||||
return ServerStorage.init()
|
||||
|
||||
return ujson.loads(Base64Encoder.decode(st))
|
||||
|
||||
@@ -80,8 +78,46 @@ class Storage:
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
QApplication.instance().Configuration = ''
|
||||
QApplication.instance().ActivatedItemIndex = str(-1)
|
||||
APP().Configuration = ''
|
||||
APP().ActivatedItemIndex = str(-1)
|
||||
|
||||
|
||||
class RoutesStorage:
|
||||
# remark, corename, routes
|
||||
EMPTY_OBJECT = {'model': []}
|
||||
|
||||
@staticmethod
|
||||
def init():
|
||||
return copy.deepcopy(RoutesStorage.EMPTY_OBJECT)
|
||||
|
||||
@staticmethod
|
||||
def sync(ob=None):
|
||||
if ob is None:
|
||||
# Object is up-to-date
|
||||
APP().CustomRouting = RoutesStorage.toStorage(
|
||||
APP().editRoutingWidget.StorageObj
|
||||
)
|
||||
else:
|
||||
# Object is up-to-date
|
||||
APP().CustomRouting = RoutesStorage.toStorage(ob)
|
||||
|
||||
@staticmethod
|
||||
def toObject(st):
|
||||
if not st:
|
||||
# Server storage does not exist, or is empty
|
||||
return RoutesStorage.init()
|
||||
|
||||
return ujson.loads(Base64Encoder.decode(st))
|
||||
|
||||
@staticmethod
|
||||
def toStorage(ob):
|
||||
return Base64Encoder.encode(
|
||||
ujson.dumps(ob, ensure_ascii=False, escape_forward_slashes=False).encode()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
APP().CustomRouting = ''
|
||||
|
||||
|
||||
class Switch:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from Furious.Widget.SystemTrayIcon import SystemTrayIcon
|
||||
from Furious.Widget.EditConfiguration import EditConfigurationWidget
|
||||
from Furious.Widget.EditRouting import EditRoutingWidget
|
||||
from Furious.Widget.LogViewer import LogViewerWidget
|
||||
from Furious.Utility.Constants import (
|
||||
APPLICATION_NAME,
|
||||
@@ -140,6 +141,8 @@ class Application(SingletonApplication):
|
||||
self.customFontEnabled = False
|
||||
self.customFontName = ''
|
||||
|
||||
self.editRoutingWidget = None
|
||||
|
||||
self.logViewerWidget = None
|
||||
self.logViewerHandle = None
|
||||
self.logStreamHandle = None
|
||||
@@ -181,7 +184,7 @@ class Application(SingletonApplication):
|
||||
|
||||
logging.basicConfig(
|
||||
format='[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s',
|
||||
level=logging.DEBUG,
|
||||
level=logging.INFO,
|
||||
handlers=(self.logViewerHandle, self.logStreamHandle),
|
||||
)
|
||||
logging.raiseExceptions = False
|
||||
@@ -244,6 +247,7 @@ class Application(SingletonApplication):
|
||||
self.aboutToQuit.connect(self.cleanup)
|
||||
|
||||
self.MainWidget = EditConfigurationWidget()
|
||||
self.editRoutingWidget = EditRoutingWidget()
|
||||
|
||||
self.tray = SystemTrayIcon()
|
||||
self.tray.show()
|
||||
@@ -257,12 +261,16 @@ class Application(SingletonApplication):
|
||||
Proxy.off()
|
||||
Proxy.daemonOff()
|
||||
|
||||
if self.tray is not None:
|
||||
self.tray.ConnectAction.stopCore()
|
||||
# Avoid calling core exit callback. Cores are cleaned by OS.
|
||||
# if self.tray is not None:
|
||||
# self.tray.ConnectAction.stopCore()
|
||||
|
||||
if self.MainWidget is not None:
|
||||
self.MainWidget.syncSettings()
|
||||
|
||||
if self.editRoutingWidget is not None:
|
||||
self.editRoutingWidget.syncSettings()
|
||||
|
||||
if self.logViewerWidget is not None:
|
||||
self.logViewerWidget.syncSettings()
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Widget.Widget import ListWidget, Menu, MessageBox
|
||||
from Furious.Utility.Constants import APPLICATION_NAME, PLATFORM, GOLDEN_RATIO, ROOT_DIR
|
||||
from Furious.Utility.Utility import (
|
||||
StateContext,
|
||||
SupportConnectedCallback,
|
||||
SupportThemeChangedCallback,
|
||||
bootstrapIcon,
|
||||
bootstrapIconWhite,
|
||||
moveToCenter,
|
||||
)
|
||||
from Furious.Utility.Translator import Translatable, gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import QFileDialog, QListWidget, QListWidgetItem, QMainWindow
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
import threading
|
||||
import watchfiles
|
||||
import darkdetect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def watchFileChanges(callback):
|
||||
for changes in watchfiles.watch(AssetViewerWidget.AssetDir):
|
||||
if callable(callback):
|
||||
callback()
|
||||
|
||||
|
||||
class QuestionDeleteBox(MessageBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.isMulti = False
|
||||
self.possibleRemark = ''
|
||||
|
||||
self.setWindowTitle(_('Delete'))
|
||||
|
||||
self.setStandardButtons(
|
||||
MessageBox.StandardButton.Yes | MessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
def getText(self):
|
||||
if self.isMulti:
|
||||
return _('Delete these asset files?')
|
||||
else:
|
||||
return _('Delete this asset file?') + f'\n\n{self.possibleRemark}'
|
||||
|
||||
def retranslate(self):
|
||||
with StateContext(self):
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(self.getText())
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class AssetExistsBox(MessageBox):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Import'))
|
||||
|
||||
self.setStandardButtons(
|
||||
MessageBox.StandardButton.Yes | MessageBox.StandardButton.No
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
with StateContext(self):
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
self.setText(_(self.text()))
|
||||
|
||||
# Ignore informative text, buttons
|
||||
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class ImportAction(Action):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(_('Import From File...'), **kwargs)
|
||||
|
||||
self.assetExistsBox = AssetExistsBox(
|
||||
icon=MessageBox.Icon.Question, parent=self.parent()
|
||||
)
|
||||
self.importErrorBox = MessageBox(
|
||||
icon=MessageBox.Icon.Critical, parent=self.parent()
|
||||
)
|
||||
self.importSuccessBox = MessageBox(
|
||||
icon=MessageBox.Icon.Information, parent=self.parent()
|
||||
)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
filename, selectedFilter = QFileDialog.getOpenFileName(
|
||||
self.parent(), _('Import File'), filter=_('All files (*)')
|
||||
)
|
||||
|
||||
if filename:
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
if os.path.isfile(AssetViewerWidget.AssetDir / os.path.basename(filename)):
|
||||
self.assetExistsBox.setWindowTitle(_('Import'))
|
||||
self.assetExistsBox.setText(_('Asset file already exists. Overwrite?'))
|
||||
self.assetExistsBox.setInformativeText(basename)
|
||||
|
||||
if self.assetExistsBox.exec() == MessageBox.StandardButton.No.value:
|
||||
# Do not overwrite
|
||||
return
|
||||
|
||||
try:
|
||||
shutil.copy(filename, AssetViewerWidget.AssetDir)
|
||||
except shutil.SameFileError:
|
||||
# Same file imported. Do nothing
|
||||
pass
|
||||
except Exception as ex:
|
||||
# Any non-exit exception
|
||||
|
||||
self.importErrorBox.setWindowTitle(_('Import'))
|
||||
self.importErrorBox.setText(_('Error import asset file.'))
|
||||
self.importErrorBox.setInformativeText(str(ex))
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
self.importErrorBox.exec()
|
||||
else:
|
||||
self.importSuccessBox.setWindowTitle(_('Import'))
|
||||
self.importSuccessBox.setText(_('Import asset file success.'))
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
self.importSuccessBox.exec()
|
||||
|
||||
|
||||
class ExitAction(Action):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(_('Exit'), **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
self.parent().hide()
|
||||
|
||||
|
||||
class WatchFiles(QtCore.QObject):
|
||||
filesChanged = QtCore.Signal()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class AssetViewerWidget(
|
||||
Translatable, SupportConnectedCallback, SupportThemeChangedCallback, QMainWindow
|
||||
):
|
||||
AssetDir = ROOT_DIR / APPLICATION_NAME / 'Data' / 'xray'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setWindowTitle(_('Asset File'))
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
|
||||
|
||||
self.questionDeleteBox = QuestionDeleteBox(
|
||||
icon=MessageBox.Icon.Question, parent=self
|
||||
)
|
||||
|
||||
self.listWidget = ListWidget(parent=self)
|
||||
self.listWidget.setSelectionBehavior(QListWidget.SelectionBehavior.SelectRows)
|
||||
self.listWidget.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
||||
self.listWidget.setIconSize(QtCore.QSize(64, 64))
|
||||
|
||||
self.flushItem()
|
||||
self.setCentralWidget(self.listWidget)
|
||||
|
||||
# Check filesystem events
|
||||
self.watchFiles = WatchFiles()
|
||||
self.watchFiles.filesChanged.connect(lambda: self.flushItem())
|
||||
|
||||
self.watchFilesListenerThread = threading.Thread(
|
||||
target=watchFileChanges,
|
||||
args=(self.watchFiles.filesChanged.emit,),
|
||||
daemon=True,
|
||||
)
|
||||
self.watchFilesListenerThread.start()
|
||||
|
||||
contextMenuActions = [
|
||||
Action(
|
||||
_('Delete'), callback=lambda: self.deleteSelectedItem(), parent=self
|
||||
),
|
||||
]
|
||||
|
||||
self.contextMenu = Menu(*contextMenuActions)
|
||||
|
||||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||||
|
||||
fileMenuActions = [
|
||||
ImportAction(parent=self),
|
||||
Seperator(),
|
||||
ExitAction(parent=self),
|
||||
]
|
||||
|
||||
for menu in (fileMenuActions,):
|
||||
for action in menu:
|
||||
if isinstance(action, Action):
|
||||
if hasattr(self, f'{action}'):
|
||||
logger.warning(f'{self} already has action {action}')
|
||||
|
||||
setattr(self, f'{action}', action)
|
||||
|
||||
self._fileMenu = Menu(*fileMenuActions, title=_('File'), parent=self)
|
||||
self.menuBar().addMenu(self._fileMenu)
|
||||
|
||||
self.setGeometry(100, 100, 512 * GOLDEN_RATIO, 512)
|
||||
|
||||
moveToCenter(self)
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
self.contextMenu.exec(self.mapToGlobal(point))
|
||||
|
||||
def deleteSelectedItem(self):
|
||||
indexes = self.listWidget.selectedIndex
|
||||
|
||||
if len(indexes) == 0:
|
||||
# Nothing selected
|
||||
return
|
||||
|
||||
self.questionDeleteBox.isMulti = bool(len(indexes) > 1)
|
||||
self.questionDeleteBox.possibleRemark = (
|
||||
f'{self.listWidget.item(indexes[0]).text()}'
|
||||
)
|
||||
self.questionDeleteBox.setText(self.questionDeleteBox.getText())
|
||||
|
||||
if self.questionDeleteBox.exec() == MessageBox.StandardButton.No.value:
|
||||
# Do not delete
|
||||
return
|
||||
|
||||
for index in indexes:
|
||||
os.remove(AssetViewerWidget.AssetDir / self.listWidget.item(index).text())
|
||||
|
||||
def flushItem(self):
|
||||
self.listWidget.clear()
|
||||
|
||||
for filename in os.listdir(AssetViewerWidget.AssetDir):
|
||||
if os.path.isfile(AssetViewerWidget.AssetDir / filename):
|
||||
item = QListWidgetItem(filename)
|
||||
|
||||
if darkdetect.theme() == 'Dark':
|
||||
if PLATFORM == 'Windows':
|
||||
# Windows. Always use black icon
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIconWhite('file-earmark.svg'))
|
||||
else:
|
||||
item.setIcon(bootstrapIcon('file-earmark.svg'))
|
||||
|
||||
self.listWidget.addItem(item)
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
self.hide()
|
||||
|
||||
def connectedCallback(self):
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
|
||||
|
||||
def themeChangedCallback(self, theme):
|
||||
# TODO
|
||||
pass
|
||||
|
||||
def retranslate(self):
|
||||
with StateContext(self):
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
@@ -1,12 +1,16 @@
|
||||
from Furious.Utility.Constants import APPLICATION_NAME, GOLDEN_RATIO
|
||||
from Furious.Utility.Utility import bootstrapIcon, StateContext
|
||||
from Furious.Utility.Utility import (
|
||||
bootstrapIcon,
|
||||
StateContext,
|
||||
SupportConnectedCallback,
|
||||
)
|
||||
from Furious.Utility.Translator import Translatable, gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QHBoxLayout, QProgressBar, QWidget
|
||||
|
||||
|
||||
class ConnectingProgressBar(Translatable, QWidget):
|
||||
class ConnectingProgressBar(Translatable, SupportConnectedCallback, QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -54,6 +58,38 @@ class ConnectingProgressBar(Translatable, QWidget):
|
||||
|
||||
self.hide()
|
||||
|
||||
def connectedCallback(self):
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
|
||||
|
||||
self.progressBar.setStyleSheet(
|
||||
f'QProgressBar {{'
|
||||
f' border-radius: 2px;'
|
||||
f' text-align: center;'
|
||||
f'}}'
|
||||
f''
|
||||
f'QProgressBar::chunk {{'
|
||||
f' background-color: #EB212E;'
|
||||
f' width: 10px;'
|
||||
f' margin: 0.5px;'
|
||||
f'}}'
|
||||
)
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg'))
|
||||
|
||||
self.progressBar.setStyleSheet(
|
||||
f'QProgressBar {{'
|
||||
f' border-radius: 2px;'
|
||||
f' text-align: center;'
|
||||
f'}}'
|
||||
f''
|
||||
f'QProgressBar::chunk {{'
|
||||
f' background-color: #43ACED;'
|
||||
f' width: 10px;'
|
||||
f' margin: 0.5px;'
|
||||
f'}}'
|
||||
)
|
||||
|
||||
def retranslate(self):
|
||||
with StateContext(self):
|
||||
self.setWindowTitle(_(self.windowTitle()))
|
||||
|
||||
@@ -4,26 +4,30 @@ from Furious.Core.Intellisense import Intellisense
|
||||
from Furious.Core.Configuration import Configuration
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Widget.Widget import (
|
||||
HeaderView,
|
||||
Menu,
|
||||
MessageBox,
|
||||
PushButton,
|
||||
StyledItemDelegate,
|
||||
TableWidget,
|
||||
TabWidget,
|
||||
ZoomablePlainTextEdit,
|
||||
)
|
||||
from Furious.Widget.IndentSpinBox import IndentSpinBox
|
||||
from Furious.Utility.Constants import (
|
||||
PLATFORM,
|
||||
APP,
|
||||
APPLICATION_NAME,
|
||||
APPLICATION_VERSION,
|
||||
APPLICATION_ABOUT_PAGE,
|
||||
APPLICATION_REPO_OWNER_NAME,
|
||||
APPLICATION_REPO_NAME,
|
||||
PLATFORM,
|
||||
GOLDEN_RATIO,
|
||||
)
|
||||
from Furious.Utility.Utility import (
|
||||
Base64Encoder,
|
||||
StateContext,
|
||||
Storage,
|
||||
ServerStorage,
|
||||
Switch,
|
||||
SupportConnectedCallback,
|
||||
bootstrapIcon,
|
||||
@@ -34,10 +38,9 @@ from Furious.Utility.Translator import Translatable, gettext as _
|
||||
from Furious.Utility.Theme import DraculaTheme
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QBrush, QColor, QDesktopServices, QFont, QTextOption
|
||||
from PySide6.QtGui import QDesktopServices, QFont, QTextOption
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QGridLayout,
|
||||
@@ -110,11 +113,12 @@ class ImportFileAction(Action):
|
||||
try:
|
||||
with open(filename, 'r', encoding='utf-8') as file:
|
||||
plainText = file.read()
|
||||
except Exception:
|
||||
except Exception as ex:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.openErrorBox.setWindowTitle(_('Error opening file'))
|
||||
self.openErrorBox.setText(_('Invalid configuration file.'))
|
||||
self.openErrorBox.setInformativeText(str(ex))
|
||||
|
||||
# Show the MessageBox and wait for user to close it
|
||||
self.openErrorBox.exec()
|
||||
@@ -218,7 +222,7 @@ class ScrollToActivatedServerAction(Action):
|
||||
)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.parent().saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -273,7 +277,7 @@ def questionReconnect(saveConfInfo):
|
||||
|
||||
if choice == MessageBox.ButtonRole.AcceptRole.value:
|
||||
# Reconnect
|
||||
QApplication.instance().tray.ConnectAction.reconnectAction()
|
||||
APP().tray.ConnectAction.reconnectAction()
|
||||
else:
|
||||
# OK. Do nothing
|
||||
pass
|
||||
@@ -308,7 +312,7 @@ class SaveAsServerAction(Action):
|
||||
)
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
self.parent().markAsSaved()
|
||||
|
||||
@@ -318,7 +322,7 @@ class SaveAsServerAction(Action):
|
||||
if myFocusIndex == self.parent().activatedItemIndex:
|
||||
# Activated configuration modified
|
||||
|
||||
if server and QApplication.instance().tray.ConnectAction.isConnected():
|
||||
if server and APP().tray.ConnectAction.isConnected():
|
||||
questionReconnect(self.saveConfInfo)
|
||||
|
||||
return True
|
||||
@@ -594,12 +598,20 @@ class ZoomOutAction(Action):
|
||||
self.parent().plainTextEdit.zoomOut()
|
||||
|
||||
|
||||
class RoutingAction(Action):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(_('Edit Routing...'), **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
APP().editRoutingWidget.show()
|
||||
|
||||
|
||||
class ShowLogAction(Action):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(_('Show Log...'), **kwargs)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
QApplication.instance().logViewerWidget.showMaximized()
|
||||
APP().logViewerWidget.showMaximized()
|
||||
|
||||
|
||||
class WhetherToUpdateInfoBox(MessageBox):
|
||||
@@ -637,7 +649,7 @@ class CheckForUpdatesAction(Action):
|
||||
)
|
||||
|
||||
def triggeredCallback(self, checked):
|
||||
proxyServer = QApplication.instance().tray.ConnectAction.proxyServer
|
||||
proxyServer = APP().tray.ConnectAction.proxyServer
|
||||
|
||||
if proxyServer:
|
||||
logger.info(f'check for updates uses proxy server {proxyServer}')
|
||||
@@ -828,7 +840,7 @@ class SaveChangeFirst(MessageBox):
|
||||
self.moveToCenter()
|
||||
|
||||
|
||||
class NormalServerHorizontalHeader(QHeaderView):
|
||||
class NormalServerHorizontalHeader(HeaderView):
|
||||
class SortOrder:
|
||||
Ascending_ = False
|
||||
Descending = True
|
||||
@@ -836,8 +848,6 @@ class NormalServerHorizontalHeader(QHeaderView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
|
||||
|
||||
self.setSectionsClickable(True)
|
||||
|
||||
self.sectionClicked.connect(self.handleSectionClicked)
|
||||
self.sectionResized.connect(self.handleSectionResized)
|
||||
|
||||
@@ -848,7 +858,7 @@ class NormalServerHorizontalHeader(QHeaderView):
|
||||
|
||||
@QtCore.Slot(int)
|
||||
def handleSectionClicked(self, clickedIndex):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.parent().saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -890,7 +900,7 @@ class NormalServerHorizontalHeader(QHeaderView):
|
||||
)
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
# Flush it
|
||||
self.parent().flushAll()
|
||||
@@ -936,37 +946,10 @@ class NormalServerHorizontalHeader(QHeaderView):
|
||||
self.parent().sectionSizeTable[str(index)] = newSize
|
||||
|
||||
|
||||
class NormalServerVerticalHeader(SupportConnectedCallback, QHeaderView):
|
||||
class NormalServerVerticalHeader(HeaderView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs)
|
||||
|
||||
self.setSectionsClickable(True)
|
||||
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #008AE1; }}'
|
||||
)
|
||||
|
||||
def connectedCallback(self):
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #EB212E; }}'
|
||||
)
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #008AE1; }}'
|
||||
)
|
||||
|
||||
|
||||
class NormalServerWidgetItemDelegate(QStyledItemDelegate):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QLineEdit(parent)
|
||||
editor.setFont(QFont(QApplication.instance().customFontName))
|
||||
|
||||
return editor
|
||||
|
||||
|
||||
def toJSONMayBeNull(text):
|
||||
try:
|
||||
@@ -977,7 +960,7 @@ def toJSONMayBeNull(text):
|
||||
return {}
|
||||
|
||||
|
||||
class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
class NormalServerWidget(Translatable, SupportConnectedCallback, TableWidget):
|
||||
# Might be extended in the future
|
||||
HEADER_LABEL = [
|
||||
'Remark',
|
||||
@@ -1028,7 +1011,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.editorTab = self.parent().editorTab
|
||||
|
||||
# Delegate
|
||||
self.delegate = NormalServerWidgetItemDelegate(parent=self)
|
||||
self.delegate = StyledItemDelegate(parent=self)
|
||||
self.setItemDelegate(self.delegate)
|
||||
|
||||
# Scroll bar value list. v, h
|
||||
@@ -1056,9 +1039,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
|
||||
try:
|
||||
# Restore horizontal section size
|
||||
self.sectionSizeTable = ujson.loads(
|
||||
QApplication.instance().ServerWidgetSectionSizeTable
|
||||
)
|
||||
self.sectionSizeTable = ujson.loads(APP().ServerWidgetSectionSizeTable)
|
||||
|
||||
# Block resize callback
|
||||
self.horizontalHeader().blockSignals(True)
|
||||
@@ -1083,9 +1064,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
)
|
||||
|
||||
for column in range(self.horizontalHeader().count()):
|
||||
self.horizontalHeaderItem(column).setFont(
|
||||
QFont(QApplication.instance().customFontName)
|
||||
)
|
||||
self.horizontalHeaderItem(column).setFont(QFont(APP().customFontName))
|
||||
|
||||
# Install custom header
|
||||
self.setVerticalHeader(NormalServerVerticalHeader(self))
|
||||
@@ -1160,7 +1139,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
@property
|
||||
def activatedItemIndex(self):
|
||||
try:
|
||||
return int(QApplication.instance().ActivatedItemIndex)
|
||||
return int(APP().ActivatedItemIndex)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
@@ -1170,10 +1149,6 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
def activatedItem(self):
|
||||
return self.item(self.activatedItemIndex, 0)
|
||||
|
||||
@property
|
||||
def selectedIndex(self):
|
||||
return sorted(list(set(index.row() for index in self.selectedIndexes())))
|
||||
|
||||
@property
|
||||
def currentFocus(self):
|
||||
try:
|
||||
@@ -1193,7 +1168,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
|
||||
if oldItem is None:
|
||||
# Item does not exists
|
||||
newItem.setFont(QFont(QApplication.instance().customFontName))
|
||||
newItem.setFont(QFont(APP().customFontName))
|
||||
else:
|
||||
# Use existing
|
||||
newItem.setFont(oldItem.font())
|
||||
@@ -1272,48 +1247,13 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
|
||||
# Avoid unnecessary encode/write, etc.
|
||||
if remarkModified:
|
||||
Storage.sync()
|
||||
|
||||
def setSelectionColor(self, color):
|
||||
self.setStyleSheet(f'QTableWidget {{ selection-background-color: {color}; }}')
|
||||
ServerStorage.sync()
|
||||
|
||||
def activateItemByIndex(self, index, activate=True):
|
||||
super().activateItemByIndex(index, activate)
|
||||
|
||||
if activate:
|
||||
for column in range(self.columnCount()):
|
||||
serverItem = self.item(int(index), column)
|
||||
|
||||
if serverItem is None:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
serverFont = serverItem.font()
|
||||
serverFont.setBold(True)
|
||||
|
||||
serverItem.setFont(serverFont)
|
||||
|
||||
if QApplication.instance().tray is None:
|
||||
# Initializing
|
||||
serverItem.setForeground(QColor('#008AE1'))
|
||||
else:
|
||||
if QApplication.instance().tray.ConnectAction.isConnected():
|
||||
serverItem.setForeground(QColor('#EB212E'))
|
||||
else:
|
||||
serverItem.setForeground(QColor('#008AE1'))
|
||||
|
||||
QApplication.instance().ActivatedItemIndex = str(index)
|
||||
else:
|
||||
for column in range(self.columnCount()):
|
||||
serverItem = self.item(int(index), column)
|
||||
|
||||
if serverItem is None:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
serverFont = serverItem.font()
|
||||
serverFont.setBold(False)
|
||||
|
||||
serverItem.setFont(serverFont)
|
||||
serverItem.setForeground(QBrush())
|
||||
APP().ActivatedItemIndex = str(index)
|
||||
|
||||
def setCurrentItemByIndex(self, index):
|
||||
self.setCurrentItem(self.item(index, 0))
|
||||
@@ -1322,7 +1262,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
swapListItem(self.ServerList, index0, index1)
|
||||
swapListItem(self.ScrollList, index0, index1)
|
||||
|
||||
# Storage sync is done by caller!!!
|
||||
# Server storage sync is done by caller!!!
|
||||
|
||||
self.flushRow(index0, self.ServerList[index0])
|
||||
self.flushRow(index1, self.ServerList[index1])
|
||||
@@ -1362,7 +1302,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.swapItem(index, index - 1)
|
||||
|
||||
def moveUpSelectedItem(self):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -1382,7 +1322,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.moveUpItemByIndex(index)
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
# Don't use setCurrentItemXXX since it will
|
||||
# trigger handleCurrentItemChanged. Just switch context
|
||||
@@ -1405,7 +1345,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.swapItem(index, index + 1)
|
||||
|
||||
def moveDownSelectedItem(self):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -1425,7 +1365,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.moveDownItemByIndex(index)
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
# Don't use setCurrentItemXXX since it will
|
||||
# trigger handleCurrentItemChanged. Just switch context
|
||||
@@ -1441,7 +1381,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
)
|
||||
|
||||
def duplicateSelectedItem(self):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -1454,15 +1394,13 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
|
||||
for index in indexes:
|
||||
if 0 <= index < len(self.ServerList):
|
||||
QApplication.instance().MainWidget.importServer(
|
||||
**self.ServerList[index]
|
||||
)
|
||||
APP().MainWidget.importServer(**self.ServerList[index])
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
def deleteSelectedItem(self):
|
||||
if QApplication.instance().MainWidget.modified:
|
||||
if APP().MainWidget.modified:
|
||||
self.saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
@@ -1499,18 +1437,16 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
self.ScrollList.pop(takedRow)
|
||||
|
||||
if not takedActivated and takedRow < self.activatedItemIndex:
|
||||
QApplication.instance().ActivatedItemIndex = str(
|
||||
self.activatedItemIndex - 1
|
||||
)
|
||||
APP().ActivatedItemIndex = str(self.activatedItemIndex - 1)
|
||||
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
if takedActivated:
|
||||
if QApplication.instance().tray.ConnectAction.isConnected():
|
||||
if APP().tray.ConnectAction.isConnected():
|
||||
# Trigger disconnect
|
||||
QApplication.instance().tray.ConnectAction.trigger()
|
||||
QApplication.instance().ActivatedItemIndex = str(-1)
|
||||
APP().tray.ConnectAction.trigger()
|
||||
APP().ActivatedItemIndex = str(-1)
|
||||
|
||||
# Don't use setCurrentItemXXX since the
|
||||
# selection may not be changed. Just switch context
|
||||
@@ -1563,15 +1499,12 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
|
||||
self.saveScrollBarValue(prevRow)
|
||||
|
||||
if (
|
||||
QApplication.instance().MainWidget is not None
|
||||
and QApplication.instance().MainWidget.modified
|
||||
):
|
||||
if APP().MainWidget is not None and APP().MainWidget.modified:
|
||||
choice = self.questionSaveBox.exec()
|
||||
|
||||
if choice == MessageBox.ButtonRole.AcceptRole.value:
|
||||
# Save
|
||||
if QApplication.instance().MainWidget.SaveAsServerAction.save(
|
||||
if APP().MainWidget.SaveAsServerAction.save(
|
||||
successCallback=lambda: self.switchContext(currRow),
|
||||
):
|
||||
pass
|
||||
@@ -1583,7 +1516,7 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
# Discard
|
||||
self.switchContext(currRow)
|
||||
|
||||
QApplication.instance().MainWidget.markAsSaved() # Fake saved
|
||||
APP().MainWidget.markAsSaved() # Fake saved
|
||||
|
||||
elif choice == MessageBox.ButtonRole.RejectRole.value:
|
||||
# Cancel. Do not switch
|
||||
@@ -1606,15 +1539,12 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
# Same item activated. Do nothing
|
||||
return
|
||||
|
||||
if (
|
||||
QApplication.instance().MainWidget is not None
|
||||
and QApplication.instance().MainWidget.modified
|
||||
):
|
||||
if APP().MainWidget is not None and APP().MainWidget.modified:
|
||||
self.saveChangeFirst.exec()
|
||||
|
||||
return
|
||||
|
||||
if QApplication.instance().tray.ConnectAction.isConnecting():
|
||||
if APP().tray.ConnectAction.isConnecting():
|
||||
# Show the MessageBox asynchronously
|
||||
self.progressWaitBox.open()
|
||||
|
||||
@@ -1629,8 +1559,8 @@ class NormalServerWidget(Translatable, SupportConnectedCallback, QTableWidget):
|
||||
# Activate
|
||||
self.activateItemByIndex(newIndex, activate=True)
|
||||
|
||||
if QApplication.instance().tray.ConnectAction.isConnected():
|
||||
QApplication.instance().tray.ConnectAction.reconnectAction()
|
||||
if APP().tray.ConnectAction.isConnected():
|
||||
APP().tray.ConnectAction.reconnectAction()
|
||||
|
||||
@QtCore.Slot(QtCore.QPoint)
|
||||
def handleCustomContextMenuRequested(self, point):
|
||||
@@ -1672,18 +1602,18 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
self.restoreErrorBox = MessageBox(parent=self)
|
||||
|
||||
try:
|
||||
self.StorageObj = Storage.toObject(QApplication.instance().Configuration)
|
||||
self.StorageObj = ServerStorage.toObject(APP().Configuration)
|
||||
# Check model. Shallow copy
|
||||
self.ServerList = self.StorageObj['model']
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
|
||||
self.StorageObj = Storage.init()
|
||||
self.StorageObj = ServerStorage.init()
|
||||
# Shallow copy
|
||||
self.ServerList = self.StorageObj['model']
|
||||
|
||||
# Clear it
|
||||
Storage.clear()
|
||||
ServerStorage.clear()
|
||||
|
||||
Configuration.corruptedError(self.restoreErrorBox)
|
||||
|
||||
@@ -1722,7 +1652,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
self.plainTextEdit.setStyleSheet(
|
||||
DraculaTheme.getStyleSheet(
|
||||
widgetName='QPlainTextEdit',
|
||||
fontFamily=QApplication.instance().customFontName,
|
||||
fontFamily=APP().customFontName,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1730,7 +1660,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
|
||||
try:
|
||||
font = self.plainTextEdit.font()
|
||||
font.setPointSize(int(QApplication.instance().EditorWidgetPointSize))
|
||||
font.setPointSize(int(APP().EditorWidgetPointSize))
|
||||
|
||||
self.plainTextEdit.setFont(font)
|
||||
except Exception:
|
||||
@@ -1849,6 +1779,13 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
],
|
||||
}
|
||||
|
||||
routingMenu = {
|
||||
'name': 'Routing',
|
||||
'actions': [
|
||||
RoutingAction(parent=self),
|
||||
],
|
||||
}
|
||||
|
||||
viewMenu = {
|
||||
'name': 'View',
|
||||
'actions': [
|
||||
@@ -1868,13 +1805,16 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
}
|
||||
|
||||
# Menus
|
||||
for menuDict in (fileMenu, editMenu, viewMenu, helpMenu):
|
||||
for menuDict in (fileMenu, editMenu, routingMenu, viewMenu, helpMenu):
|
||||
menuName = menuDict['name']
|
||||
menuObjName = f'_{menuName}Menu'
|
||||
menu = Menu(*menuDict['actions'], title=_(menuName), parent=self.menuBar())
|
||||
|
||||
for action in menuDict['actions']:
|
||||
if isinstance(action, Action):
|
||||
if hasattr(self, f'{action}'):
|
||||
logger.warning(f'{self} already has action {action}')
|
||||
|
||||
setattr(self, f'{action}', action)
|
||||
|
||||
# Set reference
|
||||
@@ -1886,10 +1826,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
self.setGeometry(
|
||||
100,
|
||||
100,
|
||||
*list(
|
||||
int(size)
|
||||
for size in QApplication.instance().MainWidgetWindowSize.split(',')
|
||||
),
|
||||
*list(int(size) for size in APP().MainWidgetWindowSize.split(',')),
|
||||
)
|
||||
except Exception:
|
||||
# Any non-exit exceptions
|
||||
@@ -1905,7 +1842,7 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
|
||||
if syncStorage:
|
||||
# Sync it
|
||||
Storage.sync()
|
||||
ServerStorage.sync()
|
||||
|
||||
row = self.normalServerWidget.rowCount()
|
||||
|
||||
@@ -1914,21 +1851,21 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
self.normalServerWidget.flushRow(row, server)
|
||||
|
||||
item = QTableWidgetItem(str(row + 1))
|
||||
item.setFont(QFont(QApplication.instance().customFontName))
|
||||
item.setFont(QFont(APP().customFontName))
|
||||
|
||||
if len(self.ServerList) == 1:
|
||||
# The first one. Click it
|
||||
self.normalServerWidget.setCurrentItemByIndex(0)
|
||||
|
||||
# Try to be user-friendly in some extreme cases
|
||||
if not QApplication.instance().tray.ConnectAction.isConnected():
|
||||
if not APP().tray.ConnectAction.isConnected():
|
||||
# Activate automatically
|
||||
self.normalServerWidget.activateItemByIndex(0)
|
||||
|
||||
def showTabAndSpacesIfNecessary(self):
|
||||
textOption = QTextOption()
|
||||
|
||||
if QApplication.instance().ShowTabAndSpacesInEditor == Switch.ON_:
|
||||
if APP().ShowTabAndSpacesInEditor == Switch.ON_:
|
||||
textOption.setFlags(QTextOption.Flag.ShowTabsAndSpaces)
|
||||
|
||||
self.plainTextEdit.document().setDefaultTextOption(textOption)
|
||||
@@ -2009,17 +1946,15 @@ class EditConfigurationWidget(Translatable, SupportConnectedCallback, QMainWindo
|
||||
self.questionSave()
|
||||
|
||||
def syncSettings(self):
|
||||
QApplication.instance().MainWidgetWindowSize = (
|
||||
APP().MainWidgetWindowSize = (
|
||||
f'{self.geometry().width()},{self.geometry().height()}'
|
||||
)
|
||||
QApplication.instance().ServerWidgetSectionSizeTable = ujson.dumps(
|
||||
APP().ServerWidgetSectionSizeTable = ujson.dumps(
|
||||
self.normalServerWidget.sectionSizeTable,
|
||||
ensure_ascii=False,
|
||||
escape_forward_slashes=False,
|
||||
)
|
||||
QApplication.instance().EditorWidgetPointSize = str(
|
||||
self.plainTextEdit.font().pointSize()
|
||||
)
|
||||
APP().EditorWidgetPointSize = str(self.plainTextEdit.font().pointSize())
|
||||
|
||||
def connectedCallback(self):
|
||||
self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg'))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Widget.Widget import Menu, MessageBox, ZoomableTextBrowser
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import (
|
||||
StateContext,
|
||||
SupportConnectedCallback,
|
||||
@@ -9,7 +10,11 @@ from Furious.Utility.Translator import Translatable, gettext as _
|
||||
from Furious.Utility.Theme import DraculaTheme
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication, QFileDialog, QMainWindow, QTextBrowser
|
||||
from PySide6.QtWidgets import QFileDialog, QMainWindow, QTextBrowser
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaveErrorBox(MessageBox):
|
||||
@@ -141,14 +146,14 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
|
||||
self.textBrowser.setStyleSheet(
|
||||
DraculaTheme.getStyleSheet(
|
||||
widgetName='QTextBrowser',
|
||||
fontFamily=QApplication.instance().customFontName,
|
||||
fontFamily=APP().customFontName,
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Restore point size
|
||||
font = self.textBrowser.font()
|
||||
font.setPointSize(int(QApplication.instance().ViewerWidgetPointSize))
|
||||
font.setPointSize(int(APP().ViewerWidgetPointSize))
|
||||
|
||||
self.textBrowser.setFont(font)
|
||||
except Exception:
|
||||
@@ -178,6 +183,9 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
|
||||
for menu in (fileMenuActions, editMenuActions, viewMenuActions):
|
||||
for action in menu:
|
||||
if isinstance(action, Action):
|
||||
if hasattr(self, f'{action}'):
|
||||
logger.warning(f'{self} already has action {action}')
|
||||
|
||||
setattr(self, f'{action}', action)
|
||||
|
||||
self._fileMenu = Menu(*fileMenuActions, title=_('File'), parent=self)
|
||||
@@ -192,9 +200,7 @@ class LogViewerWidget(Translatable, SupportConnectedCallback, QMainWindow):
|
||||
return self.textBrowser.toPlainText()
|
||||
|
||||
def syncSettings(self):
|
||||
QApplication.instance().ViewerWidgetPointSize = str(
|
||||
self.textBrowser.font().pointSize()
|
||||
)
|
||||
APP().ViewerWidgetPointSize = str(self.textBrowser.font().pointSize())
|
||||
|
||||
def closeEvent(self, event):
|
||||
event.ignore()
|
||||
|
||||
@@ -9,14 +9,23 @@ from Furious.Action import (
|
||||
)
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Widget.Widget import Menu
|
||||
from Furious.Utility.Constants import APPLICATION_NAME, APPLICATION_VERSION, PLATFORM
|
||||
from Furious.Utility.Constants import (
|
||||
APP,
|
||||
APPLICATION_NAME,
|
||||
APPLICATION_VERSION,
|
||||
PLATFORM,
|
||||
)
|
||||
from Furious.Utility.Utility import bootstrapIcon, StateContext, Switch
|
||||
from Furious.Utility.Translator import Translatable, gettext as _
|
||||
from Furious.Utility.Proxy import Proxy
|
||||
from Furious.Utility.StartupOnBoot import StartupOnBoot
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
|
||||
from PySide6.QtWidgets import QSystemTrayIcon
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SystemTrayIcon(Translatable, QSystemTrayIcon):
|
||||
@@ -41,6 +50,9 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
|
||||
|
||||
for action in actions:
|
||||
if isinstance(action, Action):
|
||||
if hasattr(self, f'{action}'):
|
||||
logger.warning(f'{self} already has action {action}')
|
||||
|
||||
setattr(self, f'{action}', action)
|
||||
|
||||
self._menu = Menu(*actions)
|
||||
@@ -48,11 +60,11 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
|
||||
self.setContextMenu(self._menu)
|
||||
|
||||
def bootstrap(self):
|
||||
if QApplication.instance().StartupOnBoot == Switch.ON_:
|
||||
if APP().StartupOnBoot == Switch.ON_:
|
||||
# Rrefresh startup application location
|
||||
StartupOnBoot.on_()
|
||||
|
||||
if QApplication.instance().Connect == Switch.ON_:
|
||||
if APP().Connect == Switch.ON_:
|
||||
# Trigger connect action
|
||||
self.ConnectAction.trigger()
|
||||
|
||||
@@ -77,7 +89,7 @@ class SystemTrayIcon(Translatable, QSystemTrayIcon):
|
||||
@QtCore.Slot(QSystemTrayIcon.ActivationReason)
|
||||
def handleActivated(self, reason):
|
||||
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
|
||||
QApplication.instance().MainWidget.show()
|
||||
APP().MainWidget.show()
|
||||
|
||||
def setApplicationToolTip(self):
|
||||
self.setToolTip(f'{_(APPLICATION_NAME)} {APPLICATION_VERSION}')
|
||||
|
||||
+132
-2
@@ -1,4 +1,5 @@
|
||||
from Furious.Gui.Action import Action, Seperator
|
||||
from Furious.Utility.Constants import APP
|
||||
from Furious.Utility.Utility import (
|
||||
StateContext,
|
||||
SupportConnectedCallback,
|
||||
@@ -8,16 +9,53 @@ from Furious.Utility.Utility import (
|
||||
from Furious.Utility.Translator import Translatable, gettext as _
|
||||
|
||||
from PySide6 import QtCore
|
||||
from PySide6.QtGui import QBrush, QColor, QFont
|
||||
from PySide6.QtWidgets import (
|
||||
QHeaderView,
|
||||
QInputDialog,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QPlainTextEdit,
|
||||
QPushButton,
|
||||
QStyledItemDelegate,
|
||||
QTableWidget,
|
||||
QTabWidget,
|
||||
QTextBrowser,
|
||||
)
|
||||
|
||||
|
||||
class HeaderView(SupportConnectedCallback, QHeaderView):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.setSectionsClickable(True)
|
||||
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #008AE1; }}'
|
||||
)
|
||||
|
||||
def connectedCallback(self):
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #EB212E; }}'
|
||||
)
|
||||
|
||||
def disconnectedCallback(self):
|
||||
self.setStyleSheet(
|
||||
f'QHeaderView::section:hover {{ background-color: #008AE1; }}'
|
||||
)
|
||||
|
||||
|
||||
class ListWidget(QListWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def selectedIndex(self):
|
||||
return sorted(list(set(index.row() for index in self.selectedIndexes())))
|
||||
|
||||
|
||||
class Menu(Translatable, QMenu):
|
||||
def __init__(self, *actions, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -99,19 +137,111 @@ class PushButton(Translatable, QPushButton):
|
||||
self.setText(_(self.text()))
|
||||
|
||||
|
||||
class StyledItemDelegate(QStyledItemDelegate):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QLineEdit(parent)
|
||||
editor.setFont(QFont(APP().customFontName))
|
||||
|
||||
return editor
|
||||
|
||||
|
||||
class TableWidget(QTableWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def selectedIndex(self):
|
||||
return sorted(list(set(index.row() for index in self.selectedIndexes())))
|
||||
|
||||
def setSelectionColor(self, color):
|
||||
self.setStyleSheet(f'QTableWidget {{ selection-background-color: {color}; }}')
|
||||
|
||||
def activateItemByIndex(self, index, activate=True):
|
||||
if activate:
|
||||
for column in range(self.columnCount()):
|
||||
item = self.item(int(index), column)
|
||||
|
||||
if item is None:
|
||||
# Do nothing
|
||||
continue
|
||||
|
||||
font = item.font()
|
||||
font.setBold(True)
|
||||
|
||||
item.setFont(font)
|
||||
|
||||
if APP().tray is None:
|
||||
# Initializing
|
||||
item.setForeground(QColor('#008AE1'))
|
||||
else:
|
||||
if APP().tray.ConnectAction.isConnected():
|
||||
item.setForeground(QColor('#EB212E'))
|
||||
else:
|
||||
item.setForeground(QColor('#008AE1'))
|
||||
else:
|
||||
for column in range(self.columnCount()):
|
||||
item = self.item(int(index), column)
|
||||
|
||||
if item is None:
|
||||
# Do nothing
|
||||
continue
|
||||
|
||||
font = item.font()
|
||||
font.setBold(False)
|
||||
|
||||
item.setFont(font)
|
||||
item.setForeground(QBrush())
|
||||
|
||||
|
||||
class TabWidget(Translatable, QTabWidget):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def retranslate(self):
|
||||
for i in range(self.tabBar().count()):
|
||||
self.tabBar().setTabText(i, _(self.tabBar().tabText(i)))
|
||||
for index in range(self.count()):
|
||||
self.setTabText(index, _(self.tabText(index)))
|
||||
|
||||
|
||||
class ZoomablePlainTextEdit(QPlainTextEdit):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def getIndent(line):
|
||||
indent = ''
|
||||
|
||||
for char in line:
|
||||
if char.isspace():
|
||||
indent += char
|
||||
else:
|
||||
break
|
||||
|
||||
return indent
|
||||
|
||||
def smartIndent(self, event):
|
||||
cursor = self.textCursor()
|
||||
indent = self.getIndent(cursor.block().text())
|
||||
|
||||
# Do newline action
|
||||
super().keyPressEvent(event)
|
||||
|
||||
# Add last line indent
|
||||
cursor.insertText(indent)
|
||||
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def keyPressEvent(self, event):
|
||||
if (
|
||||
event.key() == QtCore.Qt.Key.Key_Return
|
||||
or event.key() == QtCore.Qt.Key.Key_Enter
|
||||
):
|
||||
self.smartIndent(event)
|
||||
else:
|
||||
super().keyPressEvent(event)
|
||||
|
||||
def wheelEvent(self, event):
|
||||
if event.modifiers() == QtCore.Qt.KeyboardModifier.ControlModifier:
|
||||
delta = event.angleDelta().y()
|
||||
|
||||
Reference in New Issue
Block a user