Improve look of chat messages
This commit is contained in:
@@ -436,7 +436,6 @@ class VlcSyncApp(QMainWindow):
|
||||
#chatMessages {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
#chatMessages QScrollBar:vertical {
|
||||
@@ -446,12 +445,12 @@ class VlcSyncApp(QMainWindow):
|
||||
margin: 0px;
|
||||
}
|
||||
#chatMessages QScrollBar::handle:vertical {
|
||||
background: #555;
|
||||
background: #333;
|
||||
min-height: 20px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
#chatMessages QScrollBar::handle:vertical:hover {
|
||||
background: #666;
|
||||
background: #444;
|
||||
}
|
||||
#chatMessages QScrollBar::add-line:vertical, #chatMessages QScrollBar::sub-line:vertical {
|
||||
height: 0px;
|
||||
@@ -459,6 +458,10 @@ class VlcSyncApp(QMainWindow):
|
||||
#chatMessages QScrollBar::add-page:vertical, #chatMessages QScrollBar::sub-page:vertical {
|
||||
background: none;
|
||||
}
|
||||
|
||||
#bubble {
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,15 +5,78 @@ import re
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
||||
QPushButton, QFrame, QSlider, QTextEdit, QTextBrowser, QApplication, QToolTip, QSplitter,
|
||||
QMainWindow
|
||||
QMainWindow, QScrollArea
|
||||
)
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QEvent
|
||||
from PyQt6.QtGui import QIcon, QCursor
|
||||
|
||||
import uuid
|
||||
import urllib.parse
|
||||
|
||||
from vlc_player import VLCSyncPlayer
|
||||
|
||||
class ChatBubble(QWidget):
|
||||
def __init__(self, author, text, time_str, is_self, on_link_clicked):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(5, 2, 5, 2)
|
||||
|
||||
# Container for the actual bubble
|
||||
self.bubble = QFrame()
|
||||
self.bubble.setObjectName("bubble")
|
||||
|
||||
bg_color = "#0084FF" if is_self else "#2a2a2e"
|
||||
text_color = "#FFFFFF"
|
||||
author_color = "#E1E1E1" if is_self else "#3ea6ff"
|
||||
|
||||
self.bubble.setStyleSheet(f"""
|
||||
#bubble {{
|
||||
background-color: {bg_color};
|
||||
border-radius: 12px;
|
||||
}}
|
||||
""")
|
||||
|
||||
bubble_layout = QVBoxLayout(self.bubble)
|
||||
bubble_layout.setContentsMargins(10, 8, 10, 6)
|
||||
bubble_layout.setSpacing(2)
|
||||
|
||||
# Author
|
||||
author_lbl = QLabel("You" if is_self else author)
|
||||
author_lbl.setStyleSheet(f"color: {author_color}; font-weight: bold; font-size: 11px; background: transparent;")
|
||||
bubble_layout.addWidget(author_lbl)
|
||||
|
||||
# Text
|
||||
self.text_lbl = QLabel(text)
|
||||
self.text_lbl.setWordWrap(True)
|
||||
self.text_lbl.setStyleSheet(f"color: {text_color}; font-size: 13px; border: none; background: transparent;")
|
||||
self.text_lbl.setOpenExternalLinks(False)
|
||||
self.text_lbl.linkActivated.connect(on_link_clicked)
|
||||
bubble_layout.addWidget(self.text_lbl)
|
||||
|
||||
# Time
|
||||
time_lbl = QLabel(time_str)
|
||||
time_lbl.setAlignment(Qt.AlignmentFlag.AlignRight)
|
||||
time_lbl.setStyleSheet(f"color: {text_color}; font-size: 9px; opacity: 0.6; border: none; background: transparent;")
|
||||
bubble_layout.addWidget(time_lbl)
|
||||
|
||||
if is_self:
|
||||
layout.addStretch()
|
||||
layout.addWidget(self.bubble)
|
||||
else:
|
||||
layout.addWidget(self.bubble)
|
||||
layout.addStretch()
|
||||
|
||||
class SystemMessageWidget(QWidget):
|
||||
def __init__(self, text):
|
||||
super().__init__()
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(0, 5, 0, 5)
|
||||
lbl = QLabel(text)
|
||||
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setStyleSheet("color: #888; font-style: italic; font-size: 11px; background: transparent;")
|
||||
layout.addWidget(lbl)
|
||||
|
||||
class ChatPopoutWindow(QMainWindow):
|
||||
closed = pyqtSignal()
|
||||
|
||||
@@ -287,12 +350,24 @@ class RoomWidget(QWidget):
|
||||
tags_layout.addWidget(self.tags_header_btn)
|
||||
tags_layout.addWidget(self.tags_list)
|
||||
|
||||
self.chat_messages = QTextBrowser()
|
||||
self.chat_messages = QScrollArea()
|
||||
self.chat_messages.setWidgetResizable(True)
|
||||
self.chat_messages.setObjectName("chatMessages")
|
||||
self.chat_messages.setOpenExternalLinks(False)
|
||||
self.chat_messages.setOpenLinks(False)
|
||||
self.chat_messages.anchorClicked.connect(self.on_chat_link_clicked)
|
||||
self.chat_messages.setHtml("Welcome to the room! 👋")
|
||||
self.chat_messages.setStyleSheet("background-color: transparent; border: none;")
|
||||
|
||||
self.chat_content = QWidget()
|
||||
self.chat_content.setObjectName("chatContent")
|
||||
self.chat_content.setStyleSheet("background: transparent;")
|
||||
self.chat_content_layout = QVBoxLayout(self.chat_content)
|
||||
self.chat_content_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||
self.chat_content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.chat_content_layout.setSpacing(0)
|
||||
|
||||
self.chat_messages.setWidget(self.chat_content)
|
||||
self.chat_messages.toPlainText = self.toPlainText # For test compatibility
|
||||
|
||||
# Initial welcome message
|
||||
self.add_system_message("Welcome to the room! 👋")
|
||||
|
||||
chat_input_layout = QHBoxLayout()
|
||||
self.chat_input = QLineEdit()
|
||||
@@ -326,7 +401,7 @@ class RoomWidget(QWidget):
|
||||
self.chat_toggle_btn, self.popout_btn]:
|
||||
w.setFocusPolicy(Qt.FocusPolicy.NoFocus)
|
||||
|
||||
# specifically for chat_messages, we allow clicking links, but avoid focus stealing
|
||||
# scroll area handles focus properly
|
||||
self.chat_messages.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
|
||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
||||
|
||||
@@ -363,7 +438,12 @@ class RoomWidget(QWidget):
|
||||
self.username = username
|
||||
self.set_room_code_display(room_code)
|
||||
self.room_file_badge.setText(f"📄 {file_name}")
|
||||
self.chat_messages.setHtml("Welcome to the room! 👋")
|
||||
|
||||
# Clear chat
|
||||
for i in reversed(range(self.chat_content_layout.count())):
|
||||
self.chat_content_layout.itemAt(i).widget().setParent(None)
|
||||
|
||||
self.add_system_message("Welcome to the room! 👋")
|
||||
self.current_users = []
|
||||
self._is_first_user_update = True
|
||||
|
||||
@@ -744,9 +824,21 @@ class RoomWidget(QWidget):
|
||||
|
||||
def on_chat_link_clicked(self, url):
|
||||
link = url.toString()
|
||||
print(f"DEBUG: Link clicked: {link}")
|
||||
if link.startswith("seek:"):
|
||||
time_str = link[5:]
|
||||
self._handle_seek_command(time_str)
|
||||
# Format: 'seek:MM:SS?tag=TagName'
|
||||
data = link[5:]
|
||||
if "?" in data and "tag=" in data:
|
||||
time_str, query = data.split("?", 1)
|
||||
tag_name = ""
|
||||
for pair in query.split("&"):
|
||||
if pair.startswith("tag="):
|
||||
tag_name = urllib.parse.unquote(pair[4:])
|
||||
break
|
||||
self.add_system_message(f"Seeking to <b>{tag_name}</b> ({time_str})")
|
||||
self._handle_seek_command(time_str)
|
||||
else:
|
||||
self._handle_seek_command(data)
|
||||
|
||||
def add_chat_message(self, author: str, text: str, timestamp: int):
|
||||
try:
|
||||
@@ -762,27 +854,57 @@ class RoomWidget(QWidget):
|
||||
pattern = r'\b(\d{1,2}:\d{2}(?::\d{2})?)\b'
|
||||
linked_text = re.sub(pattern, r'<a href="seek:\1" style="color: #66b3ff; text-decoration: none;">\1</a>', safe_text)
|
||||
|
||||
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)
|
||||
is_self = author == self.username
|
||||
|
||||
# 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)
|
||||
# We look for the pattern generated by /tag: "MM:SS tag text"
|
||||
# Since we just emit the text for /tag, it won't be linked yet in the raw text
|
||||
tag_match = re.match(r'^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$', text)
|
||||
if tag_match:
|
||||
self.add_highlight(tag_match.group(1), tag_match.group(2), author)
|
||||
return # Don't show this as a chat message bubble
|
||||
|
||||
bubble = ChatBubble(author, linked_text, time_str, is_self, self._handle_link_from_bubble)
|
||||
self.chat_content_layout.addWidget(bubble)
|
||||
self._scroll_to_bottom()
|
||||
|
||||
def _handle_link_from_bubble(self, url_str):
|
||||
if url_str.startswith("seek:"):
|
||||
time_str = url_str[5:]
|
||||
self._handle_seek_command(time_str)
|
||||
|
||||
def _scroll_to_bottom(self):
|
||||
# Allow layout to finish before scrolling
|
||||
QTimer.singleShot(10, lambda: self.chat_messages.verticalScrollBar().setValue(
|
||||
self.chat_messages.verticalScrollBar().maximum()
|
||||
))
|
||||
|
||||
def toPlainText(self):
|
||||
# For compatibility with existing tests
|
||||
text = ""
|
||||
for i in range(self.chat_content_layout.count()):
|
||||
item = self.chat_content_layout.itemAt(i)
|
||||
if not item: continue
|
||||
w = item.widget()
|
||||
if isinstance(w, ChatBubble):
|
||||
text += w.text_lbl.text() + "\n"
|
||||
elif isinstance(w, SystemMessageWidget):
|
||||
lbl = w.findChild(QLabel)
|
||||
if lbl: text += lbl.text() + "\n"
|
||||
return text
|
||||
|
||||
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>"
|
||||
safe_tag = urllib.parse.quote(tag_text)
|
||||
highlight_html = f"• <a href='seek:{time_str}?tag={safe_tag}' style='color: #66b3ff; text-decoration: none;'>[{time_str}]</a> {html.escape(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)
|
||||
msg = SystemMessageWidget(text)
|
||||
self.chat_content_layout.addWidget(msg)
|
||||
self._scroll_to_bottom()
|
||||
|
||||
def send_chat(self):
|
||||
text = self.chat_input.text().strip()
|
||||
@@ -855,8 +977,8 @@ class RoomWidget(QWidget):
|
||||
"""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 None
|
||||
# Don't strictly block on length_ms > 0 here, as we might be seeking before length is known
|
||||
# or length might be reported as 0 for some streams.
|
||||
|
||||
if arg == "now":
|
||||
return current_ms
|
||||
@@ -883,7 +1005,9 @@ class RoomWidget(QWidget):
|
||||
elif arg.endswith('h'): target_ms = float(arg[:-1]) * 3600 * 1000
|
||||
else: target_ms = float(arg) * 1000
|
||||
|
||||
return max(0, min(target_ms, length_ms))
|
||||
if length_ms > 0:
|
||||
return max(0, min(target_ms, length_ms))
|
||||
return max(0, target_ms)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user