Make tags/highlights panel collapsible and add support for relative time for tags

This commit is contained in:
Peter Stockings
2026-03-05 12:38:39 +11:00
parent 777e08ff85
commit 02092bab69

View File

@@ -186,6 +186,51 @@ class RoomWidget(QWidget):
self.users_lbl = QLabel("0 watching")
self.users_lbl.setObjectName("usersLbl")
# Tags Container (Hidden by default)
self.tags_container = QFrame()
self.tags_container.setObjectName("tagsContainer")
self.tags_container.hide() # Only show when there are tags
tags_layout = QVBoxLayout(self.tags_container)
tags_layout.setContentsMargins(0, 0, 0, 10)
self.tags_header_btn = QPushButton("▼ Highlights")
self.tags_header_btn.setCursor(Qt.CursorShape.PointingHandCursor)
self.tags_header_btn.setStyleSheet("""
QPushButton {
color: #ccc;
font-weight: bold;
font-size: 12px;
text-align: left;
border: none;
background: transparent;
padding: 2px 0px;
}
QPushButton:hover {
color: #fff;
}
""")
self.tags_header_btn.clicked.connect(self.toggle_tags_panel)
self.tags_list = QTextBrowser()
self.tags_list.setObjectName("tagsList")
self.tags_list.setFixedHeight(100) # Keep it small and unobtrusive
self.tags_list.setOpenExternalLinks(False)
self.tags_list.setOpenLinks(False)
self.tags_list.anchorClicked.connect(self.on_chat_link_clicked)
self.tags_list.setStyleSheet("""
QTextBrowser {
background-color: #1e1e1e;
border: 1px solid #333;
border-radius: 4px;
padding: 4px;
color: #ddd;
font-size: 12px;
}
""")
tags_layout.addWidget(self.tags_header_btn)
tags_layout.addWidget(self.tags_list)
self.chat_messages = QTextBrowser()
self.chat_messages.setObjectName("chatMessages")
self.chat_messages.setOpenExternalLinks(False)
@@ -207,6 +252,7 @@ class RoomWidget(QWidget):
chat_layout.addWidget(chat_header)
chat_layout.addWidget(self.users_lbl)
chat_layout.addWidget(self.tags_container)
chat_layout.addWidget(self.chat_messages, 1)
chat_layout.addLayout(chat_input_layout)
@@ -215,7 +261,7 @@ class RoomWidget(QWidget):
# Prevent UI components from stealing focus (which breaks spacebar shortcuts)
for w in [self.copy_code_btn, self.leave_btn, self.play_btn, self.fullscreen_btn,
self.chat_send_btn, self.seekbar, self.volume_slider]:
self.chat_send_btn, self.seekbar, self.volume_slider, self.tags_header_btn]:
w.setFocusPolicy(Qt.FocusPolicy.NoFocus)
# specifically for chat_messages, we allow clicking links, but avoid focus stealing
@@ -259,6 +305,14 @@ class RoomWidget(QWidget):
def set_room_code_display(self, text: str):
self.room_code_display.setText(f"Room: {text}")
def toggle_tags_panel(self):
if self.tags_list.isHidden():
self.tags_list.show()
self.tags_header_btn.setText("▼ Highlights")
else:
self.tags_list.hide()
self.tags_header_btn.setText("▶ Highlights")
def toggle_fullscreen(self):
top_window = self.window()
if top_window.isFullScreen():
@@ -528,6 +582,21 @@ class RoomWidget(QWidget):
new_msg = f"<b>{safe_author}</b>: {linked_text} <span style='color: gray; font-size: 10px;'>{time_str}</span>"
self.chat_messages.append(new_msg)
# Auto-extract tags added by users directly via /tag command
# This will catch messages that look like purely a tag command
# (Since we just append the output of /tag to chat)
match = re.match(r'^<a href="seek:(\d{1,2}:\d{2}(?::\d{2})?)[^>]+>([^<]+)</a>\s+(.+)$', linked_text)
if match:
# It's a tag! Add it to the highlights panel
self.add_highlight(match.group(1), match.group(3), safe_author)
def add_highlight(self, time_str: str, tag_text: str, author: str):
if self.tags_container.isHidden():
self.tags_container.show()
highlight_html = f"• <a href='seek:{time_str}' style='color: #66b3ff; text-decoration: none;'>[{time_str}]</a> {tag_text} <span style='color: #666; font-size: 10px;'>- {author}</span>"
self.tags_list.append(highlight_html)
def add_system_message(self, text: str):
new_msg = f"<i style='color: #888;'>{text}</i>"
self.chat_messages.append(new_msg)
@@ -569,16 +638,17 @@ class RoomWidget(QWidget):
time_arg = parts[1].lower()
tag_name = " ".join(parts[2:])
if time_arg == "now":
current_ms = self.vlc_player.current_time_ms
s = max(0, current_ms) // 1000
target_ms = self._parse_time_arg(time_arg)
if target_ms is not None:
s = int(max(0, target_ms) // 1000)
if s >= 3600: time_str = f"{s//3600:02d}:{(s%3600)//60:02d}:{s%60:02d}"
else: time_str = f"{(s%3600)//60:02d}:{s%60:02d}"
else:
time_str = time_arg
msg = f"{time_str} {tag_name}".strip()
self.chat_message_ready.emit(msg)
else:
self.add_system_message("Invalid time format. Use: now, 1:23, +30s, -1m")
else:
self.add_system_message("Usage: /tag [now|time] [name]")
return
@@ -598,11 +668,15 @@ class RoomWidget(QWidget):
self.chat_message_ready.emit(text)
self.chat_input.setText("")
def _handle_seek_command(self, arg: str) -> bool:
def _parse_time_arg(self, arg: str) -> float | None:
"""Parses a time string (absolute time, offset like +30s, or 'now') and returns the target time in MS. Returns None on error."""
current_ms = self.vlc_player.current_time_ms
length_ms = self.vlc_player.get_length()
if length_ms <= 0:
return False
return None
if arg == "now":
return current_ms
try:
target_ms = 0
@@ -626,9 +700,14 @@ class RoomWidget(QWidget):
elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000
else: target_ms = float(arg) * 1000
target_ms = max(0, min(target_ms, length_ms))
return max(0, min(target_ms, length_ms))
except ValueError:
return None
def _handle_seek_command(self, arg: str) -> bool:
target_ms = self._parse_time_arg(arg)
if target_ms is not None:
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
return True
except ValueError:
return False