improved a bit the web replay, disabled user movement

This commit is contained in:
Faraphel 2024-01-03 15:25:50 +01:00
parent ca21da9f7a
commit 7e1467e7f1
6 changed files with 199 additions and 45 deletions

View file

@ -1,4 +1,5 @@
import sys
from datetime import datetime
from PyQt6.QtWidgets import QApplication
@ -9,12 +10,16 @@ if __name__ == "__main__":
# create the application
application = QApplication(sys.argv)
# TODO: cmd arguments ?
from source.utils import compress
with open(r"C:\Users\RC606\Downloads\41a6268b-72e5-47a9-8106-6c15a0be366e.rsl", "rb") as file:
with open(r"C:\Users\RC606\PycharmProjects\M1-Recherche\results\c3376670-548d-4494-b963-dc2facf7a3d1.rsl", "rb") as file:
data = compress.uncompress_data(file.read())
# create the window
window = ReplayWindow(data["surveys"]["mission-gift-card"]["event"])
window = ReplayWindow(
datetime.fromtimestamp(data["time"]),
data["surveys"]["mission-game-dlc"]["event"]
)
window.show()
# start the application

View file

@ -1,19 +1,25 @@
from PyQt6.QtCore import Qt, QUrl, QSize, QPointF
from datetime import datetime, timedelta
from typing import Callable
from PyQt6.QtCore import Qt, QUrl, QPointF, QTimer
from PyQt6.QtGui import QKeyEvent, QMouseEvent
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QApplication
from tools.web_replay.ui import ReplayWebEngineView, ReplayInfo
class ReplayEngine(QWidget):
"""
This widget allow to replay some event that occurred on a web page
"""
def __init__(self, replay_data: dict):
def __init__(self, start_time: datetime, replay_data: list):
super().__init__()
self.start_time = start_time - timedelta(days=1) # remove a day to prevent archive rounding
self.replay_data = replay_data
self.iterator = iter(self.replay_data)
self.replay_index: int = 0
self.replay_time: float = 0
# layout
self._layout = QVBoxLayout()
@ -25,97 +31,155 @@ class ReplayEngine(QWidget):
self.cursor.setStyleSheet("background-color: red; border-radius: 10px;")
# web
self.web = QWebEngineView()
self._layout.addWidget(self.web)
self.web = ReplayWebEngineView(self.start_time)
self._layout.addWidget(self.web, 1)
# information
self.information = ReplayInfo()
self._layout.addWidget(self.information)
# event timer
self.timer = QTimer()
def run_event(self, event: dict, callback: Callable):
# TODO: check if click are done correctly, check position, if event are correct, ...
def run_event(self, event: dict):
match event["type"]:
case "success":
# success event
print(f"success ! ({event['time']}s)")
callback()
case "url":
# changing url event
self.web.setUrl(QUrl(event["url"]))
# callback
self.web.loadFinished.connect(lambda ok: callback()) # NOQA: connect exist
case "resize":
# changing widget size event
self.web.resize(QSize(*event["size"]))
w, h = event["size"]
zoom_factor: float = self.web.width() / w
self.web.setZoomFactor(zoom_factor)
# TODO: better way ?
self.window().resize(QSize(*event["size"]))
# callback
callback()
case "keyboard_press":
# keyboard key pressed event
key = QKeyEvent(
qevent = QKeyEvent(
QKeyEvent.Type.KeyPress,
event["key"],
Qt.KeyboardModifier.NoModifier
)
QApplication.sendEvent(self.web.page(), key)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "keyboard_release":
# keyboard key released event
key = QKeyEvent(
qevent = QKeyEvent(
QKeyEvent.Type.KeyRelease,
event["key"],
Qt.KeyboardModifier.NoModifier
)
QApplication.sendEvent(self.web.page(), key)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "mouse_press":
# mouse pressed event
key = QMouseEvent(
qevent = QMouseEvent(
QMouseEvent.Type.KeyPress,
QPointF(*event["position"]),
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton(event["button"]),
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
QApplication.sendEvent(self.web.page(), key)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# callback
callback()
case "mouse_release":
# mouse pressed event
key = QMouseEvent(
qevent = QMouseEvent(
QMouseEvent.Type.KeyRelease,
QPointF(*event["position"]),
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton(event["button"]),
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
QApplication.sendEvent(self.web.page(), key)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# NOTE: this event is redundant
# case "mouse_double_click":
# # mouse double-clicked event
# key = QMouseEvent(QMouseEvent.Type.MouseButtonDblClick, event["position"], event["button"])
# QApplication.sendEvent(self.page(), key)
# callback
callback()
case "mouse_move":
# mouse moved event
key = QMouseEvent(
qevent = QMouseEvent(
QMouseEvent.Type.KeyRelease,
QPointF(*event["position"]),
QPointF(*event["position"]) / self.web.zoomFactor(),
Qt.MouseButton.NoButton,
Qt.MouseButton.NoButton,
Qt.KeyboardModifier.NoModifier
)
QApplication.sendEvent(self.web.page(), key)
qevent.custom = True
QApplication.postEvent(self.web.focusProxy(), qevent)
# move the fake cursor
self.cursor.move(QPointF(*event["position"]).toPoint() - self.cursor.rect().center())
self.cursor.raise_()
# callback
callback()
case "scroll":
# scroll event
x, y = event["position"]
self.web.page().runJavaScript(f"window.scrollTo({x}, {y});")
self.web.page().runJavaScript(
f"window.scrollTo({x}, {y});",
resultCallback=lambda result: callback()
)
def next(self):
try:
event = next(self.iterator)
except StopIteration:
print("end of record")
return
# get event information
event = self.replay_data[self.replay_index]
self.replay_time = event["time"]
self.replay_index = self.replay_index + 1
self.run_event(event)
# set text
self.information.set_description(f"{event}")
# run the event
self.run_event(
event,
self._next_callback
)
def _next_callback(self):
# prevent the web loading to call this function again
try:
self.web.loadFinished.disconnect(self._next_callback) # NOQA: disconnect exist
except TypeError:
pass
# if there are still events after this one
if self.replay_index < len(self.replay_data):
# next event
next_event: dict = self.replay_data[self.replay_index]
next_time: float = next_event["time"]
# prepare the timer to play the event at the corresponding time
self.timer.singleShot(
round((next_time - self.replay_time) / 1000),
self.next
)

View file

@ -0,0 +1,19 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
class ReplayNavigation(QWidget):
def __init__(self):
super().__init__()
# layout
layout = QVBoxLayout()
self.setLayout(layout)
# information of the replay
self._description = QLabel()
layout.addWidget(self._description)
self._description.setAlignment(Qt.AlignmentFlag.AlignCenter)
def set_description(self, text: str):
self._description.setText(text)

View file

@ -0,0 +1,59 @@
from datetime import datetime
from PyQt6.QtCore import QObject, QEvent, QUrl
from PyQt6.QtWebEngineWidgets import QWebEngineView
class ReplayWebEngineView(QWebEngineView):
def __init__(self, start_time: datetime):
super().__init__()
self.start_time = start_time
self.loadFinished.connect(self._initialize_proxy_event) # NOQA: connect exist
# event filter
def setUrl(self, url: QUrl) -> None:
# get the archive.org link corresponding to that time
archive_time: str = self.start_time.strftime("%Y%m%d%H%M%S")
archive_url = f"https://web.archive.org/web/{archive_time}/{url.toString()}"
# call the super function with the archive url instead
super().setUrl(QUrl(archive_url))
# clean the archive header popup that will appear
self.loadFinished.connect(self._clean_archive_header) # NOQA: connect exist
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
match event.type():
# allow scroll events (they are created automatically)
case event.Type.Scroll:
pass
# allow timed events
case event.Type.Timer:
pass
# ignore all other events
case _:
if not getattr(event, "custom", False):
return True
return super().eventFilter(obj, event)
# events
def _initialize_proxy_event(self, ok: bool):
# prevent the event from being enabled another time
self.loadFinished.disconnect(self._initialize_proxy_event) # NOQA: disconnect exist
# make self.eventFilter intercept all focusProxy events
self.focusProxy().installEventFilter(self)
def _clean_archive_header(self, ok: bool):
# prevent the event from being enabled another time
self.loadFinished.disconnect(self._clean_archive_header) # NOQA: disconnect exist
# hide archive.org header to avoid mouse movement being shifted
self.page().runJavaScript("document.getElementById('wm-ipp-base').style.display = 'none';")

View file

@ -1,18 +1,23 @@
from PyQt6.QtCore import QTimer
from datetime import datetime
from PyQt6.QtWidgets import QMainWindow
from tools.web_replay.ui import ReplayEngine
class ReplayWindow(QMainWindow):
def __init__(self, replay_data: dict):
def __init__(self, start_time: datetime, replay_data: list):
super().__init__()
self.replay_engine = ReplayEngine(replay_data)
# decoration
self.setWindowTitle("Survey Engine - Web Replay")
# setup the engine
self.replay_engine = ReplayEngine(start_time, replay_data)
self.setCentralWidget(self.replay_engine)
# TODO: TEST REMOVE
self.timer = QTimer()
self.timer.setInterval(10)
self.timer.timeout.connect(self.replay_engine.next) # NOQA: connect exist
self.timer.start()
# show the window as fullscreen
self.showFullScreen()
# TODO: remove ?
self.replay_engine.next() # play the replay

View file

@ -1,2 +1,4 @@
from .ReplayWebEngineView import ReplayWebEngineView
from .ReplayNavigation import ReplayNavigation
from .ReplayEngine import ReplayEngine
from .ReplayWindow import ReplayWindow