mirror of
https://github.com/LorenEteval/Furious.git
synced 2026-09-27 01:10:22 +03:00
534 lines
16 KiB
Python
534 lines
16 KiB
Python
# 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.
|
||
#
|
||
# 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/>.
|
||
|
||
from __future__ import annotations
|
||
|
||
from Furious.Frozenlib import *
|
||
from Furious.Library import *
|
||
from Furious.Qt import *
|
||
from Furious.Qt import gettext as _
|
||
|
||
from PySide6 import QtCore
|
||
from PySide6.QtGui import *
|
||
from PySide6.QtWidgets import *
|
||
|
||
from typing import Union, Callable
|
||
|
||
import logging
|
||
import functools
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
__all__ = ['UserSubsQTableWidget']
|
||
|
||
# Migrate legacy settings
|
||
registerAppSettings('SubscriptionWidgetSectionSizeTable')
|
||
registerAppSettings('UserSubsHeaderViewState')
|
||
|
||
|
||
class UserSubsQTableWidgetHorizontalHeader(AppQHeaderView):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs)
|
||
|
||
|
||
class UserSubsQTableWidgetVerticalHeader(AppQHeaderView):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs)
|
||
|
||
|
||
class UserSubsQTableWidgetHeaders:
|
||
def __init__(self, name: str, func: Callable[[dict], str] = None):
|
||
self.name = name
|
||
self.func = func
|
||
|
||
def __call__(self, item: dict) -> str:
|
||
if callable(self.func):
|
||
return self.func(item)
|
||
else:
|
||
return ''
|
||
|
||
def __eq__(self, other):
|
||
return str(self) == str(other)
|
||
|
||
def __str__(self):
|
||
return self.name
|
||
|
||
|
||
class UserSubsAppQComboBox(AppQComboBox):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def retranslate(self):
|
||
# Do not emit 'currentTextChanged' when retranslate
|
||
with Mixins.QBlockSignalContext(self):
|
||
super().retranslate()
|
||
|
||
|
||
# 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'),
|
||
]
|
||
|
||
|
||
class UserSubsQTableWidget(Mixins.QTranslatable, AppQTableWidget):
|
||
AutoUpdateOptions = {
|
||
'': None,
|
||
'Never': None,
|
||
'Every 5 mins': 5 * 60 * 1000,
|
||
'Every 10 mins': 10 * 60 * 1000,
|
||
'Every 15 mins': 15 * 60 * 1000,
|
||
'Every 30 mins': 30 * 60 * 1000,
|
||
'Every 45 mins': 45 * 60 * 1000,
|
||
'Every 1 hour': 1 * 60 * 60 * 1000,
|
||
'Every 2 hours': 2 * 60 * 60 * 1000,
|
||
'Every 3 hours': 3 * 60 * 60 * 1000,
|
||
'Every 6 hours': 6 * 60 * 60 * 1000,
|
||
'Every 8 hours': 8 * 60 * 60 * 1000,
|
||
'Every 10 hours': 10 * 60 * 60 * 1000,
|
||
'Every 12 hours': 12 * 60 * 60 * 1000,
|
||
'Every 24 hours': 24 * 60 * 60 * 1000,
|
||
}
|
||
|
||
ProxyOptions = {
|
||
'': lambda: None,
|
||
'Use current proxy': lambda: Storage.Extras.UserHttpProxy(),
|
||
'Force proxy': lambda: '127.0.0.1:10809',
|
||
'No proxy': lambda: None,
|
||
}
|
||
|
||
Headers = [
|
||
UserSubsQTableWidgetHeaders('Remark', lambda item: item.get('remark', '')),
|
||
UserSubsQTableWidgetHeaders('URL', lambda item: item.get('webURL', '')),
|
||
UserSubsQTableWidgetHeaders(
|
||
'Auto Update', lambda item: item.get('autoupdate', '')
|
||
),
|
||
UserSubsQTableWidgetHeaders(
|
||
'Auto Update Use Proxy', lambda item: item.get('proxy', '')
|
||
),
|
||
]
|
||
|
||
# Corresponds to 'Headers'
|
||
ItemKey = ['remark', 'webURL', 'autoupdate', 'proxy']
|
||
|
||
def __init__(self, *args, **kwargs):
|
||
self.deleteUniqueCallback = kwargs.pop('deleteUniqueCallback', None)
|
||
|
||
super().__init__(*args, **kwargs)
|
||
|
||
self.timers = list(QtCore.QTimer() for i in range(len(Storage.UserSubs())))
|
||
|
||
# Must set before flush all
|
||
self.setColumnCount(len(self.Headers))
|
||
|
||
# Flush all data to table
|
||
self.flushAll()
|
||
|
||
# Install custom header
|
||
self.setHorizontalHeader(
|
||
UserSubsQTableWidgetHorizontalHeader(
|
||
parent=self,
|
||
legacySectionSizeSettingsName='SubscriptionWidgetSectionSizeTable',
|
||
sectionSizeSettingsName='UserSubsHeaderViewState',
|
||
)
|
||
)
|
||
self.setVerticalHeader(UserSubsQTableWidgetVerticalHeader(self))
|
||
|
||
self.horizontalHeader().setCustomSectionResizeMode()
|
||
self.horizontalHeader().restoreSectionSize()
|
||
|
||
self.setHorizontalHeaderLabels(list(_(str(header)) for header in self.Headers))
|
||
|
||
# Selection
|
||
self.setSelectionColor(AppHue.disconnectedColor())
|
||
self.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||
self.setSelectionMode(QTableWidget.SelectionMode.ExtendedSelection)
|
||
|
||
# No drag and drop
|
||
self.setDragEnabled(False)
|
||
self.setDragDropMode(QAbstractItemView.DragDropMode.NoDragDrop)
|
||
self.setDropIndicatorShown(False)
|
||
self.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction)
|
||
|
||
contextMenuActions = [
|
||
AppQAction(
|
||
_('Delete'),
|
||
callback=lambda: self.deleteSelectedItem(),
|
||
),
|
||
]
|
||
|
||
self.contextMenu = AppQMenu(*contextMenuActions)
|
||
self.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
||
|
||
# Signals
|
||
self.itemChanged.connect(self.handleItemChanged)
|
||
self.customContextMenuRequested.connect(self.handleCustomContextMenuRequested)
|
||
|
||
@QtCore.Slot(QTableWidgetItem)
|
||
def handleItemChanged(self, item: QTableWidgetItem):
|
||
unique = list(Storage.UserSubs().keys())[item.row()]
|
||
mapped = UserSubsQTableWidget.ItemKey[item.column()]
|
||
|
||
# 'autoupdate', 'proxy' is not triggered here
|
||
assert mapped not in ['autoupdate', 'proxy']
|
||
|
||
item.setToolTip(item.text())
|
||
|
||
Storage.UserSubs()[unique][mapped] = item.text()
|
||
|
||
@QtCore.Slot(QtCore.QPoint)
|
||
def handleCustomContextMenuRequested(self, point):
|
||
self.contextMenu.exec(self.mapToGlobal(point))
|
||
|
||
def deleteSelectedItem(self):
|
||
indexes = self.selectedIndex
|
||
|
||
if len(indexes) == 0:
|
||
# Nothing to do
|
||
return
|
||
|
||
def handleResultCode(_indexes, 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.removeRow(deleteIndex)
|
||
|
||
Storage.UserSubs().pop(deleteUnique)
|
||
|
||
# Begin timer cleanup
|
||
qtimer = self.timers[deleteIndex]
|
||
|
||
assert isinstance(qtimer, QtCore.QTimer)
|
||
|
||
try:
|
||
qtimer.timeout.disconnect()
|
||
except Exception as ex:
|
||
# Any non-exit exceptions
|
||
|
||
# Disconnect all previous signals if possible
|
||
pass
|
||
|
||
qtimer.stop()
|
||
# End timer cleanup
|
||
|
||
# Remove timer from list
|
||
self.timers.pop(deleteIndex)
|
||
|
||
if callable(self.deleteUniqueCallback):
|
||
self.deleteUniqueCallback(deleteUnique)
|
||
else:
|
||
# Do not delete
|
||
pass
|
||
|
||
if PLATFORM == 'Windows':
|
||
# Windows
|
||
mbox = QuestionDeleteMBox(icon=AppQMessageBox.Icon.Question)
|
||
else:
|
||
# macOS & linux
|
||
mbox = QuestionDeleteMBox(
|
||
icon=AppQMessageBox.Icon.Question, parent=self.parent()
|
||
)
|
||
mbox.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
|
||
|
||
mbox.isMulti = bool(len(indexes) > 1)
|
||
mbox.possibleRemark = self.item(indexes[0], 0).text()
|
||
mbox.setText(mbox.customText())
|
||
mbox.finished.connect(functools.partial(handleResultCode, indexes))
|
||
|
||
# Show the MessageBox asynchronously
|
||
mbox.open()
|
||
|
||
def handleAutoUpdateComboBoxCurrentTextChanged(self, text: str, row: int):
|
||
textEnglish = _(text, 'EN')
|
||
|
||
unique = list(Storage.UserSubs().keys())[row]
|
||
subsob = Storage.UserSubs()[unique]
|
||
|
||
remark, webURL = subsob['remark'], subsob['webURL']
|
||
|
||
try:
|
||
timems = self.AutoUpdateOptions[textEnglish]
|
||
except KeyError:
|
||
# Invalid key. Reset
|
||
logger.error(f'\'{textEnglish}\' is not in auto update options. Reset')
|
||
|
||
textEnglish = ''
|
||
|
||
# Write to subs object
|
||
subsob['autoupdate'] = textEnglish
|
||
|
||
timems = self.AutoUpdateOptions[textEnglish]
|
||
|
||
qtimer = self.timers[row]
|
||
|
||
assert isinstance(qtimer, QtCore.QTimer)
|
||
|
||
if timems is not None:
|
||
logger.info(
|
||
f'start auto update job for subscription ({remark}, {webURL}). '
|
||
f'Interval is {timems // (60 * 1000)} mins'
|
||
)
|
||
|
||
def getHttpProxy(_subsob):
|
||
return self.ProxyOptions[_subsob.get('proxy', '')]()
|
||
|
||
try:
|
||
qtimer.timeout.disconnect()
|
||
except Exception as ex:
|
||
# Any non-exit exceptions
|
||
|
||
# Disconnect all previous signals if possible
|
||
pass
|
||
|
||
qtimer.timeout.connect(
|
||
functools.partial(
|
||
self.updateSubsByUnique,
|
||
unique=unique,
|
||
httpProxy=functools.partial(getHttpProxy, subsob),
|
||
)
|
||
)
|
||
qtimer.start(timems)
|
||
else:
|
||
logger.info(f'stop auto update job for subscription ({remark}, {webURL})')
|
||
|
||
try:
|
||
qtimer.timeout.disconnect()
|
||
except Exception as ex:
|
||
# Any non-exit exceptions
|
||
|
||
# Disconnect all previous signals if possible
|
||
pass
|
||
|
||
qtimer.stop()
|
||
|
||
# Write to subs object
|
||
subsob['autoupdate'] = textEnglish
|
||
|
||
# Return potentially fixed value
|
||
return textEnglish
|
||
|
||
def handleProxyComboBoxCurrentTextChanged(self, text: str, row: int):
|
||
textEnglish = _(text, 'EN')
|
||
|
||
unique = list(Storage.UserSubs().keys())[row]
|
||
subsob = Storage.UserSubs()[unique]
|
||
|
||
try:
|
||
proxyFn = self.ProxyOptions[textEnglish]
|
||
except KeyError:
|
||
# Invalid key. Reset
|
||
logger.error(f'\'{textEnglish}\' is not in proxy options. Reset')
|
||
|
||
textEnglish = ''
|
||
|
||
# Write to subs object
|
||
subsob['proxy'] = textEnglish
|
||
|
||
proxyFn = self.ProxyOptions[textEnglish]
|
||
|
||
assert callable(proxyFn)
|
||
|
||
# Write to subs object
|
||
subsob['proxy'] = textEnglish
|
||
|
||
# Return potentially fixed value
|
||
return textEnglish
|
||
|
||
def flushItem(self, row, column, item):
|
||
if column == self.Headers.index('Auto Update'):
|
||
header = self.Headers[column]
|
||
|
||
oldItem = self.item(row, column)
|
||
newItem = UserSubsAppQComboBox()
|
||
newItem.addItems(list(_(key) for key in self.AutoUpdateOptions.keys()))
|
||
|
||
if oldItem is None:
|
||
# Item does not exist
|
||
newItem.setFont(QFont(AppFontName()))
|
||
else:
|
||
# Use existing
|
||
newItem.setFont(oldItem.font())
|
||
|
||
index = newItem.findText(
|
||
_(self.handleAutoUpdateComboBoxCurrentTextChanged(header(item), row))
|
||
)
|
||
|
||
if index >= 0:
|
||
newItem.setCurrentIndex(index)
|
||
|
||
newItem.currentTextChanged.connect(
|
||
functools.partial(
|
||
self.handleAutoUpdateComboBoxCurrentTextChanged,
|
||
row=row,
|
||
)
|
||
)
|
||
|
||
self.setCellWidget(row, column, newItem)
|
||
elif column == self.Headers.index('Auto Update Use Proxy'):
|
||
header = self.Headers[column]
|
||
|
||
oldItem = self.item(row, column)
|
||
newItem = UserSubsAppQComboBox()
|
||
newItem.addItems(list(_(key) for key in self.ProxyOptions.keys()))
|
||
|
||
if oldItem is None:
|
||
# Item does not exist
|
||
newItem.setFont(QFont(AppFontName()))
|
||
else:
|
||
# Use existing
|
||
newItem.setFont(oldItem.font())
|
||
|
||
index = newItem.findText(
|
||
_(self.handleProxyComboBoxCurrentTextChanged(header(item), row))
|
||
)
|
||
|
||
if index >= 0:
|
||
newItem.setCurrentIndex(index)
|
||
|
||
newItem.currentTextChanged.connect(
|
||
functools.partial(
|
||
self.handleProxyComboBoxCurrentTextChanged,
|
||
row=row,
|
||
)
|
||
)
|
||
|
||
self.setCellWidget(row, column, newItem)
|
||
else:
|
||
header = self.Headers[column]
|
||
text = header(item)
|
||
|
||
oldItem = self.item(row, column)
|
||
newItem = QTableWidgetItem(text)
|
||
newItem.setToolTip(text)
|
||
|
||
if oldItem is None:
|
||
# Item does not exist
|
||
newItem.setFont(QFont(AppFontName()))
|
||
else:
|
||
# Use existing
|
||
newItem.setFont(oldItem.font())
|
||
newItem.setForeground(oldItem.foreground())
|
||
|
||
if oldItem.textAlignment() != 0:
|
||
newItem.setTextAlignment(oldItem.textAlignment())
|
||
|
||
# Editable
|
||
newItem.setFlags(
|
||
QtCore.Qt.ItemFlag.ItemIsEnabled
|
||
| QtCore.Qt.ItemFlag.ItemIsSelectable
|
||
| QtCore.Qt.ItemFlag.ItemIsEditable
|
||
)
|
||
|
||
self.setItem(row, column, newItem)
|
||
|
||
def flushRow(self, row, item):
|
||
for column in list(range(self.columnCount())):
|
||
self.flushItem(row, column, item)
|
||
|
||
def flushAll(self):
|
||
if self.rowCount() == 0:
|
||
# Should insert row
|
||
for index, key in enumerate(Storage.UserSubs()):
|
||
self.insertRow(index)
|
||
self.flushRow(index, Storage.UserSubs()[key])
|
||
else:
|
||
for index, key in enumerate(Storage.UserSubs()):
|
||
self.flushRow(index, Storage.UserSubs()[key])
|
||
|
||
def appendNewItem(self, **kwargs):
|
||
unique, remark, webURL, autoupdate, proxy = (
|
||
kwargs.pop('unique', ''),
|
||
kwargs.pop('remark', ''),
|
||
kwargs.pop('webURL', ''),
|
||
# Below values are not filled when adding item via Gui
|
||
kwargs.pop('autoupdate', ''),
|
||
kwargs.pop('proxy', ''),
|
||
)
|
||
|
||
# The internal subs object
|
||
subsob = {
|
||
unique: {
|
||
'remark': remark,
|
||
'webURL': webURL,
|
||
'autoupdate': autoupdate,
|
||
'proxy': proxy,
|
||
}
|
||
}
|
||
|
||
# 'unique' should be unique in subscription storage,
|
||
# but protect it anyway.
|
||
if unique not in Storage.UserSubs():
|
||
# Add timer
|
||
self.timers.append(QtCore.QTimer())
|
||
|
||
Storage.UserSubs().update(subsob)
|
||
|
||
row = self.rowCount()
|
||
|
||
self.insertRow(row)
|
||
self.flushRow(row, subsob[unique])
|
||
|
||
@staticmethod
|
||
def updateSubsByUnique(
|
||
unique: str, httpProxy: Union[str, Callable, None], **kwargs
|
||
):
|
||
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
|
||
|
||
APP().mainWindow.updateSubsByUnique(
|
||
unique, realHttpProxy, showMessageBox=showMessageBox, **kwargs
|
||
)
|
||
|
||
def retranslate(self):
|
||
self.setHorizontalHeaderLabels(list(_(str(header)) for header in self.Headers))
|