from core.interface.dialogs.trackerhoverdelegate import TrackerHoverDelegate from core.interface.dialogs.elideditemdelegate import ElidedItemDelegate from core.interface.dialogs.trackertable import _create_tracker_table from core.interface.dialogs.downloadlist import _create_download_list from core.interface.assets.base64_icons import settings_white_base64 from core.interface.assets.base64_icons import settings_black_base64 from core.interface.dialogs.downloadlist import download_list_update from core.utils.logging.logs import consoleLog, flush_log_buffer from core.interface.dialogs.downloadmodel import DownloadModel from core.interface.utils.searchhelper import return_pressed from core.interface.dialogs.settings import settings_dialog from core.interface.assets.base64_icons import logo_base64 from core.interface.dialogs.eventfilter import eventFilter from core.utils.network.download import download_selected from core.interface.utils.tabhelper import create_tab from core.utils.general.shutdown import closehelper from core.utils.general.wrappers import run_thread from core.interface.dialogs.image import Image from core.utils.data.state import state import core.interface.dialogs.hoverrowdelegate import core.interface.dialogs.contextmenu import core.interface.dialogs.update from PySide6 import QtWidgets from PySide6.QtCore import ( Qt, QTimer, Signal, QSize ) from PySide6.QtWidgets import ( QLineEdit, QTableView, QWidget, QVBoxLayout, QLabel, QHBoxLayout, QComboBox, QTabWidget, QHeaderView, QTableWidgetItem, QTextEdit, QStatusBar ) from PySide6.QtGui import ( QIcon, QCloseEvent, QPixmap, QGuiApplication, ) import darkdetect import threading import platform import base64 def windowCloseHelper(): QGuiApplication.quit() class MainWindow(QtWidgets.QMainWindow, QWidget): eventFilter = eventFilter log_signal = Signal(str) search_results_signal = Signal(list) _instance = None def __init__(self): super().__init__() MainWindow._instance = self self.log_signal.connect(self._on_log_signal) self.search_results_signal.connect(self._on_search_results) pixmap = QPixmap() image_data = base64.b64decode(logo_base64) pixmap.loadFromData(image_data) self.setWindowIcon(QIcon(pixmap)) # set appid on windows if platform.system() == "Windows": try: import ctypes appid = 'SoftwareManager' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) except Exception as e: consoleLog(f"Could not set app ID: {e}") # core.interface.dialogs.update self.setWindowTitle("Software Manager") self.setGeometry(100, 100, 800, 600) self.controls = QWidget() self.controlsLayout = QVBoxLayout() self.searchbar = QLineEdit() self.searchbar.setPlaceholderText("Search for software...") self.searchbar.setClearButtonEnabled(True) self.searchbar.setMinimumHeight(30) self.searchbar.returnPressed.connect(self._start_search) self.dlbutton = QtWidgets.QPushButton("Download") self.dlbutton.setCursor(Qt.CursorShape.PointingHandCursor) self.emptyResults = QLabel("No Results") self.emptyResults.setAlignment(Qt.AlignmentFlag.AlignCenter) self.emptyResults.hide() self.downloadList = QTableView() self.downloadList.setMouseTracking(True) self._hovered_row = -1 self._tracker_hovered_row = -1 self._tracker_hovered_table = None self._last_dl_row_count = 0 self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload.setAlignment(Qt.AlignmentFlag.AlignCenter) self.consoleLog = QTextEdit() self.statusbar = QStatusBar() flush_log_buffer() self._tracker_elided_delegate = ElidedItemDelegate(lambda: self._tracker_hovered_row, self) self._tracker_hover_delegate = TrackerHoverDelegate(lambda: self._tracker_hovered_row, self) state.trackertable = _create_tracker_table(self) container = QWidget() containerLayout = QVBoxLayout() containerLayout.addWidget(self.searchbar) containerLayout.addWidget(state.trackertable) # core.interface.dialogs.hoverrowdelegate self.download_model = DownloadModel() self.downloadList = _create_download_list(self) self.downloadList.viewport().installEventFilter(self) self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(state.trackertable.selectedItems(),)))) container.setLayout(containerLayout) self.setCentralWidget(container) self.tabs = QTabWidget() self.tabs.setStyleSheet(""" QTabBar::tab { font-size: 11px; padding: 2px 8px; height: 25px; } QTabWidget::pane { margin-top: -20px; } """) self.horizontal_layout = QHBoxLayout() self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(state.trackertable) self.tab1 = create_tab("Search", self.searchbar, state.trackertable, self.tabs, self.dlbutton, self.horizontal_layout) self.tab2 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) self.corner_widget = QWidget() self.corner_layout = QHBoxLayout(self.corner_widget) self.corner_layout.setContentsMargins(0, 0, 0, 0) self.tracker_list = QComboBox() self.tracker_list.addItems(list(state.trackers.keys())) self.tracker_list.setCursor(Qt.CursorShape.PointingHandCursor) self.tracker_list.activated.connect(self.set_tracker) self.corner_layout.addWidget(self.tracker_list) self.settings_btn = QtWidgets.QToolButton() self.settings_btn.setIconSize(QSize(21, 21)) self.settings_btn.setStyleSheet("background: transparent;") if darkdetect.isDark(): settings_white_pixmap = QPixmap() settings_white = base64.b64decode(settings_white_base64) settings_white_pixmap.loadFromData(settings_white) self.settings_btn.setIcon(QIcon(settings_white_pixmap)) else: settings_black_pixmap = QPixmap() settings_black = base64.b64decode(settings_black_base64) settings_black_pixmap.loadFromData(settings_black) self.settings_btn.setIcon(QIcon(settings_black_pixmap)) self.settings_btn.setToolTip("Settings") self.settings_btn.setCursor(Qt.CursorShape.PointingHandCursor) self.settings_btn.clicked.connect(lambda: settings_dialog(self)) self.corner_layout.addWidget(self.settings_btn) self.tab_wrapper = QWidget() self.tab_layout = QVBoxLayout() self.tab_layout.setContentsMargins(0, 0, 0, 0) self.tab_layout.setSpacing(0) self.tab_bar_layout = QHBoxLayout() self.tab_bar_layout.setContentsMargins(0, 0, 0, 0) self.tab_bar_layout.setSpacing(0) self.tab_bar_layout.addWidget(self.tabs.tabBar(), stretch=0) self.tab_bar_layout.addStretch() self.tab_bar_layout.addWidget(self.corner_widget, stretch=0) self.tab_layout.addLayout(self.tab_bar_layout) self.tab_layout.addWidget(self.tabs) self.tab_wrapper.setLayout(self.tab_layout) containerLayout.addWidget(self.tab_wrapper) containerLayout.setAlignment(Qt.AlignmentFlag.AlignTop) self.consoleLog.setReadOnly(True) self.consoleLog.setFixedHeight(150) self.consoleLog.setStyleSheet("margin-bottom: 0px; background: transparent;") containerLayout.addWidget(self.consoleLog) self.statusbar.show() self.statusbar.setStyleSheet("background: transparent; border: none; padding-right: 1px; padding-left: 6px; padding-bottom: 1px; margin-top: -8px") self.version = QLabel(f"Version: {state.version}") self.statusbar.addPermanentWidget(self.version, Qt.AlignmentFlag.AlignLeft) self.speed_label = QLabel("\u2193 0.0 kB/s \u2191 0.0 kB/s") self.speed_label.setStyleSheet("padding-right: 6px;") self.statusbar.addPermanentWidget(self.speed_label) self.setStatusBar(self.statusbar) self.download_timer = QTimer() self.download_timer.timeout.connect(lambda: download_list_update(self)) self.download_timer.start(500) self.active_timer = QTimer() self.active_timer.timeout.connect(self.show_empty_downloads) self.active_timer.start(500) self._context_menu = core.interface.dialogs.contextmenu.ContextMenu(self) self.image_overlay = Image(self) def _apply_default_headers(self, table): tracker_name = state.currenttracker if hasattr(state, 'currenttracker') and state.currenttracker else None if tracker_name is None: keys = list(state.trackers.keys()) tracker_name = keys[0] if keys else None headers = [] if tracker_name and tracker_name in state.trackers: tracker_mod = state.trackers[tracker_name] if hasattr(tracker_mod, 'headers'): headers = tracker_mod.headers elif hasattr(tracker_mod, 'HEADERS'): headers = tracker_mod.HEADERS if not headers: if state.currenttracker == "rutracker": headers = ["Post Title", "Author", "Seeders", "Leechers"] elif state.currenttracker == "steamrip": headers = ["Game"] else: headers = ["Post Title", "Author"] # i know this makes it less "modular", but its qol table.setRowCount(0) col_count = table.columnCount() if col_count != len(headers): for i in range(col_count): table.setItemDelegateForColumn(i, None) table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) self._apply_tracker_column_modes(table, len(headers)) def _apply_tracker_column_modes(self, table, col_count): header = table.horizontalHeader() if col_count > 0: header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) table.setItemDelegateForColumn(0, self._tracker_elided_delegate) for i in range(1, col_count): header.setSectionResizeMode(i, QHeaderView.ResizeMode.ResizeToContents) table.setItemDelegateForColumn(i, self._tracker_hover_delegate) def _start_search(self): self.searchbar.setEnabled(False) def _search_thread(): try: return_pressed(self) except Exception as e: consoleLog(f"Search error: {e}", True) self.search_results_signal.emit([]) run_thread(threading.Thread(target=_search_thread)) @staticmethod def add_log(text): if hasattr(MainWindow, "_instance") and MainWindow._instance: MainWindow._instance.log_signal.emit(text) return True return False def _on_log_signal(self, text): if hasattr(self, 'consoleLog'): self.consoleLog.append(text) self.consoleLog.verticalScrollBar().setValue( self.consoleLog.verticalScrollBar().maximum() ) def _on_search_results(self, headers): if state.posts is None or state.posts == []: self.show_empty_results(True) self.searchbar.setEnabled(True) self.searchbar.setFocus() return table = state.trackertable table.setRowCount(0) col_count = table.columnCount() need_reconfig = col_count != len(headers) if need_reconfig: for i in range(col_count): table.setItemDelegateForColumn(i, None) table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) self._apply_tracker_column_modes(table, len(headers)) else: table.setHorizontalHeaderLabels(headers) table.setRowCount(len(state.posts)) for x, rowdata in enumerate(state.posts): for y, (key, data) in enumerate(rowdata.items()): item = QTableWidgetItem(str(data)) item.setData(Qt.ItemDataRole.UserRole, x) table.setItem(x, y, item) self.show_empty_results(False) self.searchbar.setEnabled(True) self.searchbar.setFocus() self._update_speed_label() def _update_speed_label(self): total_down = 0 total_up = 0 with state.downloads_lock: active_items = list(state.active_downloads.values()) for handle in active_items: try: s = handle.status() total_down += s.download_rate total_up += s.upload_rate except Exception as e: consoleLog(f"Exception while updating speed label: {e}") down_kb = total_down / 1024 up_kb = total_up / 1024 down_text = f"{down_kb / 1024:.1f} MB/s" if down_kb > 1024 else f"{down_kb:.1f} kB/s" up_text = f"{up_kb / 1024:.1f} MB/s" if up_kb > 1024 else f"{up_kb:.1f} kB/s" if hasattr(self, 'speed_label'): self.speed_label.setText(f"↓ {down_text} ↑ {up_text}") def mousePressEvent(self, event): state.trackertable.clearSelection() self.downloadList.clearSelection() super().mousePressEvent(event) def changeEvent(self, event): super().changeEvent(event) def closeEvent(self, event: QCloseEvent): closehelper() event.accept() from core.utils.general.shutdown import force_exit force_exit() def _invalidate_hover_row(self, row): if row >= 0 and self.download_model: self.downloadList.viewport().update() def set_tracker(self, _): state.currenttracker = self.tracker_list.currentText() self._apply_default_headers(state.trackertable) def show_empty_results(self, show: bool): if show: state.trackertable.hide() self.emptyResults.show() else: state.trackertable.show() self.emptyResults.hide() def show_empty_downloads(self): with state.downloads_lock: has_downloads = len(state.active_downloads) > 0 if has_downloads: self.emptyDownload.hide() self.downloadList.show() else: self.emptyDownload.show() self.downloadList.hide()