Fix session save/restore losing folders, add frame column and UI improvements

Session restore fixes:
- AUTO-typed folders now default to MAIN on restore instead of using
  position-based index%2, which silently flipped half the folders to
  TRANSITION when restoring legacy sessions
- All restored folders get explicit type overrides so no folder relies
  on position-based typing after restore
- TRANSITION folders with symlink data are auto-recovered as MAIN
  (catches incorrectly saved types from older export paths)
- Export Sequence path now saves with save_effective_types=True,
  preventing folder type loss
- Removed redundant trim-only save that used unresolved paths
- Auto-save guards against overwriting sessions with empty file lists

UI improvements:
- Added 4th "Frame" column to Sequence Order tab showing overall
  frame number (1-based)
- Last frame of each sequence is bold for visual clarity
- Fixed column resizing (ResizeToContents + Stretch) to prevent
  column collapse bugs
- Save Session dialog now reports main + transition folder counts
- Default optical flow preset changed to Max

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 11:55:48 +01:00
parent 82a1c2ff9f
commit c03ee0665b

View File

@@ -495,12 +495,15 @@ class SequenceLinkerUI(QWidget):
# File list (Sequence Order tab)
self.file_list = QTreeWidget()
self.file_list.setHeaderLabels(["Sequence Name", "Original Filename", "Source Folder"])
self.file_list.setHeaderLabels(["Sequence Name", "Original Filename", "Source Folder", "Frame"])
self.file_list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
self.file_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
self.file_list.setRootIsDecorated(False)
self.file_list.header().setStretchLastSection(True)
self.file_list.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
self.file_list.header().setStretchLastSection(False)
self.file_list.header().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
self.file_list.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
self.file_list.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
self.file_list.header().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
self.file_list.setToolTip("Drag to reorder within folder, Del to remove")
# Action buttons
@@ -766,7 +769,7 @@ class SequenceLinkerUI(QWidget):
self.of_preset_combo.addItem("Quality", "quality")
self.of_preset_combo.addItem("Max", "max")
self.of_preset_combo.addItem("Custom", "custom")
self.of_preset_combo.setCurrentIndex(1) # Default to Balanced
self.of_preset_combo.setCurrentIndex(3) # Default to Max
self.of_preset_combo.setToolTip(
"Optical flow quality preset:\n"
"- Fast: Quick processing, lower quality\n"
@@ -2853,8 +2856,14 @@ class SequenceLinkerUI(QWidget):
seen_resolved.add(resolved_str)
if trim_start > 0 or trim_end > 0:
self._folder_trim_settings[folder_path] = (trim_start, trim_end)
# Always set explicit type override so position-based index%2
# fallback never silently flips a folder's type after reordering
# or transition insertion. AUTO defaults to MAIN (safe for
# legacy sessions that pre-date the type system).
if folder_type != FolderType.AUTO:
self._folder_type_overrides[folder_path] = folder_type
else:
self._folder_type_overrides[folder_path] = FolderType.MAIN
pts_key = _resolve_lookup(folder_str, db_per_trans_settings)
if pts_key is None:
pts_key = _resolve_lookup(resolved_str, db_per_trans_settings)
@@ -2866,10 +2875,21 @@ class SequenceLinkerUI(QWidget):
if rm_key is not None:
self._removed_files[folder_path] = db_removed_files[rm_key]
# Assign folder_data index for MAIN folders used by _restore_files_from_session
# Remove TRANSITION folders from folder_data — their files aren't shown
# in the file list (they participate in blending only).
# AUTO defaults to MAIN so legacy/incomplete saves don't lose data.
effective_type = folder_type if folder_type != FolderType.AUTO else FolderType.MAIN
fd_key = _resolve_lookup(folder_str, folder_data)
if fd_key is None:
fd_key = _resolve_lookup(resolved_str, folder_data)
if fd_key is not None:
if effective_type == FolderType.TRANSITION:
# TRANSITION folders shouldn't have symlink data — if
# they do, the type was likely saved incorrectly (e.g.
# by an older export path). Recover by treating as
# MAIN so the files aren't lost.
effective_type = FolderType.MAIN
self._folder_type_overrides[folder_path] = FolderType.MAIN
folder_data[fd_key] = (main_idx, folder_data[fd_key][1])
main_idx += 1
@@ -2922,6 +2942,8 @@ class SequenceLinkerUI(QWidget):
self._folder_trim_settings[folder_path] = (ts, te)
if ft != FolderType.AUTO:
self._folder_type_overrides[folder_path] = ft
else:
self._folder_type_overrides[folder_path] = FolderType.MAIN
# Apply per-transition settings
pts_key = _resolve_lookup(folder, db_per_trans_settings)
if pts_key is None:
@@ -3130,25 +3152,31 @@ class SequenceLinkerUI(QWidget):
session_id = self.db.create_session(dest)
self._current_session_id = session_id
# Get current file list before clearing — don't clear if empty
# to avoid corrupting the session
files = self._get_files_in_order()
if not files and self.source_folders:
# file_list is empty but we have folders — don't overwrite
# the session, just save settings
return
# Clear all stale data for this session before re-saving
self.db.clear_session_data(session_id)
self._save_session_settings(session_id, save_effective_types=True)
# Also save the file list so the exact sequence can be restored
files = self._get_files_in_order()
if files:
for i, (source_dir, filename, folder_idx, file_idx) in enumerate(files):
source_path = source_dir / filename
ext = source_path.suffix
link_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}"
self.db.record_symlink(
session_id=session_id,
source=str(source_path.resolve()),
link=str(Path(dest) / link_name),
filename=filename,
seq=i,
)
# Save the file list so the exact sequence can be restored
for i, (source_dir, filename, folder_idx, file_idx) in enumerate(files):
source_path = source_dir / filename
ext = source_path.suffix
link_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}"
self.db.record_symlink(
session_id=session_id,
source=str(source_path.resolve()),
link=str(Path(dest) / link_name),
filename=filename,
seq=i,
)
except Exception:
pass # Best-effort save on close
@@ -3186,9 +3214,17 @@ class SequenceLinkerUI(QWidget):
seq=i,
)
main_count = sum(
1 for i, f in enumerate(self.source_folders)
if self._get_effective_folder_type(i, f) == FolderType.MAIN
)
trans_count = len(self.source_folders) - main_count
folder_info = f"{main_count} main"
if trans_count > 0:
folder_info += f" + {trans_count} transition"
QMessageBox.information(
self, "Session Saved",
f"Saved {len(files)} files from {len(self.source_folders)} folders."
f"Saved {len(files)} files from {folder_info} folders."
)
except Exception as e:
@@ -3567,15 +3603,24 @@ class SequenceLinkerUI(QWidget):
self.file_list.addTopLevelItem(separator)
is_first_folder = False
for filename in trimmed_files:
for file_i, filename in enumerate(trimmed_files):
file_idx = folder_file_counts.get(folder, 0)
folder_file_counts[folder] = file_idx + 1
ext = Path(filename).suffix
seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}"
overall_frame = sum(folder_file_counts.values())
item = QTreeWidgetItem([seq_name, filename, str(folder)])
item = QTreeWidgetItem([seq_name, filename, str(folder), str(overall_frame)])
item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx))
# Bold the last frame of each sequence
if file_i == len(trimmed_files) - 1:
font = item.font(0)
font.setBold(True)
for col in range(4):
item.setFont(col, font)
self.file_list.addTopLevelItem(item)
total = self.file_list.topLevelItemCount()
@@ -3609,6 +3654,7 @@ class SequenceLinkerUI(QWidget):
self._folder_file_counts = {}
is_first_folder = True
overall_frame = 0
# Batch UI updates for performance
self.file_list.setUpdatesEnabled(False)
@@ -3634,12 +3680,21 @@ class SequenceLinkerUI(QWidget):
self.file_list.addTopLevelItem(separator)
is_first_folder = False
for file_idx, filename in sorted_files:
for file_i, (file_idx, filename) in enumerate(sorted_files):
ext = Path(filename).suffix
seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}"
overall_frame += 1
item = QTreeWidgetItem([seq_name, filename, str(folder_path)])
item = QTreeWidgetItem([seq_name, filename, str(folder_path), str(overall_frame)])
item.setData(0, Qt.ItemDataRole.UserRole, (folder_path, filename, folder_idx, file_idx))
# Bold the last frame of each sequence
if file_i == len(sorted_files) - 1:
font = item.font(0)
font.setBold(True)
for col in range(4):
item.setFont(col, font)
self.file_list.addTopLevelItem(item)
self.file_list.setUpdatesEnabled(True)
@@ -3655,11 +3710,11 @@ class SequenceLinkerUI(QWidget):
def _create_folder_separator(self, next_folder_idx: int) -> QTreeWidgetItem:
"""Create a visual separator item between folders."""
separator = QTreeWidgetItem(["", f"── Sequence {next_folder_idx + 1} ──", ""])
separator = QTreeWidgetItem(["", f"── Sequence {next_folder_idx + 1} ──", "", ""])
separator.setData(0, Qt.ItemDataRole.UserRole, None) # No data = separator
# Light grey background
grey = QColor(220, 220, 220)
for col in range(3):
for col in range(4):
separator.setBackground(col, grey)
# Make it non-selectable and non-draggable
separator.setFlags(Qt.ItemFlag.NoItemFlags)
@@ -3688,6 +3743,17 @@ class SequenceLinkerUI(QWidget):
folder_file_counts: dict[Path, int] = {}
last_folder_idx = -1
# Collect items per folder to detect last file
folder_items: dict[Path, list[int]] = {}
for i in range(self.file_list.topLevelItemCount()):
item = self.file_list.topLevelItem(i)
data = item.data(0, Qt.ItemDataRole.UserRole)
if data:
src = data[0]
if src not in folder_items:
folder_items[src] = []
folder_items[src].append(i)
for i in range(self.file_list.topLevelItemCount()):
item = self.file_list.topLevelItem(i)
data = item.data(0, Qt.ItemDataRole.UserRole)
@@ -3700,8 +3766,18 @@ class SequenceLinkerUI(QWidget):
ext = Path(filename).suffix
seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}"
overall_frame = sum(folder_file_counts.values())
item.setText(0, seq_name)
item.setText(3, str(overall_frame))
item.setData(0, Qt.ItemDataRole.UserRole, (source_dir, filename, folder_idx, file_idx))
# Bold the last frame of each sequence
is_last = (folder_items.get(source_dir, [])[-1] == i)
font = item.font(0)
font.setBold(is_last)
for col in range(4):
item.setFont(col, font)
last_folder_idx = folder_idx
elif self._is_separator_item(item):
# Update separator label based on next file's folder
@@ -4122,11 +4198,7 @@ class SequenceLinkerUI(QWidget):
self._current_session_id = session_id
if session_id:
# Save trim settings
for folder, (trim_start, trim_end) in self._folder_trim_settings.items():
if trim_start > 0 or trim_end > 0:
self.db.save_trim_settings(session_id, str(folder), trim_start, trim_end)
self._save_session_settings(session_id)
self._save_session_settings(session_id, save_effective_types=True)
total = len(files)
link_type = "copies" if copy_files else "symlinks"