Compare commits
2 Commits
v0.9.0
...
7abf0b4d4c
| Author | SHA1 | Date | |
|---|---|---|---|
| 7abf0b4d4c | |||
| 9e5bd4a8ec |
@@ -839,6 +839,10 @@ class TimelineWidget(QWidget):
|
|||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def set_play_position(self, t: float | None) -> None:
|
def set_play_position(self, t: float | None) -> None:
|
||||||
|
# In lock mode, ignore mpv position updates while the user is dragging
|
||||||
|
# — the async seek hasn't caught up yet, so mpv reports stale values.
|
||||||
|
if self._locked and self._play_pos is not None and self._seek_timer.isActive():
|
||||||
|
return
|
||||||
self._play_pos = t
|
self._play_pos = t
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
@@ -1026,6 +1030,7 @@ class TimelineWidget(QWidget):
|
|||||||
if abs(x - frac * w) <= 10:
|
if abs(x - frac * w) <= 10:
|
||||||
t = frac * self._duration
|
t = frac * self._duration
|
||||||
self.marker_clicked.emit(t, output_path)
|
self.marker_clicked.emit(t, output_path)
|
||||||
|
if not self._locked:
|
||||||
self._seek(x)
|
self._seek(x)
|
||||||
return
|
return
|
||||||
self.marker_deselected.emit()
|
self.marker_deselected.emit()
|
||||||
@@ -1332,12 +1337,17 @@ class MpvWidget(QWidget):
|
|||||||
except SystemError:
|
except SystemError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def play_loop(self, a: float, b: float):
|
def play_loop(self, a: float, b: float, resume: bool = False):
|
||||||
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)
|
||||||
|
if not resume:
|
||||||
self._player.seek(a, "absolute")
|
self._player.seek(a, "absolute")
|
||||||
self._player.pause = False
|
self._player.pause = False
|
||||||
|
|
||||||
|
def update_loop_end(self, b: float):
|
||||||
|
"""Adjust the B point of the current loop without seeking."""
|
||||||
|
self._player["ab-loop-b"] = min(b, self._player.duration or b)
|
||||||
|
|
||||||
def stop_loop(self):
|
def stop_loop(self):
|
||||||
self._player["ab-loop-a"] = "no"
|
self._player["ab-loop-a"] = "no"
|
||||||
self._player["ab-loop-b"] = "no"
|
self._player["ab-loop-b"] = "no"
|
||||||
@@ -1793,12 +1803,20 @@ class PlaylistWidget(QListWidget):
|
|||||||
|
|
||||||
|
|
||||||
class _KeyFilter(QObject):
|
class _KeyFilter(QObject):
|
||||||
"""Suppress global keyboard shortcuts when a text input widget has focus."""
|
"""Suppress global keyboard shortcuts when a text input widget has focus,
|
||||||
|
and release focus from input widgets on click-away."""
|
||||||
|
_INPUT_TYPES = (QSpinBox, QDoubleSpinBox, QLineEdit, QComboBox)
|
||||||
|
|
||||||
def eventFilter(self, obj, event):
|
def eventFilter(self, obj, event):
|
||||||
from PyQt6.QtCore import QEvent
|
from PyQt6.QtCore import QEvent
|
||||||
if event.type() == QEvent.Type.ShortcutOverride and isinstance(obj, QLineEdit):
|
if event.type() == QEvent.Type.ShortcutOverride and isinstance(obj, QLineEdit):
|
||||||
event.accept()
|
event.accept()
|
||||||
return True
|
return True
|
||||||
|
if event.type() == QEvent.Type.MouseButtonPress:
|
||||||
|
if not isinstance(obj, self._INPUT_TYPES):
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if isinstance(focused, self._INPUT_TYPES):
|
||||||
|
focused.clearFocus()
|
||||||
return super().eventFilter(obj, event)
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
|
||||||
@@ -1867,6 +1885,15 @@ class MainWindow(QMainWindow):
|
|||||||
self._frame_grabber: FrameGrabber | None = None
|
self._frame_grabber: FrameGrabber | None = None
|
||||||
self._fps: float = 25.0 # cached on file load via get_fps()
|
self._fps: float = 25.0 # cached on file load via get_fps()
|
||||||
self._crop_keyframes: list[tuple[float, float, str | None, bool, bool]] = [] # sorted by time
|
self._crop_keyframes: list[tuple[float, float, str | None, bool, bool]] = [] # sorted by time
|
||||||
|
self._export_folder: str = "" # actual folder used for current export (may include suffix)
|
||||||
|
self._export_folder_suffix: str = ""
|
||||||
|
|
||||||
|
# Subprofiles — lightweight export variants that append a suffix to the
|
||||||
|
# export folder. Stored in QSettings only (no DB impact).
|
||||||
|
_raw = self._settings.value("subprofiles", [])
|
||||||
|
if isinstance(_raw, str):
|
||||||
|
_raw = [_raw] if _raw else []
|
||||||
|
self._subprofiles: list[str] = _raw or []
|
||||||
|
|
||||||
# Widgets
|
# Widgets
|
||||||
self._playlist = PlaylistWidget()
|
self._playlist = PlaylistWidget()
|
||||||
@@ -2005,6 +2032,7 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
self._spn_clips.valueChanged.connect(lambda: self._update_next_label())
|
self._spn_clips.valueChanged.connect(lambda: self._update_next_label())
|
||||||
self._spn_clips.valueChanged.connect(lambda: self._preview_timer.start())
|
self._spn_clips.valueChanged.connect(lambda: self._preview_timer.start())
|
||||||
|
self._spn_clips.valueChanged.connect(self._update_play_loop)
|
||||||
|
|
||||||
self._spn_spread = QDoubleSpinBox()
|
self._spn_spread = QDoubleSpinBox()
|
||||||
self._spn_spread.setRange(2.0, 8.0)
|
self._spn_spread.setRange(2.0, 8.0)
|
||||||
@@ -2020,6 +2048,7 @@ class MainWindow(QMainWindow):
|
|||||||
lambda: self._timeline.set_clip_span(self._clip_span)
|
lambda: self._timeline.set_clip_span(self._clip_span)
|
||||||
)
|
)
|
||||||
self._spn_spread.valueChanged.connect(lambda: self._preview_timer.start())
|
self._spn_spread.valueChanged.connect(lambda: self._preview_timer.start())
|
||||||
|
self._spn_spread.valueChanged.connect(self._update_play_loop)
|
||||||
|
|
||||||
self._chk_rand_portrait = QCheckBox("1 random portrait")
|
self._chk_rand_portrait = QCheckBox("1 random portrait")
|
||||||
self._chk_rand_portrait.setToolTip(
|
self._chk_rand_portrait.setToolTip(
|
||||||
@@ -2147,9 +2176,19 @@ class MainWindow(QMainWindow):
|
|||||||
transport_row.addStretch()
|
transport_row.addStretch()
|
||||||
transport_row.addWidget(self._lbl_next)
|
transport_row.addWidget(self._lbl_next)
|
||||||
transport_row.addWidget(self._btn_export)
|
transport_row.addWidget(self._btn_export)
|
||||||
|
# Subprofile export buttons sit right after Export
|
||||||
|
self._subprofile_btns: list[QPushButton] = []
|
||||||
|
self._sub_insert_anchor = self._btn_cancel # buttons inserted before this
|
||||||
|
self._btn_add_sub = QPushButton("+")
|
||||||
|
self._btn_add_sub.setFixedWidth(28)
|
||||||
|
self._btn_add_sub.setToolTip("Add a subprofile — exports to folder_suffix")
|
||||||
|
self._btn_add_sub.clicked.connect(self._add_subprofile)
|
||||||
|
transport_row.addWidget(self._btn_add_sub)
|
||||||
transport_row.addWidget(self._btn_cancel)
|
transport_row.addWidget(self._btn_cancel)
|
||||||
transport_row.addWidget(self._spn_workers)
|
transport_row.addWidget(self._spn_workers)
|
||||||
transport_row.addWidget(self._btn_delete)
|
transport_row.addWidget(self._btn_delete)
|
||||||
|
self._transport_row = transport_row
|
||||||
|
self._rebuild_subprofile_buttons()
|
||||||
|
|
||||||
# Row 2 — annotation + output path
|
# Row 2 — annotation + output path
|
||||||
path_row = QHBoxLayout()
|
path_row = QHBoxLayout()
|
||||||
@@ -2268,9 +2307,14 @@ class MainWindow(QMainWindow):
|
|||||||
)
|
)
|
||||||
QShortcut(QKeySequence("K"), self, context=ctx).activated.connect(self._on_pause)
|
QShortcut(QKeySequence("K"), self, context=ctx).activated.connect(self._on_pause)
|
||||||
QShortcut(QKeySequence("E"), self, context=ctx).activated.connect(self._on_export)
|
QShortcut(QKeySequence("E"), self, context=ctx).activated.connect(self._on_export)
|
||||||
|
for i in range(1, 10):
|
||||||
|
QShortcut(QKeySequence(str(i)), self, context=ctx).activated.connect(
|
||||||
|
lambda _, idx=i - 1: self._export_subprofile(idx)
|
||||||
|
)
|
||||||
QShortcut(QKeySequence("M"), self, context=ctx).activated.connect(self._jump_to_next_marker)
|
QShortcut(QKeySequence("M"), self, context=ctx).activated.connect(self._jump_to_next_marker)
|
||||||
QShortcut(QKeySequence("N"), self, context=ctx).activated.connect(self._playlist.advance)
|
QShortcut(QKeySequence("N"), self, context=ctx).activated.connect(self._playlist.advance)
|
||||||
QShortcut(QKeySequence("G"), self, context=ctx).activated.connect(self._btn_lock.toggle)
|
QShortcut(QKeySequence("G"), self, context=ctx).activated.connect(self._btn_lock.toggle)
|
||||||
|
QShortcut(QKeySequence("A"), self, context=ctx).activated.connect(self._autoclip)
|
||||||
for key in ("?", "F1"):
|
for key in ("?", "F1"):
|
||||||
QShortcut(QKeySequence(key), self, context=ctx).activated.connect(self._show_shortcuts)
|
QShortcut(QKeySequence(key), self, context=ctx).activated.connect(self._show_shortcuts)
|
||||||
|
|
||||||
@@ -2295,12 +2339,14 @@ class MainWindow(QMainWindow):
|
|||||||
"<tr><td><b>Space / P</b></td><td>Play / Pause</td></tr>"
|
"<tr><td><b>Space / P</b></td><td>Play / Pause</td></tr>"
|
||||||
"<tr><td><b>K</b></td><td>Pause and snap to cursor</td></tr>"
|
"<tr><td><b>K</b></td><td>Pause and snap to cursor</td></tr>"
|
||||||
"<tr><td><b>E</b></td><td>Export</td></tr>"
|
"<tr><td><b>E</b></td><td>Export</td></tr>"
|
||||||
|
"<tr><td><b>1–9</b></td><td>Export to subprofile 1–9</td></tr>"
|
||||||
"<tr><td><b>M</b></td><td>Jump to next marker</td></tr>"
|
"<tr><td><b>M</b></td><td>Jump to next marker</td></tr>"
|
||||||
"<tr><td><b>N</b></td><td>Next file in playlist</td></tr>"
|
"<tr><td><b>N</b></td><td>Next file in playlist</td></tr>"
|
||||||
"<tr><td><b>G</b></td><td>Toggle cursor lock</td></tr>"
|
"<tr><td><b>G</b></td><td>Toggle cursor lock</td></tr>"
|
||||||
|
"<tr><td><b>A</b></td><td>Autoclip — fit clip count to pause position</td></tr>"
|
||||||
"<tr><td><b>? / F1</b></td><td>This help</td></tr>"
|
"<tr><td><b>? / F1</b></td><td>This help</td></tr>"
|
||||||
"<tr><td colspan='2'><hr></td></tr>"
|
"<tr><td colspan='2'><hr></td></tr>"
|
||||||
"<tr><td><b>Double-click marker</b></td><td>Enter overwrite mode</td></tr>"
|
"<tr><td><b>Double-click marker</b></td><td>Enter overwrite mode (locked: jump to end of clip span)</td></tr>"
|
||||||
"<tr><td><b>Right-click marker</b></td><td>Delete clip group</td></tr>"
|
"<tr><td><b>Right-click marker</b></td><td>Delete clip group</td></tr>"
|
||||||
"<tr><td><b>Click video / crop bar</b></td><td>Reposition portrait crop</td></tr>"
|
"<tr><td><b>Click video / crop bar</b></td><td>Reposition portrait crop</td></tr>"
|
||||||
"</table>"
|
"</table>"
|
||||||
@@ -2367,6 +2413,58 @@ class MainWindow(QMainWindow):
|
|||||||
_log(f"Profile switched: {text}")
|
_log(f"Profile switched: {text}")
|
||||||
self._show_status(f"Profile: {text}", 3000)
|
self._show_status(f"Profile: {text}", 3000)
|
||||||
|
|
||||||
|
# ── Subprofiles ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def _rebuild_subprofile_buttons(self):
|
||||||
|
"""Recreate the per-subprofile export buttons in the transport row."""
|
||||||
|
for btn in self._subprofile_btns:
|
||||||
|
self._transport_row.removeWidget(btn)
|
||||||
|
btn.deleteLater()
|
||||||
|
self._subprofile_btns.clear()
|
||||||
|
# Find where to insert: right after the main Export button.
|
||||||
|
anchor = self._transport_row.indexOf(self._btn_add_sub)
|
||||||
|
has_file = bool(self._file_path)
|
||||||
|
for i, name in enumerate(self._subprofiles):
|
||||||
|
btn = QPushButton(f"▸ {name}")
|
||||||
|
btn.setToolTip(f"Export to folder_{name} (right-click to remove)")
|
||||||
|
btn.setEnabled(has_file)
|
||||||
|
btn.clicked.connect(lambda _, s=name: self._on_export(folder_suffix=s))
|
||||||
|
self._transport_row.insertWidget(anchor + i, btn)
|
||||||
|
self._subprofile_btns.append(btn)
|
||||||
|
|
||||||
|
def _add_subprofile(self):
|
||||||
|
from PyQt6.QtWidgets import QMenu
|
||||||
|
menu = QMenu(self)
|
||||||
|
for name in self._subprofiles:
|
||||||
|
menu.addAction(f"Remove '{name}'", lambda n=name: self._remove_subprofile(n))
|
||||||
|
if self._subprofiles:
|
||||||
|
menu.addSeparator()
|
||||||
|
menu.addAction("Add new…", self._new_subprofile)
|
||||||
|
menu.exec(self._btn_add_sub.mapToGlobal(self._btn_add_sub.rect().bottomLeft()))
|
||||||
|
|
||||||
|
def _new_subprofile(self):
|
||||||
|
name, ok = QInputDialog.getText(self, "New subprofile", "Suffix name:")
|
||||||
|
if ok and name.strip():
|
||||||
|
name = name.strip().replace(" ", "_")
|
||||||
|
if name not in self._subprofiles:
|
||||||
|
self._subprofiles.append(name)
|
||||||
|
self._settings.setValue("subprofiles", self._subprofiles)
|
||||||
|
self._rebuild_subprofile_buttons()
|
||||||
|
|
||||||
|
def _export_subprofile(self, idx: int):
|
||||||
|
if idx < len(self._subprofiles):
|
||||||
|
self._on_export(folder_suffix=self._subprofiles[idx])
|
||||||
|
|
||||||
|
def _remove_subprofile(self, name: str):
|
||||||
|
if name in self._subprofiles:
|
||||||
|
self._subprofiles.remove(name)
|
||||||
|
self._settings.setValue("subprofiles", self._subprofiles)
|
||||||
|
self._rebuild_subprofile_buttons()
|
||||||
|
|
||||||
|
def _set_subprofile_btns_enabled(self, enabled: bool):
|
||||||
|
for btn in self._subprofile_btns:
|
||||||
|
btn.setEnabled(enabled)
|
||||||
|
|
||||||
def _show_status(self, msg: str, timeout: int = 0) -> None:
|
def _show_status(self, msg: str, timeout: int = 0) -> None:
|
||||||
"""Show a message in the inline status label. Timeout in ms (0 = sticky)."""
|
"""Show a message in the inline status label. Timeout in ms (0 = sticky)."""
|
||||||
self._lbl_status.setText(msg)
|
self._lbl_status.setText(msg)
|
||||||
@@ -2437,6 +2535,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._btn_play.setEnabled(True)
|
self._btn_play.setEnabled(True)
|
||||||
self._btn_pause.setEnabled(True)
|
self._btn_pause.setEnabled(True)
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._set_subprofile_btns_enabled(True)
|
||||||
# Reset stale state from previous file
|
# Reset stale state from previous file
|
||||||
self._overwrite_path = ""
|
self._overwrite_path = ""
|
||||||
self._overwrite_group = []
|
self._overwrite_group = []
|
||||||
@@ -2511,6 +2610,20 @@ class MainWindow(QMainWindow):
|
|||||||
self._show_status(f"Deleted keyframe @ {format_time(time)}", 3000)
|
self._show_status(f"Deleted keyframe @ {format_time(time)}", 3000)
|
||||||
|
|
||||||
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
|
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
|
||||||
|
# In lock mode, move cursor to the end of this marker's span.
|
||||||
|
if self._btn_lock.isChecked():
|
||||||
|
meta = self._db.get_by_output_path(output_path)
|
||||||
|
clip_count = meta["clip_count"] or self._spn_clips.value() if meta else self._spn_clips.value()
|
||||||
|
spread = meta["spread"] or self._spn_spread.value() if meta else self._spn_spread.value()
|
||||||
|
next_pos = start_time + 8.0 + (clip_count - 1) * spread
|
||||||
|
self._cursor = next_pos
|
||||||
|
self._timeline.set_cursor(next_pos)
|
||||||
|
self._mpv.seek(next_pos)
|
||||||
|
self._lbl_time.setText(f"{format_time(next_pos)} / {format_time(self._mpv.get_duration())}")
|
||||||
|
self._update_next_label()
|
||||||
|
self._preview_timer.start()
|
||||||
|
self._show_status(f"Cursor → end of {os.path.basename(os.path.dirname(output_path))}", 3000)
|
||||||
|
return
|
||||||
self._overwrite_path = output_path
|
self._overwrite_path = output_path
|
||||||
self._overwrite_group = self._db.get_group(output_path)
|
self._overwrite_group = self._db.get_group(output_path)
|
||||||
n = len(self._overwrite_group)
|
n = len(self._overwrite_group)
|
||||||
@@ -2826,22 +2939,38 @@ class MainWindow(QMainWindow):
|
|||||||
if self._mpv.is_playing():
|
if self._mpv.is_playing():
|
||||||
self._on_pause()
|
self._on_pause()
|
||||||
else:
|
else:
|
||||||
self._on_play()
|
self._on_play(resume=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _clip_span(self) -> float:
|
def _clip_span(self) -> float:
|
||||||
"""Total time covered by the overlapping clips."""
|
"""Total time covered by the overlapping clips."""
|
||||||
return 8.0 + (self._spn_clips.value() - 1) * self._spn_spread.value()
|
return 8.0 + (self._spn_clips.value() - 1) * self._spn_spread.value()
|
||||||
|
|
||||||
def _on_play(self):
|
def _on_play(self, resume: bool = False):
|
||||||
if not self._file_path:
|
if not self._file_path:
|
||||||
return
|
return
|
||||||
self._mpv.play_loop(self._cursor, self._cursor + self._clip_span)
|
self._mpv.play_loop(self._cursor, self._cursor + self._clip_span, resume=resume)
|
||||||
|
|
||||||
|
def _update_play_loop(self):
|
||||||
|
if self._file_path and self._mpv.is_playing():
|
||||||
|
self._mpv.update_loop_end(self._cursor + self._clip_span)
|
||||||
|
|
||||||
def _on_pause(self):
|
def _on_pause(self):
|
||||||
self._mpv.stop_loop()
|
self._mpv.stop_loop()
|
||||||
self._mpv.seek(self._cursor)
|
|
||||||
self._timeline.set_play_position(None)
|
def _autoclip(self):
|
||||||
|
"""Set clip count to fit the current pause position."""
|
||||||
|
if not self._file_path:
|
||||||
|
return
|
||||||
|
play_t = self._timeline._play_pos
|
||||||
|
if play_t is None or play_t <= self._cursor:
|
||||||
|
return
|
||||||
|
elapsed = play_t - self._cursor
|
||||||
|
spread = self._spn_spread.value()
|
||||||
|
# n clips span 8 + (n-1)*spread seconds
|
||||||
|
n = int((elapsed - 8.0) / spread) + 1
|
||||||
|
n = max(1, n)
|
||||||
|
self._spn_clips.setValue(n)
|
||||||
|
|
||||||
def _step_cursor(self, delta: float) -> None:
|
def _step_cursor(self, delta: float) -> None:
|
||||||
if not self._file_path:
|
if not self._file_path:
|
||||||
@@ -2897,7 +3026,7 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self._lbl_next.setText(f"→ {base}_0..{n - 1}")
|
self._lbl_next.setText(f"→ {base}_0..{n - 1}")
|
||||||
|
|
||||||
def _on_export(self):
|
def _on_export(self, _=None, folder_suffix: str = ""):
|
||||||
if not self._file_path:
|
if not self._file_path:
|
||||||
return
|
return
|
||||||
if self._export_worker and self._export_worker.isRunning():
|
if self._export_worker and self._export_worker.isRunning():
|
||||||
@@ -2907,12 +3036,15 @@ 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()
|
||||||
|
if folder_suffix:
|
||||||
|
folder = folder.rstrip(os.sep) + "_" + folder_suffix
|
||||||
os.makedirs(folder, exist_ok=True)
|
os.makedirs(folder, exist_ok=True)
|
||||||
spread = self._spn_spread.value()
|
spread = self._spn_spread.value()
|
||||||
|
|
||||||
ratio_text = self._cmb_portrait.currentText()
|
ratio_text = self._cmb_portrait.currentText()
|
||||||
base_ratio = None if ratio_text == "Off" else ratio_text
|
base_ratio = None if ratio_text == "Off" else ratio_text
|
||||||
base_center = self._crop_center
|
base_center = self._crop_center
|
||||||
|
counter = self._export_counter
|
||||||
|
|
||||||
if self._overwrite_path:
|
if self._overwrite_path:
|
||||||
# Group overwrite mode — re-export all sub-clips at this marker.
|
# Group overwrite mode — re-export all sub-clips at this marker.
|
||||||
@@ -2940,16 +3072,29 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
name = self._txt_name.text() or "clip"
|
name = self._txt_name.text() or "clip"
|
||||||
n_clips = self._spn_clips.value()
|
n_clips = self._spn_clips.value()
|
||||||
|
# For subprofile exports, calculate counter independently.
|
||||||
|
if folder_suffix:
|
||||||
|
counter = 1
|
||||||
|
while True:
|
||||||
|
if image_sequence:
|
||||||
|
p = build_sequence_dir(folder, name, counter, sub=0)
|
||||||
|
else:
|
||||||
|
p = build_export_path(folder, name, counter, sub=0)
|
||||||
|
if not os.path.exists(p):
|
||||||
|
break
|
||||||
|
counter += 1
|
||||||
|
else:
|
||||||
|
counter = self._export_counter
|
||||||
# Create the group subfolder
|
# Create the group subfolder
|
||||||
group_dir = os.path.join(folder, f"{name}_{self._export_counter:03d}")
|
group_dir = os.path.join(folder, f"{name}_{counter:03d}")
|
||||||
os.makedirs(group_dir, exist_ok=True)
|
os.makedirs(group_dir, exist_ok=True)
|
||||||
jobs = []
|
jobs = []
|
||||||
for sub in range(n_clips):
|
for sub in range(n_clips):
|
||||||
start = self._cursor + sub * spread
|
start = self._cursor + sub * spread
|
||||||
if image_sequence:
|
if image_sequence:
|
||||||
out = build_sequence_dir(folder, name, self._export_counter, sub=sub)
|
out = build_sequence_dir(folder, name, counter, sub=sub)
|
||||||
else:
|
else:
|
||||||
out = build_export_path(folder, name, self._export_counter, sub=sub)
|
out = build_export_path(folder, name, counter, sub=sub)
|
||||||
jobs.append((start, out, base_ratio, base_center))
|
jobs.append((start, out, base_ratio, base_center))
|
||||||
|
|
||||||
# Apply crop keyframes (or fall back to base state).
|
# Apply crop keyframes (or fall back to base state).
|
||||||
@@ -3004,14 +3149,18 @@ class MainWindow(QMainWindow):
|
|||||||
self._export_format = fmt
|
self._export_format = fmt
|
||||||
self._export_clip_count = self._spn_clips.value()
|
self._export_clip_count = self._spn_clips.value()
|
||||||
self._export_spread = self._spn_spread.value()
|
self._export_spread = self._spn_spread.value()
|
||||||
|
self._export_folder = folder
|
||||||
|
self._export_folder_suffix = folder_suffix
|
||||||
|
|
||||||
self._btn_export.setEnabled(False)
|
self._btn_export.setEnabled(False)
|
||||||
self._show_status(f"Exporting {len(jobs)} clip(s)…")
|
self._set_subprofile_btns_enabled(False)
|
||||||
|
suffix_tag = f" [{folder_suffix}]" if folder_suffix else ""
|
||||||
|
self._show_status(f"Exporting {len(jobs)} clip(s){suffix_tag}…")
|
||||||
|
|
||||||
# Show one pending marker at the cursor position for the whole batch.
|
# Show one pending marker at the cursor position for the whole batch.
|
||||||
first_out = jobs[0][1]
|
first_out = jobs[0][1]
|
||||||
pending = list(self._timeline._markers)
|
pending = list(self._timeline._markers)
|
||||||
pending.append((self._cursor, self._export_counter, first_out))
|
pending.append((self._cursor, counter, first_out))
|
||||||
self._timeline.set_markers(pending)
|
self._timeline.set_markers(pending)
|
||||||
|
|
||||||
hw_on = self._chk_hw.isChecked() and self._hw_encoders
|
hw_on = self._chk_hw.isChecked() and self._hw_encoders
|
||||||
@@ -3054,8 +3203,7 @@ class MainWindow(QMainWindow):
|
|||||||
spread=self._export_spread,
|
spread=self._export_spread,
|
||||||
profile=self._profile,
|
profile=self._profile,
|
||||||
)
|
)
|
||||||
folder = self._txt_folder.text()
|
upsert_clip_annotation(self._export_folder, path, label)
|
||||||
upsert_clip_annotation(folder, path, label)
|
|
||||||
self._last_export_path = path
|
self._last_export_path = path
|
||||||
_log(f" clip done: {os.path.basename(path)}")
|
_log(f" clip done: {os.path.basename(path)}")
|
||||||
self._show_status(f"Exported: {os.path.basename(path)}")
|
self._show_status(f"Exported: {os.path.basename(path)}")
|
||||||
@@ -3064,9 +3212,9 @@ class MainWindow(QMainWindow):
|
|||||||
"""Called once after all clips in the batch are done."""
|
"""Called once after all clips in the batch are done."""
|
||||||
_log("Batch complete")
|
_log("Batch complete")
|
||||||
self._btn_cancel.setEnabled(False)
|
self._btn_cancel.setEnabled(False)
|
||||||
self._export_counter += 1
|
|
||||||
self._update_next_label()
|
self._update_next_label()
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._set_subprofile_btns_enabled(True)
|
||||||
self._btn_export.setText("Export")
|
self._btn_export.setText("Export")
|
||||||
self._btn_export.setStyleSheet("")
|
self._btn_export.setStyleSheet("")
|
||||||
if self._last_export_path:
|
if self._last_export_path:
|
||||||
@@ -3093,6 +3241,7 @@ class MainWindow(QMainWindow):
|
|||||||
_log(f"Export error: {msg}")
|
_log(f"Export error: {msg}")
|
||||||
self._btn_cancel.setEnabled(False)
|
self._btn_cancel.setEnabled(False)
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._set_subprofile_btns_enabled(True)
|
||||||
self._btn_export.setText("Export")
|
self._btn_export.setText("Export")
|
||||||
self._btn_export.setStyleSheet("")
|
self._btn_export.setStyleSheet("")
|
||||||
self._refresh_markers() # remove stale pending marker
|
self._refresh_markers() # remove stale pending marker
|
||||||
@@ -3107,6 +3256,7 @@ class MainWindow(QMainWindow):
|
|||||||
def _on_export_cancelled(self):
|
def _on_export_cancelled(self):
|
||||||
_log("Export cancelled")
|
_log("Export cancelled")
|
||||||
self._btn_export.setEnabled(True)
|
self._btn_export.setEnabled(True)
|
||||||
|
self._set_subprofile_btns_enabled(True)
|
||||||
self._btn_export.setText("Export")
|
self._btn_export.setText("Export")
|
||||||
self._btn_export.setStyleSheet("")
|
self._btn_export.setStyleSheet("")
|
||||||
self._update_next_label()
|
self._update_next_label()
|
||||||
|
|||||||
Reference in New Issue
Block a user