From 322d89b6886382f2ff60e7f7bf14bfb81f122739 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 6 Apr 2026 12:19:54 +0200 Subject: [PATCH] feat: complete MainWindow UI with playback and export Co-Authored-By: Claude Sonnet 4.6 --- main.py | 185 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 180 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 4fcdd03..b579994 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,15 @@ +import sys import os import subprocess -import sys -import mpv +from pathlib import Path + +from PyQt6.QtWidgets import ( + QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLabel, QPushButton, QLineEdit, QFileDialog, QFrame, QStatusBar, +) from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal -from PyQt6.QtGui import QColor, QPainter, QPen -from PyQt6.QtWidgets import QApplication, QFrame, QMainWindow, QWidget +from PyQt6.QtGui import QPainter, QColor, QPen, QDragEnterEvent, QDropEvent +import mpv def build_export_path(folder: str, basename: str, counter: int) -> str: @@ -189,7 +194,177 @@ class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("8-cut") - self.resize(900, 650) + self.resize(900, 680) + self.setAcceptDrops(True) + + # State + self._file_path: str = "" + self._cursor: float = 0.0 + self._export_counter: int = 1 + self._export_worker: ExportWorker | None = None + + # Widgets + self._mpv = MpvWidget() + self._mpv.file_loaded.connect(self._after_load) + self._timeline = TimelineWidget() + self._timeline.cursor_changed.connect(self._on_cursor_changed) + + self._lbl_file = QLabel("Drop a video file here") + self._lbl_file.setAlignment(Qt.AlignmentFlag.AlignCenter) + self._lbl_file.setStyleSheet("color: #aaa; padding: 6px;") + + self._btn_play = QPushButton("▶ Play 8s") + self._btn_play.setEnabled(False) + self._btn_play.clicked.connect(self._on_play) + + self._btn_pause = QPushButton("⏸ Pause") + self._btn_pause.setEnabled(False) + self._btn_pause.clicked.connect(self._on_pause) + + self._lbl_cursor = QLabel("cursor: --") + self._lbl_duration = QLabel("dur: --") + + self._txt_name = QLineEdit("clip") + self._txt_name.setPlaceholderText("base name") + self._txt_name.setMaximumWidth(150) + self._txt_name.textChanged.connect(self._reset_counter) + + self._txt_folder = QLineEdit(str(Path.home())) + self._btn_folder = QPushButton("Browse") + self._btn_folder.clicked.connect(self._pick_folder) + + self._lbl_next = QLabel() + self._update_next_label() + + self._btn_export = QPushButton("Export") + self._btn_export.setEnabled(False) + self._btn_export.clicked.connect(self._on_export) + + # Layout + top_bar = QHBoxLayout() + top_bar.addWidget(self._lbl_file, stretch=1) + + controls = QHBoxLayout() + controls.addWidget(self._btn_play) + controls.addWidget(self._btn_pause) + controls.addStretch() + controls.addWidget(self._lbl_cursor) + controls.addWidget(self._lbl_duration) + + export_row = QHBoxLayout() + export_row.addWidget(QLabel("Name:")) + export_row.addWidget(self._txt_name) + export_row.addWidget(QLabel("Folder:")) + export_row.addWidget(self._txt_folder, stretch=1) + export_row.addWidget(self._btn_folder) + export_row.addWidget(self._lbl_next) + export_row.addWidget(self._btn_export) + + root = QVBoxLayout() + root.addLayout(top_bar) + root.addWidget(self._mpv, stretch=1) + root.addWidget(self._timeline) + root.addLayout(controls) + root.addLayout(export_row) + + container = QWidget() + container.setLayout(root) + self.setCentralWidget(container) + self.setStatusBar(QStatusBar()) + + # --- Drag & Drop --- + + def dragEnterEvent(self, event: QDragEnterEvent): + if event.mimeData().hasUrls(): + event.acceptProposedAction() + + def dropEvent(self, event: QDropEvent): + urls = event.mimeData().urls() + if urls: + path = urls[0].toLocalFile() + self._load_file(path) + + def _load_file(self, path: str): + self._file_path = path + self._lbl_file.setText(os.path.basename(path)) + self._mpv.load(path) + # _after_load is triggered by MpvWidget.file_loaded signal (connected in __init__) + + def _after_load(self): + dur = self._mpv.get_duration() + self._timeline.set_duration(dur) + self._cursor = 0.0 + self._lbl_duration.setText(f"dur: {format_time(dur)}") + self._lbl_cursor.setText(f"cursor: {format_time(0.0)}") + self._btn_play.setEnabled(True) + self._btn_pause.setEnabled(True) + self._btn_export.setEnabled(True) + + # --- Playback --- + + def _on_cursor_changed(self, t: float): + self._cursor = t + self._lbl_cursor.setText(f"cursor: {format_time(t)}") + self._mpv.seek(t) + + def _on_play(self): + if not self._file_path: + return + self._mpv.play_loop(self._cursor, self._cursor + 8.0) + + def _on_pause(self): + self._mpv.stop_loop() + self._mpv.seek(self._cursor) + + # --- Export --- + + def _pick_folder(self): + folder = QFileDialog.getExistingDirectory(self, "Select output folder") + if folder: + self._txt_folder.setText(folder) + self._reset_counter() + + def _reset_counter(self): + self._export_counter = 1 + self._update_next_label() + + def _update_next_label(self): + path = build_export_path( + self._txt_folder.text(), + self._txt_name.text() or "clip", + self._export_counter, + ) + self._lbl_next.setText(f"→ {os.path.basename(path)}") + + def _on_export(self): + if not self._file_path: + return + if self._export_worker and self._export_worker.isRunning(): + self.statusBar().showMessage("Export already running…") + return + + output = build_export_path( + self._txt_folder.text(), + self._txt_name.text() or "clip", + self._export_counter, + ) + self._btn_export.setEnabled(False) + self.statusBar().showMessage(f"Exporting {os.path.basename(output)}…") + + self._export_worker = ExportWorker(self._file_path, self._cursor, output) + self._export_worker.finished.connect(self._on_export_done) + self._export_worker.error.connect(self._on_export_error) + self._export_worker.start() + + def _on_export_done(self, path: str): + self._export_counter += 1 + self._update_next_label() + self._btn_export.setEnabled(True) + self.statusBar().showMessage(f"Exported: {os.path.basename(path)}") + + def _on_export_error(self, msg: str): + self._btn_export.setEnabled(True) + self.statusBar().showMessage(f"Export error: {msg}") if __name__ == "__main__":