Skip to content
Snippets Groups Projects
Commit b264dcd2 authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

semi-finish realtime updates

parent 8cba374a
No related branches found
No related tags found
1 merge request!9Release
Pipeline #15144 passed
......@@ -4,6 +4,14 @@ import Timer from "easytimer.js"
import alertify from "alertifyjs";
import "alertifyjs/build/css/alertify.css";
const disableInputs = () => {
$("#pause_play,#minutes,#seconds,#update_time").prop("disabled", true)
}
const enableInputs = () => {
$("#pause_play,#minutes,#seconds,#update_time").prop("disabled", false)
}
const updateTimeText = (timer) => {
const timeValues = timer.getTimeValues()
......@@ -32,10 +40,47 @@ const assignEventListeners = (timer) => {
updateTimeText(timer)
}
const updateTimer = (timer, data, continuePlaying) => {
console.info(`Updating timer: ${JSON.stringify(data.sync_time)}`)
window.startingTime = {
minutes: data["sync_time"]["minutes"],
seconds: data["sync_time"]["seconds"]
}
timer.removeAllEventListeners()
timer = new Timer({
countdown: true,
startValues: {
minutes: window.startingTime.minutes,
seconds: window.startingTime.seconds,
}
})
assignEventListeners(timer)
if (window.timerIsRunning) {
timer.start()
}
return timer
}
const syncTime = (timerSocket, timer) => {
timerSocket.send(JSON.stringify({
"sync": timer.getTimeValues()
}))
}
$(window).ready(
() => {
disableInputs()
// --- BEGIN Timer ---
window.timerIsRunning = false
let timer = new Timer({
countdown: true,
startValues: {
......@@ -55,6 +100,8 @@ $(window).ready(
)
+ window.location.host
+ "/ws/timer/"
+ window.timerId
+ "/"
)
if (!isInitialConnect) {
......@@ -64,12 +111,27 @@ $(window).ready(
isInitialConnect = false
timerSocket.onmessage = (event) => {
console.log("Received timer message:", event.data)
enableInputs()
console.info("Received timer message:", event.data)
const data = JSON.parse(event.data)
if ("status" in data) {
if (data["status"] === "playing") {
if ("sync_time" in data) {
// Only update if there is any real difference.
const remainingTime = timer.getTimeValues()
if (
data.sync_time.minutes !== remainingTime.minutes ||
data.sync_time.seconds !== remainingTime.seconds
) {
timer = updateTimer(timer, data)
}
}
if ("is_running" in data) {
if (data["is_running"]) {
// Reset if we are playing again.
const remainingTime = timer.getTimeValues()
......@@ -86,33 +148,92 @@ $(window).ready(
}
timer.start()
} else if (data["status"] === "paused") {
$("#is_counting").prop("checked", true)
$("#pause_play > .btn__body").html("⏸︎")
window.timerIsRunning = true
} else {
timer.pause()
$("#is_counting").prop("checked", false)
$("#pause_play > .btn__body").html("⏵︎")
window.timerIsRunning = false
}
} else if ("time" in data) {
timer.pause()
}
}
let interval = null
timerSocket.onopen = () => {
syncTime(timerSocket, timer)
interval = setInterval(syncTime, 5000, timerSocket, timer)
window.startingTime = {
minutes: data["time"]["minutes"],
seconds: data["time"]["seconds"]
// --- BEGIN Controls ---
$("#pause_play").on(
"click",
(event) => {
$("#is_counting").click()
}
)
timer = new Timer({
countdown: true,
startValues: {
minutes: window.startingTime.minutes,
seconds: window.startingTime.seconds,
$("#is_counting").on(
"change",
(event) => {
disableInputs()
if (event.target.checked) {
console.info("Starting timer")
$("#pause_play > .btn__body").html("⏸︎")
timerSocket.send(JSON.stringify({
"is_running": true
}))
} else {
console.info("Stopping timer")
$("#pause_play > .btn__body").html("⏵︎")
timerSocket.send(JSON.stringify({
"is_running": false
}))
}
})
}
)
assignEventListeners(timer)
}
$("#update_time").on(
"click",
(event) => {
disableInputs()
timer.pause()
let minutes = Number($("#minutes").val())
let seconds = Number($("#seconds").val())
timerSocket.send(JSON.stringify({
"time": {
"minutes": minutes,
"seconds": seconds
},
"is_running": false
}))
$("#is_counting").prop("checked", false)
}
)
// --- END Controls ---
}
timerSocket.onclose = (event) => {
disableInputs()
alertify.error("Ztráta spojení, pokoušíme se o zpětné připojení.")
setTimeout(connectToSocket, 5000)
clearInterval(interval)
$("#is_counting,#pause_play,#update_time").unbind("click")
}
}
......@@ -120,52 +241,5 @@ $(window).ready(
assignEventListeners(timer)
// --- END Timer ---
// --- BEGIN Controls ---
$("#pause_play").on(
"click",
(event) => {
$("#is_counting").click()
}
)
$("#is_counting").on(
"change",
(event) => {
if (event.target.checked) {
$("#pause_play > .btn__body").html("⏸︎")
timerSocket.send(JSON.stringify({
"status": "playing"
}))
} else {
$("#pause_play > .btn__body").html("⏵︎")
timerSocket.send(JSON.stringify({
"status": "paused"
}))
}
}
)
$("#update-time").on(
"click",
(event) => {
let minutes = Number($("#minutes").val())
let seconds = Number($("#seconds").val())
timerSocket.send(JSON.stringify({
"time": {
"minutes": minutes,
"seconds": seconds
}
}))
$("#is_counting").prop("checked", false)
}
)
// --- END Controls ---
}
)
import copy
import datetime
import json
import threading
import time
from timeit import default_timer
from asgiref.sync import async_to_sync
from asgiref.sync import sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
import time
from .models import OngoingTimer
class TimerConsumer(AsyncWebsocketConsumer):
timer_is_running = False
running_timer_threads = []
def tick_timer(timer, iteration: int, total_seconds: int) -> None:
second_compensation = 0
start_time = default_timer()
timer.refresh_from_db()
end_time = default_timer()
second_compensation = end_time - start_time
while not timer.is_running:
time.sleep(0.05)
timer.refresh_from_db()
if iteration != timer.iteration:
# Stop the thread if there is a new timer
running_timer_threads.remove(timer.id)
return
start_time = default_timer()
if iteration != timer.iteration:
# Stop the thread if there is a new timer
running_timer_threads.remove(timer.id)
return
if total_seconds < 1:
return
total_seconds -= 1
seconds = total_seconds % 60
minutes = round((total_seconds - seconds) / 60)
timer.minutes = minutes
timer.seconds = seconds
timer.save()
end_time = default_timer()
second_compensation += end_time - start_time
threading.Timer(
interval=1 - second_compensation,
function=tick_timer,
args=(
timer,
iteration,
total_seconds,
),
).start()
class TimerConsumer(AsyncWebsocketConsumer):
async def connect(self) -> None:
await self.channel_layer.group_add("timer", self.channel_name)
await self.accept()
timer_id = self.scope["url_route"]["kwargs"]["id"]
timer = await sync_to_async(OngoingTimer.objects.filter(id=timer_id).first)()
async def disconnect(self, close_code) -> None:
await self.channel_layer.group_discard("timer", self.channel_name)
if timer is None:
# Not found
async def run_timer(self, minutes: int, seconds: int) -> None:
total_seconds = (minutes * 60) + seconds
first_run = True
await self.close()
return
print("running timer")
self.timer = timer
for second in range(total_seconds + 1):
if not first_run:
while not self.timer_is_running:
time.sleep(0.05)
await self.channel_layer.group_add(str(self.timer.id), self.channel_name)
await self.accept()
if self.timer.is_running and self.timer.id not in running_timer_threads:
await self.run_timer()
async def disconnect(self, close_code) -> None:
if hasattr(self, "timer"):
await self.channel_layer.group_discard(
str(self.timer.id), self.channel_name
)
# Do on the first run, then every 5 seconds
if first_run or total_seconds % 5 == 0:
new_seconds = total_seconds % 60
new_minutes = round((total_seconds - new_seconds) / 60)
async def run_timer(self, minutes: int = None, seconds: int = None) -> None:
self.timer.iteration += 1
await sync_to_async(self.timer.save)()
print("sending")
while self.timer.id in running_timer_threads:
# Wait until the thread is freed
time.sleep(0.05)
await self.channel_layer.group_send(
"timer",
{
"type": "timer_message",
"text": json.dumps({
"time": {
"minutes": new_minutes,
"seconds": new_seconds
}
})
}
)
running_timer_threads.append(self.timer.id)
if not first_run:
total_seconds -= 1
time.sleep(1)
total_seconds = (self.timer.minutes * 60) + self.timer.seconds
first_run = False
time.sleep(1)
threading.Thread(
target=tick_timer,
args=(
self.timer,
self.timer.iteration,
total_seconds,
),
).start()
async def receive(self, text_data: str) -> None:
await sync_to_async(self.timer.refresh_from_db)()
json_data = json.loads(text_data)
response = None
time_updated = False
response = {}
if "status" in json_data:
if json_data["status"] == "playing":
response = {"status": "playing"}
self.timer_is_running = True
elif json_data["status"] == "paused":
response = {"status": "paused"}
self.timer_is_running = False
reset_timer = False
if "is_running" in json_data:
if json_data["is_running"]:
self.timer.is_running = True
await sync_to_async(self.timer.save)()
else:
self.timer.is_running = False
await sync_to_async(self.timer.save)()
if "time" in json_data:
minutes = json_data["time"]["minutes"]
seconds = json_data["time"]["seconds"]
response = {
"time": {
"minutes": minutes,
"seconds": seconds,
}
self.timer.minutes = json_data["time"]["minutes"]
self.timer.seconds = json_data["time"]["seconds"]
reset_timer = True
response.update(
{
"sync_time": {
"minutes": self.timer.minutes,
"seconds": self.timer.seconds,
},
"is_running": self.timer.is_running,
}
)
time_updated = True
if response is not None:
if time_updated:
await self.run_timer(minutes, seconds)
if reset_timer:
await self.run_timer()
await self.send(text_data=json.dumps({}))
await self.channel_layer.group_send(
str(self.timer.id), {"type": "timer_message", "text": json.dumps(response)}
)
async def timer_message(self, event: dict) -> None:
await self.send(text_data=event["text"])
# Generated by Django 4.1.5 on 2023-10-22 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('timer', '0003_remove_ongoingtimer_hours'),
]
operations = [
migrations.AddField(
model_name='ongoingtimer',
name='is_running',
field=models.BooleanField(default=False, verbose_name='Aktuálně běží'),
),
migrations.AddField(
model_name='ongoingtimer',
name='iteration',
field=models.IntegerField(default=0, verbose_name='Iterace'),
),
]
......@@ -30,3 +30,7 @@ class OngoingTimer(models.Model):
MaxValueValidator(limit_value=60),
],
)
is_running = models.BooleanField(verbose_name="Aktuálně běží", default=False)
iteration = models.IntegerField(verbose_name="Iterace", default=0)
......@@ -3,5 +3,5 @@ from django.urls import path
from . import consumers
websocket_urlpatterns = [
path("ws/timer/", consumers.TimerConsumer.as_asgi()),
path("ws/timer/<int:id>/", consumers.TimerConsumer.as_asgi()),
]
......@@ -20,11 +20,18 @@
minutes: {{ timer.minutes }},
seconds: {{ timer.seconds }},
}
window.timerId = "{{ timer.id }}"
</script>
<main>
<h1 class="text-6xl font-bebas">Úprava časovače {{ timer.name }}</h1>
<a class="hover:no-underline" href="{% url 'timer:view_timer' timer.id %}">
<i class="ico--chevron-left"></i>
<span class="underline">Zpět na zobrazení</span>
</a>
<hr>
<div class="text-xl">
......@@ -75,7 +82,7 @@
autocomplete="off"
>
<button
id="update-time"
id="update_time"
class="btn w-64"
>
<div class="btn__body">Aktualizovat čas</div>
......
......@@ -22,6 +22,8 @@
minutes: {{ timer.minutes }},
seconds: {{ timer.seconds }},
}
window.timerId = "{{ timer.id }}"
</script>
<main class="text-center bg-black text-white h-screen flex flex-col justify-between py-5">
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment