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.
|
||||
# -2 keeps aspect ratio with even-pixel rounding (libx264 requirement).
|
||||
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:
|
||||
cmd += ["-vf", ",".join(filters)]
|
||||
@@ -73,7 +73,7 @@ def build_ffmpeg_command(
|
||||
cmd += [
|
||||
"-an",
|
||||
"-c:v", "libwebp",
|
||||
"-quality", "85",
|
||||
"-quality", "92",
|
||||
"-compression_level", "1",
|
||||
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 seek(self, t: float):
|
||||
self._player.pause = True
|
||||
if self._player.duration is None:
|
||||
return
|
||||
try:
|
||||
self._player.seek(t, "absolute")
|
||||
except SystemError:
|
||||
pass
|
||||
|
||||
def play_loop(self, a: float, b: float):
|
||||
self._player["ab-loop-a"] = a
|
||||
self._player["ab-loop-b"] = min(b, self._player.duration or b)
|
||||
self._player.seek(a, "absolute")
|
||||
self._player.pause = False
|
||||
|
||||
def stop_loop(self):
|
||||
@@ -813,16 +818,37 @@ class PlaylistWidget(QListWidget):
|
||||
row = self.currentRow()
|
||||
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:
|
||||
prev = self.currentRow()
|
||||
self.setCurrentRow(row)
|
||||
# Only update the two items that actually changed label.
|
||||
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):
|
||||
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])
|
||||
|
||||
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:
|
||||
self._select(self.row(item))
|
||||
|
||||
@@ -1313,6 +1339,9 @@ class MainWindow(QMainWindow):
|
||||
def _on_cursor_changed(self, t: float):
|
||||
self._cursor = 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)
|
||||
|
||||
def _toggle_play(self):
|
||||
@@ -1370,10 +1399,13 @@ class MainWindow(QMainWindow):
|
||||
def _update_next_label(self):
|
||||
folder = self._txt_folder.text()
|
||||
name = self._txt_name.text() or "clip"
|
||||
if self._cmb_format.currentText() == "WebP sequence":
|
||||
path = build_sequence_dir(folder, name, self._export_counter)
|
||||
else:
|
||||
path = build_export_path(folder, name, self._export_counter)
|
||||
is_seq = self._cmb_format.currentText() == "WebP sequence"
|
||||
while True:
|
||||
path = build_sequence_dir(folder, name, self._export_counter) if is_seq \
|
||||
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)}")
|
||||
|
||||
def _on_export(self):
|
||||
@@ -1386,6 +1418,7 @@ class MainWindow(QMainWindow):
|
||||
fmt = self._cmb_format.currentText()
|
||||
image_sequence = fmt == "WebP sequence"
|
||||
folder = self._txt_folder.text()
|
||||
os.makedirs(folder, exist_ok=True)
|
||||
name = self._txt_name.text() or "clip"
|
||||
if image_sequence:
|
||||
output = build_sequence_dir(folder, name, self._export_counter)
|
||||
@@ -1441,6 +1474,7 @@ class MainWindow(QMainWindow):
|
||||
self._btn_export.setEnabled(True)
|
||||
self.statusBar().showMessage(f"Exported: {os.path.basename(path)}")
|
||||
self._refresh_markers()
|
||||
self._playlist.mark_done(self._file_path)
|
||||
self._playlist.advance()
|
||||
|
||||
def _on_export_error(self, msg: str):
|
||||
@@ -1468,6 +1502,9 @@ class MainWindow(QMainWindow):
|
||||
]
|
||||
if 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:
|
||||
self._btn_masks.setEnabled(True)
|
||||
|
||||
Reference in New Issue
Block a user