diff --git a/Furious/Externals/GenTranslation.py b/Furious/Externals/GenTranslation.py index 959f3f0..8a56c98 100644 --- a/Furious/Externals/GenTranslation.py +++ b/Furious/Externals/GenTranslation.py @@ -6,6 +6,7 @@ TRANSLATION = { "Furious.Qt.QtWidgets", "Furious.Widget.ServerTableView", "Furious.Widget.SubscriptionTableView", + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow" ], "RU": "Удалить", @@ -86,7 +87,8 @@ TRANSLATION = { }, "Invalid data": { "source": [ - "Furious.Actions.Import" + "Furious.Actions.Import", + "Furious.Window.SubscriptionPage" ], "RU": "Неверные данные", "ZH": "无效的数据", @@ -225,6 +227,7 @@ TRANSLATION = { "Furious.Window.IndentDialog", "Furious.Window.NetworkTestDialog", "Furious.Window.ProxyBypassDialog", + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow", "Furious.Window.TextEditorWindow" ], @@ -361,6 +364,7 @@ TRANSLATION = { "Edit": { "source": [ "Furious.Window.LogPage", + "Furious.Window.SubscriptionPage", "Furious.Window.TextEditorWindow" ], "RU": "Редактировать", @@ -448,7 +452,8 @@ TRANSLATION = { "Furious.Backends.Xray.VlessEditor", "Furious.Backends.Xray.VmessEditor", "Furious.Widget.ServerTableView", - "Furious.Widget.SubscriptionTableView" + "Furious.Widget.SubscriptionTableView", + "Furious.Window.SubscriptionPage" ], "RU": "Примечание", "ZH": "别名", @@ -509,7 +514,8 @@ TRANSLATION = { "Subscription": { "source": [ "Furious.Widget.ServerTableView", - "Furious.Window.HomePage" + "Furious.Window.MainWindow", + "Furious.Window.SubscriptionPage" ], "RU": "Подписка", "ZH": "订阅", @@ -532,14 +538,6 @@ TRANSLATION = { "ZH": "速度", "isReviewed": "True" }, - "Edit Configuration...": { - "source": [ - "Furious.Actions.Configuration" - ], - "RU": "Изменить конфигурацию...", - "ZH": "编辑配置...", - "isReviewed": "True" - }, "Move Up": { "source": [ "Furious.Widget.ServerTableView" @@ -664,6 +662,7 @@ TRANSLATION = { }, "Add Subscription": { "source": [ + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow" ], "RU": "Добавить подписку", @@ -688,6 +687,7 @@ TRANSLATION = { }, "Edit Subscription": { "source": [ + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow" ], "RU": "Редактировать подписку", @@ -705,6 +705,7 @@ TRANSLATION = { "Add": { "source": [ "Furious.Backends.Xray.RoutingWindow", + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow" ], "RU": "Добавить", @@ -843,54 +844,6 @@ TRANSLATION = { "ZH": "折叠导航", "isReviewed": "True" }, - "Update Subscription (Use Current Proxy)": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Обновить подписки (использовать текущий прокси-сервер)", - "ZH": "更新订阅(使用当前代理)", - "isReviewed": "True" - }, - "Update Subscription (Force Proxy)": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Обновить подписки (обязательно через прокси-сервер)", - "ZH": "更新订阅(强制代理)", - "isReviewed": "True" - }, - "Update Subscription (No Proxy)": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Обновить подписки (не использовать прокси)", - "ZH": "更新订阅(无代理)", - "isReviewed": "True" - }, - "Edit Subscription...": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Редактировать подписки...", - "ZH": "编辑订阅...", - "isReviewed": "True" - }, - "Plugins": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Плагины", - "ZH": "插件", - "isReviewed": "True" - }, - "Core": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Core", - "ZH": "Core", - "isReviewed": "True" - }, "Open Application Folder": { "source": [ "Furious.Window.SettingsPage" @@ -1042,7 +995,6 @@ TRANSLATION = { }, "Settings": { "source": [ - "Furious.Actions.Settings", "Furious.Window.MainWindow", "Furious.Window.SettingsPage" ], @@ -1356,6 +1308,7 @@ TRANSLATION = { "Furious.Window.IndentDialog", "Furious.Window.NetworkTestDialog", "Furious.Window.ProxyBypassDialog", + "Furious.Window.SubscriptionPage", "Furious.Window.SubscriptionWindow" ], "RU": "OK", @@ -1412,7 +1365,8 @@ TRANSLATION = { }, "Auto Update": { "source": [ - "Furious.Widget.SubscriptionTableView" + "Furious.Widget.SubscriptionTableView", + "Furious.Window.SubscriptionPage" ], "RU": "Автообновление", "ZH": "自动更新", @@ -1420,7 +1374,8 @@ TRANSLATION = { }, "Auto Update Use Proxy": { "source": [ - "Furious.Widget.SubscriptionTableView" + "Furious.Widget.SubscriptionTableView", + "Furious.Window.SubscriptionPage" ], "RU": "Автообновление (используя прокси)", "ZH": "自动更新使用代理", @@ -2014,7 +1969,9 @@ TRANSLATION = { }, "Enabled": { "source": [ - "Furious.Backends.Xray.RoutingWindow" + "Furious.Backends.Xray.RoutingWindow", + "Furious.Widget.SubscriptionTableView", + "Furious.Window.SubscriptionPage" ], "RU": "Включено", "ZH": "已启用", @@ -2022,7 +1979,8 @@ TRANSLATION = { }, "Disabled": { "source": [ - "Furious.Backends.Xray.RoutingWindow" + "Furious.Backends.Xray.RoutingWindow", + "Furious.Widget.SubscriptionTableView" ], "RU": "Отключено", "ZH": "已禁用", @@ -2265,14 +2223,6 @@ TRANSLATION = { "ZH": "自定义Hysteria2 TUN设置...", "isReviewed": "True" }, - "Extensions": { - "source": [ - "Furious.Window.HomePage" - ], - "RU": "Расширения", - "ZH": "扩展功能", - "isReviewed": "True" - }, "Log Type": { "source": [ "Furious.Window.LogPage" @@ -2496,7 +2446,7 @@ TRANSLATION = { }, "Hysteria2 Traffic Statistics": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Статистика трафика Hysteria2", "ZH": "Hysteria2流量统计", @@ -2624,7 +2574,7 @@ TRANSLATION = { }, "Traffic Stats API URL": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "URL API статистики трафика", "ZH": "流量统计API URL", @@ -2632,7 +2582,7 @@ TRANSLATION = { }, "Hysteria2 server API address; /traffic is appended automatically.": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Адрес API сервера Hysteria2; /traffic добавляется автоматически.", "ZH": "Hysteria2服务器API地址;将自动附加 /traffic。", @@ -2640,7 +2590,7 @@ TRANSLATION = { }, "Traffic Stats Client ID": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Идентификатор клиента статистики", "ZH": "流量统计客户端ID", @@ -2648,7 +2598,7 @@ TRANSLATION = { }, "Traffic Stats API Secret": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Секрет API статистики трафика", "ZH": "流量统计API密钥", @@ -2656,7 +2606,7 @@ TRANSLATION = { }, "Authorization value configured by the Hysteria2 server.": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Значение авторизации, настроенное на сервере Hysteria2.", "ZH": "Hysteria2服务器配置的Authorization值。", @@ -2704,7 +2654,7 @@ TRANSLATION = { }, "Authentication ID reported by the Hysteria2 server.": { "source": [ - "Furious.Window.SettingsPage" + "Furious.Backends.Hysteria2.Plugin" ], "RU": "Идентификатор аутентификации, указанный сервером Hysteria2.", "ZH": "Hysteria2服务器报告的身份验证 ID。", @@ -2725,5 +2675,149 @@ TRANSLATION = { "ZH": "重新连接后,将累计上传和下载流量从零开始计算。", "isReviewed": "True", "RU": "После переподключения начинать подсчёт накопленного исходящего и входящего трафика с нуля." + }, + "Show Home Page...": { + "source": [ + "Furious.Actions.Configuration" + ], + "ZH": "显示主页...", + "isReviewed": "True", + "RU": "Показать главную страницу..." + }, + "Show the server management Home page": { + "source": [ + "Furious.Actions.Configuration" + ], + "ZH": "显示服务器管理主页", + "isReviewed": "True", + "RU": "Показать главную страницу управления серверами" + }, + "Last Updated": { + "source": [ + "Furious.Widget.SubscriptionTableView" + ], + "ZH": "上次更新", + "isReviewed": "True", + "RU": "Последнее обновление" + }, + "Profiles": { + "source": [ + "Furious.Widget.SubscriptionTableView" + ], + "ZH": "配置文件", + "isReviewed": "True", + "RU": "Профили" + }, + "Plugin Settings": { + "source": [ + "Furious.Window.SettingsPage" + ], + "ZH": "插件设置", + "isReviewed": "True", + "RU": "Настройки плагинов" + }, + "Enable Metrics Collection": { + "source": [ + "Furious.Window.SettingsPage" + ], + "ZH": "启用指标收集", + "isReviewed": "True", + "RU": "Включить сбор метрик" + }, + "Collect network speed and traffic history while connected.": { + "source": [ + "Furious.Window.SettingsPage" + ], + "ZH": "连接时收集网络速度和流量历史记录。", + "isReviewed": "True", + "RU": "Собирать историю скорости сети и трафика при подключении." + }, + "URL": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "URL", + "isReviewed": "True", + "RU": "URL" + }, + "User Agent": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "用户代理", + "isReviewed": "True", + "RU": "User-Agent" + }, + "Profile Filter (Regex)": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "配置文件筛选器(正则表达式)", + "isReviewed": "True", + "RU": "Фильтр профилей (регулярное выражение)" + }, + "Please enter a subscription remark.": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "请输入订阅备注。", + "isReviewed": "True", + "RU": "Введите название подписки." + }, + "Please enter a valid subscription URL.": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "请输入有效的订阅 URL。", + "isReviewed": "True", + "RU": "Введите корректный URL подписки." + }, + "Subscriptions": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "订阅管理", + "isReviewed": "True", + "RU": "Подписки" + }, + "Update Using": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "更新方式", + "isReviewed": "True", + "RU": "Способ обновления" + }, + "Add From Clipboard": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "从剪贴板添加", + "isReviewed": "True", + "RU": "Добавить из буфера обмена" + }, + "Copy URL": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "复制 URL", + "isReviewed": "True", + "RU": "Копировать URL" + }, + "Update Selected": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "更新所选项", + "isReviewed": "True", + "RU": "Обновить выбранные" + }, + "Update All": { + "source": [ + "Furious.Window.SubscriptionPage" + ], + "ZH": "全部更新", + "isReviewed": "True", + "RU": "Обновить все" } } diff --git a/Furious/Repository/Subscriptions.py b/Furious/Repository/Subscriptions.py index 842af1e..4ce8dad 100644 --- a/Furious/Repository/Subscriptions.py +++ b/Furious/Repository/Subscriptions.py @@ -33,8 +33,12 @@ class UserSubEntry: remark: str webURL: str + enabled: bool autoupdate: str proxy: str + userAgent: str + filter: str + lastUpdated: str class UserSub: diff --git a/Furious/Widget/ServerTableView.py b/Furious/Widget/ServerTableView.py index 4f0c703..1b2b074 100644 --- a/Furious/Widget/ServerTableView.py +++ b/Furious/Widget/ServerTableView.py @@ -51,6 +51,7 @@ import logging import icmplib import functools import collections +import datetime __all__ = ['ServerTableView'] @@ -151,6 +152,8 @@ class MBoxUpdateSubsInfo(AppQMessageBox): class SubscriptionManager(WebGETManager): """Coordinate subscription operations.""" + subscriptionsChanged = QtCore.Signal() + def __init__(self, parent, **kwargs): """Initialize the SubscriptionManager.""" actionMessage = kwargs.pop('actionMessage', 'update subs') @@ -229,6 +232,8 @@ class SubscriptionManager(WebGETManager): # Show the MessageBox asynchronously mbox.open() + self.subscriptionsChanged.emit() + def mustCall(self, **kwargs): """Perform the required completion hook.""" depthMap = kwargs.get('depthMap', {}) @@ -253,6 +258,27 @@ class SubscriptionManager(WebGETManager): ) result = self.importer.importPayload(data, source) + profileFilter = str(kwargs.get('filter', '')).strip() + + if result is not None and profileFilter: + try: + pattern = re.compile(profileFilter, re.IGNORECASE) + except re.error as ex: + logger.error( + f'invalid subscription filter for {remark!r}: {ex}. ' + f'Importing all profiles' + ) + else: + result = type(result)( + result.decoderId, + tuple( + profile + for profile in result.profiles + if pattern.search(str(getattr(profile, 'itemRemark', ''))) + ), + result.rejectedItems, + ) + if result is None or not result.profiles: failureArgs.append({'error': 'UnsupportedSubscriptionFormat', **kwargs}) else: @@ -264,6 +290,13 @@ class SubscriptionManager(WebGETManager): successArgs.append({'profiles': result.profiles, **kwargs}) + unique = kwargs.get('unique', '') + + if unique in Storage.UserSubs(): + Storage.UserSubs()[unique]['lastUpdated'] = ( + datetime.datetime.now().astimezone().isoformat(timespec='seconds') + ) + def failureCallback(self, networkReply, **kwargs): """Handle a failed network operation.""" remark = kwargs.get('remark', '') @@ -290,15 +323,21 @@ class SubscriptionManager(WebGETManager): # Some providers reject the default Qt User-Agent (e.g. with 503), # so identify ourselves explicitly. request = QNetworkRequest(QtCore.QUrl(url)) + userAgent = str(kwargs.get('userAgent', '')).strip() request.setRawHeader( b'User-Agent', - f'{APPLICATION_NAME}/{APPLICATION_VERSION}'.encode(), + (userAgent or f'{APPLICATION_NAME}/{APPLICATION_VERSION}').encode(), ) self.webGET(request, logActionMessage=logActionMessage, **kwargs) def updateSubsByUnique(self, unique: str, **kwargs): """Update subs by unique.""" + subscription = Storage.UserSubs().get(unique) + + if not subscription or not subscription.get('enabled', True): + return + depthMap = kwargs.get('depthMap', {'depth': 1}) successArgs = kwargs.get('successArgs', list()) failureArgs = kwargs.get('failureArgs', list()) @@ -316,11 +355,20 @@ class SubscriptionManager(WebGETManager): def updateSubs(self, **kwargs): """Update subs.""" - depthMap = {'depth': len(Storage.UserSubs())} + enabledKeys = tuple( + key + for key, subscription in Storage.UserSubs().items() + if subscription.get('enabled', True) and subscription.get('webURL') + ) + + if not enabledKeys: + return + + depthMap = {'depth': len(enabledKeys)} successArgs = list() failureArgs = list() - for key in Storage.UserSubs().keys(): + for key in enabledKeys: self.updateSubsByUnique( key, depthMap=depthMap, diff --git a/Furious/Widget/SubscriptionTableView.py b/Furious/Widget/SubscriptionTableView.py index 4d5e620..164a992 100644 --- a/Furious/Widget/SubscriptionTableView.py +++ b/Furious/Widget/SubscriptionTableView.py @@ -132,7 +132,13 @@ class UserSubsTableModel(QtCore.QAbstractTableModel): flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable - if self.itemKey[index.column()] not in ['autoupdate', 'proxy']: + if self.itemKey[index.column()] not in [ + 'autoupdate', + 'proxy', + 'enabled', + 'lastUpdated', + 'profiles', + ]: flags |= QtCore.Qt.ItemFlag.ItemIsEditable return flags @@ -153,9 +159,20 @@ class UserSubsTableModel(QtCore.QAbstractTableModel): subsob = self.subsObjectByRow(row) text = self.headers[column](subsob) + mapped = self.itemKey[column] + + if mapped == 'profiles': + unique = self.uniqueByRow(row) + text = str( + sum( + 1 + for profile in Storage.UserServers() + if profile.itemSubscription == unique + ) + ) if role == QtCore.Qt.ItemDataRole.DisplayRole: - if self.itemKey[column] in ['autoupdate', 'proxy']: + if mapped in ['autoupdate', 'proxy', 'enabled']: return _(text) return text @@ -184,7 +201,13 @@ class UserSubsTableModel(QtCore.QAbstractTableModel): mapped = self.itemKey[column] - if mapped in ['autoupdate', 'proxy']: + if mapped in [ + 'autoupdate', + 'proxy', + 'enabled', + 'lastUpdated', + 'profiles', + ]: return False self.subsObjectByRow(row)[mapped] = str(value) @@ -268,6 +291,10 @@ _TRANSLATABLE_HEADERS = [ _('Remark'), _('Auto Update'), _('Auto Update Use Proxy'), + _('Enabled'), + _('Disabled'), + _('Last Updated'), + _('Profiles'), ] @@ -304,14 +331,31 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): Headers = [ SubscriptionTableColumn('Remark', lambda item: item.get('remark', '')), SubscriptionTableColumn('URL', lambda item: item.get('webURL', '')), + SubscriptionTableColumn( + 'Enabled', + lambda item: 'Enabled' if item.get('enabled', True) else 'Disabled', + ), SubscriptionTableColumn('Auto Update', lambda item: item.get('autoupdate', '')), SubscriptionTableColumn( 'Auto Update Use Proxy', lambda item: item.get('proxy', '') ), + SubscriptionTableColumn( + 'Last Updated', + lambda item: item.get('lastUpdated', ''), + ), + SubscriptionTableColumn('Profiles'), ] # Corresponds to 'Headers' - ItemKey = ['remark', 'webURL', 'autoupdate', 'proxy'] + ItemKey = [ + 'remark', + 'webURL', + 'enabled', + 'autoupdate', + 'proxy', + 'lastUpdated', + 'profiles', + ] def __init__(self, *args, **kwargs): """Initialize the subscription table view.""" @@ -319,7 +363,9 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): super().__init__(*args, **kwargs) - self.timers = list(QtCore.QTimer() for i in range(len(Storage.UserSubs()))) + self.timers = list( + QtCore.QTimer(self) for _index in range(len(Storage.UserSubs())) + ) self.timerConnected = list(False for i in range(len(Storage.UserSubs()))) self.sourceModel = UserSubsTableModel(self.Headers, self.ItemKey, parent=self) self.setModel(self.sourceModel) @@ -373,6 +419,13 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): list(set(index.row() for index in self.selectionModel().selectedRows())) ) + @property + def selectedUniques(self): + """Return stable subscription IDs for the selected rows.""" + keys = tuple(Storage.UserSubs()) + + return tuple(keys[row] for row in self.selectedIndex if row < len(keys)) + @QtCore.Slot(QtCore.QPoint) def handleCustomContextMenuRequested(self, point): """Handle custom context menu requested.""" @@ -481,7 +534,7 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): assert isinstance(qtimer, QtCore.QTimer) - if timems is not None: + if timems is not None and subsob.get('enabled', True): logger.info( f'start auto update job for subscription ({remark}, {webURL}). ' f'Interval is {timems // (60 * 1000)} mins' @@ -626,6 +679,12 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): def flushAll(self): """Refresh all.""" + for item in Storage.UserSubs().values(): + item.setdefault('enabled', True) + item.setdefault('userAgent', '') + item.setdefault('filter', '') + item.setdefault('lastUpdated', '') + self.sourceModel.emitAllChanged() for index, key in enumerate(Storage.UserSubs()): @@ -633,13 +692,26 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): def appendNewItem(self, **kwargs): """Append new item.""" - unique, remark, webURL, autoupdate, proxy = ( + ( + unique, + remark, + webURL, + enabled, + autoupdate, + proxy, + userAgent, + profileFilter, + lastUpdated, + ) = ( kwargs.pop('unique', ''), kwargs.pop('remark', ''), kwargs.pop('webURL', ''), - # Below values are not filled when adding item via Gui + kwargs.pop('enabled', True), kwargs.pop('autoupdate', ''), kwargs.pop('proxy', ''), + kwargs.pop('userAgent', ''), + kwargs.pop('filter', ''), + kwargs.pop('lastUpdated', ''), ) # The internal subs object @@ -647,8 +719,12 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): unique: { 'remark': remark, 'webURL': webURL, + 'enabled': bool(enabled), 'autoupdate': autoupdate, 'proxy': proxy, + 'userAgent': userAgent, + 'filter': profileFilter, + 'lastUpdated': lastUpdated, } } @@ -660,7 +736,7 @@ class SubscriptionTableView(Mixins.QTranslatable, AppQTableView): row = self.sourceModel.rowCount() # Add timer - self.timers.append(QtCore.QTimer()) + self.timers.append(QtCore.QTimer(self)) self.timerConnected.append(False) self.sourceModel.beginInsertRows(QtCore.QModelIndex(), row, row) diff --git a/Furious/Window/HomePage.py b/Furious/Window/HomePage.py index c5f0513..56825f0 100644 --- a/Furious/Window/HomePage.py +++ b/Furious/Window/HomePage.py @@ -23,7 +23,7 @@ from Furious.Frozenlib import * from Furious.Interface import * from Furious.Models import * from Furious.Repository import * -from Furious.Plugins import CapabilityKind, getPluginRegistry +from Furious.Plugins import getPluginRegistry from Furious.Qt import * from Furious.Qt import gettext as _ from Furious.Service import ( @@ -43,7 +43,6 @@ from Furious.Widget.ServerTableView import * from Furious.Window.NetworkTestDialog import * from Furious.Window.ProxyBypassDialog import * from Furious.Window.QRCodeWindow import QRCodeWindow -from Furious.Window.SubscriptionWindow import * from Furious.Window.TextEditorWindow import TextEditorWindow from Furious.Window.TunSettingsDialog import * @@ -62,20 +61,6 @@ __all__ = ['HomePage'] logger = logging.getLogger(__name__) -def _isCoreActive(coreType) -> bool: - """Return whether the tray's active connection owns *coreType*.""" - try: - connectAction = APP().systemTray.ConnectAction - - return connectAction.isConnected() and any( - isinstance(process, coreType) - for process in connectAction.coreManager.processesPool - ) - except (AttributeError, RuntimeError): - # The home page is built before the system tray is attached. - return False - - class AppConnectivityManager(ConnectivityManager): """Coordinate app network connectivity operations.""" @@ -254,7 +239,7 @@ class NetworkStateBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget): class TrafficStatsBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget): - """Display independently updated traffic speeds and usage.""" + """Display compact Fluent direction groups for live network statistics.""" UploadIconFileName = 'cloud-upload.svg' DownloadIconFileName = 'cloud-download.svg' @@ -274,19 +259,38 @@ class TrafficStatsBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget): self.uploadTextLabel = AppQLabel(translatable=False, parent=self) self.uploadUsageLabel = AppQLabel(translatable=False, parent=self) + self.downloadTextLabel.setObjectName('TrafficSpeedLabel') + self.uploadTextLabel.setObjectName('TrafficSpeedLabel') self.uploadUsageLabel.setObjectName('TrafficUsageLabel') self.downloadUsageLabel.setObjectName('TrafficUsageLabel') + self.downloadBadge = QFrame(parent=self) + self.downloadBadge.setObjectName('TrafficDirectionBadge') + self.downloadBadge.setProperty('direction', 'download') + + self.uploadBadge = QFrame(parent=self) + self.uploadBadge.setObjectName('TrafficDirectionBadge') + self.uploadBadge.setProperty('direction', 'upload') + + downloadLayout = QHBoxLayout(self.downloadBadge) + downloadLayout.setContentsMargins(8, 2, 8, 2) + downloadLayout.setSpacing(5) + downloadLayout.addWidget(self.downloadIconLabel) + downloadLayout.addWidget(self.downloadTextLabel) + downloadLayout.addWidget(self.downloadUsageLabel) + + uploadLayout = QHBoxLayout(self.uploadBadge) + uploadLayout.setContentsMargins(8, 2, 8, 2) + uploadLayout.setSpacing(5) + uploadLayout.addWidget(self.uploadIconLabel) + uploadLayout.addWidget(self.uploadTextLabel) + uploadLayout.addWidget(self.uploadUsageLabel) + self._layout = QHBoxLayout(self) - self._layout.setContentsMargins(8, 3, 8, 3) - self._layout.setSpacing(6) - self._layout.addWidget(self.downloadIconLabel) - self._layout.addWidget(self.downloadTextLabel) - self._layout.addWidget(self.downloadUsageLabel) - self._layout.addSpacing(4) - self._layout.addWidget(self.uploadIconLabel) - self._layout.addWidget(self.uploadTextLabel) - self._layout.addWidget(self.uploadUsageLabel) + self._layout.setContentsMargins(0, 1, 0, 1) + self._layout.setSpacing(5) + self._layout.addWidget(self.downloadBadge) + self._layout.addWidget(self.uploadBadge) self.setIconByTheme(APP().theme()) self.updateToolTips() @@ -330,8 +334,8 @@ class TrafficStatsBadge(Mixins.QTranslatable, Mixins.ThemeAware, QWidget): @QtCore.Slot(object, object) def setUsage(self, upload: int, download: int): """Display formatted cumulative upload and download usage.""" - self.uploadUsageLabel.setText(f'({formatTrafficUsage(upload)})') - self.downloadUsageLabel.setText(f'({formatTrafficUsage(download)})') + self.uploadUsageLabel.setText(f'• {formatTrafficUsage(upload)}') + self.downloadUsageLabel.setText(f'• {formatTrafficUsage(download)}') self.setVisible(True) @QtCore.Slot() @@ -429,17 +433,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow): qrCodeWindowFactory=QRCodeWindow, importActionsFactory=self.serverImportActions, ) - self.userSubsWindow = SubscriptionWindow( - parent=self, - deleteUniqueCallback=lambda unique: self.userServersQTableWidget.deleteItemByIndex( - list( - index - for index, server in enumerate(Storage.UserServers()) - if server.itemSubscription == unique - ), - showProgress=False, - ), - ) pluginRegistry = getPluginRegistry() self.customizeProxyBypassDialog = ProxyBypassDialog(parent=self) self.customizeNetworkTestDialog = NetworkTestDialog(parent=self) @@ -481,124 +474,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow): ) ) - 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(), - ), - ] - - corePluginActions = [] - - for plugin in pluginRegistry.pluginsWithCapability( - CapabilityKind.KernelFactory - ): - pluginMetadata = pluginRegistry.metadataFor(plugin) - managementActions = pluginRegistry.managementActions( - plugin, - parent=self, - isCoreActive=_isCoreActive, - ) - if managementActions: - corePluginActions.append( - AppQAction( - pluginMetadata.displayName, - menu=AppQMenu(*managementActions), - useActionGroup=False, - checkable=False, - ) - ) - else: - corePluginActions.append( - AppQAction( - pluginMetadata.displayName, - checkable=False, - ) - ) - - extensionPluginActions = [] - - corePlugins = set( - pluginRegistry.pluginsWithCapability(CapabilityKind.KernelFactory) - ) - - for plugin in pluginRegistry.pluginsWithCapability( - CapabilityKind.ActionProvider - ): - if plugin in corePlugins: - continue - - managementActions = pluginRegistry.managementActions( - plugin, - parent=self, - isCoreActive=_isCoreActive, - ) - - if managementActions: - pluginMetadata = pluginRegistry.metadataFor(plugin) - extensionPluginActions.append( - AppQAction( - pluginMetadata.displayName, - menu=AppQMenu(*managementActions), - useActionGroup=False, - checkable=False, - ) - ) - - pluginActions = [ - AppQAction( - _('Core'), - menu=AppQMenu(*corePluginActions), - useActionGroup=False, - checkable=False, - ), - ] - - if extensionPluginActions: - pluginActions.append( - AppQAction( - _('Extensions'), - menu=AppQMenu(*extensionPluginActions), - useActionGroup=False, - checkable=False, - ) - ) - - pluginsToolbarActions = [ - AppQSeperator(), - AppQAction( - _('Plugins'), - icon=bootstrapIcon('plugin.svg'), - menu=AppQMenu(*pluginActions), - useSetMenu=False, - useActionGroup=False, - checkable=False, - ), - ] - if hasattr(AppQAction, 'setMenu'): self.toolbar = AppQToolBar( AppQAction( @@ -609,16 +484,6 @@ class HomePage(Mixins.QTranslatable, QMainWindow): useActionGroup=False, checkable=False, ), - AppQSeperator(), - AppQAction( - _('Subscription'), - icon=bootstrapIcon('collection.svg'), - menu=AppQMenu(*subsActions), - useSetMenu=False, - useActionGroup=False, - checkable=False, - ), - *pluginsToolbarActions, ) self.toolbar.setObjectName('HomePageToolBar') self.toolbar.setMovable(False) @@ -635,29 +500,13 @@ class HomePage(Mixins.QTranslatable, QMainWindow): 'actions': [*serverActions], } - subsMenu = { - 'name': 'Subscription', - 'actions': [*subsActions], - } - - pluginsMenu = { - 'name': 'Plugins', - 'actions': [*pluginActions], - } - # Corresponds to menus defined above _TRANSLATABLE_MENU_NAME = [ _('Server'), - _('Subscription'), - _('Plugins'), ] # Menus - for menuDict in ( - serverMenu, - subsMenu, - pluginsMenu, - ): + for menuDict in (serverMenu,): menuName = menuDict['name'] menuObjName = f'_{menuName}Menu' menu = AppQMenu( diff --git a/Furious/Window/MainWindow.py b/Furious/Window/MainWindow.py index 5f04b22..ca5181a 100644 --- a/Furious/Window/MainWindow.py +++ b/Furious/Window/MainWindow.py @@ -23,12 +23,13 @@ from Furious.Frozenlib import * from Furious.Models import ConfigFactory, ServerProfile from Furious.Qt import * from Furious.Qt import gettext as _ -from Furious.Service import MetricsDataManager +from Furious.Service import MetricsDataManager, PluginNavigationManager from Furious.Widget.NavigationView import NavigationView from Furious.Window.HomePage import HomePage from Furious.Window.LogPage import LogPage from Furious.Window.MetricsPage import MetricsPage from Furious.Window.SettingsPage import SettingsPage +from Furious.Window.SubscriptionPage import SubscriptionPage from PySide6 import QtCore @@ -50,6 +51,7 @@ registerAppSettings('AppMainWindowNavigationExpanded', isBinary=True) _TRANSLATABLE_NAVIGATION_LABELS = ( _('Home'), _('Log'), + _('Subscription'), _('Metrics'), _('Settings'), ) @@ -72,6 +74,10 @@ class MainWindow(AppQMainWindow): self.navigationView = NavigationView(parent=self) self.homePage = HomePage(parent=self.navigationView) + self.subscriptionPage = SubscriptionPage( + self.homePage.userServersQTableWidget, + parent=self.navigationView, + ) self.metricsDataManager = MetricsDataManager(parent=self) self.homePage.trafficStatsManager.sampleChanged.connect( self.metricsDataManager.recordTrafficSample @@ -110,6 +116,14 @@ class MainWindow(AppQMainWindow): 'Log', 'pin-angle.svg', ) + self.navigationView.addPage( + 'subscription', + self.subscriptionPage, + 'Subscription', + 'collection.svg', + ) + self.pluginNavigationManager = PluginNavigationManager() + self.pluginNavigationManager.registerPages(self.navigationView) self.navigationView.addPage( 'metrics', self.metricsPage, @@ -137,7 +151,7 @@ class MainWindow(AppQMainWindow): # Preserve the established application-facing server-management API. self.userServersQTableWidget = self.homePage.userServersQTableWidget - self.userSubsWindow = self.homePage.userSubsWindow + self.userSubsWindow = self.subscriptionPage self.networkConnectivityManager = self.homePage.networkConnectivityManager self.networkState = self.homePage.networkState self.trafficStats = self.homePage.trafficStats @@ -171,9 +185,14 @@ class MainWindow(AppQMainWindow): self.show() self.showPage('settings') + def showSubscriptionPage(self): + """Navigate directly to subscription management.""" + self.show() + self.showPage('subscription') + def updateSubsByUnique(self, unique: str, httpProxy: Union[str, None], **kwargs): - """Forward a subscription update to the home page.""" - self.homePage.updateSubsByUnique(unique, httpProxy, **kwargs) + """Forward a subscription update to the dedicated page controller.""" + self.subscriptionPage.updateSubsByUnique(unique, httpProxy, **kwargs) def appendNewItemByFactory(self, factory: ConfigFactory | ServerProfile): """Forward a new server profile to the home page.""" diff --git a/Furious/Window/SubscriptionPage.py b/Furious/Window/SubscriptionPage.py new file mode 100644 index 0000000..6e9d149 --- /dev/null +++ b/Furious/Window/SubscriptionPage.py @@ -0,0 +1,379 @@ +# Copyright (C) 2024–present Loren Eteval & contributors +# +# 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. + +"""Present subscription definitions and synchronization as a dedicated page.""" + +from __future__ import annotations + +from Furious.Frozenlib import APP, Mixins, PySide6Legacy +from Furious.Qt import * +from Furious.Qt import gettext as _ +from Furious.Repository import Storage +from Furious.Widget.SubscriptionTableView import SubscriptionTableView + +from PySide6 import QtCore +from PySide6.QtWidgets import * + +from urllib.parse import urlsplit +import uuid + +__all__ = ['SubscriptionPage'] + + +class _SubscriptionEditorDialog(AppQDialog): + """Edit one complete subscription definition with validation.""" + + def __init__(self, subscription=None, parent=None): + """Initialize fields from an existing definition or defaults.""" + super().__init__(parent) + + subscription = dict(subscription or {}) + self.remarkEdit = QLineEdit(subscription.get('remark', '')) + self.urlEdit = QLineEdit(subscription.get('webURL', '')) + self.enabledCheckBox = QCheckBox() + self.enabledCheckBox.setChecked(subscription.get('enabled', True)) + self.autoUpdateComboBox = QComboBox() + self.proxyComboBox = QComboBox() + self.userAgentEdit = QLineEdit(subscription.get('userAgent', '')) + self.filterEdit = QLineEdit(subscription.get('filter', '')) + + self.remarkEdit.setMaximumWidth(520) + self.urlEdit.setMaximumWidth(520) + self.userAgentEdit.setMaximumWidth(520) + self.filterEdit.setMaximumWidth(520) + + for value in SubscriptionTableView.AutoUpdateOptions: + self.autoUpdateComboBox.addItem(_(value), value) + + for value in SubscriptionTableView.ProxyOptions: + self.proxyComboBox.addItem(_(value), value) + + autoIndex = self.autoUpdateComboBox.findData(subscription.get('autoupdate', '')) + proxyIndex = self.proxyComboBox.findData(subscription.get('proxy', '')) + self.autoUpdateComboBox.setCurrentIndex(max(autoIndex, 0)) + self.proxyComboBox.setCurrentIndex(max(proxyIndex, 0)) + + self.buttons = AppQDialogButtonBox(QtCore.Qt.Orientation.Horizontal) + self.buttons.addButton(_('OK'), AppQDialogButtonBox.ButtonRole.AcceptRole) + self.buttons.addButton( + _('Cancel'), + AppQDialogButtonBox.ButtonRole.RejectRole, + ) + self.buttons.accepted.connect(self.accept) + self.buttons.rejected.connect(self.reject) + + form = QFormLayout() + form.setContentsMargins(18, 18, 18, 18) + form.setSpacing(12) + form.addRow(_('Remark'), self.remarkEdit) + form.addRow(_('URL'), self.urlEdit) + form.addRow(_('Enabled'), self.enabledCheckBox) + form.addRow(_('Auto Update'), self.autoUpdateComboBox) + form.addRow(_('Auto Update Use Proxy'), self.proxyComboBox) + form.addRow(_('User Agent'), self.userAgentEdit) + form.addRow(_('Profile Filter (Regex)'), self.filterEdit) + form.addRow(self.buttons) + self.setLayout(form) + + self.setWindowTitle( + _('Edit Subscription') if subscription else _('Add Subscription') + ) + self.resize(700, 360) + + def _showValidationError(self, message: str): + """Show one non-blocking validation message owned by this dialog.""" + messageBox = AppQMessageBox( + icon=AppQMessageBox.Icon.Warning, + parent=self, + ) + messageBox.setWindowTitle(_('Invalid data')) + messageBox.setText(message) + messageBox.open() + + def accept(self): + """Validate required fields before accepting the definition.""" + remark = self.remarkEdit.text().strip() + url = self.urlEdit.text().strip() + parsed = urlsplit(url) + + if not remark: + self._showValidationError(_('Please enter a subscription remark.')) + + return + + if parsed.scheme.casefold() not in ('http', 'https') or not parsed.netloc: + self._showValidationError(_('Please enter a valid subscription URL.')) + + return + + super().accept() + + def subscription(self): + """Return the normalized values entered by the user.""" + return { + 'remark': self.remarkEdit.text().strip(), + 'webURL': self.urlEdit.text().strip(), + 'enabled': self.enabledCheckBox.isChecked(), + 'autoupdate': self.autoUpdateComboBox.currentData() or '', + 'proxy': self.proxyComboBox.currentData() or '', + 'userAgent': self.userAgentEdit.text().strip(), + 'filter': self.filterEdit.text().strip(), + } + + +class SubscriptionPage(Mixins.QTranslatable, Mixins.ThemeAware, QMainWindow): + """Own subscription editing, grouping, and synchronization workflows.""" + + def __init__(self, serverTable, parent=None): + """Initialize around the existing profile synchronization backend.""" + super().__init__(parent) + + self.setObjectName('SubscriptionPage') + self.serverTable = serverTable + self.pageTitleLabel = AppQLabel(translatable=False) + self.pageTitleLabel.setObjectName('SubscriptionPageTitle') + self.proxyLabel = AppQLabel(translatable=False) + self.proxyComboBox = QComboBox() + self.proxyComboBox.setMinimumWidth(160) + + self.addButton = QPushButton() + self.addFromClipboardButton = QPushButton() + self.editButton = QPushButton() + self.deleteButton = QPushButton() + self.copyURLButton = QPushButton() + self.updateSelectedButton = QPushButton() + self.updateAllButton = QPushButton() + + self.table = SubscriptionTableView( + deleteUniqueCallback=self._deleteProfilesForSubscription, + parent=self, + ) + self.table.doubleClicked.connect(lambda _index: self.editSelected()) + self.serverTable.subsManager.subscriptionsChanged.connect(self.table.flushAll) + + self.addButton.clicked.connect(self.addSubscription) + self.addFromClipboardButton.clicked.connect(self.addFromClipboard) + self.editButton.clicked.connect(self.editSelected) + self.deleteButton.clicked.connect(self.table.deleteSelectedItem) + self.copyURLButton.clicked.connect(self.copySelectedURL) + self.updateSelectedButton.clicked.connect(self.updateSelected) + self.updateAllButton.clicked.connect(self.updateAll) + + for key in SubscriptionTableView.ProxyOptions: + self.proxyComboBox.addItem(_(key), key) + + controls = QHBoxLayout() + controls.setContentsMargins(0, 0, 0, 0) + controls.setSpacing(8) + controls.addWidget(self.pageTitleLabel) + controls.addStretch(1) + controls.addWidget(self.proxyLabel) + controls.addWidget(self.proxyComboBox) + controls.addWidget(self.updateSelectedButton) + controls.addWidget(self.updateAllButton) + + actions = QHBoxLayout() + actions.setContentsMargins(0, 0, 0, 0) + actions.setSpacing(8) + actions.addWidget(self.addButton) + actions.addWidget(self.addFromClipboardButton) + actions.addWidget(self.editButton) + actions.addWidget(self.deleteButton) + actions.addWidget(self.copyURLButton) + actions.addStretch(1) + + content = QWidget() + content.setObjectName('SubscriptionPageContent') + layout = QVBoxLayout(content) + layout.setContentsMargins(20, 18, 20, 20) + layout.setSpacing(12) + layout.addLayout(controls) + layout.addLayout(actions) + layout.addWidget(self.table, 1) + self.setCentralWidget(content) + + self.setIconsByTheme(APP().theme()) + self.retranslate() + + def _deleteProfilesForSubscription(self, unique: str): + """Remove profiles belonging to a deleted subscription group.""" + indexes = [ + index + for index, server in enumerate(Storage.UserServers()) + if server.itemSubscription == unique + ] + self.serverTable.deleteItemByIndex(indexes, showProgress=False) + + def _selectedUnique(self): + """Return the first selected subscription ID, if any.""" + selected = self.table.selectedUniques + + return selected[0] if selected else None + + def _openEditor(self, unique=None, initial=None): + """Open and retain an asynchronous add/edit dialog.""" + source = Storage.UserSubs().get(unique, initial or {}) + dialog = _SubscriptionEditorDialog(source, parent=self) + dialog.setWindowTitle( + _('Edit Subscription') if unique else _('Add Subscription') + ) + dialog.setWindowModality(QtCore.Qt.WindowModality.WindowModal) + + def finished(code): + """Persist accepted values through the existing table model.""" + if code != PySide6Legacy.enumValueWrapper(AppQDialog.DialogCode.Accepted): + return + + subscriptionUnique = unique or str(uuid.uuid4()) + existing = Storage.UserSubs().get(subscriptionUnique, {}) + self.table.appendNewItem( + unique=subscriptionUnique, + lastUpdated=existing.get('lastUpdated', ''), + **dialog.subscription(), + ) + self.table.selectRow(list(Storage.UserSubs()).index(subscriptionUnique)) + + dialog.finished.connect(finished) + dialog.open() + + @QtCore.Slot() + def addSubscription(self): + """Open the full subscription editor for a new group.""" + self._openEditor() + + @QtCore.Slot() + def addFromClipboard(self): + """Seed a subscription from an HTTP(S) URL on the clipboard.""" + url = QApplication.clipboard().text().strip() + parsed = urlsplit(url) + + if parsed.scheme.casefold() not in ('http', 'https') or not parsed.netloc: + self._openEditor() + + return + + self._openEditor( + initial={ + 'remark': parsed.hostname or _('Subscription'), + 'webURL': url, + 'enabled': True, + } + ) + + @QtCore.Slot() + def editSelected(self): + """Edit the selected subscription without relying on inline cells.""" + unique = self._selectedUnique() + + if unique is not None: + self._openEditor(unique) + + @QtCore.Slot() + def copySelectedURL(self): + """Copy the selected subscription URL for sharing/export.""" + unique = self._selectedUnique() + + if unique is not None: + QApplication.clipboard().setText( + str(Storage.UserSubs()[unique].get('webURL', '')) + ) + + def _selectedProxy(self): + """Resolve the proxy policy selected for manual synchronization.""" + key = self.proxyComboBox.currentData() or '' + resolver = SubscriptionTableView.ProxyOptions.get(key) + + return resolver() if callable(resolver) else None + + @QtCore.Slot() + def updateSelected(self): + """Synchronize selected enabled subscription groups together.""" + keys = tuple( + key + for key in self.table.selectedUniques + if Storage.UserSubs().get(key, {}).get('enabled', True) + and Storage.UserSubs().get(key, {}).get('webURL') + ) + + if not keys: + return + + depthMap = {'depth': len(keys)} + successArgs = [] + failureArgs = [] + httpProxy = self._selectedProxy() + + for key in keys: + self.serverTable.updateSubsByUnique( + key, + httpProxy, + depthMap=depthMap, + successArgs=successArgs, + failureArgs=failureArgs, + showMessageBox=True, + parent=self, + ) + + @QtCore.Slot() + def updateAll(self): + """Synchronize every enabled subscription with one proxy policy.""" + self.serverTable.updateSubs(self._selectedProxy(), parent=self) + + def updateSubsByUnique(self, unique: str, httpProxy, **kwargs): + """Preserve the established application subscription-update API.""" + self.serverTable.updateSubsByUnique(unique, httpProxy, **kwargs) + + def setIconsByTheme(self, theme: str): + """Apply theme-aware Fluent icons to subscription commands.""" + iconFactory = ( + bootstrapIconWhite if theme == AppStyleSheet.Dark else bootstrapIcon + ) + + for button, iconName in ( + (self.addButton, 'plus-lg.svg'), + (self.addFromClipboardButton, 'clipboard-plus.svg'), + (self.editButton, 'pencil-square.svg'), + (self.deleteButton, 'trash.svg'), + (self.copyURLButton, 'link-45deg.svg'), + (self.updateSelectedButton, 'arrow-repeat.svg'), + (self.updateAllButton, 'cloud-arrow-down.svg'), + ): + button.setIcon(iconFactory(iconName)) + + def themeChangedCallback(self, theme: str): + """Refresh command icons after a theme change.""" + self.setIconsByTheme(theme) + + def showEvent(self, event): + """Refresh persisted subscription data whenever the page is shown.""" + super().showEvent(event) + self.table.flushAll() + + def retranslate(self): + """Refresh page labels, commands, and proxy choices.""" + selectedProxy = self.proxyComboBox.currentData() + + with Mixins.QBlockSignalContext(self.proxyComboBox): + self.proxyComboBox.clear() + + for key in SubscriptionTableView.ProxyOptions: + self.proxyComboBox.addItem(_(key), key) + + index = self.proxyComboBox.findData(selectedProxy) + self.proxyComboBox.setCurrentIndex(max(index, 0)) + + self.pageTitleLabel.setText(_('Subscriptions')) + self.proxyLabel.setText(_('Update Using')) + self.addButton.setText(_('Add')) + self.addFromClipboardButton.setText(_('Add From Clipboard')) + self.editButton.setText(_('Edit')) + self.deleteButton.setText(_('Delete')) + self.copyURLButton.setText(_('Copy URL')) + self.updateSelectedButton.setText(_('Update Selected')) + self.updateAllButton.setText(_('Update All')) diff --git a/Furious/Window/__init__.py b/Furious/Window/__init__.py index 1a9a322..8189c2d 100644 --- a/Furious/Window/__init__.py +++ b/Furious/Window/__init__.py @@ -25,6 +25,7 @@ from .LogPage import LogPage from .MetricsPage import MetricsPage from .MainWindow import MainWindow from .SettingsPage import SettingsPage +from .SubscriptionPage import SubscriptionPage from .NetworkTestDialog import NetworkTestDialog from .ProxyBypassDialog import ProxyBypassDialog from .QRCodeWindow import QRCodeWindow @@ -39,6 +40,7 @@ __all__ = [ 'MetricsPage', 'MainWindow', 'SettingsPage', + 'SubscriptionPage', 'NetworkTestDialog', 'ProxyBypassDialog', 'QRCodeWindow',