feat: session resume, hide exported files, profile-based file hiding
- Resume last session: reload previous playlist files on startup - Hide exported checkbox: filter out files with existing clips - Profile-based hiding: right-click → "Hide in profile" persists via DB - Playlist scrollbar fix: disable updates during batch add - Drop event and profile switch use unified _apply_playlist_filters() Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -400,6 +400,13 @@ class ProcessedDB:
|
||||
self._con.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_filename ON processed(filename)"
|
||||
)
|
||||
self._con.execute(
|
||||
"CREATE TABLE IF NOT EXISTS hidden_files ("
|
||||
" filename TEXT NOT NULL,"
|
||||
" profile TEXT NOT NULL DEFAULT 'default',"
|
||||
" PRIMARY KEY (filename, profile)"
|
||||
")"
|
||||
)
|
||||
self._con.commit()
|
||||
|
||||
def add(self, filename: str, start_time: float, output_path: str,
|
||||
@@ -532,6 +539,32 @@ class ProcessedDB:
|
||||
).fetchall()
|
||||
return [r[0] for r in rows]
|
||||
|
||||
def hide_file(self, filename: str, profile: str = "default") -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
self._con.execute(
|
||||
"INSERT OR IGNORE INTO hidden_files (filename, profile) VALUES (?, ?)",
|
||||
(filename, profile),
|
||||
)
|
||||
self._con.commit()
|
||||
|
||||
def unhide_file(self, filename: str, profile: str = "default") -> None:
|
||||
if not self._enabled:
|
||||
return
|
||||
self._con.execute(
|
||||
"DELETE FROM hidden_files WHERE filename = ? AND profile = ?",
|
||||
(filename, profile),
|
||||
)
|
||||
self._con.commit()
|
||||
|
||||
def get_hidden_files(self, profile: str = "default") -> set[str]:
|
||||
if not self._enabled:
|
||||
return set()
|
||||
rows = self._con.execute(
|
||||
"SELECT filename FROM hidden_files WHERE profile = ?", (profile,)
|
||||
).fetchall()
|
||||
return {r[0] for r in rows}
|
||||
|
||||
|
||||
class _DBWorker(QThread):
|
||||
"""Runs ProcessedDB fuzzy-match lookup off the main thread."""
|
||||
@@ -1362,23 +1395,29 @@ class PlaylistWidget(QListWidget):
|
||||
self.setTextElideMode(Qt.TextElideMode.ElideMiddle)
|
||||
self._paths: list[str] = []
|
||||
self._path_set: set[str] = set() # O(1) duplicate check
|
||||
self._done_set: set[str] = set() # paths with exported clips
|
||||
self._hide_exported = False
|
||||
self.itemClicked.connect(self._on_item_clicked)
|
||||
|
||||
def add_files(self, paths: list[str]) -> None:
|
||||
"""Append paths not already in queue; auto-select first if queue was empty."""
|
||||
was_empty = len(self._paths) == 0
|
||||
self.setUpdatesEnabled(False)
|
||||
for path in paths:
|
||||
if path not in self._path_set and os.path.isfile(path):
|
||||
self._paths.append(path)
|
||||
self._path_set.add(path)
|
||||
self.addItem(os.path.basename(path))
|
||||
self.setUpdatesEnabled(True)
|
||||
if was_empty and self._paths:
|
||||
self._select(0)
|
||||
self.scrollToItem(self.item(0))
|
||||
|
||||
def mark_done(self, path: str, n_clips: int = 0) -> None:
|
||||
"""Gray out and show clip count on the queue item for path."""
|
||||
if path not in self._path_set:
|
||||
return
|
||||
self._done_set.add(path)
|
||||
row = self._paths.index(path)
|
||||
item = self.item(row)
|
||||
if item is None:
|
||||
@@ -1387,23 +1426,46 @@ class PlaylistWidget(QListWidget):
|
||||
tag = f"[{n_clips}]" if n_clips else "✓"
|
||||
item.setText(f"{tag} {name}")
|
||||
item.setForeground(QColor(100, 180, 100))
|
||||
if self._hide_exported:
|
||||
item.setHidden(True)
|
||||
|
||||
def unmark_done(self, path: str) -> None:
|
||||
"""Remove the ✓ prefix and restore default color."""
|
||||
"""Remove the done mark and restore default color."""
|
||||
if path not in self._path_set:
|
||||
return
|
||||
self._done_set.discard(path)
|
||||
row = self._paths.index(path)
|
||||
item = self.item(row)
|
||||
if item is None:
|
||||
return
|
||||
item.setText(os.path.basename(path))
|
||||
item.setForeground(QColor(200, 200, 200))
|
||||
if self._hide_exported:
|
||||
item.setHidden(False)
|
||||
|
||||
def hide_paths(self, basenames: set[str]) -> None:
|
||||
"""Hide items whose basename is in the set (profile-based hiding)."""
|
||||
for i, path in enumerate(self._paths):
|
||||
if os.path.basename(path) in basenames:
|
||||
item = self.item(i)
|
||||
if item:
|
||||
item.setHidden(True)
|
||||
|
||||
def set_hide_exported(self, hide: bool) -> None:
|
||||
self._hide_exported = hide
|
||||
for i, path in enumerate(self._paths):
|
||||
item = self.item(i)
|
||||
if item:
|
||||
item.setHidden(hide and path in self._done_set)
|
||||
|
||||
def advance(self) -> None:
|
||||
"""Move to next item in queue. Does nothing if at end or nothing selected."""
|
||||
"""Move to next visible item in queue."""
|
||||
row = self.currentRow()
|
||||
if row >= 0 and row < self.count() - 1:
|
||||
self._select(row + 1)
|
||||
for r in range(row + 1, self.count()):
|
||||
item = self.item(r)
|
||||
if item and not item.isHidden():
|
||||
self._select(r)
|
||||
return
|
||||
|
||||
def current_path(self) -> str | None:
|
||||
row = self.currentRow()
|
||||
@@ -1416,8 +1478,15 @@ class PlaylistWidget(QListWidget):
|
||||
self._refresh_item_text(prev)
|
||||
if self.item(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])}")
|
||||
cur = item.text()
|
||||
# Preserve [N] tag from mark_done.
|
||||
if cur.startswith("[") and "] " in cur:
|
||||
tag = cur[:cur.index("] ") + 2]
|
||||
elif item.foreground().color() == QColor(100, 180, 100):
|
||||
tag = "✓ "
|
||||
else:
|
||||
tag = ""
|
||||
item.setText(f"▶ {tag}{os.path.basename(self._paths[row])}")
|
||||
self.file_selected.emit(self._paths[row])
|
||||
|
||||
def _refresh_item_text(self, row: int) -> None:
|
||||
@@ -1425,7 +1494,12 @@ class PlaylistWidget(QListWidget):
|
||||
if item is None:
|
||||
return
|
||||
name = os.path.basename(self._paths[row])
|
||||
if item.foreground().color() == QColor(100, 180, 100):
|
||||
# Preserve the [N] prefix from mark_done if present.
|
||||
cur = item.text()
|
||||
if cur.startswith("[") and "] " in cur:
|
||||
prefix = cur[:cur.index("] ") + 2]
|
||||
item.setText(f"{prefix}{name}")
|
||||
elif item.foreground().color() == QColor(100, 180, 100):
|
||||
item.setText(f"✓ {name}")
|
||||
else:
|
||||
item.setText(name)
|
||||
@@ -1433,6 +1507,8 @@ class PlaylistWidget(QListWidget):
|
||||
def _on_item_clicked(self, item: QListWidgetItem) -> None:
|
||||
self._select(self.row(item))
|
||||
|
||||
hide_requested = pyqtSignal(str) # emits full path to hide in current profile
|
||||
|
||||
def contextMenuEvent(self, event) -> None:
|
||||
item = self.itemAt(event.pos())
|
||||
if item is None:
|
||||
@@ -1440,11 +1516,20 @@ class PlaylistWidget(QListWidget):
|
||||
row = self.row(item)
|
||||
from PyQt6.QtWidgets import QMenu
|
||||
menu = QMenu(self)
|
||||
action = menu.addAction(f"Remove: {os.path.basename(self._paths[row])}")
|
||||
if menu.exec(event.globalPos()) == action:
|
||||
name = os.path.basename(self._paths[row])
|
||||
act_remove = menu.addAction(f"Remove: {name}")
|
||||
act_hide = menu.addAction(f"Hide in profile: {name}")
|
||||
chosen = menu.exec(event.globalPos())
|
||||
if chosen == act_remove:
|
||||
path = self._paths.pop(row)
|
||||
self._path_set.discard(path)
|
||||
self._done_set.discard(path)
|
||||
self.takeItem(row)
|
||||
elif chosen == act_hide:
|
||||
path = self._paths[row]
|
||||
self.hide_requested.emit(path)
|
||||
# Visually hide the item immediately.
|
||||
item.setHidden(True)
|
||||
|
||||
|
||||
class _KeyFilter(QObject):
|
||||
@@ -1527,6 +1612,7 @@ class MainWindow(QMainWindow):
|
||||
# Widgets
|
||||
self._playlist = PlaylistWidget()
|
||||
self._playlist.file_selected.connect(self._load_file)
|
||||
self._playlist.hide_requested.connect(self._on_hide_file)
|
||||
|
||||
self._mpv = MpvWidget()
|
||||
self._mpv.file_loaded.connect(self._after_load)
|
||||
@@ -1847,10 +1933,21 @@ class MainWindow(QMainWindow):
|
||||
self._btn_open = QPushButton("+ Open Files")
|
||||
self._btn_open.setToolTip("Add video files to the queue")
|
||||
self._btn_open.clicked.connect(self._on_open_files)
|
||||
|
||||
self._chk_hide_exported = QCheckBox("Hide exported")
|
||||
self._chk_hide_exported.setToolTip("Hide files that already have exported clips")
|
||||
self._chk_hide_exported.setChecked(
|
||||
self._settings.value("hide_exported", "false") == "true"
|
||||
)
|
||||
self._chk_hide_exported.toggled.connect(self._on_hide_exported_toggled)
|
||||
|
||||
left = QWidget()
|
||||
left_layout = QVBoxLayout(left)
|
||||
left_layout.setContentsMargins(4, 4, 4, 4)
|
||||
left_layout.addWidget(self._btn_open)
|
||||
left_top = QHBoxLayout()
|
||||
left_top.addWidget(self._btn_open)
|
||||
left_top.addWidget(self._chk_hide_exported)
|
||||
left_layout.addLayout(left_top)
|
||||
left_layout.addWidget(self._playlist)
|
||||
|
||||
# Root: horizontal splitter
|
||||
@@ -1899,6 +1996,15 @@ class MainWindow(QMainWindow):
|
||||
for key in ("?", "F1"):
|
||||
QShortcut(QKeySequence(key), self, context=ctx).activated.connect(self._show_shortcuts)
|
||||
|
||||
# Resume last session: reload previous playlist files.
|
||||
session_files = self._settings.value("session_files", [])
|
||||
if session_files:
|
||||
valid = [p for p in session_files if os.path.isfile(p)]
|
||||
if valid:
|
||||
self._playlist.add_files(valid)
|
||||
self._apply_playlist_filters()
|
||||
_log(f"Resumed session: {len(valid)} file(s)")
|
||||
|
||||
def _show_shortcuts(self) -> None:
|
||||
text = (
|
||||
"<table cellpadding='4' style='font-size:13px'>"
|
||||
@@ -1975,12 +2081,30 @@ class MainWindow(QMainWindow):
|
||||
if not self._last_export_path:
|
||||
self._btn_delete.setEnabled(False)
|
||||
self._update_next_label()
|
||||
self._refresh_playlist_checks()
|
||||
self._apply_playlist_filters()
|
||||
if self._file_path:
|
||||
self._refresh_markers()
|
||||
_log(f"Profile switched: {text}")
|
||||
self.statusBar().showMessage(f"Profile: {text}", 3000)
|
||||
|
||||
def _on_hide_exported_toggled(self, hide: bool) -> None:
|
||||
self._settings.setValue("hide_exported", "true" if hide else "false")
|
||||
self._playlist.set_hide_exported(hide)
|
||||
|
||||
def _on_hide_file(self, path: str) -> None:
|
||||
"""Persistently hide a file in the current profile."""
|
||||
self._db.hide_file(os.path.basename(path), self._profile)
|
||||
_log(f"Hidden file: {os.path.basename(path)} in profile {self._profile}")
|
||||
|
||||
def _apply_playlist_filters(self) -> None:
|
||||
"""Apply profile-hidden files, export marks, and hide-exported filter."""
|
||||
self._refresh_playlist_checks()
|
||||
hidden = self._db.get_hidden_files(self._profile)
|
||||
if hidden:
|
||||
self._playlist.hide_paths(hidden)
|
||||
if self._chk_hide_exported.isChecked():
|
||||
self._playlist.set_hide_exported(True)
|
||||
|
||||
def _on_open_files(self) -> None:
|
||||
paths, _ = QFileDialog.getOpenFileNames(
|
||||
self, "Open video files", "",
|
||||
@@ -1988,10 +2112,7 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
if paths:
|
||||
self._playlist.add_files(paths)
|
||||
for p in paths:
|
||||
markers = self._db.get_markers(os.path.basename(p), self._profile)
|
||||
if markers:
|
||||
self._playlist.mark_done(p, len(markers))
|
||||
self._apply_playlist_filters()
|
||||
|
||||
def _load_file(self, path: str):
|
||||
self._file_path = path
|
||||
@@ -2593,6 +2714,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def closeEvent(self, event):
|
||||
_log("Shutting down…")
|
||||
# Save session playlist for resume.
|
||||
self._settings.setValue("session_files", self._playlist._paths)
|
||||
# Stop timers first to prevent callbacks into dead objects.
|
||||
self._preview_timer.stop()
|
||||
self._mpv._render_timer.stop()
|
||||
@@ -2636,10 +2759,7 @@ class MainWindow(QMainWindow):
|
||||
]
|
||||
if paths:
|
||||
self._playlist.add_files(paths)
|
||||
for p in paths:
|
||||
markers = self._db.get_markers(os.path.basename(p), self._profile)
|
||||
if markers:
|
||||
self._playlist.mark_done(p, len(markers))
|
||||
self._apply_playlist_filters()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user