diff --git a/Furious/Utility/Settings.py b/Furious/Utility/Settings.py index 7df1821..36e64a6 100644 --- a/Furious/Utility/Settings.py +++ b/Furious/Utility/Settings.py @@ -82,6 +82,8 @@ SUPPORTED_SETTINGS = ( Settings('CustomRouting'), # User Configuration Settings('Configuration'), + # User Custom Subscription object + Settings('CustomSubscription'), # User Activated Server Index Settings('ActivatedItemIndex'), # User Tor Relay Settings, @@ -90,10 +92,14 @@ SUPPORTED_SETTINGS = ( Settings('ServerWidgetWindowSize'), # Routes Widget Window Size Settings('RoutesWidgetWindowSize'), + # Subscription Widget Window Size + Settings('SubscriptionWidgetWindowSize'), # Server Widget Section Size Settings('ServerWidgetSectionSizeTable'), # Routes Widget Section Size Settings('RoutesWidgetSectionSizeTable'), + # Subscription Widget Section Size + Settings('SubscriptionWidgetSectionSizeTable'), # Server Widget Font Point Size Settings('ServerWidgetPointSize'), # Log Viewer Widget Font Point Size diff --git a/Furious/Utility/Translator.py b/Furious/Utility/Translator.py index 56ec284..5f7fc65 100644 --- a/Furious/Utility/Translator.py +++ b/Furious/Utility/Translator.py @@ -108,6 +108,7 @@ NO_TRANSLATION = [ TOR_FAQ_LABEL, 'socks', 'http', + 'URL', ] NO_TRANSLATION_DICT = {key: True for key in NO_TRANSLATION} @@ -169,20 +170,20 @@ TRANSLATION = { 'ZH': '自定义', 'TW': '自定義', }, - 'VPN Mode': { - 'ES': 'Modo VPN', - 'ZH': 'VPN模式', - 'TW': 'VPN模式', + 'Subscription': { + 'ES': 'Suscripción', + 'ZH': '订阅', + 'TW': '訂閲', }, - 'VPN Mode Disabled (Administrator)': { - 'ES': 'Modo VPN Deshabilitado (Administrador)', - 'ZH': 'VPN模式已禁用(管理员)', - 'TW': 'VPN模式已禁用(管理員)', + 'Update Subscription': { + 'ES': 'Actualizar Suscripción', + 'ZH': '更新订阅', + 'TW': '更新訂閲', }, - 'VPN Mode Disabled (root)': { - 'ES': 'Modo VPN Deshabilitado (root)', - 'ZH': 'VPN模式已禁用(root)', - 'TW': 'VPN模式已禁用(root)', + 'Edit Subscription...': { + 'ES': 'Editar Suscripción...', + 'ZH': '编辑订阅...', + 'TW': '編輯訂閲...', }, 'Import': { 'ES': 'Importar', @@ -229,6 +230,21 @@ TRANSLATION = { 'ZH': '在编辑器中显示制表符和空格', 'TW': '在編輯器中顯示制表符和空格', }, + 'VPN Mode': { + 'ES': 'Modo VPN', + 'ZH': 'VPN模式', + 'TW': 'VPN模式', + }, + 'VPN Mode Disabled (Administrator)': { + 'ES': 'Modo VPN Deshabilitado (Administrador)', + 'ZH': 'VPN模式已禁用(管理员)', + 'TW': 'VPN模式已禁用(管理員)', + }, + 'VPN Mode Disabled (root)': { + 'ES': 'Modo VPN Deshabilitado (root)', + 'ZH': 'VPN模式已禁用(root)', + 'TW': 'VPN模式已禁用(root)', + }, 'Routing Settings...': { 'ES': 'Ajustes de Enrutamiento...', 'ZH': '路由设置...', @@ -791,6 +807,32 @@ TRANSLATION = { 'ZH': f'注意:"自定义"将使用当前用户配置中定义的acl和mmdb。', 'TW': f'注意:"自定義"將使用當前用戶配置中定義的acl和mmdb。', }, + # Edit Subscription Widget + 'Edit Subscription': { + 'ES': 'Editar Suscripción', + 'ZH': '编辑订阅', + 'TW': '編輯訂閲', + }, + 'Subscription List': { + 'ES': 'Lista de Suscripción', + 'ZH': '订阅列表', + 'TW': '訂閲列表', + }, + 'Add subscription': { + 'ES': 'Agregar suscripción', + 'ZH': '添加订阅', + 'TW': '添加訂閲', + }, + 'Enter subscription remark:': { + 'ES': 'Introducir comentario de suscripción:', + 'ZH': '输入订阅名称:', + 'TW': '輸入訂閲名稱:', + }, + 'Enter subscription URL:': { + 'ES': 'Introducir URL de suscripción:', + 'ZH': '输入订阅URL:', + 'TW': '輸入訂閲URL:', + }, # Asset Viewer 'Asset File': { 'ES': 'Activos', diff --git a/Furious/Utility/Utility.py b/Furious/Utility/Utility.py index dfe41ad..e903c7d 100644 --- a/Furious/Utility/Utility.py +++ b/Furious/Utility/Utility.py @@ -152,6 +152,44 @@ class RoutesStorage: APP().CustomRouting = '' +class SubscriptionStorage: + # remark, webURL, unique + EMPTY_OBJECT = {'model': []} + + @staticmethod + def init(): + return copy.deepcopy(SubscriptionStorage.EMPTY_OBJECT) + + @staticmethod + def sync(ob=None): + if ob is None: + # Object is up-to-date + APP().CustomSubscription = SubscriptionStorage.toStorage( + APP().SubscriptionWidget.StorageObj + ) + else: + # Object is up-to-date + APP().CustomSubscription = SubscriptionStorage.toStorage(ob) + + @staticmethod + def toObject(st): + if not st: + # Server storage does not exist, or is empty + return SubscriptionStorage.init() + + return ujson.loads(Base64Encoder.decode(st)) + + @staticmethod + def toStorage(ob): + return Base64Encoder.encode( + ujson.dumps(ob, ensure_ascii=False, escape_forward_slashes=False).encode() + ) + + @staticmethod + def clear(): + APP().CustomSubscription = '' + + class TorRelaySettingsStorage: EMPTY_OBJECT = { 'socksTunnelPort': DEFAULT_TOR_SOCKS_PORT, diff --git a/Furious/Widget/Application.py b/Furious/Widget/Application.py index 98d1269..ae69dfd 100644 --- a/Furious/Widget/Application.py +++ b/Furious/Widget/Application.py @@ -18,6 +18,7 @@ from Furious.Widget.SystemTrayIcon import SystemTrayIcon from Furious.Widget.EditConfiguration import EditConfigurationWidget from Furious.Widget.EditRouting import EditRoutingWidget +from Furious.Widget.EditSubscription import EditSubscriptionWidget from Furious.Widget.LogViewer import LogViewerWidget, TorViewerWidget from Furious.Widget.TorRelaySettings import TorRelaySettingsWidget from Furious.Utility.Constants import ( @@ -170,6 +171,7 @@ class Application(SingletonApplication): self.ServerWidget = None self.RoutesWidget = None + self.SubscriptionWidget = None self.torRelaySettingsWidget = None def __getattr__(self, key): @@ -270,6 +272,7 @@ class Application(SingletonApplication): self.ServerWidget = EditConfigurationWidget() self.RoutesWidget = EditRoutingWidget() + self.SubscriptionWidget = EditSubscriptionWidget() self.torRelaySettingsWidget = TorRelaySettingsWidget() self.tray = SystemTrayIcon() diff --git a/Furious/Widget/AssetViewer.py b/Furious/Widget/AssetViewer.py index d6848c5..dda60d3 100644 --- a/Furious/Widget/AssetViewer.py +++ b/Furious/Widget/AssetViewer.py @@ -209,20 +209,9 @@ class AssetViewerWidget(SupportThemeChangedCallback, MainWindow): self._fileMenu = Menu(*fileMenuActions, title=_('File'), parent=self) self.menuBar().addMenu(self._fileMenu) - if PLATFORM != 'Darwin': - self.setWidthAndHeight() - - def show(self): - super().show() - - if PLATFORM == 'Darwin': - self.setWidthAndHeight() - def setWidthAndHeight(self): self.setGeometry(100, 100, 360, 360 * GOLDEN_RATIO) - moveToCenter(self) - @QtCore.Slot(QtCore.QPoint) def handleCustomContextMenuRequested(self, point): self.contextMenu.exec(self.mapToGlobal(point)) diff --git a/Furious/Widget/EditConfiguration.py b/Furious/Widget/EditConfiguration.py index 7bc7123..71c167b 100644 --- a/Furious/Widget/EditConfiguration.py +++ b/Furious/Widget/EditConfiguration.py @@ -622,6 +622,23 @@ class ZoomOutAction(Action): self.parent().plainTextEdit.zoomOut() +class UpdateSubscriptionAction(Action): + def __init__(self, **kwargs): + super().__init__(_('Update Subscription'), **kwargs) + + +class EditSubscriptionAction(Action): + def __init__(self, **kwargs): + super().__init__( + _('Edit Subscription...'), + icon=bootstrapIcon('star.svg'), + **kwargs, + ) + + def triggeredCallback(self, checked): + APP().SubscriptionWidget.show() + + class ShowLogAction(Action): def __init__(self, **kwargs): super().__init__(_(f'Show {APPLICATION_NAME} Log...'), **kwargs) @@ -1827,6 +1844,15 @@ class EditConfigurationWidget(MainWindow): ], } + subsMenu = { + 'name': 'Subscription', + 'actions': [ + UpdateSubscriptionAction(parent=self), + Seperator(), + EditSubscriptionAction(parent=self), + ], + } + helpMenu = { 'name': 'Help', 'actions': [ @@ -1839,7 +1865,7 @@ class EditConfigurationWidget(MainWindow): } # Menus - for menuDict in (fileMenu, editMenu, viewMenu, helpMenu): + for menuDict in (fileMenu, editMenu, viewMenu, subsMenu, helpMenu): menuName = menuDict['name'] menuObjName = f'_{menuName}Menu' menu = Menu(*menuDict['actions'], title=_(menuName), parent=self.menuBar()) @@ -1856,15 +1882,6 @@ class EditConfigurationWidget(MainWindow): self.menuBar().addMenu(menu) - if PLATFORM != 'Darwin': - self.setWidthAndHeight() - - def show(self): - super().show() - - if PLATFORM == 'Darwin': - self.setWidthAndHeight() - def setWidthAndHeight(self): try: self.setGeometry( @@ -1877,8 +1894,6 @@ class EditConfigurationWidget(MainWindow): self.setGeometry(100, 100, 1800, 960) - moveToCenter(self) - def importServer(self, remark, config, syncStorage=False): server = {'remark': remark, 'config': config} diff --git a/Furious/Widget/EditRouting.py b/Furious/Widget/EditRouting.py index a5f170f..fbfc785 100644 --- a/Furious/Widget/EditRouting.py +++ b/Furious/Widget/EditRouting.py @@ -24,6 +24,7 @@ from Furious.Action.Routing import ( from Furious.Core.Core import XrayCore, Hysteria1 from Furious.Gui.Action import Action, Seperator from Furious.Widget.Widget import ( + Dialog, HeaderView, Menu, MainWindow, @@ -612,20 +613,9 @@ class RoutingEditor(MainWindow): self.menuBar().addMenu(self._fileMenu) self.menuBar().addMenu(self._viewMenu) - if PLATFORM != 'Darwin': - self.setWidthAndHeight() - - def show(self): - super().show() - - if PLATFORM == 'Darwin': - self.setWidthAndHeight() - def setWidthAndHeight(self): self.setGeometry(100, 100, 656, 856) - moveToCenter(self) - def addTabWithData(self, core, text): if self.isBuiltin: editor = RoutingTextBrowser(parent=self) @@ -722,7 +712,7 @@ class RoutingEditor(MainWindow): self.setWindowTitle(_(self.windowTitle())) -class RoutingDialog(Translatable, SupportConnectedCallback, QDialog): +class RoutingDialog(Dialog): LINE_EDIT_FIXED_WIDTH = 500 BUTTON_FIXED_WIDTH = 30 @@ -825,12 +815,6 @@ class RoutingDialog(Translatable, SupportConnectedCallback, QDialog): def mmdbValue(self): return self.mmdbEdit.text() - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def retranslate(self): with StateContext(self): if self.isBuiltin: @@ -924,6 +908,13 @@ class EditRoutingTableWidget(Translatable, SupportConnectedCallback, TableWidget # Restore horizontal section size self.sectionSizeTable = ujson.loads(APP().RoutesWidgetSectionSizeTable) + # Fill missing value + for column in range(self.columnCount()): + if self.sectionSizeTable.get(str(column)) is None: + self.sectionSizeTable[ + str(column) + ] = self.horizontalHeader().defaultSectionSize() + # Block resize callback self.horizontalHeader().blockSignals(True) @@ -1163,7 +1154,7 @@ class EditRoutingTableWidget(Translatable, SupportConnectedCallback, TableWidget item.setText(_(item.text())) -class AddRoutingDialog(Translatable, SupportConnectedCallback, QDialog): +class AddRoutingDialog(Dialog): def __init__(self, parent=None): super().__init__(parent) @@ -1189,15 +1180,12 @@ class AddRoutingDialog(Translatable, SupportConnectedCallback, QDialog): self.setLayout(layout) + def setWidthAndHeight(self): + self.setGeometry(100, 100, 276, 80) + def routingRemark(self): return self.remarkEdit.text() - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) @@ -1207,6 +1195,8 @@ class AddRoutingDialog(Translatable, SupportConnectedCallback, QDialog): for button in self.dialogBtns.buttons(): button.setText(_(button.text())) + moveToCenter(self) + class EditRoutingWidget(MainWindow): def __init__(self, *args, **kwargs): @@ -1256,15 +1246,6 @@ class EditRoutingWidget(MainWindow): self.setCentralWidget(self.fakeCentralWidget) - if PLATFORM != 'Darwin': - self.setWidthAndHeight() - - def show(self): - super().show() - - if PLATFORM == 'Darwin': - self.setWidthAndHeight() - def setWidthAndHeight(self): try: self.setGeometry( @@ -1277,8 +1258,6 @@ class EditRoutingWidget(MainWindow): self.setGeometry(100, 100, 360 * GOLDEN_RATIO, 360) - moveToCenter(self) - def addRoute(self): choice = self.addRoutingDialog.exec() diff --git a/Furious/Widget/EditSubscription.py b/Furious/Widget/EditSubscription.py new file mode 100644 index 0000000..4753ce2 --- /dev/null +++ b/Furious/Widget/EditSubscription.py @@ -0,0 +1,404 @@ +# Copyright (C) 2023 Loren Eteval +# +# 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 . + +from Furious.Widget.Widget import ( + Dialog, + HeaderView, + MainWindow, + MessageBox, + PushButton, + StyledItemDelegate, + TableWidget, + TabWidget, +) +from Furious.Widget.EditConfiguration import QuestionDeleteBox +from Furious.Utility.Constants import ( + APP, + PLATFORM, + GOLDEN_RATIO, + Color, +) +from Furious.Utility.Utility import ( + SubscriptionStorage, + StateContext, + SupportConnectedCallback, + bootstrapIcon, + moveToCenter, +) +from Furious.Utility.Translator import Translatable, gettext as _ + +from PySide6 import QtCore +from PySide6.QtGui import QFont +from PySide6.QtWidgets import ( + QAbstractItemView, + QDialog, + QDialogButtonBox, + QFormLayout, + QGridLayout, + QHeaderView, + QLabel, + QLineEdit, + QTableWidget, + QTableWidgetItem, + QVBoxLayout, + QWidget, +) + +import uuid +import ujson +import logging + +logger = logging.getLogger(__name__) + + +class EditSubsHorizontalHeader(HeaderView): + def __init__(self, *args, **kwargs): + super().__init__(QtCore.Qt.Orientation.Horizontal, *args, **kwargs) + + self.sectionResized.connect(self.handleSectionResized) + + @QtCore.Slot(int, int, int) + def handleSectionResized(self, index, oldSize, newSize): + # Keys are string when loaded from json + self.parent().sectionSizeTable[str(index)] = newSize + + +class EditSubsVerticalHeader(HeaderView): + def __init__(self, *args, **kwargs): + super().__init__(QtCore.Qt.Orientation.Vertical, *args, **kwargs) + + +class EditSubsTableWidget(Translatable, SupportConnectedCallback, TableWidget): + # Might be extended in the future + HEADER_LABEL = [ + 'Remark', + 'URL', + ] + + # Corresponds to header label + HEADER_LABEL_GET_FUNC = [ + lambda subscription: subscription['remark'], + lambda subscription: subscription['webURL'], + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.SubscriptionWidget = kwargs.get('parent') + + self.questionDeleteBox = QuestionDeleteBox( + icon=MessageBox.Icon.Question, parent=self.parent() + ) + + # Handy reference + self.SubscriptionList = self.SubscriptionWidget.SubscriptionList + + # Column count + self.setColumnCount(len(EditSubsTableWidget.HEADER_LABEL)) + + # Delegate + self.delegate = StyledItemDelegate(parent=self) + self.setItemDelegate(self.delegate) + + # Install custom header + self.setHorizontalHeader(EditSubsHorizontalHeader(self)) + + self.initTableFromData() + + # Horizontal header resize mode + for index in range(self.columnCount()): + if index < self.columnCount() - 1: + self.horizontalHeader().setSectionResizeMode( + index, QHeaderView.ResizeMode.Interactive + ) + else: + self.horizontalHeader().setSectionResizeMode( + index, QHeaderView.ResizeMode.Stretch + ) + + try: + # Restore horizontal section size + self.sectionSizeTable = ujson.loads( + APP().SubscriptionWidgetSectionSizeTable + ) + + # Fill missing value + for column in range(self.columnCount()): + if self.sectionSizeTable.get(str(column)) is None: + self.sectionSizeTable[ + str(column) + ] = self.horizontalHeader().defaultSectionSize() + + # Block resize callback + self.horizontalHeader().blockSignals(True) + + for key, value in self.sectionSizeTable.items(): + self.horizontalHeader().resizeSection(int(key), value) + + # Unblock resize callback + self.horizontalHeader().blockSignals(False) + except Exception: + # Any non-exit exceptions + + # Leave keys as strings since they will be + # loaded as string from json + self.sectionSizeTable = { + str(row): self.horizontalHeader().defaultSectionSize() + for row in range(self.columnCount()) + } + + self.setHorizontalHeaderLabels( + list(_(label) for label in EditSubsTableWidget.HEADER_LABEL) + ) + + for column in range(self.horizontalHeader().count()): + self.horizontalHeaderItem(column).setFont(QFont(APP().customFontName)) + + # Install custom header + self.setVerticalHeader(EditSubsVerticalHeader(self)) + + # Selection + self.setSelectionColor(Color.LIGHT_BLUE) + 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) + + def initTableFromData(self): + for subscription in self.SubscriptionList: + self.appendDataByColumn( + lambda column: EditSubsTableWidget.HEADER_LABEL_GET_FUNC[column]( + subscription + ) + ) + + def deleteSelectedItem(self): + indexes = self.selectedIndex + + if len(indexes) == 0: + # Nothing to do + return + + self.questionDeleteBox.isMulti = bool(len(indexes) > 1) + self.questionDeleteBox.possibleRemark = f'{self.item(indexes[0], 0).text()}' + self.questionDeleteBox.setText(self.questionDeleteBox.getText()) + + if self.questionDeleteBox.exec() == MessageBox.StandardButton.No.value: + # Do not delete + return + + for i in range(len(indexes)): + takedRow = indexes[i] - i + + self.removeRow(takedRow) + self.SubscriptionList.pop(takedRow) + + # TODO: Remove related server + + # Sync it + SubscriptionStorage.sync() + + def appendDataByColumn(self, func): + row = self.rowCount() + + self.insertRow(row) + + for column in range(self.columnCount()): + item = QTableWidgetItem(func(column)) + + item.setFont(QFont(APP().customFontName)) + item.setFlags( + QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable + ) + + self.setItem(row, column, item) + + def connectedCallback(self): + self.setSelectionColor(Color.LIGHT_RED_) + + def disconnectedCallback(self): + self.setSelectionColor(Color.LIGHT_BLUE) + + def retranslate(self): + self.setHorizontalHeaderLabels( + list(_(label) for label in EditSubsTableWidget.HEADER_LABEL) + ) + + +class AddSubsDialog(Dialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle(_('Add subscription')) + self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) + + self.remarkText = QLabel(_('Enter subscription remark:')) + self.remarkEdit = QLineEdit() + + self.webURLText = QLabel(_('Enter subscription URL:')) + self.webURLEdit = QLineEdit() + + self.dialogBtns = QDialogButtonBox(QtCore.Qt.Orientation.Horizontal) + + self.dialogBtns.addButton(_('OK'), QDialogButtonBox.ButtonRole.AcceptRole) + self.dialogBtns.addButton(_('Cancel'), QDialogButtonBox.ButtonRole.RejectRole) + self.dialogBtns.accepted.connect(self.accept) + self.dialogBtns.rejected.connect(self.reject) + + layout = QFormLayout() + + layout.addRow(self.remarkText) + layout.addRow(self.remarkEdit) + layout.addRow(self.webURLText) + layout.addRow(self.webURLEdit) + layout.addRow(self.dialogBtns) + layout.setFormAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.setLayout(layout) + + def setWidthAndHeight(self): + self.setGeometry(100, 100, 456, 150) + + def subscriptionRemark(self): + return self.remarkEdit.text() + + def subscriptionWebURL(self): + return self.webURLEdit.text() + + def retranslate(self): + with StateContext(self): + self.setWindowTitle(_(self.windowTitle())) + + self.remarkText.setText(_(self.remarkText.text())) + self.webURLText.setText(_(self.webURLText.text())) + + for button in self.dialogBtns.buttons(): + button.setText(_(button.text())) + + moveToCenter(self) + + +class EditSubscriptionWidget(MainWindow): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.setWindowTitle(_('Edit Subscription')) + self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) + + self.addSubsDialog = AddSubsDialog() + + try: + self.StorageObj = SubscriptionStorage.toObject(APP().CustomSubscription) + # Check model. Shallow copy + self.SubscriptionList = self.StorageObj['model'] + except Exception: + # Any non-exit exceptions + + self.StorageObj = SubscriptionStorage.init() + # Shallow copy + self.SubscriptionList = self.StorageObj['model'] + + # Clear it + SubscriptionStorage.clear() + + self.editSubsTableWidget = EditSubsTableWidget(parent=self) + + self.editorTab = TabWidget(self) + self.editorTab.addTab(self.editSubsTableWidget, _('Subscription List')) + + # Buttons + self.addButton = PushButton(_('Add')) + self.addButton.clicked.connect(lambda: self.addSubscription()) + self.deleteButton = PushButton(_('Delete')) + self.deleteButton.clicked.connect(lambda: self.deleteSelectedItem()) + + # Button Layout + self.buttonWidget = QWidget() + self.buttonWidgetLayout = QGridLayout(parent=self.buttonWidget) + self.buttonWidgetLayout.addWidget(self.addButton, 0, 0) + self.buttonWidgetLayout.addWidget(self.deleteButton, 0, 1) + + self.fakeCentralWidget = QWidget() + self.fakeCentralWidgetLayout = QVBoxLayout(self.fakeCentralWidget) + self.fakeCentralWidgetLayout.addWidget(self.editorTab) + self.fakeCentralWidgetLayout.addWidget(self.buttonWidget) + + self.setCentralWidget(self.fakeCentralWidget) + + def setWidthAndHeight(self): + try: + self.setGeometry( + 100, + 100, + *list( + int(size) for size in APP().SubscriptionWidgetWindowSize.split(',') + ), + ) + except Exception: + # Any non-exit exceptions + + self.setGeometry(100, 100, 360 * GOLDEN_RATIO, 360) + + def addSubscription(self): + choice = self.addSubsDialog.exec() + + if choice == QDialog.DialogCode.Accepted.value: + subscriptionRemark = self.addSubsDialog.subscriptionRemark() + subscriptionWebURL = self.addSubsDialog.subscriptionWebURL() + + if subscriptionRemark: + subscription = { + 'remark': subscriptionRemark, + 'webURL': subscriptionWebURL, + # Unique id. Used by display and deletion + 'unique': str(uuid.uuid4()), + } + + self.appendDataByColumn( + lambda column: EditSubsTableWidget.HEADER_LABEL_GET_FUNC[column]( + subscription + ) + ) + + self.SubscriptionList.append(subscription) + + # Sync it + SubscriptionStorage.sync() + else: + # Do nothing + pass + + def appendDataByColumn(self, func): + self.editSubsTableWidget.appendDataByColumn(func) + + def deleteSelectedItem(self): + self.editSubsTableWidget.deleteSelectedItem() + + def syncSettings(self): + APP().SubscriptionWidgetWindowSize = ( + f'{self.geometry().width()},{self.geometry().height()}' + ) + + APP().SubscriptionWidgetSectionSizeTable = ujson.dumps( + self.editSubsTableWidget.sectionSizeTable, + ensure_ascii=False, + escape_forward_slashes=False, + ) diff --git a/Furious/Widget/IndentSpinBox.py b/Furious/Widget/IndentSpinBox.py index 4dbd797..1d3ce0a 100644 --- a/Furious/Widget/IndentSpinBox.py +++ b/Furious/Widget/IndentSpinBox.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from Furious.Widget.Widget import Dialog from Furious.Utility.Utility import ( StateContext, SupportConnectedCallback, @@ -24,7 +25,6 @@ from Furious.Utility.Translator import Translatable, gettext as _ from PySide6 import QtCore from PySide6.QtWidgets import ( - QDialog, QDialogButtonBox, QFormLayout, QLabel, @@ -32,7 +32,7 @@ from PySide6.QtWidgets import ( ) -class IndentSpinBox(Translatable, SupportConnectedCallback, QDialog): +class IndentSpinBox(Dialog): def __init__(self, parent=None): super().__init__(parent) @@ -63,12 +63,6 @@ class IndentSpinBox(Translatable, SupportConnectedCallback, QDialog): def value(self): return self.indentSpin.value() - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) diff --git a/Furious/Widget/TorRelaySettings.py b/Furious/Widget/TorRelaySettings.py index f256242..ee913ff 100644 --- a/Furious/Widget/TorRelaySettings.py +++ b/Furious/Widget/TorRelaySettings.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from Furious.Widget.Widget import GroupBox, Label +from Furious.Widget.Widget import Dialog, GroupBox, Label from Furious.Utility.Constants import ( APP, PLATFORM, @@ -36,7 +36,6 @@ from PySide6 import QtCore from PySide6.QtWidgets import ( QCheckBox, QComboBox, - QDialog, QDialogButtonBox, QFormLayout, QGridLayout, @@ -45,7 +44,7 @@ from PySide6.QtWidgets import ( ) -class TorRelaySettingsWidget(Translatable, SupportConnectedCallback, QDialog): +class TorRelaySettingsWidget(Dialog): TOR_LOG_LEVEL = ['err', 'warn', 'notice', 'info', 'debug'] def __init__(self, *args, **kwargs): @@ -227,12 +226,6 @@ class TorRelaySettingsWidget(Translatable, SupportConnectedCallback, QDialog): self.restoreValueFromObject() self.hide() - def connectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) - - def disconnectedCallback(self): - self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) - def retranslate(self): with StateContext(self): self.setWindowTitle(_(self.windowTitle())) diff --git a/Furious/Widget/Widget.py b/Furious/Widget/Widget.py index ac9c7ee..d41a474 100644 --- a/Furious/Widget/Widget.py +++ b/Furious/Widget/Widget.py @@ -16,7 +16,7 @@ # along with this program. If not, see . from Furious.Gui.Action import Action, Seperator -from Furious.Utility.Constants import APP, Color +from Furious.Utility.Constants import APP, PLATFORM, Color from Furious.Utility.Utility import ( StateContext, SupportConnectedCallback, @@ -29,6 +29,7 @@ from Furious.Utility.Translator import Translatable, gettext as _ from PySide6 import QtCore from PySide6.QtGui import QBrush, QColor, QFont, QTextCursor from PySide6.QtWidgets import ( + QDialog, QGroupBox, QHeaderView, QLabel, @@ -49,6 +50,44 @@ from PySide6.QtWidgets import ( import functools +class Dialog(Translatable, SupportConnectedCallback, QDialog): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if PLATFORM != 'Darwin': + self.setWidthAndHeight() + + def setWidthAndHeight(self): + pass + + def exec(self): + self.show() + + return super().exec() + + def open(self): + self.show() + + return super().open() + + def show(self): + super().show() + + if PLATFORM == 'Darwin': + self.setWidthAndHeight() + + moveToCenter(self) + + def connectedCallback(self): + self.setWindowIcon(bootstrapIcon('rocket-takeoff-connected-dark.svg')) + + def disconnectedCallback(self): + self.setWindowIcon(bootstrapIcon('rocket-takeoff-window.svg')) + + def retranslate(self): + pass + + class GroupBox(Translatable, QGroupBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -123,9 +162,23 @@ class MainWindow(Translatable, SupportConnectedCallback, NeedSyncSettings, QMain self._menuBar = MenuBar(parent=self) self.setMenuBar(self._menuBar) + if PLATFORM != 'Darwin': + self.setWidthAndHeight() + def syncSettings(self): pass + def setWidthAndHeight(self): + pass + + def show(self): + super().show() + + if PLATFORM == 'Darwin': + self.setWidthAndHeight() + + moveToCenter(self) + def closeEvent(self, event): event.ignore()