Make tags/highlights panel collapsible and add support for relative time for tags
This commit is contained in:
@@ -186,6 +186,51 @@ class RoomWidget(QWidget):
|
|||||||
self.users_lbl = QLabel("0 watching")
|
self.users_lbl = QLabel("0 watching")
|
||||||
self.users_lbl.setObjectName("usersLbl")
|
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 = QTextBrowser()
|
||||||
self.chat_messages.setObjectName("chatMessages")
|
self.chat_messages.setObjectName("chatMessages")
|
||||||
self.chat_messages.setOpenExternalLinks(False)
|
self.chat_messages.setOpenExternalLinks(False)
|
||||||
@@ -207,6 +252,7 @@ class RoomWidget(QWidget):
|
|||||||
|
|
||||||
chat_layout.addWidget(chat_header)
|
chat_layout.addWidget(chat_header)
|
||||||
chat_layout.addWidget(self.users_lbl)
|
chat_layout.addWidget(self.users_lbl)
|
||||||
|
chat_layout.addWidget(self.tags_container)
|
||||||
chat_layout.addWidget(self.chat_messages, 1)
|
chat_layout.addWidget(self.chat_messages, 1)
|
||||||
chat_layout.addLayout(chat_input_layout)
|
chat_layout.addLayout(chat_input_layout)
|
||||||
|
|
||||||
@@ -215,7 +261,7 @@ class RoomWidget(QWidget):
|
|||||||
|
|
||||||
# Prevent UI components from stealing focus (which breaks spacebar shortcuts)
|
# 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,
|
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)
|
w.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||||
|
|
||||||
# specifically for chat_messages, we allow clicking links, but avoid focus stealing
|
# 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):
|
def set_room_code_display(self, text: str):
|
||||||
self.room_code_display.setText(f"Room: {text}")
|
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):
|
def toggle_fullscreen(self):
|
||||||
top_window = self.window()
|
top_window = self.window()
|
||||||
if top_window.isFullScreen():
|
if top_window.isFullScreen():
|
||||||
@@ -527,6 +581,21 @@ class RoomWidget(QWidget):
|
|||||||
|
|
||||||
new_msg = f"<b>{safe_author}</b>: {linked_text} <span style='color: gray; font-size: 10px;'>{time_str}</span>"
|
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)
|
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):
|
def add_system_message(self, text: str):
|
||||||
new_msg = f"<i style='color: #888;'>{text}</i>"
|
new_msg = f"<i style='color: #888;'>{text}</i>"
|
||||||
@@ -569,16 +638,17 @@ class RoomWidget(QWidget):
|
|||||||
time_arg = parts[1].lower()
|
time_arg = parts[1].lower()
|
||||||
tag_name = " ".join(parts[2:])
|
tag_name = " ".join(parts[2:])
|
||||||
|
|
||||||
if time_arg == "now":
|
target_ms = self._parse_time_arg(time_arg)
|
||||||
current_ms = self.vlc_player.current_time_ms
|
|
||||||
s = max(0, current_ms) // 1000
|
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}"
|
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 = f"{(s%3600)//60:02d}:{s%60:02d}"
|
||||||
|
|
||||||
|
msg = f"{time_str} {tag_name}".strip()
|
||||||
|
self.chat_message_ready.emit(msg)
|
||||||
else:
|
else:
|
||||||
time_str = time_arg
|
self.add_system_message("Invalid time format. Use: now, 1:23, +30s, -1m")
|
||||||
|
|
||||||
msg = f"{time_str} {tag_name}".strip()
|
|
||||||
self.chat_message_ready.emit(msg)
|
|
||||||
else:
|
else:
|
||||||
self.add_system_message("Usage: /tag [now|time] [name]")
|
self.add_system_message("Usage: /tag [now|time] [name]")
|
||||||
return
|
return
|
||||||
@@ -598,12 +668,16 @@ class RoomWidget(QWidget):
|
|||||||
self.chat_message_ready.emit(text)
|
self.chat_message_ready.emit(text)
|
||||||
self.chat_input.setText("")
|
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
|
current_ms = self.vlc_player.current_time_ms
|
||||||
length_ms = self.vlc_player.get_length()
|
length_ms = self.vlc_player.get_length()
|
||||||
if length_ms <= 0:
|
if length_ms <= 0:
|
||||||
return False
|
return None
|
||||||
|
|
||||||
|
if arg == "now":
|
||||||
|
return current_ms
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_ms = 0
|
target_ms = 0
|
||||||
if arg.startswith('+') or arg.startswith('-'):
|
if arg.startswith('+') or arg.startswith('-'):
|
||||||
@@ -626,9 +700,14 @@ class RoomWidget(QWidget):
|
|||||||
elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000
|
elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000
|
||||||
else: target_ms = float(arg) * 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)
|
req = self._tell_vlc_and_expect("seek", target_ms / 1000.0)
|
||||||
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
self.sync_action_requested.emit("seek", target_ms / 1000.0, req)
|
||||||
return True
|
return True
|
||||||
except ValueError:
|
return False
|
||||||
return False
|
|
||||||
|
|||||||
Reference in New Issue
Block a user