Files
LorenEteval_Furious/Furious/Window/AppMainWindow.py
2025-03-20 00:22:32 +08:00

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()}',
)