mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-26 16:58:17 +03:00
Note: 1. some shortcuts are not working 2. copy & paste not working, clipboard may not be useable Signed-off-by: Loren Eteval <loren.eteval@proton.me>
588 lines
20 KiB
Python
588 lines
20 KiB
Python
# Copyright (C) 2024–present Loren Eteval & contributors <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.Frozenlib import *
|
||
from Furious.Interface import *
|
||
from Furious.Library import *
|
||
from Furious.Qt import *
|
||
from Furious.Qt import gettext as _
|
||
from Furious.Utility import *
|
||
from Furious.Widget.UserServersQTableWidget import *
|
||
from Furious.Widget.GuiTUNSettings import *
|
||
from Furious.Window.UserSubsWindow import *
|
||
from Furious.Window.XrayAssetViewerWindow import *
|
||
|
||
from PySide6 import QtCore
|
||
from PySide6.QtGui import *
|
||
from PySide6.QtWidgets import *
|
||
|
||
from typing import Union
|
||
|
||
import os
|
||
import logging
|
||
import functools
|
||
|
||
__all__ = ['AppMainWindow']
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Migrate legacy settings
|
||
registerAppSettings('ServerWidgetWindowSize')
|
||
registerAppSettings('AppMainWindowGeometry')
|
||
registerAppSettings('AppMainWindowState')
|
||
|
||
|
||
class AppNetworkStateManager(NetworkStateManager):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
|
||
def successCallback(self, networkReply, **kwargs):
|
||
parent = self.parent()
|
||
|
||
if isinstance(parent, AppMainWindow):
|
||
parent.setNetworkState(True)
|
||
|
||
super().successCallback(networkReply, **kwargs)
|
||
|
||
def failureCallback(self, networkReply, **kwargs):
|
||
parent = self.parent()
|
||
|
||
if isinstance(parent, AppMainWindow):
|
||
parent.setNetworkState(False, errorString=networkReply.errorString())
|
||
|
||
super().failureCallback(networkReply, **kwargs)
|
||
|
||
def startSingleTest(self):
|
||
if not APP().isSystemTrayConnected():
|
||
parent = self.parent()
|
||
|
||
if isinstance(parent, AppMainWindow):
|
||
parent.resetNetworkState()
|
||
|
||
self.stopTest()
|
||
|
||
connectedHttpProxy = Storage.Extras.UserHttpProxy()
|
||
|
||
if connectedHttpProxy is None:
|
||
parent = self.parent()
|
||
|
||
if isinstance(parent, AppMainWindow):
|
||
parent.resetNetworkState()
|
||
else:
|
||
self.configureHttpProxy(connectedHttpProxy)
|
||
|
||
super().startSingleTest()
|
||
|
||
def disconnectedCallback(self):
|
||
parent = self.parent()
|
||
|
||
if isinstance(parent, AppMainWindow):
|
||
parent.resetNetworkState()
|
||
|
||
super().disconnectedCallback()
|
||
|
||
|
||
class AppMainWindow(AppQMainWindow):
|
||
DEFAULT_WINDOW_SIZE_DARWIN = QtCore.QSize(1500, 780)
|
||
DEFAULT_WINDOW_SIZE = (
|
||
QtCore.QSize(1800, 960) if PLATFORM != 'Darwin' else DEFAULT_WINDOW_SIZE_DARWIN
|
||
)
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
if SystemRuntime.isAdmin():
|
||
self.setWindowTitle(f'{_(APPLICATION_NAME)} ({_(ADMINISTRATOR_NAME)})')
|
||
else:
|
||
self.setWindowTitle(f'{_(APPLICATION_NAME)}')
|
||
# TODO: Need this?
|
||
# self.setAttribute(QtCore.Qt.WidgetAttribute.WA_ShowWithoutActivating)
|
||
|
||
self.updatesManager = UpdatesManager(parent=self)
|
||
self.networkStateManager = AppNetworkStateManager(parent=self)
|
||
|
||
self.userServersQTableWidget = UserServersQTableWidget(parent=self)
|
||
self.userSubsWindow = UserSubsWindow(
|
||
parent=self,
|
||
deleteUniqueCallback=lambda unique: self.userServersQTableWidget.deleteItemByIndex(
|
||
list(
|
||
index
|
||
for index, server in enumerate(Storage.UserServers())
|
||
if server.getExtras('subsId') == unique
|
||
)
|
||
),
|
||
)
|
||
self.xrayAssetViewerWindow = XrayAssetViewerWindow(parent=self)
|
||
|
||
self.mainTab = AppQTabWidget()
|
||
self.mainTab.addTab(self.userServersQTableWidget, _('Server'))
|
||
|
||
logActions = [
|
||
AppQAction(
|
||
_('Show Furious Log...'),
|
||
callback=lambda: AppLoggerWindow.Self().showMaximized(),
|
||
shortcut=QtCore.QKeyCombination(
|
||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
||
QtCore.Qt.Key.Key_F,
|
||
),
|
||
),
|
||
AppQAction(
|
||
_('Show Core Log...'),
|
||
callback=lambda: AppLoggerWindow.Core().showMaximized(),
|
||
shortcut=QtCore.QKeyCombination(
|
||
QtCore.Qt.KeyboardModifier.ControlModifier
|
||
| QtCore.Qt.KeyboardModifier.ShiftModifier,
|
||
QtCore.Qt.Key.Key_C,
|
||
),
|
||
),
|
||
AppQAction(
|
||
_('Show Tun2socks Log...'),
|
||
callback=lambda: AppLoggerWindow.TUN_().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(
|
||
Storage.Extras.UserHttpProxy(),
|
||
parent=self,
|
||
),
|
||
),
|
||
AppQAction(
|
||
_('Update Subscription (Force Proxy)'),
|
||
callback=lambda: self.userServersQTableWidget.updateSubs(
|
||
'127.0.0.1:10809',
|
||
parent=self,
|
||
),
|
||
),
|
||
AppQAction(
|
||
_('Update Subscription (No Proxy)'),
|
||
callback=lambda: self.userServersQTableWidget.updateSubs(
|
||
None,
|
||
parent=self,
|
||
),
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('Edit Subscription...'),
|
||
icon=bootstrapIcon('star.svg'),
|
||
callback=lambda: self.userSubsWindow.show(),
|
||
),
|
||
]
|
||
|
||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||
customizeTUNSettingsAction = [
|
||
AppQAction(
|
||
_('Customize TUN Settings...'),
|
||
icon=bootstrapIcon('diagram-3.svg'),
|
||
checkable=False,
|
||
callback=lambda: self.getGuiTUNSettings().open(),
|
||
),
|
||
AppQSeperator(),
|
||
]
|
||
else:
|
||
customizeTUNSettingsAction = []
|
||
|
||
if PLATFORM == 'Windows' or PLATFORM == 'Darwin':
|
||
_TRANSLATABLE_RESTART_AS_ADMIN = [
|
||
_('Restart The Application As Administrator'),
|
||
_('Restart The Application As Superuser'),
|
||
]
|
||
|
||
restartAsAdminAction = [
|
||
AppQAction(
|
||
_(f'Restart The Application As {ADMINISTRATOR_NAME}'),
|
||
icon=bootstrapIcon('arrow-clockwise.svg'),
|
||
checkable=False,
|
||
callback=lambda: self.restartAsAdmin(),
|
||
),
|
||
]
|
||
else:
|
||
restartAsAdminAction = []
|
||
|
||
if PLATFORM == 'Darwin':
|
||
openAppFolderAction = [
|
||
AppQSeperator(),
|
||
]
|
||
else:
|
||
openAppFolderAction = [
|
||
AppQAction(
|
||
_('Open Application Folder'),
|
||
icon=bootstrapIcon('folder2.svg'),
|
||
checkable=False,
|
||
callback=lambda: self.openApplicationFolder(),
|
||
),
|
||
AppQSeperator(),
|
||
]
|
||
|
||
toolsActions = [
|
||
*customizeTUNSettingsAction,
|
||
*restartAsAdminAction,
|
||
*openAppFolderAction,
|
||
AppQAction(
|
||
_('Manage Xray-core Asset File...'),
|
||
callback=lambda: self.xrayAssetViewerWindow.show(),
|
||
),
|
||
]
|
||
|
||
if hasattr(AppQAction, 'setMenu'):
|
||
self.toolbar = AppQToolBar(
|
||
AppQAction(
|
||
_('Log'),
|
||
icon=bootstrapIcon('pin-angle.svg'),
|
||
menu=AppQMenu(*logActions),
|
||
useSetMenu=False,
|
||
useActionGroup=False,
|
||
checkable=False,
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('Server'),
|
||
icon=bootstrapIcon('database.svg'),
|
||
menu=AppQMenu(*serverActions),
|
||
useSetMenu=False,
|
||
useActionGroup=False,
|
||
checkable=False,
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('Subscription'),
|
||
icon=bootstrapIcon('collection.svg'),
|
||
menu=AppQMenu(*subsActions),
|
||
useSetMenu=False,
|
||
useActionGroup=False,
|
||
checkable=False,
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('Tools'),
|
||
icon=bootstrapIcon('tools.svg'),
|
||
menu=AppQMenu(*toolsActions),
|
||
useSetMenu=False,
|
||
useActionGroup=False,
|
||
checkable=False,
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('Check For Updates'),
|
||
icon=bootstrapIcon('download.svg'),
|
||
checkable=False,
|
||
callback=lambda: self.checkForUpdates(parent=self),
|
||
),
|
||
AppQSeperator(),
|
||
AppQAction(
|
||
_('About'),
|
||
icon=bootstrapIcon('info-circle.svg'),
|
||
checkable=False,
|
||
callback=lambda: self.openAboutPage(),
|
||
),
|
||
)
|
||
self.toolbar.setObjectName('AppMainWindow_AppQToolBar')
|
||
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(parent=self),
|
||
),
|
||
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 updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs):
|
||
self.userServersQTableWidget.updateSubsByUnique(unique, httpProxy, **kwargs)
|
||
|
||
def appendNewItemByFactory(self, factory: ConfigFactory):
|
||
self.userServersQTableWidget.appendNewItemByFactory(factory)
|
||
|
||
def flushRow(self, row: int, item: ConfigFactory):
|
||
self.userServersQTableWidget.flushRow(row, item)
|
||
|
||
def showTabAndSpaces(self):
|
||
self.userServersQTableWidget.showTabAndSpaces()
|
||
|
||
def hideTabAndSpaces(self):
|
||
self.userServersQTableWidget.hideTabAndSpaces()
|
||
|
||
def getGuiTUNSettings(self, **kwargs):
|
||
@functools.lru_cache(None)
|
||
def cachedGuiTUNSettings():
|
||
parent = kwargs.pop('parent', self)
|
||
|
||
return GuiTUNSettings(parent=parent, **kwargs)
|
||
|
||
guiTUNSettings = cachedGuiTUNSettings()
|
||
guiTUNSettings.factoryToInput(Storage.UserTUNSettings())
|
||
|
||
return guiTUNSettings
|
||
|
||
def checkForUpdates(self, **kwargs):
|
||
self.updatesManager.configureHttpProxy(Storage.Extras.UserHttpProxy())
|
||
self.updatesManager.checkForUpdates(**kwargs)
|
||
|
||
def resetNetworkState(self):
|
||
self.networkState.setText('')
|
||
|
||
def setNetworkState(self, success: bool, **kwargs):
|
||
remark = Storage.Extras.UserServerRemark()
|
||
|
||
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 restartAsAdmin():
|
||
if not SystemRuntime.isScriptMode():
|
||
if not SystemRuntime.isAdmin():
|
||
process = QtCore.QProcess()
|
||
|
||
if PLATFORM == 'Windows':
|
||
process.startDetached(
|
||
'powershell',
|
||
arguments=[
|
||
'-Command',
|
||
f'Start-Process \'{APP().applicationFilePath()}\' '
|
||
f'\'{AppCommands.RunAs.value}\' -Verb runAs',
|
||
],
|
||
)
|
||
elif PLATFORM == 'Darwin':
|
||
process.startDetached(
|
||
'osascript',
|
||
arguments=[
|
||
'-e',
|
||
f'do shell script \"{APP().applicationFilePath()} '
|
||
f'{AppCommands.RunAs.value}\" with administrator privileges',
|
||
],
|
||
)
|
||
else:
|
||
mbox = AppQMessageBox(icon=AppQMessageBox.Icon.Information)
|
||
mbox.setWindowTitle(_(APPLICATION_NAME))
|
||
mbox.setText(_('I am, the supreme authority.'))
|
||
|
||
# Show the MessageBox asynchronously
|
||
mbox.open()
|
||
|
||
logger.info('ignored request to restart as admin as already is')
|
||
else:
|
||
logger.info(f'ignored request to restart as admin in script mode')
|
||
|
||
@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):
|
||
if AppSettings.get('AppMainWindowGeometry') is None:
|
||
# Migrate legacy settings
|
||
try:
|
||
windowSize = AppSettings.get('ServerWidgetWindowSize').split(',')
|
||
|
||
width, height = tuple(int(size) for size in windowSize)
|
||
|
||
if (width, height) == (640, 480):
|
||
self.resize(AppMainWindow.DEFAULT_WINDOW_SIZE)
|
||
else:
|
||
self.resize(width, height)
|
||
except Exception:
|
||
# Any non-exit exceptions
|
||
|
||
self.resize(AppMainWindow.DEFAULT_WINDOW_SIZE)
|
||
else:
|
||
try:
|
||
self.restoreGeometry(AppSettings.get('AppMainWindowGeometry'))
|
||
except Exception:
|
||
# Any non-exit exceptions
|
||
|
||
self.resize(AppMainWindow.DEFAULT_WINDOW_SIZE)
|
||
|
||
try:
|
||
self.restoreState(AppSettings.get('AppMainWindowState'))
|
||
except Exception:
|
||
# Any non-exit exceptions
|
||
|
||
pass
|
||
|
||
if PLATFORM == 'Darwin':
|
||
APP().processEvents()
|
||
|
||
size = self.size()
|
||
|
||
if size == QtCore.QSize(640, 480):
|
||
logger.error(
|
||
f'detected unresolve Qt bug on macOS. '
|
||
f'Resizing main window to default '
|
||
f'{AppMainWindow.DEFAULT_WINDOW_SIZE.toTuple()}'
|
||
)
|
||
self.resize(AppMainWindow.DEFAULT_WINDOW_SIZE)
|
||
else:
|
||
logger.info(
|
||
f'restore main window size on macOS success: '
|
||
f'{size.toTuple()}'
|
||
)
|
||
|
||
def cleanup(self):
|
||
AppSettings.set('AppMainWindowGeometry', self.saveGeometry())
|
||
AppSettings.set('AppMainWindowState', self.saveState())
|