diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index 930c4ec..faefdc0 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -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" } } diff --git a/Furious/Widget/UserServersQTableView.py b/Furious/Widget/UserServersQTableView.py index 9a398b1..8baffc8 100644 --- a/Furious/Widget/UserServersQTableView.py +++ b/Furious/Widget/UserServersQTableView.py @@ -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: diff --git a/Furious/Window/AppMainWindow.py b/Furious/Window/AppMainWindow.py index 50fb423..c8792df 100644 --- a/Furious/Window/AppMainWindow.py +++ b/Furious/Window/AppMainWindow.py @@ -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)