Add download video and save playlist support.

This commit is contained in:
wei
2026-08-25 17:46:48 +08:00
parent eadc5c6267
commit 04e03c132d
19 changed files with 2514 additions and 1386 deletions
+108
View File
@@ -0,0 +1,108 @@
from PySide6.QtWidgets import QMessageBox, QMainWindow, QWidget, QDialog, QVBoxLayout, QLabel, QPlainTextEdit, \
QDialogButtonBox, QSizePolicy
def create_messagebox(parent: QWidget | None, title: str, message: str,
icon: QMessageBox.Icon = QMessageBox.Icon.Information,
button: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok) -> QMessageBox:
msg = QMessageBox(parent)
msg.setWindowTitle(title)
msg.setText(message)
msg.setIcon(icon)
msg.setStandardButtons(button)
return msg
def error(parent: QWidget | None, title: str, message: str,
button: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok) -> None:
create_messagebox(
parent,
title=title,
message=message,
icon=QMessageBox.Icon.Critical,
button=button
).exec()
def info(parent: QWidget | None, title: str, message: str,
button: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok) -> None:
create_messagebox(
parent,
title=title,
message=message,
icon=QMessageBox.Icon.Information,
button=button
).exec()
def warning(parent: QWidget | None, title: str, message: str,
button: QMessageBox.StandardButton = QMessageBox.StandardButton.Ok) -> None:
create_messagebox(
parent,
title=title,
message=message,
icon=QMessageBox.Icon.Warning,
button=button
).exec()
def error_with_plain_text(
parent: QWidget | None,
title: str,
message: str,
content: str,
) -> None:
dialog = QDialog(parent)
dialog.setWindowTitle(title)
dialog.resize(720, 420)
layout = QVBoxLayout(dialog)
label = QLabel(message)
label.setWordWrap(True)
details_box = QPlainTextEdit()
details_box.setReadOnly(True)
details_box.setPlainText(content)
details_box.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Expanding,
)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
buttons.accepted.connect(dialog.accept)
layout.addWidget(label)
layout.addWidget(details_box, 1)
layout.addWidget(buttons)
dialog.exec()
def ask_yes_no_with_plain_text(parent: QWidget | None, title: str, message: str, content: str):
dialog = QDialog(parent)
dialog.setWindowTitle(title)
dialog.resize(720, 420)
layout = QVBoxLayout(dialog)
label = QLabel(message)
label.setWordWrap(True)
label.setWordWrap(True)
details_box = QPlainTextEdit()
details_box.setReadOnly(True)
details_box.setPlainText(content)
details_box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Yes |
QDialogButtonBox.StandardButton.No
)
buttons.button(QDialogButtonBox.StandardButton.Yes).clicked.connect(dialog.accept)
buttons.button(QDialogButtonBox.StandardButton.No).clicked.connect(dialog.reject)
layout.addWidget(label)
layout.addWidget(details_box, 1)
layout.addWidget(buttons)
return dialog.exec() == QDialog.DialogCode.Accepted
+171
View File
@@ -0,0 +1,171 @@
from PySide6.QtCore import Qt, Slot, QTimer
from PySide6.QtWidgets import (
QDialog,
QLabel,
QProgressBar,
QPushButton,
QScrollArea,
QSizePolicy,
QVBoxLayout,
QWidget,
)
class ProgressRow(QWidget):
def __init__(self, name: str, parent=None):
super().__init__(parent)
self.setSizePolicy(
QSizePolicy.Policy.Expanding,
QSizePolicy.Policy.Fixed,
)
self.name_label = QLabel(name, self)
self.name_label.setWordWrap(True)
self.name_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
self.status_label = QLabel("Preparing...", self)
self.status_label.setWordWrap(True)
self.status_label.setTextInteractionFlags(
Qt.TextInteractionFlag.TextSelectableByMouse
)
self.progress_bar = QProgressBar(self)
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
# Flags
self.finished = False
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(4)
layout.addWidget(self.name_label)
layout.addWidget(self.status_label)
layout.addWidget(self.progress_bar)
class ProgressDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Downloads")
self.resize(500, 300)
self.setMinimumSize(360, 220)
self.rows: dict[str, ProgressRow] = {}
self.scroll_area = QScrollArea(self)
self.download_widget = QWidget(self.scroll_area)
self.download_layout = QVBoxLayout(self.download_widget)
self.download_layout.setContentsMargins(4, 4, 4, 4)
self.download_layout.setSpacing(8)
self.download_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
self.scroll_area.setWidget(self.download_widget)
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setHorizontalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAlwaysOff
)
self.close_button = QPushButton("Close", self)
self.close_button.clicked.connect(self.hide)
layout = QVBoxLayout(self)
layout.setContentsMargins(8, 8, 8, 8)
layout.setSpacing(8)
layout.addWidget(self.scroll_area, 1)
layout.addWidget(
self.close_button,
0,
Qt.AlignmentFlag.AlignRight,
)
self.close_timer = QTimer(self)
self.close_timer.setSingleShot(True)
self.close_timer.timeout.connect(self.cleanup)
@Slot(str, str)
def add_task(self, task_id: str, name: str):
# Prevent new tasks from being deleted by cleanup timer
if task_id in self.rows:
return
row = ProgressRow(name, self.download_widget)
self.rows[task_id] = row
self.download_layout.addWidget(row)
if not self.isVisible():
self.show()
@Slot(str, str, float)
def update_task(
self,
task_id: str,
status: str,
percent: float,
):
row = self.rows.get(task_id)
if row is None:
# Prevent started、progress missing due to timing issues
self.add_task(task_id, task_id)
row = self.rows[task_id]
value = max(0, min(100, round(percent)))
row.status_label.setText(status)
row.progress_bar.setValue(value)
@Slot(str)
def finish_task(self, task_id: str):
row = self.rows.get(task_id)
if row is None:
return
row.status_label.setText("Completed")
row.progress_bar.setValue(100)
row.finished = True
if self.all_tasks_finished():
self.close_timer.start(600)
def all_tasks_finished(self):
return bool(self.rows) and all(
row.finished
for row in self.rows.values()
)
def cleanup(self):
"""
If you want to use this function with a timer. use self.close_timer.start(TIMEOUT)
:return:
"""
for task_id in list(self.rows):
self.remove_task(task_id)
self.hide()
@Slot(str, str)
def fail_task(self, task_id: str, error: str):
row = self.rows.get(task_id)
if row is None:
self.add_task(task_id, task_id)
row = self.rows[task_id]
row.status_label.setText(f"Failed: {error}")
row.progress_bar.setStyleSheet(
"QProgressBar::chunk { background-color: #c62828; }"
)
# Move to top
self.download_layout.removeWidget(row)
self.download_layout.insertWidget(0, row)
@Slot(str)
def remove_task(self, task_id: str):
row = self.rows.pop(task_id, None)
if row is not None:
self.download_layout.removeWidget(row)
row.setParent(None)
row.deleteLater()