feat: queue done marks, export UX, scrub-while-playing, counter fix
- Queue ✓: mark files green after export and on drop if already in DB - Export folder auto-created if missing - Scrub while playing: cursor drag seeks and continues playback - Counter auto-advances past existing files on disk - Instant pending marker on timeline when export starts - WebP quality 92 + lanczos scaling - seek() guard against unloaded state (SystemError fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -64,7 +64,7 @@ def build_ffmpeg_command(
|
|||||||
# if(lt(iw,ih),...) → portrait output: fix width; landscape: fix height.
|
# if(lt(iw,ih),...) → portrait output: fix width; landscape: fix height.
|
||||||
# -2 keeps aspect ratio with even-pixel rounding (libx264 requirement).
|
# -2 keeps aspect ratio with even-pixel rounding (libx264 requirement).
|
||||||
filters.append(
|
filters.append(
|
||||||
f"scale='if(lt(iw,ih),{short_side},-2)':'if(lt(iw,ih),-2,{short_side})'"
|
f"scale='if(lt(iw,ih),{short_side},-2)':'if(lt(iw,ih),-2,{short_side})':flags=lanczos"
|
||||||
)
|
)
|
||||||
if filters:
|
if filters:
|
||||||
cmd += ["-vf", ",".join(filters)]
|
cmd += ["-vf", ",".join(filters)]
|
||||||
@@ -73,7 +73,7 @@ def build_ffmpeg_command(
|
|||||||
cmd += [
|
cmd += [
|
||||||
"-an",
|
"-an",
|
||||||
"-c:v", "libwebp",
|
"-c:v", "libwebp",
|
||||||
"-quality", "85",
|
"-quality", "92",
|
||||||
"-compression_level", "1",
|
"-compression_level", "1",
|
||||||
os.path.join(output_path, "frame_%04d.webp"),
|
os.path.join(output_path, "frame_%04d.webp"),
|
||||||
]
|
]
|
||||||
@@ -662,12 +662,17 @@ class MpvWidget(QWidget):
|
|||||||
def load(self, path: str): self._player.play(path)
|
def load(self, path: str): self._player.play(path)
|
||||||
|
|
||||||
def seek(self, t: float):
|
def seek(self, t: float):
|
||||||
self._player.pause = True
|
if self._player.duration is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
self._player.seek(t, "absolute")
|
self._player.seek(t, "absolute")
|
||||||
|
except SystemError:
|
||||||
|
pass
|
||||||
|
|
||||||
def play_loop(self, a: float, b: float):
|
def play_loop(self, a: float, b: float):
|
||||||
self._player["ab-loop-a"] = a
|
self._player["ab-loop-a"] = a
|
||||||
self._player["ab-loop-b"] = min(b, self._player.duration or b)
|
self._player["ab-loop-b"] = min(b, self._player.duration or b)
|
||||||
|
self._player.seek(a, "absolute")
|
||||||
self._player.pause = False
|
self._player.pause = False
|
||||||
|
|
||||||
def stop_loop(self):
|
def stop_loop(self):
|
||||||
@@ -813,16 +818,37 @@ class PlaylistWidget(QListWidget):
|
|||||||
row = self.currentRow()
|
row = self.currentRow()
|
||||||
return self._paths[row] if 0 <= row < len(self._paths) else None
|
return self._paths[row] if 0 <= row < len(self._paths) else None
|
||||||
|
|
||||||
|
def mark_done(self, path: str) -> None:
|
||||||
|
if path not in self._path_set:
|
||||||
|
return
|
||||||
|
row = self._paths.index(path)
|
||||||
|
item = self.item(row)
|
||||||
|
if item is None:
|
||||||
|
return
|
||||||
|
item.setText(f"✓ {os.path.basename(path)}")
|
||||||
|
item.setForeground(QColor(100, 180, 100))
|
||||||
|
|
||||||
def _select(self, row: int) -> None:
|
def _select(self, row: int) -> None:
|
||||||
prev = self.currentRow()
|
prev = self.currentRow()
|
||||||
self.setCurrentRow(row)
|
self.setCurrentRow(row)
|
||||||
# Only update the two items that actually changed label.
|
|
||||||
if prev >= 0 and prev != row and self.item(prev):
|
if prev >= 0 and prev != row and self.item(prev):
|
||||||
self.item(prev).setText(os.path.basename(self._paths[prev]))
|
self._refresh_item_text(prev)
|
||||||
if self.item(row):
|
if self.item(row):
|
||||||
self.item(row).setText(f"▶ {os.path.basename(self._paths[row])}")
|
item = self.item(row)
|
||||||
|
prefix = "✓ " if item.foreground().color() == QColor(100, 180, 100) else ""
|
||||||
|
item.setText(f"▶ {prefix}{os.path.basename(self._paths[row])}")
|
||||||
self.file_selected.emit(self._paths[row])
|
self.file_selected.emit(self._paths[row])
|
||||||
|
|
||||||
|
def _refresh_item_text(self, row: int) -> None:
|
||||||
|
item = self.item(row)
|
||||||
|
if item is None:
|
||||||
|
return
|
||||||
|
name = os.path.basename(self._paths[row])
|
||||||
|
if item.foreground().color() == QColor(100, 180, 100):
|
||||||
|
item.setText(f"✓ {name}")
|
||||||
|
else:
|
||||||
|
item.setText(name)
|
||||||
|
|
||||||
def _on_item_clicked(self, item: QListWidgetItem) -> None:
|
def _on_item_clicked(self, item: QListWidgetItem) -> None:
|
||||||
self._select(self.row(item))
|
self._select(self.row(item))
|
||||||
|
|
||||||
@@ -1313,6 +1339,9 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_cursor_changed(self, t: float):
|
def _on_cursor_changed(self, t: float):
|
||||||
self._cursor = t
|
self._cursor = t
|
||||||
self._lbl_cursor.setText(f"cursor: {format_time(t)}")
|
self._lbl_cursor.setText(f"cursor: {format_time(t)}")
|
||||||
|
if self._mpv.is_playing():
|
||||||
|
self._mpv.play_loop(t, t + 8.0)
|
||||||
|
else:
|
||||||
self._mpv.seek(t)
|
self._mpv.seek(t)
|
||||||
|
|
||||||
def _toggle_play(self):
|
def _toggle_play(self):
|
||||||
@@ -1370,10 +1399,13 @@ class MainWindow(QMainWindow):
|
|||||||
def _update_next_label(self):
|
def _update_next_label(self):
|
||||||
folder = self._txt_folder.text()
|
folder = self._txt_folder.text()
|
||||||
name = self._txt_name.text() or "clip"
|
name = self._txt_name.text() or "clip"
|
||||||
if self._cmb_format.currentText() == "WebP sequence":
|
is_seq = self._cmb_format.currentText() == "WebP sequence"
|
||||||
path = build_sequence_dir(folder, name, self._export_counter)
|
while True:
|
||||||
else:
|
path = build_sequence_dir(folder, name, self._export_counter) if is_seq \
|
||||||
path = build_export_path(folder, name, self._export_counter)
|
else build_export_path(folder, name, self._export_counter)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
break
|
||||||
|
self._export_counter += 1
|
||||||
self._lbl_next.setText(f"→ {os.path.basename(path)}")
|
self._lbl_next.setText(f"→ {os.path.basename(path)}")
|
||||||
|
|
||||||
def _on_export(self):
|
def _on_export(self):
|
||||||
@@ -1386,6 +1418,7 @@ class MainWindow(QMainWindow):
|
|||||||
fmt = self._cmb_format.currentText()
|
fmt = self._cmb_format.currentText()
|
||||||
image_sequence = fmt == "WebP sequence"
|
image_sequence = fmt == "WebP sequence"
|
||||||
folder = self._txt_folder.text()
|
folder = self._txt_folder.text()
|
||||||
|
os.makedirs(folder, exist_ok=True)
|
||||||
name = self._txt_name.text() or "clip"
|
name = self._txt_name.text() or "clip"
|
||||||
if image_sequence:
|
if image_sequence:
|
||||||
output = build_sequence_dir(folder, name, self._export_counter)
|
output = build_sequence_dir(folder, name, self._export_counter)
|
||||||
@@ -1441,6 +1474,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
self.statusBar().showMessage(f"Exported: {os.path.basename(path)}")
|
self.statusBar().showMessage(f"Exported: {os.path.basename(path)}")
|
||||||
self._refresh_markers()
|
self._refresh_markers()
|
||||||
|
self._playlist.mark_done(self._file_path)
|
||||||
self._playlist.advance()
|
self._playlist.advance()
|
||||||
|
|
||||||
def _on_export_error(self, msg: str):
|
def _on_export_error(self, msg: str):
|
||||||
@@ -1468,6 +1502,9 @@ class MainWindow(QMainWindow):
|
|||||||
]
|
]
|
||||||
if paths:
|
if paths:
|
||||||
self._playlist.add_files(paths)
|
self._playlist.add_files(paths)
|
||||||
|
for p in paths:
|
||||||
|
if self._db.get_markers(os.path.basename(p)):
|
||||||
|
self._playlist.mark_done(p)
|
||||||
|
|
||||||
def _on_venv_installed(self) -> None:
|
def _on_venv_installed(self) -> None:
|
||||||
self._btn_masks.setEnabled(True)
|
self._btn_masks.setEnabled(True)
|
||||||
|
|||||||
Reference in New Issue
Block a user