from PySide6 import QtWidgets from PySide6.QtCore import Qt, QTimer, QModelIndex, QAbstractTableModel, Signal, QEvent from PySide6.QtWidgets import ( QLineEdit, QTableView, QWidget, QVBoxLayout, QListWidget, QToolBar, QLabel, QHBoxLayout, QComboBox, QTabWidget, QProgressBar, QHeaderView, QMessageBox, QTableWidget, QTextEdit, QStyle, QStyleOptionButton, QStyledItemDelegate, QApplication, ) from PySide6.QtGui import QIcon, QAction, QCloseEvent, QImage, QPixmap import darkdetect import threading import platform import requests as r import os import subprocess import libtorrent as lt import time import sys import json from core.utils.general.logs import consoleLog from core.utils.general.wrappers import run_thread from core.utils.data.state import state from core.utils.network.download import download_selected from core.utils.network.update_checker import check_for_updates from core.interface.utils.tabhelper import create_tab from core.interface.utils.searchhelper import return_pressed from core.interface.dialogs.settings import settings_dialog from core.network.libtorrent_misc import cleanup_session from core.utils.general.shutdown import closehelper def download_update(latest_version): new_filename = f"SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" url = f"https://github.com/KeksPirates/SoftwareManager/releases/latest/download/SoftwareManager-dev-{latest_version.replace('-dev', '')}-windows.exe" print("Downloading update...", True) response = r.get(url, allow_redirects=True) with open(new_filename, "wb") as f: f.write(response.content) if not os.path.exists(new_filename): raise FileNotFoundError("Executable not found") subprocess.Popen([new_filename], shell=True) time.sleep(0.5) msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Information) msg.setWindowTitle("New Version") msg.setText("New version installed.") msg.setInformativeText("Please remove the old exe.") msg.setStandardButtons(QMessageBox.StandardButton.Ok) sys.exit(0) class MainWindow(QtWidgets.QMainWindow, QWidget): log_signal = Signal(str) # Thread-safe signal for logging def __init__(self): super().__init__() MainWindow._instance = self self.log_signal.connect(self._on_log_signal) def get_asset_path(filename): if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(__file__) asset_path = os.path.join(base_path, 'core', 'interface', 'assets', filename) if os.path.exists(asset_path): return asset_path return os.path.join('core', 'interface', 'assets', filename) self.setWindowIcon(QIcon(get_asset_path("logo.png"))) 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}") build_info_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "build_info.json") if os.path.exists(build_info_path): with open(build_info_path, "r") as f: build_info = json.load(f) state.version = build_info.get("version") # Check for updates on Windows if state.ignore_updates is False: if platform.system() == "Windows": result = check_for_updates() if result != (None, None): assets, latest_version = result if assets: msg = QMessageBox() msg.setIcon(QMessageBox.Icon.Information) msg.setWindowTitle("Update Available") msg.setText("A new version is available.") msg.setInformativeText("Press Ok to download the update.") msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Ignore) response = msg.exec_() if response == QMessageBox.StandardButton.Ok: download_update(latest_version) self.setWindowTitle("Software Manager") self.setGeometry(100, 100, 800, 600) self.controls = QWidget() self.controlsLayout = QVBoxLayout() # Widgets self.searchbar = QLineEdit() self.searchbar.setPlaceholderText("Search for software...") self.searchbar.setClearButtonEnabled(True) self.searchbar.setMinimumHeight(30) self.searchbar.returnPressed.connect(lambda: run_thread(threading.Thread(target=return_pressed, args=(self,)))) # Triggers data function thread on enter self.dlbutton = QtWidgets.QPushButton("Download") self.libraryList = QListWidget() self.emptyResults = QLabel("No Results") self.emptyResults.setAlignment(Qt.AlignCenter) self.emptyResults.hide() self.download_model = None self.downloadList = QTableView() self.emptyLibrary = QLabel("No items in library.") self.emptyDownload = QLabel("No items in downloads.") self.emptyDownload.setAlignment(Qt.AlignCenter) self.consoleLog = QTextEdit() self.progressbar = QProgressBar() # Table Widget for Item List self.qtablewidget = QTableWidget() self.qtablewidget.setColumnCount(2) self.qtablewidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.qtablewidget.verticalHeader().setVisible(False) self.qtablewidget.setHorizontalHeaderLabels(["Post Title", "Author"]) header = self.qtablewidget.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Stretch) header.setSectionResizeMode(1, QHeaderView.Fixed) header.setStretchLastSection(False) self.qtablewidget.setAttribute(Qt.WA_TranslucentBackground) self.qtablewidget.viewport().setAttribute(Qt.WA_TranslucentBackground) header = self.qtablewidget.horizontalHeader() header.setSectionResizeMode(1, QHeaderView.Fixed) header.resizeSection(1, 500) container = QWidget() containerLayout = QVBoxLayout() containerLayout.addWidget(self.searchbar) containerLayout.addWidget(self.qtablewidget) class DownloadModel(QAbstractTableModel): def __init__(self): super().__init__() self.headers = ["Action", "Name", "Status", "Progress", "Speed", "Size", "Total Size"] def rowCount(self, parent=QModelIndex()): return len(state.active_downloads) def columnCount(self, parent=QModelIndex()): return len(self.headers) def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole and orientation == Qt.Horizontal: return self.headers[section] return None def data(self, index, role=Qt.DisplayRole): if role == Qt.DisplayRole: col = index.column() magnet_link = list(state.active_downloads.keys())[index.row()] magnetdl = state.active_downloads[magnet_link] status = magnetdl.status() if col == 0: is_paused = magnetdl.status().paused return "▶︎" if is_paused else "⏸︎" elif col == 1: return status.name if status.has_metadata else "Fetching metadata..." elif col == 2: if status.state == lt.torrent_status.downloading: return "Downloading" elif status.state == lt.torrent_status.seeding: return "Seeding" elif status.paused: return "Paused" else: return "Queued" elif col == 3: return f"{status.progress * 100:.1f}%" elif col == 4: speed_kbs = status.download_rate / 1024 if speed_kbs > 1024: return f"{speed_kbs / 1024:.1f} MB/s" else: return f"{speed_kbs:.1f} kB/s" elif col == 5: downloaded_mb = status.total_wanted_done / (1024 * 1024) if downloaded_mb > 1024: return f"{downloaded_mb / 1024:.2f} GB" else: return f"{downloaded_mb:.1f} MB" elif col == 6: total_mb = status.total_wanted / (1024 * 1024) if total_mb > 1024: return f"{total_mb / 1024:.2f} GB" else: return f"{total_mb:.1f} MB" elif col == 7: if status.download_rate > 0: bytes_left = status.total_wanted - status.total_wanted_done eta_seconds = bytes_left / status.download_rate if eta_seconds < 60: return f"{int(eta_seconds)}s" elif eta_seconds < 3600: minutes = int(eta_seconds / 60) seconds = int(eta_seconds % 60) return f"{minutes}m {seconds}s" else: hours = int(eta_seconds / 3600) minutes = int((eta_seconds % 3600) / 60) return f"{hours}h {minutes}m" else: return "∞" if status.paused else "Stalled" if role == Qt.UserRole and index.column() == 0: magnet_link = list(state.active_downloads.keys())[index.row()] magnetdl = state.active_downloads[magnet_link] return magnetdl.status().paused return None def toggle_pause_resume(self, row): magnet_link = list(state.active_downloads.keys())[row] magnetdl = state.active_downloads[magnet_link] status = magnetdl.status() if status.state == lt.torrent_status.seeding: if state.download_path is not None and os.path.exists(state.download_path): if platform.system() == "Windows": os.startfile(os.path.normpath(state.download_path)) elif platform.system() == "Linux": subprocess.Popen(["xdg-open", state.download_path]) elif platform.system() == "Darwin": subprocess.Popen(["open", state.download_path]) return if status.paused: magnetdl.resume() consoleLog(f"Resumed download: {status.name}", True) else: magnetdl.pause() consoleLog(f"Paused download: {status.name}", True) idx = self.index(row, 0) self.dataChanged.emit(idx, idx, [Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.UserRole]) class PauseResumeDelegate(QStyledItemDelegate): clicked = Signal(int) def paint(self, painter, option, index): if index.column() != 0: super().paint(painter, option, index) return paused = index.data(Qt.UserRole) magnet_link = list(state.active_downloads.keys())[index.row()] magnetdl = state.active_downloads[magnet_link] status = magnetdl.status() button = QStyleOptionButton() button.rect = option.rect if status.state == lt.torrent_status.seeding: button.text = "📁" else: button.text = "▶︎" if paused else "⏸︎" button.state = QStyle.State_Enabled QApplication.style().drawControl( QStyle.CE_PushButton, button, painter ) def editorEvent(self, event, model, option, index): if index.column() == 0 and event.type() == QEvent.Type.MouseButtonPress: self.clicked.emit(index.row()) return True return False self.download_model = DownloadModel() self.downloadList.setModel(self.download_model) self.downloadList.horizontalHeader().setStretchLastSection(False) self.downloadList.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch) self.downloadList.setSelectionBehavior(QTableView.SelectRows) self.downloadList.setAttribute(Qt.WA_TranslucentBackground) self.downloadList.viewport().setAttribute(Qt.WA_TranslucentBackground) download_model = self.download_model def on_pause_resume_clicked(row): download_model.toggle_pause_resume(row) delegate = PauseResumeDelegate(self.downloadList) self.downloadList.setItemDelegateForColumn(0, delegate) delegate.clicked.connect(on_pause_resume_clicked) # download button triggers self.dlbutton.clicked.connect(lambda: run_thread(threading.Thread(target=download_selected, args=(self.qtablewidget.currentItem(), state.posts, state.post_titles)))) container.setLayout(containerLayout) self.setCentralWidget(container) # Tabs self.tabs = QTabWidget() self.horizontal_layout = QHBoxLayout() self.horizontal_layout.addWidget(self.emptyResults, stretch=3) self.horizontal_layout.addWidget(self.qtablewidget) self.tab1 = create_tab("Search", self.searchbar, self.qtablewidget, self.tabs, self.dlbutton, self.horizontal_layout) self.tab2 = create_tab("Library", self.emptyLibrary, self.libraryList, self.tabs, None, None) self.tab3 = create_tab("Downloads", self.emptyDownload, self.downloadList, self.tabs, None, None) if state.image_path is not None and os.path.exists(state.image_path): self.image = QImage(state.image_path) self.pixmap = QPixmap.fromImage(self.image) self.overlay_label = QLabel(self) self.overlay_label.setPixmap(self.pixmap) self.overlay_label.adjustSize() self.overlay_label.raise_() offset_x = -1350 offset_y = -550 x = self.width() - self.overlay_label.width() - offset_x y = self.height() - self.overlay_label.height() - offset_y self.overlay_label.move(x, y) # temporarily disabled # state.image_changed.connect(self.update_image_overlay) containerLayout.addWidget(self.tabs) containerLayout.setAlignment(Qt.AlignmentFlag.AlignBottom) toolbar = QToolBar("Main Toolbar") self.addToolBar(toolbar) toolbar.setLayoutDirection(Qt.RightToLeft) self.tracker_list = QComboBox() self.tracker_list.addItems(["rutracker", "uztracker", "m0nkrus"]) self.tracker_list.activated.connect(self.set_tracker) if darkdetect.isDark(): settings_action = QAction(QIcon(get_asset_path("settings_white.png")), "Settings", self) else: settings_action = QAction(QIcon(get_asset_path("settings_black.png")), "Settings", self) settings_action.triggered.connect(lambda: settings_dialog(self)) toolbar.addAction(settings_action) toolbar.addWidget(self.tracker_list) toolbar.setMovable(False) # self.progressbar.setValue(0) self.consoleLog.setReadOnly(True) self.consoleLog.setFixedHeight(150) containerLayout.addWidget(self.consoleLog) self.download_timer = QTimer() self.download_timer.timeout.connect(lambda: run_thread(threading.Thread(target=self.download_list_update))) self.download_timer.start(500) self.active_timer = QTimer() self.active_timer.timeout.connect(self.show_empty_downloads) self.active_timer.start(500) @staticmethod def add_log(text): if MainWindow._instance: MainWindow._instance.log_signal.emit(text) def _on_log_signal(self, text): self.consoleLog.append(text) self.consoleLog.verticalScrollBar().setValue( self.consoleLog.verticalScrollBar().maximum() ) def update_image_overlay(self, new_image_path): self.image = QImage(new_image_path) self.pixmap = QPixmap.fromImage(self.image) self.overlay_label.setPixmap(self.pixmap) self.overlay_label.adjustSize() def download_list_update(self): if self.download_model: self.download_model.layoutAboutToBeChanged.emit() self.download_model.layoutChanged.emit() def closeEvent(self, event: QCloseEvent): closehelper() event.accept() def set_tracker(self, _): state.tracker = self.tracker_list.currentText() def show_empty_results(self, show: bool): if show: self.qtablewidget.hide() self.emptyResults.show() else: self.qtablewidget.show() self.emptyResults.hide() def show_empty_downloads(self): if len(state.active_downloads) > 0: self.emptyDownload.hide() self.downloadList.show() else: self.emptyDownload.show() self.downloadList.hide() # thank you claude def resizeEvent(self, event): super().resizeEvent(event) table_width = self.qtablewidget.viewport().width() self.qtablewidget.setColumnWidth(1, int(table_width * 0.3))