Add desktop ready checks, smarter chat scrolling, and fight-night reactions

This commit is contained in:
Peter Stockings
2026-10-05 15:46:55 +11:00
parent 19576de166
commit f3eeda71e6
13 changed files with 1228 additions and 10 deletions
+146
View File
@@ -0,0 +1,146 @@
"""Native ringside overlay and small, original synthesized reaction sounds."""
import math
from pathlib import Path
import struct
import wave
from PyQt6.QtCore import Qt, QTimer, QPropertyAnimation, QPoint, QEasingCurve, QEvent
from PyQt6.QtWidgets import QApplication, QWidget, QFrame, QLabel, QHBoxLayout, QVBoxLayout
REACTIONS = {
"ko": ("K.O.", "That's a wrap.", "#ff5676"),
"bell": ("DING!", "Back to your corners.", "#f5c86c"),
"tap": ("TAP!", "Someone's had enough.", "#e1e1e1"),
"robbery": ("ROBBERY?", "Judges, explain yourselves.", "#f5c86c"),
}
def write_reaction_sound(directory, kind):
"""Generate mono PCM once per cache directory, with no downloaded samples."""
if kind not in REACTIONS:
raise ValueError("Unknown reaction")
path = Path(directory) / f"{kind}.wav"
if path.exists():
return path
path.parent.mkdir(parents=True, exist_ok=True)
rate = 22050
duration = .85 if kind == "bell" else .4
samples = bytearray()
for i in range(int(rate * duration)):
t = i / rate
attack = min(1, t / .008)
if kind == "ko":
value = math.sin(2 * math.pi * (130 * t - 100 * t * t)) * math.exp(-13 * t)
elif kind == "bell":
value = sum(math.sin(2 * math.pi * f * t) * a for f, a in
((880, .55), (1765, .25), (2410, .12))) * math.exp(-5 * t)
elif kind == "tap":
pulse = t if t < .16 else t - .16
value = math.sin(2 * math.pi * 420 * pulse) * math.exp(-45 * pulse)
else:
value = math.sin(2 * math.pi * 145 * t) * math.sin(2 * math.pi * 22 * t) * math.exp(-8 * t)
release = min(1, (duration - t) / .02)
samples.extend(struct.pack("<h", int(16000 * value * attack * release)))
with wave.open(str(path), "wb") as output:
output.setnchannels(1)
output.setsampwidth(2)
output.setframerate(rate)
output.writeframes(samples)
return path
class RingsideStrip(QWidget):
"""A separate native window stays above VLC's native video surface."""
def __init__(self, video_frame):
super().__init__(video_frame, Qt.WindowType.ToolTip | Qt.WindowType.FramelessWindowHint
| Qt.WindowType.WindowDoesNotAcceptFocus | Qt.WindowType.WindowTransparentForInput)
self.video_frame = video_frame
self.owner_window = None
self.setAttribute(Qt.WidgetAttribute.WA_ShowWithoutActivating)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
outer = QVBoxLayout(self)
outer.setContentsMargins(0, 0, 0, 0)
self.panel = QFrame()
outer.addWidget(self.panel)
row = QHBoxLayout(self.panel)
row.setContentsMargins(14, 10, 14, 10)
row.setSpacing(14)
self.headline = QLabel()
self.headline.setTextFormat(Qt.TextFormat.PlainText)
self.caption = QLabel()
self.caption.setTextFormat(Qt.TextFormat.PlainText)
self.caption.setWordWrap(True)
self.caption.setMinimumWidth(0)
self.caption.setStyleSheet("color: #f2f2f5; background: transparent; font-size: 13px;")
self.metadata = QLabel()
self.metadata.setStyleSheet("color: #a6a9b5; background: transparent; font-size: 11px;")
details = QVBoxLayout()
details.setSpacing(3)
details.addWidget(self.caption)
details.addWidget(self.metadata)
row.addWidget(self.headline)
row.addLayout(details, 1)
self.dismiss_timer = QTimer(self)
self.dismiss_timer.setSingleShot(True)
self.dismiss_timer.timeout.connect(self.dismiss)
self.slide = QPropertyAnimation(self, b"pos", self)
self.slide.setDuration(180)
self.slide.setEasingCurve(QEasingCurve.Type.OutCubic)
video_frame.installEventFilter(self)
def show_reaction(self, kind, author, position):
if kind not in REACTIONS or not self.video_frame.isVisible():
return False
owner = self.video_frame.window()
# Avoid displaying an always-on-top effect over another application.
active = QApplication.activeWindow()
while active is not None and active is not owner:
active = active.parentWidget()
if active is None or owner.isMinimized():
return False
if self.owner_window is not owner:
if self.owner_window is not None:
self.owner_window.removeEventFilter(self)
self.owner_window = owner
owner.installEventFilter(self)
title, caption, color = REACTIONS[kind]
self.headline.setText(title)
self.headline.setStyleSheet(f"color: {color}; background: transparent; font-size: 24px; font-weight: bold;")
self.caption.setText(f"{author}: {caption}")
self.metadata.setText(f"Room reaction · {position}")
self.panel.setStyleSheet(f"QFrame {{ background: #1a1b20; border-left: 3px solid {color}; border-radius: 6px; }} QLabel {{ border: none; }}")
self.slide.stop()
self._place()
end = self.pos()
self.move(end + QPoint(0, 12))
self.show()
self.slide.setStartValue(self.pos())
self.slide.setEndValue(end)
self.slide.start()
self.dismiss_timer.start(2200)
return True
def _place(self):
width = max(1, min(540, self.video_frame.width() - 32))
self.setFixedWidth(width)
height = self.layout().heightForWidth(width)
self.setFixedHeight(max(64, height))
origin = self.video_frame.mapToGlobal(QPoint(16, self.video_frame.height() - self.height() - 16))
self.move(origin)
def eventFilter(self, watched, event):
if self.isVisible():
if event.type() in (QEvent.Type.Hide, QEvent.Type.WindowDeactivate, QEvent.Type.Close):
self.dismiss()
elif event.type() in (QEvent.Type.Move, QEvent.Type.Resize):
self.slide.stop()
self._place()
return super().eventFilter(watched, event)
def dismiss(self):
self.slide.stop()
self.dismiss_timer.stop()
self.hide()