mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-25 16:28:08 +03:00
473 lines
15 KiB
Python
473 lines
15 KiB
Python
# 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.Library import *
|
|
from Furious.Utility import *
|
|
from Furious.Widget.UserServersQTableWidget import *
|
|
from Furious.Window.UserSubsWindow import *
|
|
from Furious.Window.LogViewerWindow import *
|
|
from Furious.Window.XrayAssetViewerWindow import *
|
|
|
|
from PySide6 import QtCore
|
|
from PySide6.QtGui import *
|
|
from PySide6.QtWidgets import *
|
|
from PySide6.QtNetwork import *
|
|
|
|
from typing import Union
|
|
|
|
import os
|
|
import logging
|
|
import functools
|
|
|
|
__all__ = ['AppMainWindow']
|
|
|
|
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):
|
|
parent = self.parent()
|
|
|
|
if isinstance(parent, AppMainWindow):
|
|
parent.setNetworkState(True)
|
|
|
|
def errorCallback(self, errorString: str):
|
|
parent = self.parent()
|
|
|
|
if isinstance(parent, AppMainWindow):
|
|
parent.setNetworkState(False, errorString=errorString)
|
|
|
|
def startSingleTest(self):
|
|
if not APP().isSystemTrayConnected():
|
|
parent = self.parent()
|
|
|
|
if isinstance(parent, AppMainWindow):
|
|
parent.resetNetworkState()
|
|
|
|
self.stopTest()
|
|
|
|
httpProxyEndpoint = connectedHttpProxyEndpoint()
|
|
|
|
if httpProxyEndpoint is None:
|
|
parent = self.parent()
|
|
|
|
if isinstance(parent, AppMainWindow):
|
|
parent.resetNetworkState()
|
|
else:
|
|
self.configureHttpProxy(httpProxyEndpoint)
|
|
|
|
super().startSingleTest()
|
|
|
|
def disconnectedCallback(self):
|
|
parent = self.parent()
|
|
|
|
if isinstance(parent, AppMainWindow):
|
|
parent.resetNetworkState()
|
|
|
|
self.stopTest()
|
|
|
|
|
|
class AppMainWindow(AppQMainWindow):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setWindowTitle(_(APPLICATION_NAME))
|
|
self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
|
|
|
self.updatesManager = UpdatesManager()
|
|
self.networkStateManager = AppNetworkStateManager(parent=self)
|
|
|
|
self.userServersQTableWidget = UserServersQTableWidget(parent=self)
|
|
self.userSubsWindow = UserSubsWindow(
|
|
deleteUniqueCallback=lambda unique: self.userServersQTableWidget.deleteItemByIndex(
|
|
list(
|
|
index
|
|
for index, server in enumerate(AS_UserServers())
|
|
if server.getExtras('subsId') == unique
|
|
)
|
|
),
|
|
)
|
|
self.xrayAssetViewerWindow = XrayAssetViewerWindow()
|
|
|
|
self.mainTab = AppQTabWidget()
|
|
self.mainTab.addTab(self.userServersQTableWidget, _('Server'))
|
|
|
|
logActions = [
|
|
AppQAction(
|
|
_('Show Furious Log'),
|
|
callback=lambda: loggerApp_().showMaximized(),
|
|
shortcut=QtCore.QKeyCombination(
|
|
QtCore.Qt.KeyboardModifier.ControlModifier
|
|
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
|
QtCore.Qt.Key.Key_F,
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Show Core Log'),
|
|
callback=lambda: loggerCore().showMaximized(),
|
|
shortcut=QtCore.QKeyCombination(
|
|
QtCore.Qt.KeyboardModifier.ControlModifier
|
|
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
|
QtCore.Qt.Key.Key_C,
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Show Tun2socks Log'),
|
|
callback=lambda: loggerTun_().showMaximized(),
|
|
shortcut=QtCore.QKeyCombination(
|
|
QtCore.Qt.KeyboardModifier.ControlModifier
|
|
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
|
QtCore.Qt.Key.Key_T,
|
|
),
|
|
),
|
|
]
|
|
|
|
serverActions = [
|
|
AppQAction(
|
|
_('Add VMess Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.VMess, _('Add VMess Server...')
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Add VLESS Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.VLESS, _('Add VLESS Server...')
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Add Shadowsocks Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.Shadowsocks, _('Add Shadowsocks Server...')
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Add Trojan Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.Trojan, _('Add Trojan Server...')
|
|
),
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Add Hysteria1 Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.Hysteria1, _('Add Hysteria1 Server...')
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Add Hysteria2 Server...'),
|
|
callback=lambda: self.userServersQTableWidget.addServerViaGui(
|
|
Protocol.Hysteria2, _('Add Hysteria2 Server...')
|
|
),
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('New Empty Configuration'),
|
|
callback=lambda: self.userServersQTableWidget.newEmptyItem(),
|
|
),
|
|
]
|
|
|
|
subsActions = [
|
|
AppQAction(
|
|
_('Update Subscription (Use Current Proxy)'),
|
|
callback=lambda: self.userServersQTableWidget.updateSubs(
|
|
connectedHttpProxyEndpoint()
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Update Subscription (Force Proxy)'),
|
|
callback=lambda: self.userServersQTableWidget.updateSubs(
|
|
'127.0.0.1:10809'
|
|
),
|
|
),
|
|
AppQAction(
|
|
_('Update Subscription (No Proxy)'),
|
|
callback=lambda: self.userServersQTableWidget.updateSubs(None),
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Edit Subscription...'),
|
|
icon=bootstrapIcon('star.svg'),
|
|
callback=lambda: self.userSubsWindow.show(),
|
|
),
|
|
]
|
|
|
|
if PLATFORM == 'Darwin':
|
|
openAppFolderAction = [None]
|
|
else:
|
|
openAppFolderAction = [
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Open Application Folder'),
|
|
callback=lambda: self.openApplicationFolder(),
|
|
),
|
|
]
|
|
|
|
toolsActions = [
|
|
AppQAction(
|
|
_('Manage Xray-core Asset File...'),
|
|
callback=lambda: self.xrayAssetViewerWindow.show(),
|
|
),
|
|
*openAppFolderAction,
|
|
]
|
|
|
|
if hasattr(AppQAction, 'setMenu'):
|
|
self.toolbar = AppQToolBar(
|
|
AppQAction(
|
|
_('Log'),
|
|
icon=bootstrapIcon('pin-angle.svg'),
|
|
menu=AppQMenu(*logActions),
|
|
useActionGroup=False,
|
|
checkable=False,
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Server'),
|
|
icon=bootstrapIcon('database.svg'),
|
|
menu=AppQMenu(*serverActions),
|
|
useActionGroup=False,
|
|
checkable=False,
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Subscription'),
|
|
icon=bootstrapIcon('collection.svg'),
|
|
menu=AppQMenu(*subsActions),
|
|
useActionGroup=False,
|
|
checkable=False,
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Tools'),
|
|
icon=bootstrapIcon('tools.svg'),
|
|
menu=AppQMenu(*toolsActions),
|
|
useActionGroup=False,
|
|
checkable=False,
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('Check For Updates'),
|
|
icon=bootstrapIcon('download.svg'),
|
|
checkable=False,
|
|
callback=lambda: self.checkForUpdates(),
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('About'),
|
|
icon=bootstrapIcon('info-circle.svg'),
|
|
checkable=False,
|
|
callback=lambda: self.openAboutPage(),
|
|
),
|
|
)
|
|
self.toolbar.setIconSize(QtCore.QSize(64, 32))
|
|
self.toolbar.setToolButtonStyle(
|
|
QtCore.Qt.ToolButtonStyle.ToolButtonTextUnderIcon
|
|
)
|
|
self.addToolBar(self.toolbar)
|
|
else:
|
|
# Menu actions
|
|
logMenu = {
|
|
'name': 'Log',
|
|
'actions': [*logActions],
|
|
}
|
|
|
|
serverMenu = {
|
|
'name': 'Server',
|
|
'actions': [*serverActions],
|
|
}
|
|
|
|
subsMenu = {
|
|
'name': 'Subscription',
|
|
'actions': [*subsActions],
|
|
}
|
|
|
|
toolsMenu = {
|
|
'name': 'Tools',
|
|
'actions': [*toolsActions],
|
|
}
|
|
|
|
helpMenu = {
|
|
'name': 'Help',
|
|
'actions': [
|
|
AppQAction(
|
|
_('Check For Updates'),
|
|
icon=bootstrapIcon('download.svg'),
|
|
checkable=False,
|
|
callback=lambda: self.checkForUpdates(),
|
|
),
|
|
AppQSeperator(),
|
|
AppQAction(
|
|
_('About'),
|
|
icon=bootstrapIcon('info-circle.svg'),
|
|
checkable=False,
|
|
callback=lambda: self.openAboutPage(),
|
|
),
|
|
],
|
|
}
|
|
|
|
# Corresponds to menus defined above
|
|
TRANSLATABLE_MENU_NAME = [
|
|
_('Log'),
|
|
_('Server'),
|
|
_('Subscription'),
|
|
_('Tools'),
|
|
_('Help'),
|
|
]
|
|
|
|
# Menus
|
|
for menuDict in (logMenu, serverMenu, subsMenu, toolsMenu, helpMenu):
|
|
menuName = menuDict['name']
|
|
menuObjName = f'_{menuName}Menu'
|
|
menu = AppQMenu(
|
|
*menuDict['actions'], title=_(menuName), parent=self.menuBar()
|
|
)
|
|
|
|
# Set reference
|
|
setattr(self, menuObjName, menu)
|
|
|
|
self.menuBar().addMenu(menu)
|
|
|
|
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu)
|
|
|
|
# TODO: Custom status tip
|
|
# self.setStatusBar(QStatusBar(self))
|
|
|
|
self.networkState = AppQLabel(translatable=False)
|
|
|
|
self.statusBar().addPermanentWidget(self.networkState)
|
|
|
|
self._widget = QWidget()
|
|
self._layout = QVBoxLayout(self._widget)
|
|
self._layout.addWidget(self.mainTab)
|
|
|
|
self.setCentralWidget(self._widget)
|
|
|
|
def appendNewItemByFactory(self, factory: ConfigurationFactory):
|
|
self.userServersQTableWidget.appendNewItemByFactory(factory)
|
|
|
|
def flushRow(self, row: int, item: ConfigurationFactory):
|
|
self.userServersQTableWidget.flushRow(row, item)
|
|
|
|
def showTabAndSpaces(self):
|
|
self.userServersQTableWidget.showTabAndSpaces()
|
|
|
|
def hideTabAndSpaces(self):
|
|
self.userServersQTableWidget.hideTabAndSpaces()
|
|
|
|
def checkForUpdates(self):
|
|
self.updatesManager.configureHttpProxy(connectedHttpProxyEndpoint())
|
|
self.updatesManager.checkForUpdates()
|
|
|
|
def resetNetworkState(self):
|
|
self.networkState.setText('')
|
|
|
|
def setNetworkState(self, success: bool, **kwargs):
|
|
remark = connectedRemark()
|
|
|
|
if success:
|
|
if remark:
|
|
self.networkState.setText(f'{remark} {UNICODE_LARGE_GREEN_CIRCLE}')
|
|
else:
|
|
self.resetNetworkState()
|
|
else:
|
|
errorString = kwargs.pop('errorString', '')
|
|
|
|
if remark:
|
|
self.networkState.setText(
|
|
f'{remark} - {errorString} {UNICODE_LARGE_RED_CIRCLE}'
|
|
)
|
|
else:
|
|
self.resetNetworkState()
|
|
|
|
@staticmethod
|
|
def openApplicationFolder():
|
|
appFolder = os.path.dirname(APP().applicationFilePath())
|
|
|
|
if QDesktopServices.openUrl(QtCore.QUrl(appFolder)):
|
|
logger.info(f'open application folder \'{appFolder}\' success')
|
|
else:
|
|
logger.error(f'open application folder \'{appFolder}\' failed')
|
|
|
|
@staticmethod
|
|
def openAboutPage():
|
|
if QDesktopServices.openUrl(QtCore.QUrl(APPLICATION_ABOUT_PAGE)):
|
|
logger.info('open about page success')
|
|
else:
|
|
logger.error('open about page failed')
|
|
|
|
def setWidthAndHeight(self):
|
|
try:
|
|
windowSize = AppSettings.get('ServerWidgetWindowSize').split(',')
|
|
|
|
self.setGeometry(100, 100, *list(int(size) for size in windowSize))
|
|
except Exception:
|
|
# Any non-exit exceptions
|
|
|
|
self.setGeometry(100, 100, 1800, 960)
|
|
|
|
def cleanup(self):
|
|
AppSettings.set(
|
|
'ServerWidgetWindowSize',
|
|
f'{self.geometry().width()},{self.geometry().height()}',
|
|
)
|