Files
LorenEteval_Furious/Furious/Widget/SubscriptionTableView.py
T
2026-09-08 10:54:14 +08:00

954 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (C) 2024present Loren Eteval & contributors <loren.eteval@proton.me>
#
# This file is part of Furious.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Provide widgets for user subs Qt table view."""
from __future__ import annotations
from Furious.Frozenlib import (
PLATFORM,
AppFontName,
AppMainWindow,
AppSettings,
Mixins,
PySide6Legacy,
registerAppSettings,
)
from Furious.Models import UJSONEncoder
from Furious.Repository import Storage, SubscriptionGroup
from Furious.Service import (
SUBSCRIPTION_AUTO_UPDATE_OPTIONS,
SUBSCRIPTION_PROXY_OPTIONS,
formatTrafficUsage,
resolveSubscriptionProxy,
)
from Furious.Qt import (
MBoxQuestionDelete,
AppQAction,
AppQHeaderView,
AppQMenu,
AppQMessageBox,
AppQTableView,
)
from Furious.Qt import gettext as _
from PySide6 import QtCore
from PySide6.QtGui import QFont, QStandardItemModel
from PySide6.QtWidgets import QAbstractItemView, QHeaderView, QTableView
from typing import Union, Callable
import datetime
import functools
import logging
logger = logging.getLogger(__name__)
__all__ = ['SubscriptionTableView']
# Keep the original setting names. The primary value now stores semantic widths
# instead of QHeaderView's schema-dependent opaque state.
LEGACY_SUBSCRIPTION_SECTION_SIZE_SETTING = 'SubscriptionWidgetSectionSizeTable'
SUBSCRIPTION_HEADER_STATE_SETTING = 'UserSubsHeaderViewState'
registerAppSettings(LEGACY_SUBSCRIPTION_SECTION_SIZE_SETTING)
registerAppSettings(SUBSCRIPTION_HEADER_STATE_SETTING)
class SubscriptionTableHorizontalHeader(AppQHeaderView):
"""Provide the user subs Qt table view horizontal table header."""
ColumnKeys = (
'remark',
'webURL',
'enabled',
'lastSyncStatus',
'lastUpdated',
'subscriptionInfo',
'profiles',
)
DefaultSectionSizes = (260, 520, 120, 150, 220, 220, 140)
LegacyColumnKeys = ('remark', 'webURL', 'autoupdate', 'proxy')
# Format discriminator for the semantic JSON stored under
# UserSubsHeaderViewState. The namespace and version let us reject
# unrelated strings or a future incompatible semantic format.
StateFormat = 'Furious.SubscriptionHeaderWidths/1'
def __init__(self, *args, **kwargs):
"""Initialize the SubscriptionTableHorizontalHeader."""
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
def _configureInteractiveSections(self):
"""Normalize interaction without changing the logical column order."""
self.setSectionsMovable(False)
self.setCascadingSectionResizes(False)
self.setMinimumSectionSize(80)
self.setStretchLastSection(False)
self.setCustomSectionResizeMode()
def _applyDefaultSectionSizes(self):
"""Apply balanced widths for the concise subscription table."""
self.setStretchLastSection(False)
for section, size in enumerate(self.DefaultSectionSizes):
if section < self.count():
self.resizeSection(section, size)
@classmethod
def _semanticWidths(cls, state) -> dict[str, int]:
"""Decode the schema-independent header format, if present."""
if not isinstance(state, str):
return {}
try:
decoded = UJSONEncoder.decode(state)
except Exception:
# Any non-exit exceptions
return {}
if not isinstance(decoded, dict) or decoded.get('format') != cls.StateFormat:
return {}
widths = decoded.get('widths')
if not isinstance(widths, dict):
return {}
return {
key: width
for key, width in widths.items()
if key in cls.ColumnKeys
and isinstance(width, (int, float))
and not isinstance(width, bool)
and width > 0
}
@classmethod
def _legacyHeaderWidths(cls, state) -> dict[str, int]:
"""Extract widths from a legacy Qt state without touching this header."""
if state is None or isinstance(state, str):
return {}
probe = QTableView()
probeModel = QStandardItemModel(0, 1, probe)
probe.setModel(probeModel)
probeHeader = probe.horizontalHeader()
# restoreState() is intentionally used only on this disposable probe
# as a read-only, one-time compatibility path for the original
# four-column UserSubsHeaderViewState. Applying the opaque Qt state to
# the live five-column header could recreate phantom sections, legacy
# ordering, hidden state, and incompatible resize behavior.
try:
if not probeHeader.restoreState(state):
return {}
except Exception:
# Any non-exit exceptions
return {}
if probeHeader.count() != len(cls.LegacyColumnKeys):
return {}
for section in range(probeHeader.count()):
probeHeader.showSection(section)
return {
key: probeHeader.sectionSize(section)
for section, key in enumerate(cls.LegacyColumnKeys)
if key in cls.ColumnKeys and section < probeHeader.count()
}
@classmethod
def _oldestSectionWidths(cls) -> dict[str, int]:
"""Read the original index-based width table as a final fallback."""
try:
widths = UJSONEncoder.decode(
AppSettings.get(LEGACY_SUBSCRIPTION_SECTION_SIZE_SETTING)
)
except Exception:
# Any non-exit exceptions
return {}
if not isinstance(widths, dict):
return {}
result = {}
for section, key in enumerate(cls.LegacyColumnKeys):
width = widths.get(str(section))
if (
key in cls.ColumnKeys
and isinstance(width, (int, float))
and not isinstance(width, bool)
and width > 0
):
result[key] = width
return result
def _applyWidths(self, widths: dict[str, int]):
"""Apply semantic widths to the matching logical sections."""
for section, key in enumerate(self.ColumnKeys):
width = widths.get(key)
if width is not None:
self.resizeSection(
section,
max(self.minimumSectionSize(), round(width)),
)
def configureSections(self):
"""Restore semantic widths or safely read either historical format."""
self._configureInteractiveSections()
self._applyDefaultSectionSizes()
state = AppSettings.get(SUBSCRIPTION_HEADER_STATE_SETTING)
widths = self._semanticWidths(state) or self._legacyHeaderWidths(state)
if not widths:
widths = self._oldestSectionWidths()
self._applyWidths(widths)
# Saved Qt state may contain moved, hidden, or non-interactive sections.
# It is never restored onto the live header; the current schema owns
# order and interaction behavior.
for section in range(self.count()):
self.showSection(section)
self._configureInteractiveSections()
def cleanup(self):
"""Persist semantic widths under the established setting name."""
# Do not use QHeaderView.saveState() here. Its opaque binary data also
# records the section count, order, visibility, and resize modes, so it
# is unsafe to restore after the table schema changes. Persist only
# widths keyed by stable field names instead.
AppSettings.set(
SUBSCRIPTION_HEADER_STATE_SETTING,
UJSONEncoder.encode(
{
'format': self.StateFormat,
'widths': {
key: self.sectionSize(section)
for section, key in enumerate(self.ColumnKeys)
if section < self.count()
},
}
),
)
class SubscriptionTableVerticalHeader(AppQHeaderView):
"""Provide the user subs Qt table view vertical table header."""
def __init__(self, *args, **kwargs):
"""Initialize the SubscriptionTableVerticalHeader."""
super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs)
class SubscriptionTableColumn:
"""Describe and render user subs Qt table view table columns."""
def __init__(self, name: str, func: Callable[[dict], str] = None):
"""Initialize the SubscriptionTableColumn."""
self.name = name
self.func = func
def __call__(self, item: dict) -> str:
"""Invoke the user subs Qt table view headers as a callable."""
if callable(self.func):
return self.func(item)
else:
return ''
def __eq__(self, other):
"""Compare the user subs Qt table view headers with another value."""
return str(self) == str(other)
def __str__(self):
"""Return the display text for the user subs Qt table view headers."""
return self.name
def subscriptionSyncStatusText(item: dict) -> str:
"""Return one concise, localized subscription synchronization state."""
return {
'syncing': _('Updating...'),
'success': _('Updated'),
'error': _('Update Failed'),
}.get(str(item.get('lastSyncStatus', '')), _('Never'))
def _nonnegativeInteger(value) -> int:
"""Return a safe non-negative integer from persisted presentation input."""
try:
return max(int(value), 0)
except (TypeError, ValueError, OverflowError):
return 0
def subscriptionInfoText(item: dict) -> str:
"""Return compact provider-reported traffic usage and expiry metadata."""
parts = []
total = _nonnegativeInteger(item.get('subscriptionTotal', 0))
if total:
used = _nonnegativeInteger(
item.get('subscriptionUpload', 0)
) + _nonnegativeInteger(item.get('subscriptionDownload', 0))
parts.append(f'{formatTrafficUsage(used)} / {formatTrafficUsage(total)}')
expiresAt = _nonnegativeInteger(item.get('subscriptionExpire', 0))
if expiresAt:
try:
expires = datetime.datetime.fromtimestamp(
expiresAt, tz=datetime.timezone.utc
).date()
except (OverflowError, OSError, ValueError):
pass
else:
parts.append(expires.isoformat())
return ' · '.join(parts)
class UserSubsTableModel(QtCore.QAbstractTableModel):
"""Expose user subs table data through a Qt item model."""
def __init__(
self,
headers: list[SubscriptionTableColumn],
itemKey: list[str],
parent=None,
):
"""Initialize the UserSubsTableModel."""
super().__init__(parent)
self.headers = headers
self.itemKey = itemKey
def rowCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of rows exposed by the model."""
if parent.isValid():
return 0
return len(Storage.UserSubs())
def columnCount(self, parent=QtCore.QModelIndex()) -> int:
"""Return the number of columns exposed by the model."""
if parent.isValid():
return 0
return len(self.headers)
def flags(self, index):
"""Return the Qt item flags for a model index."""
if not index.isValid():
return QtCore.Qt.ItemFlag.NoItemFlags
flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable
if self.itemKey[index.column()] not in [
'autoupdate',
'proxy',
'enabled',
'lastSyncStatus',
'lastUpdated',
'subscriptionInfo',
'profiles',
]:
flags |= QtCore.Qt.ItemFlag.ItemIsEditable
return flags
def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
"""Return the data managed by the user subs table model."""
if not index.isValid():
return None
row = index.row()
column = index.column()
if row < 0 or row >= len(Storage.UserSubs()):
return None
if column < 0 or column >= len(self.headers):
return None
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
and profile.itemSubscriptionManaged
)
)
if role == QtCore.Qt.ItemDataRole.DisplayRole:
if mapped in ['autoupdate', 'proxy', 'enabled']:
return _(text)
return text
if role == QtCore.Qt.ItemDataRole.EditRole:
return text
if role == QtCore.Qt.ItemDataRole.ToolTipRole:
return text
if role == QtCore.Qt.ItemDataRole.FontRole:
return QFont(AppFontName())
return None
def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole) -> bool:
"""Update model data for the requested role."""
if role != QtCore.Qt.ItemDataRole.EditRole or not index.isValid():
return False
row = index.row()
column = index.column()
if row < 0 or row >= len(Storage.UserSubs()):
return False
mapped = self.itemKey[column]
if mapped in [
'autoupdate',
'proxy',
'enabled',
'lastSyncStatus',
'lastUpdated',
'subscriptionInfo',
'profiles',
]:
return False
self.subsObjectByRow(row)[mapped] = str(value)
self.dataChanged.emit(index, index, [])
return True
def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role=QtCore.Qt.ItemDataRole.DisplayRole,
):
"""Return display data for a table header section."""
if role != QtCore.Qt.ItemDataRole.DisplayRole:
return None
if orientation == QtCore.Qt.Orientation.Horizontal:
if 0 <= section < len(self.headers):
return _(str(self.headers[section]))
return None
return section + 1
@staticmethod
def uniqueByRow(row: int) -> str:
"""Return the unique by row value used by the user subs table model."""
return list(Storage.UserSubs().keys())[row]
@classmethod
def subsObjectByRow(cls, row: int) -> dict:
"""Return the subs object by row value used by the user subs table model."""
return Storage.UserSubs()[cls.uniqueByRow(row)]
def emitRowChanged(self, row: int, column: Union[int, None] = None):
"""Handle emit row changed for the user subs table model."""
if row < 0 or row >= self.rowCount():
return
if column is None:
left = self.index(row, 0)
right = self.index(row, self.columnCount() - 1)
else:
left = self.index(row, column)
right = left
self.dataChanged.emit(left, right, [])
def emitAllChanged(self):
"""Handle emit all changed for the user subs table model."""
if self.rowCount() == 0 or self.columnCount() == 0:
return
self.dataChanged.emit(
self.index(0, 0),
self.index(self.rowCount() - 1, self.columnCount() - 1),
[],
)
# Headers VALUE
_TRANSLATABLE_HEADERS = [
_('Never'),
_('Every 5 mins'),
_('Every 10 mins'),
_('Every 15 mins'),
_('Every 30 mins'),
_('Every 45 mins'),
_('Every 1 hour'),
_('Every 2 hours'),
_('Every 3 hours'),
_('Every 6 hours'),
_('Every 8 hours'),
_('Every 10 hours'),
_('Every 12 hours'),
_('Every 24 hours'),
_('Use current proxy'),
_('Force proxy'),
_('No proxy'),
_('Remark'),
_('Auto Update'),
_('Auto Update Use Proxy'),
_('Enabled'),
_('Disabled'),
_('Sync Status'),
_('Updating...'),
_('Updated'),
_('Update Failed'),
_('Last Updated'),
_('Usage / Expiry'),
_('Profiles'),
]
class SubscriptionTableView(Mixins.QTranslatable, AppQTableView):
"""Represent user subs Qt table view."""
groupsChanged = QtCore.Signal()
RowHeight = 42
AutoUpdateOptions = SUBSCRIPTION_AUTO_UPDATE_OPTIONS
ProxyOptions = {
option: functools.partial(resolveSubscriptionProxy, option)
for option in SUBSCRIPTION_PROXY_OPTIONS
}
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('Sync Status', subscriptionSyncStatusText),
SubscriptionTableColumn(
'Last Updated',
lambda item: item.get('lastUpdated', ''),
),
SubscriptionTableColumn('Usage / Expiry', subscriptionInfoText),
SubscriptionTableColumn('Profiles'),
]
# Corresponds to 'Headers'
ItemKey = [
'remark',
'webURL',
'enabled',
'lastSyncStatus',
'lastUpdated',
'subscriptionInfo',
'profiles',
]
def __init__(self, *args, **kwargs):
"""Initialize the subscription table view."""
self.deleteUniqueCallback, self.subsManager = (
kwargs.pop('deleteUniqueCallback', None),
kwargs.pop('subscriptionManager', None),
)
super().__init__(*args, **kwargs)
self.sourceModel = UserSubsTableModel(self.Headers, self.ItemKey, parent=self)
self.setModel(self.sourceModel)
# Install custom header
self.setHorizontalHeader(
SubscriptionTableHorizontalHeader(
parent=self,
sectionSizeSettingsName=SUBSCRIPTION_HEADER_STATE_SETTING,
)
)
self.setVerticalHeader(SubscriptionTableVerticalHeader(self))
self.setDefaultRowHeight(self.RowHeight)
header = self.horizontalHeader()
header.configureSections()
self.setSortingEnabled(False)
# Selection
self.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
# No drag and drop
self.setDragEnabled(False)
self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
self.setDropIndicatorShown(False)
self.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction)
self.moveUpActionRef = AppQAction(
_('Move Up'),
callback=lambda: self.moveSelectedGroups('up'),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_Up,
),
parent=self,
)
self.moveDownActionRef = AppQAction(
_('Move Down'),
callback=lambda: self.moveSelectedGroups('down'),
shortcut=QtCore.QKeyCombination(
QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.Key.Key_Down,
),
parent=self,
)
self.contextMenu = AppQMenu(
self.moveUpActionRef,
self.moveDownActionRef,
parent=self,
)
for action in (self.moveUpActionRef, self.moveDownActionRef):
action.setShortcutContext(QtCore.Qt.ShortcutContext.WidgetShortcut)
self.addAction(action)
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
# Signals
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
# Flush all data to table
self.flushAll()
@property
def selectedIndex(self):
"""Return the selected index value."""
return sorted(
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))
def _currentUnique(self):
"""Return the stable subscription ID represented by the current row."""
row = self.currentIndex().row()
keys = tuple(Storage.UserSubs())
return keys[row] if 0 <= row < len(keys) else None
def _restoreGroupSelection(self, selectedUniques, currentUnique):
"""Restore logical selection, current item, and keyboard focus by ID."""
rows = {unique: row for row, unique in enumerate(Storage.UserSubs())}
selection = self.selectionModel()
selection.clearSelection()
flags = (
QtCore.QItemSelectionModel.SelectionFlag.Select
| QtCore.QItemSelectionModel.SelectionFlag.Rows
)
for unique in selectedUniques:
row = rows.get(unique)
if row is not None:
selection.select(self.sourceModel.index(row, 0), flags)
currentRow = rows.get(currentUnique)
if currentRow is not None:
selection.setCurrentIndex(
self.sourceModel.index(currentRow, 0),
QtCore.QItemSelectionModel.SelectionFlag.NoUpdate,
)
self.setFocus()
@QtCore.Slot(QtCore.QPoint)
def handleCustomContextMenuRequested(self, point):
"""Handle custom context menu requested."""
self.contextMenu.exec(self.viewport().mapToGlobal(point))
def deleteSelectedItem(self):
"""Delete selected item."""
indexes = self.selectedIndex
if len(indexes) == 0:
# Nothing to do
return
def handleResultCode(_indexes, code):
"""Handle result code."""
if code == PySide6Legacy.enumValueWrapper(
AppQMessageBox.StandardButton.Yes
):
for i in range(len(_indexes)):
deleteIndex = _indexes[i] - i
deleteUnique = list(Storage.UserSubs().keys())[deleteIndex]
self.sourceModel.beginRemoveRows(
QtCore.QModelIndex(),
deleteIndex,
deleteIndex,
)
Storage.removeSubscriptionGroup(deleteUnique)
if self.subsManager is not None:
self.subsManager.removeAutoUpdate(deleteUnique)
self.sourceModel.endRemoveRows()
if callable(self.deleteUniqueCallback):
self.deleteUniqueCallback(deleteUnique)
for order, value in enumerate(Storage.UserSubs().values()):
value['sortOrder'] = order
self.flushAll()
self.groupsChanged.emit()
else:
# Do not delete
pass
if PLATFORM == 'Windows':
# Windows
mbox = MBoxQuestionDelete(icon=AppQMessageBox.Icon.Question)
else:
# macOS & linux
mbox = MBoxQuestionDelete(
icon=AppQMessageBox.Icon.Question, parent=self.parent()
)
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
mbox.isMulti = bool(len(indexes) > 1)
mbox.possibleRemark = self.sourceModel.data(
self.sourceModel.index(indexes[0], 0),
QtCore.Qt.ItemDataRole.DisplayRole,
)
mbox.setText(mbox.customText())
mbox.finished.connect(functools.partial(handleResultCode, indexes))
# Show the MessageBox asynchronously
mbox.open()
def moveSelectedGroups(self, position: str):
"""Move selected groups while preserving identity, selection, and timers."""
selectedUniques = self.selectedUniques
if not selectedUniques or position not in ('up', 'down'):
return
currentUnique = self._currentUnique()
self.sourceModel.layoutAboutToBeChanged.emit()
changed = Storage.moveSubscriptionGroups(selectedUniques, position)
self.sourceModel.layoutChanged.emit()
self._restoreGroupSelection(selectedUniques, currentUnique)
if changed:
self.groupsChanged.emit()
@QtCore.Slot(object)
def refreshSubscriptionState(self, uniques):
"""Repaint metadata cells for stable subscription IDs only."""
rows = {unique: row for row, unique in enumerate(Storage.UserSubs())}
firstColumn, lastColumn = (
self.ItemKey.index('lastSyncStatus'),
self.ItemKey.index('profiles'),
)
changedRows = sorted(
row
for unique in dict.fromkeys(uniques)
if (row := rows.get(unique)) is not None
)
if not changedRows:
return
firstRow = previousRow = changedRows[0]
for row in (*changedRows[1:], None):
if row is not None and row == previousRow + 1:
previousRow = row
continue
self.sourceModel.dataChanged.emit(
self.sourceModel.index(firstRow, firstColumn),
self.sourceModel.index(previousRow, lastColumn),
[],
)
if row is not None:
firstRow = previousRow = row
def flushItem(self, row, column, item):
"""Refresh item."""
if row < 0 or row >= self.sourceModel.rowCount():
return
self.sourceModel.emitRowChanged(row, column)
def flushRow(self, row, item):
"""Refresh row."""
for column in list(range(self.sourceModel.columnCount())):
self.flushItem(row, column, item)
def flushAll(self):
"""Refresh all."""
for item in Storage.UserSubs().values():
item.setdefault('enabled', True)
item.setdefault('userAgent', '')
item.setdefault('filter', '')
item.setdefault('lastUpdated', '')
item.setdefault('lastSyncStatus', '')
self.sourceModel.emitAllChanged()
for index, key in enumerate(Storage.UserSubs()):
self.flushRow(index, Storage.UserSubs()[key])
def appendNewItem(self, **kwargs):
"""Append new item."""
(
unique,
remark,
webURL,
enabled,
autoupdate,
proxy,
userAgent,
profileFilter,
lastUpdated,
) = (
kwargs.pop('unique', ''),
kwargs.pop('remark', ''),
kwargs.pop('webURL', ''),
kwargs.pop('enabled', True),
kwargs.pop('autoupdate', ''),
kwargs.pop('proxy', ''),
kwargs.pop('userAgent', ''),
kwargs.pop('filter', ''),
kwargs.pop('lastUpdated', ''),
)
group = Storage.SubscriptionGroup(unique) or SubscriptionGroup(
id=unique,
sortOrder=len(Storage.UserSubs()),
)
group.remark = remark
group.webURL = webURL
group.enabled = bool(enabled)
group.autoupdate = autoupdate
group.proxy = proxy
group.userAgent = userAgent
group.filter = profileFilter
group.lastUpdated = lastUpdated
subsob = {unique: group.toMapping()}
if unique in Storage.UserSubs():
if self.subsManager is not None:
self.subsManager.cancelUpdates(unique)
row = list(Storage.UserSubs().keys()).index(unique)
Storage.upsertSubscriptionGroup(group)
self.flushRow(row, subsob[unique])
else:
row = self.sourceModel.rowCount()
self.sourceModel.beginInsertRows(QtCore.QModelIndex(), row, row)
Storage.upsertSubscriptionGroup(group)
self.sourceModel.endInsertRows()
self.flushRow(row, subsob[unique])
if self.subsManager is not None:
self.subsManager.configureAutoUpdate(unique)
self.groupsChanged.emit()
@staticmethod
def updateSubsByUnique(
unique: str, httpProxy: Union[str, Callable, None], **kwargs
):
"""Update subs by unique."""
showMessageBox = kwargs.pop('showMessageBox', False)
if callable(httpProxy):
try:
realHttpProxy = httpProxy()
except Exception as ex:
# Any non-exit exceptions
logger.error(
f'error while configuring http proxy: {ex}. '
f'Update subscription uses no proxy'
)
realHttpProxy = None
else:
realHttpProxy = httpProxy
AppMainWindow().updateSubsByUnique(
unique, realHttpProxy, showMessageBox=showMessageBox, **kwargs
)
def retranslate(self):
"""Refresh translated text for the user subs Qt table view."""
self.sourceModel.headerDataChanged.emit(
QtCore.Qt.Orientation.Horizontal,
0,
len(self.Headers) - 1,
)