From 02092bab69f89ed437b52224957fa8df3cec3846 Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Thu, 5 Mar 2026 12:38:39 +1100 Subject: [PATCH] Make tags/highlights panel collapsible and add support for relative time for tags --- desktop-client/room_widget.py | 105 +++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 13 deletions(-) diff --git a/desktop-client/room_widget.py b/desktop-client/room_widget.py index 0a1987d..8ec0c0d 100644 --- a/desktop-client/room_widget.py +++ b/desktop-client/room_widget.py @@ -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(): @@ -527,6 +581,21 @@ class RoomWidget(QWidget): new_msg = f"{safe_author}: {linked_text} {time_str}" 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'^[{time_str}] {tag_text} - {author}" + self.tags_list.append(highlight_html) def add_system_message(self, text: str): new_msg = f"{text}" @@ -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}" + + msg = f"{time_str} {tag_name}".strip() + self.chat_message_ready.emit(msg) else: - time_str = time_arg - - msg = f"{time_str} {tag_name}".strip() - self.chat_message_ready.emit(msg) + 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,12 +668,16 @@ 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 if arg.startswith('+') or arg.startswith('-'): @@ -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 + return False