Feature: Added waiting spinner support while deleting servers

Signed-off-by: LorenEteval <loren.eteval@proton.me>
This commit is contained in:
LorenEteval
2026-06-21 15:56:17 +08:00
committed by Loren Eteval
parent 7a2ccf9a17
commit 5d8e55ce81
3 changed files with 204 additions and 4 deletions
+19 -1
View File
@@ -222,6 +222,7 @@ TRANSLATION = {
"Furious.Widget.GuiCustomizeNetworkTest",
"Furious.Widget.GuiCustomizeProxyBypass",
"Furious.Widget.IndentSpinBox",
"Furious.Widget.UserServersQTableView",
"Furious.Window.TextEditorWindow",
"Furious.Window.UserSubsWindow"
],
@@ -1902,10 +1903,27 @@ TRANSLATION = {
},
"Current": {
"source": [
"Furious.TrayActions.Import"
"Furious.TrayActions.Import",
"Furious.Widget.UserServersQTableView"
],
"RU": "Текущий",
"ZH": "当前",
"isReviewed": "True"
},
"Canceling delete": {
"source": [
"Furious.Widget.UserServersQTableView"
],
"RU": "Отмена удаления",
"ZH": "正在取消删除",
"isReviewed": "True"
},
"Deleting": {
"source": [
"Furious.Widget.UserServersQTableView"
],
"RU": "Удаление",
"ZH": "正在删除",
"isReviewed": "True"
}
}
+183 -2
View File
@@ -30,6 +30,7 @@ from Furious.Widget.GuiShadowsocks import *
from Furious.Widget.GuiTrojan import *
from Furious.Widget.GuiVLESS import *
from Furious.Widget.GuiVMess import *
from Furious.Widget.WaitingSpinner import *
from Furious.Window.QRCodeWindow import *
from Furious.Window.TextEditorWindow import *
@@ -172,7 +173,9 @@ class SubscriptionManager(WebGETManager):
break
parent.deleteItemByIndex(
subsIndexes, showTrayMessage=bool(subsGroupIndex < 0)
subsIndexes,
showTrayMessage=bool(subsGroupIndex < 0),
showProgress=False,
)
remaining = len(Storage.UserServers())
@@ -850,6 +853,169 @@ class DownloadSpeedTestScheduler(QtCore.QObject):
self.scheduleDrain()
class DeleteServersProgressDialog(AppQDialog):
def __init__(self, table, indexes, showTrayMessage=True, parent=None):
super().__init__(parent)
self.table = table
self.indexes = list(indexes)
self.showTrayMessage = showTrayMessage
self.total = len(self.indexes)
self.nextIndex = 0
self.deletedCount = 0
self.deletedActivated = False
self.canceled = False
self.finishedDeletion = False
self.currentRemark = ''
self.setWindowTitle(_('Delete'))
self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
self.spinner = WaitingSpinner(
self,
center_on_parent=False,
lines=12,
line_length=7,
line_width=3,
radius=7,
color=QColor(96, 160, 255),
)
self.statusLabel = AppQLabel()
self.detailLabel = AppQLabel()
self.detailLabel.setWordWrap(True)
self.cancelButton = AppQPushButton(_('Cancel'))
self.cancelButton.clicked.connect(self.cancel)
statusLayout = QHBoxLayout()
statusLayout.addWidget(self.spinner)
statusLayout.addWidget(self.statusLabel, 1)
layout = QVBoxLayout()
layout.addLayout(statusLayout)
layout.addWidget(self.detailLabel)
layout.addWidget(self.cancelButton)
self.setLayout(layout)
self.updateStatus()
def setWidthAndHeight(self):
self.resize(420, 150)
def exec(self):
self.spinner.start()
QtCore.QTimer.singleShot(0, self.deleteNext)
return super().exec()
def reject(self):
self.cancel()
def cancel(self):
self.canceled = True
self.cancelButton.setEnabled(False)
self.updateStatus()
def updateStatus(self):
if self.canceled:
self.statusLabel.setText(
_('Canceling delete') + f'... {self.deletedCount}/{self.total}'
)
else:
self.statusLabel.setText(
_('Deleting') + f'... {self.deletedCount}/{self.total}'
)
if self.currentRemark:
self.detailLabel.setText(_('Current') + f': {self.currentRemark}')
else:
self.detailLabel.setText('')
@staticmethod
def limitedRemark(remark: str) -> str:
remark = str(remark).strip()
if len(remark) <= 120:
return remark
return remark[:117] + '...'
def deleteNext(self):
if self.canceled or self.nextIndex >= self.total:
self.finishDeletion()
return
originalIndex = self.indexes[self.nextIndex]
self.nextIndex += 1
deleteIndex = originalIndex - self.deletedCount
if deleteIndex < 0 or deleteIndex >= len(Storage.UserServers()):
self.updateStatus()
QtCore.QTimer.singleShot(0, self.deleteNext)
return
factory = Storage.UserServers()[deleteIndex]
self.currentRemark = self.limitedRemark(factory.getExtras('remark'))
if originalIndex == Storage.UserActivatedItemIndex():
self.deletedActivated = True
self.table.sourceModel.beginRemoveRows(
QtCore.QModelIndex(),
deleteIndex,
deleteIndex,
)
factory.deleted = True
Storage.UserServers().pop(deleteIndex)
self.table.sourceModel.endRemoveRows()
if not self.deletedActivated and deleteIndex < Storage.UserActivatedItemIndex():
AppSettings.set(
'ActivatedItemIndex', str(Storage.UserActivatedItemIndex() - 1)
)
self.deletedCount += 1
self.updateStatus()
QtCore.QTimer.singleShot(0, self.deleteNext)
def finishDeletion(self):
if self.finishedDeletion:
return
self.finishedDeletion = True
self.spinner.stop()
self.table.sourceModel.refreshIndexes()
self.table.sourceModel.emitAllChanged()
if self.deletedActivated:
# Set invalid first
AppSettings.set('ActivatedItemIndex', str(-1))
if APP().isSystemTrayConnected():
if self.showTrayMessage:
# Trigger disconnect
APP().systemTray.ConnectAction.trigger()
else:
# Trigger disconnect silently
APP().systemTray.ConnectAction.doDisconnect()
self.accept()
def retranslate(self):
self.setWindowTitle(_(self.windowTitle()))
self.cancelButton.setText(_(self.cancelButton.text()))
self.updateStatus()
class UserServersQTableViewHorizontalHeader(AppQHeaderView):
def __init__(self, *args, **kwargs):
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
@@ -1933,11 +2099,26 @@ class UserServersQTableView(
config=deepcopy,
)
def deleteItemByIndex(self, indexes, showTrayMessage=True) -> int:
def deleteItemByIndex(
self, indexes, showTrayMessage=True, showProgress=True
) -> int:
indexes = sorted(set(indexes))
if len(indexes) == 0:
# Nothing selected. Do nothing
return 0
if showProgress and len(indexes) > 1:
dialog = DeleteServersProgressDialog(
self,
indexes,
showTrayMessage=showTrayMessage,
parent=self.window(),
)
dialog.exec()
return dialog.deletedCount
if Storage.UserActivatedItemIndex() in indexes:
deleteActivated = True
else:
+2 -1
View File
@@ -235,7 +235,8 @@ class AppMainWindow(AppQMainWindow):
index
for index, server in enumerate(Storage.UserServers())
if server.getExtras('subsId') == unique
)
),
showProgress=False,
),
)
self.xrayAssetViewerWindow = XrayAssetViewerWindow(parent=self)