mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-10-07 06:18:05 +03:00
Add dedicated subscription management page
Signed-off-by: Loren Eteval <loren.eteval@proton.me>
This commit is contained in:
Vendored
+173
-79
@@ -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": "Обновить все"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,12 @@ class UserSubEntry:
|
||||
|
||||
remark: str
|
||||
webURL: str
|
||||
enabled: bool
|
||||
autoupdate: str
|
||||
proxy: str
|
||||
userAgent: str
|
||||
filter: str
|
||||
lastUpdated: str
|
||||
|
||||
|
||||
class UserSub:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
+33
-184
@@ -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(
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
# 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.
|
||||
|
||||
"""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'))
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user