feat: show other-folder markers in distinct colors on timeline
Subprofile/subfolder exports now appear as colored markers (yellow, green, blue, purple, orange) with their own numbering, separate from the main folder's red markers. Each folder gets its own color and independent sequence numbers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+38
@@ -396,6 +396,44 @@ class ProcessedDB:
|
|||||||
return []
|
return []
|
||||||
return self._get_markers_for(filename, profile, export_folder)
|
return self._get_markers_for(filename, profile, export_folder)
|
||||||
|
|
||||||
|
def get_other_folder_markers(self, filename: str, profile: str = "default",
|
||||||
|
export_folder: str = ""
|
||||||
|
) -> dict[str, list[tuple[float, int, str, float]]]:
|
||||||
|
"""Return {folder_name: [(start_time, num, path, span), ...]} for
|
||||||
|
markers NOT in export_folder, grouped by their base export folder."""
|
||||||
|
if not self._enabled or not export_folder:
|
||||||
|
return {}
|
||||||
|
rows = self._con.execute(
|
||||||
|
"SELECT start_time, output_path, clip_duration, clip_count, spread"
|
||||||
|
" FROM processed"
|
||||||
|
" WHERE filename = ? AND profile = ? AND scan_export = 0"
|
||||||
|
" AND output_path NOT LIKE ?"
|
||||||
|
" ORDER BY start_time",
|
||||||
|
(filename, profile, export_folder.rstrip("/") + "/%"),
|
||||||
|
).fetchall()
|
||||||
|
by_folder: dict[str, list] = {}
|
||||||
|
for t, p, dur, cnt, spr in rows:
|
||||||
|
parts = p.split("/")
|
||||||
|
for i, part in enumerate(parts):
|
||||||
|
if part.startswith("vid_"):
|
||||||
|
folder = "/".join(parts[:i])
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
folder = os.path.dirname(os.path.dirname(p))
|
||||||
|
by_folder.setdefault(folder, []).append((t, p, dur, cnt, spr))
|
||||||
|
result: dict[str, list[tuple[float, int, str, float]]] = {}
|
||||||
|
for folder, folder_rows in by_folder.items():
|
||||||
|
seen: dict[float, tuple[float, int, str, float]] = {}
|
||||||
|
n = 0
|
||||||
|
for t, p, dur, cnt, spr in folder_rows:
|
||||||
|
if t not in seen:
|
||||||
|
n += 1
|
||||||
|
span = (dur or 8.0) + ((cnt or 1) - 1) * (spr or 3.0)
|
||||||
|
seen[t] = (t, n, p, span)
|
||||||
|
name = os.path.basename(folder)
|
||||||
|
result[name] = list(seen.values())
|
||||||
|
return result
|
||||||
|
|
||||||
def get_manual_export_groups(self, filename: str, profile: str = "default"
|
def get_manual_export_groups(self, filename: str, profile: str = "default"
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""Return manual (non-scan) export groups for *filename*.
|
"""Return manual (non-scan) export groups for *filename*.
|
||||||
|
|||||||
@@ -1692,6 +1692,7 @@ class TimelineWidget(QWidget):
|
|||||||
self._locked = False # when True, clicks scrub playback, not cursor
|
self._locked = False # when True, clicks scrub playback, not cursor
|
||||||
self._crop_keyframes: list[tuple[float, float, str | None, bool, bool]] = []
|
self._crop_keyframes: list[tuple[float, float, str | None, bool, bool]] = []
|
||||||
self._markers: list[tuple[float, int, str, float]] = []
|
self._markers: list[tuple[float, int, str, float]] = []
|
||||||
|
self._other_markers: list[tuple[str, list[tuple[float, int, str, float]]]] = []
|
||||||
# (start, end, score, orig_start, orig_end)
|
# (start, end, score, orig_start, orig_end)
|
||||||
self._scan_regions: list[tuple[float, float, float, float, float]] = []
|
self._scan_regions: list[tuple[float, float, float, float, float]] = []
|
||||||
self._scan_neg_times: set[float] = set()
|
self._scan_neg_times: set[float] = set()
|
||||||
@@ -1743,6 +1744,7 @@ class TimelineWidget(QWidget):
|
|||||||
self._play_pos = None
|
self._play_pos = None
|
||||||
self._view_start = 0.0
|
self._view_start = 0.0
|
||||||
self._view_span = 0.0
|
self._view_span = 0.0
|
||||||
|
self._other_markers = []
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def set_waveform(self, peaks) -> None:
|
def set_waveform(self, peaks) -> None:
|
||||||
@@ -1768,6 +1770,10 @@ class TimelineWidget(QWidget):
|
|||||||
self._markers = markers
|
self._markers = markers
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
def set_other_markers(self, groups: dict[str, list[tuple[float, int, str, float]]]) -> None:
|
||||||
|
self._other_markers = list(groups.items())
|
||||||
|
self.update()
|
||||||
|
|
||||||
def set_scan_regions(self, regions: list, neg_times: set[float] | None = None) -> None:
|
def set_scan_regions(self, regions: list, neg_times: set[float] | None = None) -> None:
|
||||||
"""regions: list of (start, end, score) or (start, end, score, orig_start, orig_end)"""
|
"""regions: list of (start, end, score) or (start, end, score, orig_start, orig_end)"""
|
||||||
normed: list[tuple[float, float, float, float, float]] = []
|
normed: list[tuple[float, float, float, float, float]] = []
|
||||||
@@ -2060,6 +2066,33 @@ class TimelineWidget(QWidget):
|
|||||||
p.drawText(mx + 1, rh + 2, 13, 12,
|
p.drawText(mx + 1, rh + 2, 13, 12,
|
||||||
Qt.AlignmentFlag.AlignCenter, str(num))
|
Qt.AlignmentFlag.AlignCenter, str(num))
|
||||||
|
|
||||||
|
# ── other-folder markers (subprofile exports) ─────────────────
|
||||||
|
_OTHER_COLORS = [
|
||||||
|
QColor(220, 190, 50), # yellow
|
||||||
|
QColor(60, 190, 100), # green
|
||||||
|
QColor(80, 160, 220), # blue
|
||||||
|
QColor(200, 120, 220), # purple
|
||||||
|
QColor(220, 140, 60), # orange
|
||||||
|
]
|
||||||
|
for gi, (folder_name, group) in enumerate(self._other_markers):
|
||||||
|
color = _OTHER_COLORS[gi % len(_OTHER_COLORS)]
|
||||||
|
dim = QColor(color.red(), color.green(), color.blue(), 35)
|
||||||
|
pen = QPen(color, 1)
|
||||||
|
for (t, num, _path, span) in group:
|
||||||
|
mx = int(self._time_to_x(t))
|
||||||
|
if mx < -20 or mx > w + 20:
|
||||||
|
continue
|
||||||
|
mx2 = int(self._time_to_x(min(t + span, self._duration)))
|
||||||
|
if mx2 > mx:
|
||||||
|
p.fillRect(mx, rh, mx2 - mx, th, dim)
|
||||||
|
p.setPen(pen)
|
||||||
|
p.drawLine(mx, rh, mx, h)
|
||||||
|
p.fillRect(mx, rh + 2, 14, 12, color)
|
||||||
|
p.setPen(QColor(0, 0, 0))
|
||||||
|
p.setFont(self._marker_font)
|
||||||
|
p.drawText(mx + 1, rh + 2, 13, 12,
|
||||||
|
Qt.AlignmentFlag.AlignCenter, str(num))
|
||||||
|
|
||||||
# ── scan mode cursor + playback line ─────────────────────────
|
# ── scan mode cursor + playback line ─────────────────────────
|
||||||
if self._scan_mode:
|
if self._scan_mode:
|
||||||
# Export cursor (dim)
|
# Export cursor (dim)
|
||||||
@@ -4210,12 +4243,26 @@ class MainWindow(QMainWindow):
|
|||||||
else:
|
else:
|
||||||
self._lbl_status.clear()
|
self._lbl_status.clear()
|
||||||
self._timeline.set_markers(markers)
|
self._timeline.set_markers(markers)
|
||||||
|
self._refresh_other_markers()
|
||||||
|
|
||||||
def _refresh_markers(self) -> None:
|
def _refresh_markers(self) -> None:
|
||||||
filename = os.path.basename(self._file_path)
|
filename = os.path.basename(self._file_path)
|
||||||
markers = self._db.get_markers(filename, self._profile,
|
folder = self._txt_folder.text()
|
||||||
self._txt_folder.text())
|
markers = self._db.get_markers(filename, self._profile, folder)
|
||||||
self._timeline.set_markers(markers)
|
self._timeline.set_markers(markers)
|
||||||
|
others = self._db.get_other_folder_markers(
|
||||||
|
filename, self._profile, folder)
|
||||||
|
self._timeline.set_other_markers(others)
|
||||||
|
|
||||||
|
def _refresh_other_markers(self) -> None:
|
||||||
|
if not self._file_path:
|
||||||
|
self._timeline.set_other_markers({})
|
||||||
|
return
|
||||||
|
filename = os.path.basename(self._file_path)
|
||||||
|
folder = self._txt_folder.text()
|
||||||
|
others = self._db.get_other_folder_markers(
|
||||||
|
filename, self._profile, folder)
|
||||||
|
self._timeline.set_other_markers(others)
|
||||||
|
|
||||||
def _refresh_playlist_checks(self) -> None:
|
def _refresh_playlist_checks(self) -> None:
|
||||||
"""Re-evaluate marks on every playlist item for the current profile."""
|
"""Re-evaluate marks on every playlist item for the current profile."""
|
||||||
|
|||||||
Reference in New Issue
Block a user