From 206c24e2844a48db1a6ef3a12700b37b15bb0d65 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Mon, 3 Jul 2023 13:38:26 +0200 Subject: [PATCH 01/11] Feat: support start_stop_delay and new pre_start_behaviour in web --- web/src/components/HelpPopup.svelte | 5 +- web/src/routes/manage/[id]/+page.svelte | 4 +- .../routes/manage/[id]/edit/TimerForm.svelte | 116 ++++++++++-------- web/src/routes/manage/create/+page.svelte | 2 +- .../manage/create/CreateTimerForm.svelte | 42 ++++++- web/src/routes/t/[timerId]/Timer.svelte | 13 +- web/src/types/displayOptions.ts | 2 +- web/src/types/timer.ts | 9 +- web/src/types/timerMetadata.ts | 3 + web/src/utils/timer.ts | 11 +- 10 files changed, 134 insertions(+), 73 deletions(-) create mode 100644 web/src/types/timerMetadata.ts diff --git a/web/src/components/HelpPopup.svelte b/web/src/components/HelpPopup.svelte index d88f09e..400f9ab 100644 --- a/web/src/components/HelpPopup.svelte +++ b/web/src/components/HelpPopup.svelte @@ -4,10 +4,13 @@ import Fa from 'svelte-fa'; import newUniqueId from 'locally-unique-id-generator'; + let clazz = ''; + export { clazz as class }; + const id = newUniqueId(); - +
{ - _updateTimer(new Date().getTime()); + _updateTimer(new Date().getTime() + timerData.metadata.delay_start_stop); }; const stopTimer = () => { - _updateTimer(undefined, new Date().getTime()); + _updateTimer(undefined, new Date().getTime() + timerData.metadata.delay_start_stop); }; const resumeTimer = () => { diff --git a/web/src/routes/manage/[id]/edit/TimerForm.svelte b/web/src/routes/manage/[id]/edit/TimerForm.svelte index 4e88200..a9f7935 100644 --- a/web/src/routes/manage/[id]/edit/TimerForm.svelte +++ b/web/src/routes/manage/[id]/edit/TimerForm.svelte @@ -18,16 +18,14 @@ import HelpPopup from 'components/HelpPopup.svelte'; import SegmentInfoBox from './SegmentInfoBox.svelte'; import { slide } from 'svelte/transition'; - + import type { DisplayOptions } from 'types/displayOptions'; interface TimerFormData { start_at_date: string; start_at_time: string; repeat: boolean; segments: Segment[]; - display_options: { - clock: boolean; - run_before_started: boolean; - }; + display_options: DisplayOptions; + precision_mode: boolean; } export let timerData: Timer; @@ -51,10 +49,8 @@ }), repeat: timerData.repeat, segments: timerData.segments, - display_options: { - clock: timerData.display_options.clock, - run_before_started: timerData.display_options.pre_start_behaviour === 'RunNormally' - } + display_options: timerData.display_options, + precision_mode: timerData.metadata.delay_start_stop !== 0 }; }; @@ -64,11 +60,8 @@ start_at: new Date(`${formData.start_at_date} ${formData.start_at_time}`).getTime(), repeat: formData.repeat, segments: formData.segments, - display_options: { - clock: formData.display_options.clock, - pre_start_behaviour: formData.display_options.run_before_started - ? 'RunNormally' - : 'ShowZero' + metadata: { + delay_start_stop: formData.precision_mode ? 3000 : 0 } }; }; @@ -87,7 +80,7 @@   back to overview -
+ {#if timerData.stop_at} {/if} - Timer sequence: + Timer sequence: {#each formData.segments as segment, i}
@@ -177,43 +170,70 @@ }}> New segment - - repeat - +
+ + repeat + +
- Display options: - -
- show clock - - Shows a clock at the bottom of the timer.
Can be overridden on one timer by adding - ?clock=false to the URL. -
-
-
+ Timer options: - -
- run before start time +
+ +
+ show clock + + Shows a clock at the bottom of the timer.
Can be overridden on one timer by adding + ?clock=false to the URL. +
+
+
+
+ +
+ +
+ precision mode + + Delays changes to the timer by 3 seconds to make sure all displays have received the + update before anything changes. This is useful to make sure all displays are in perfect + sync if you have a lot of displays and/or a slow internet connection. + +
+
+
+ + -
- When to start the timer: +
+ When to start the timer: The time when the {#if formData.repeat} @@ -224,7 +244,7 @@
-
+
void; - const templates = [ + const templates: { + name: string; + repeat: boolean; + segments: Segment[]; + display_options: DisplayOptions; + metadata: TimerMetadata; + }[] = [ { name: 'Boulder quali 4min + 15s', repeat: true, @@ -15,7 +24,14 @@ { label: 'Boulder', time: 230000, sound: true, color: '#26A269', count_to: 11000 }, { label: 'Boulder', time: 11000, sound: true, color: '#A51D2D', count_to: 0 }, { label: 'Change', time: 14000, sound: true, color: '#E66100', count_to: 1000 } - ] + ], + display_options: { + clock: true, + pre_start_behaviour: 'RunNormally' + }, + metadata: { + delay_start_stop: 0 + } }, { name: 'Boulder quali 5min + 15s', @@ -24,7 +40,14 @@ { label: 'Boulder', time: 290000, sound: true, color: '#26A269', count_to: 11000 }, { label: 'Boulder', time: 11000, sound: true, color: '#A51D2D', count_to: 0 }, { label: 'Change', time: 14000, sound: true, color: '#E66100', count_to: 1000 } - ] + ], + display_options: { + clock: true, + pre_start_behaviour: 'RunNormally' + }, + metadata: { + delay_start_stop: 0 + } }, { name: 'Boulder final 4min + wait', @@ -33,7 +56,14 @@ { label: 'Boulder', time: 230000, sound: true, color: '#26A269', count_to: 11000 }, { label: 'Boulder', time: 11000, sound: true, color: '#A51D2D', count_to: 0 }, { label: 'Wait', time: 1000, sound: true, color: '#1C71D8', count_to: 240000 } - ] + ], + display_options: { + clock: true, + pre_start_behaviour: 'ShowLastSegment' + }, + metadata: { + delay_start_stop: 3000 + } } ]; @@ -53,7 +83,9 @@ password: values.password, start_at: new Date().getTime(), repeat: templates[values.segments].repeat, - segments: templates[values.segments].segments + segments: templates[values.segments].segments, + display_options: templates[values.segments].display_options, + metadata: templates[values.segments].metadata }; onSubmit(formData); }, diff --git a/web/src/routes/t/[timerId]/Timer.svelte b/web/src/routes/t/[timerId]/Timer.svelte index 8bcf99b..b59bdf0 100644 --- a/web/src/routes/t/[timerId]/Timer.svelte +++ b/web/src/routes/t/[timerId]/Timer.svelte @@ -51,18 +51,7 @@ currentTime: number; } = () => { const currentTime = performance.now() + timeOffset; - const segments = timerData.segments; - let { timeInCurrentRound, state } = calculateTimeInCurrentRound(timerData, currentTime); - - if (state == 'waiting') { - return { - timerText: getTimerText(0), - label: '', - seconds: 0, - sound: false, - currentTime: currentTime - }; - } + let { timeInCurrentRound } = calculateTimeInCurrentRound(timerData, currentTime); const { timeInCurrentSegment, currentSegment } = calculateTimeInCurrentSegment( timeInCurrentRound, diff --git a/web/src/types/displayOptions.ts b/web/src/types/displayOptions.ts index 39f4330..3849a33 100644 --- a/web/src/types/displayOptions.ts +++ b/web/src/types/displayOptions.ts @@ -1,4 +1,4 @@ export interface DisplayOptions { clock: boolean; - pre_start_behaviour: 'ShowZero' | 'RunNormally'; + pre_start_behaviour: 'ShowFirstSegment' | 'ShowLastSegment' | 'RunNormally'; } diff --git a/web/src/types/timer.ts b/web/src/types/timer.ts index 3e807f8..e2de274 100644 --- a/web/src/types/timer.ts +++ b/web/src/types/timer.ts @@ -1,5 +1,6 @@ import type { DisplayOptions } from './displayOptions'; import type { Segment } from './segment'; +import type { TimerMetadata } from './timerMetadata'; export interface TimerCreationRequest { id: string; @@ -7,12 +8,17 @@ export interface TimerCreationRequest { start_at: number; repeat: boolean; segments: Segment[]; + metadata: TimerMetadata; + display_options: DisplayOptions; } export interface TimerUpdateRequest { start_at: number; + stop_at?: number; repeat: boolean; segments: Segment[]; + metadata: TimerMetadata; + display_options: DisplayOptions; } export interface Timer { @@ -20,6 +26,7 @@ export interface Timer { start_at: number; stop_at?: number; repeat: boolean; - display_options: DisplayOptions; segments: Segment[]; + metadata: TimerMetadata; + display_options: DisplayOptions; } diff --git a/web/src/types/timerMetadata.ts b/web/src/types/timerMetadata.ts new file mode 100644 index 0000000..44b0161 --- /dev/null +++ b/web/src/types/timerMetadata.ts @@ -0,0 +1,3 @@ +export interface TimerMetadata { + delay_start_stop: number; +} diff --git a/web/src/utils/timer.ts b/web/src/utils/timer.ts index 4642612..6179dd7 100644 --- a/web/src/utils/timer.ts +++ b/web/src/utils/timer.ts @@ -36,15 +36,22 @@ function calculateTimeInCurrentRound( const elapsedTime = currentTime - timerData.start_at; - if (elapsedTime < 0 && timerData.display_options.pre_start_behaviour === 'ShowZero') { + if (elapsedTime < 0 && timerData.display_options.pre_start_behaviour === 'ShowFirstSegment') { return { - timeInCurrentRound: 0, + timeInCurrentRound: 1, state: 'waiting' }; } const totalTimePerRound = timerData.segments.reduce((acc, curr) => acc + curr.time, 0); + if (elapsedTime < 0 && timerData.display_options.pre_start_behaviour === 'ShowLastSegment') { + return { + timeInCurrentRound: totalTimePerRound, + state: 'waiting' + }; + } + if (!timerData.repeat && !stopped && elapsedTime > totalTimePerRound) { return { timeInCurrentRound: totalTimePerRound, From 9ce859bf6092dfae3472640c4aecb75606bab4de Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Mon, 3 Jul 2023 18:59:50 +0200 Subject: [PATCH 02/11] Feat: support modular sounds in server --- src/redis_migrations/mod.rs | 1 + src/redis_migrations/segment.rs | 50 +++++++++++++++++++++++++++++++-- src/redis_migrations/sound.rs | 32 +++++++++++++++++++++ src/redis_migrations/tests.rs | 17 +++++++++-- src/repository.rs | 4 +-- 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 src/redis_migrations/sound.rs diff --git a/src/redis_migrations/mod.rs b/src/redis_migrations/mod.rs index 2cd32c3..47254bd 100644 --- a/src/redis_migrations/mod.rs +++ b/src/redis_migrations/mod.rs @@ -1,6 +1,7 @@ mod display_options; mod pre_start_behaviour; mod segment; +mod sound; mod tests; mod timer; mod timer_metadata; diff --git a/src/redis_migrations/segment.rs b/src/redis_migrations/segment.rs index 34c53fa..5859549 100644 --- a/src/redis_migrations/segment.rs +++ b/src/redis_migrations/segment.rs @@ -1,10 +1,16 @@ use serde::Deserialize; -use crate::{color::Color, repository::Segment}; +use crate::{ + color::Color, + repository::{Segment, Sound}, +}; + +use super::sound::RedisSound; #[derive(Deserialize, Clone)] #[serde(untagged)] pub enum RedisSegment { + V1(SegmentV1), V0(SegmentV0), } @@ -12,6 +18,7 @@ impl Into for RedisSegment { fn into(self) -> Segment { match self { RedisSegment::V0(v0) => v0.into(), + RedisSegment::V1(v1) => v1.into(), } } } @@ -20,6 +27,32 @@ fn default_zero() -> u32 { 0 } +/// === V1 === + +#[derive(Deserialize, Clone)] +pub struct SegmentV1 { + label: String, + time: u32, + color: Option, + #[serde(default = "default_zero")] + count_to: u32, + sounds: Vec, +} + +impl Into for SegmentV1 { + fn into(self) -> Segment { + Segment { + label: self.label, + time: self.time, + color: self.color, + count_to: self.count_to, + sounds: self.sounds.into_iter().map(|s| s.into()).collect(), + } + } +} + +/// === V0 === + #[derive(Deserialize, Clone)] pub struct SegmentV0 { label: String, @@ -32,12 +65,25 @@ pub struct SegmentV0 { impl Into for SegmentV0 { fn into(self) -> Segment { + let mut sounds: Vec = Vec::new(); + + if self.sound { + sounds.push(Sound { + filename: "beep.mp3".to_string(), + trigger_time: 60, + }); + sounds.push(Sound { + filename: "countdown.mp3".to_string(), + trigger_time: 5, + }); + } + Segment { label: self.label, time: self.time, - sound: self.sound, color: self.color, count_to: self.count_to, + sounds: sounds, } } } diff --git a/src/redis_migrations/sound.rs b/src/redis_migrations/sound.rs new file mode 100644 index 0000000..3df8bcb --- /dev/null +++ b/src/redis_migrations/sound.rs @@ -0,0 +1,32 @@ +use serde::Deserialize; + +use crate::repository::Sound; + +#[derive(Deserialize, Clone)] +#[serde(untagged)] +pub enum RedisSound { + V0(SoundV0), +} + +impl Into for RedisSound { + fn into(self) -> Sound { + match self { + RedisSound::V0(v0) => v0.into(), + } + } +} + +#[derive(Deserialize, Clone)] +pub struct SoundV0 { + pub filename: String, + pub trigger_time: u32, +} + +impl Into for SoundV0 { + fn into(self) -> Sound { + Sound { + filename: self.filename, + trigger_time: self.trigger_time, + } + } +} diff --git a/src/redis_migrations/tests.rs b/src/redis_migrations/tests.rs index 10fd6c6..14c4817 100644 --- a/src/redis_migrations/tests.rs +++ b/src/redis_migrations/tests.rs @@ -32,6 +32,11 @@ fn test_v0() { let timer: Timer = timer.into(); assert_eq!(timer.segments.len(), 1); assert_eq!(timer.segments[0].label, "Boulder"); + assert_eq!(timer.segments[0].sounds.len(), 2); + assert_eq!(timer.segments[0].sounds[0].filename, "beep.mp3"); + assert_eq!(timer.segments[0].sounds[0].trigger_time, 60); + assert_eq!(timer.segments[0].sounds[1].filename, "countdown.mp3"); + assert_eq!(timer.segments[0].sounds[1].trigger_time, 5); assert_eq!(timer.metadata.delay_start_stop, 0); assert_eq!( timer.display_options.pre_start_behaviour, @@ -47,9 +52,14 @@ fn test_v1() { { "label":"Boulder", "time":230000, - "sound":true, "color":"#26A269", - "count_to":11000 + "count_to":11000, + "sounds": [ + { + "filename": "beep.mp3", + "trigger_time": 60 + } + ] } ], "id":"v0", @@ -71,6 +81,9 @@ fn test_v1() { let timer: Timer = timer.into(); assert_eq!(timer.segments.len(), 1); assert_eq!(timer.segments[0].label, "Boulder"); + assert_eq!(timer.segments[0].sounds.len(), 1); + assert_eq!(timer.segments[0].sounds[0].filename, "beep.mp3"); + assert_eq!(timer.segments[0].sounds[0].trigger_time, 60); assert_eq!(timer.metadata.delay_start_stop, 5); assert_eq!( timer.display_options.pre_start_behaviour, diff --git a/src/repository.rs b/src/repository.rs index 3210dc8..167b611 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -14,15 +14,15 @@ use tokio::{ pub struct Segment { pub label: String, pub time: u32, - pub sound: bool, pub color: Option, pub count_to: u32, + pub sounds: Vec, } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct Sound { pub filename: String, - pub play_at: u32, + pub trigger_time: u32, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] From f84c30755c80e54e11aa71b199cc39de89ddca53 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Mon, 3 Jul 2023 19:10:55 +0200 Subject: [PATCH 03/11] Feat: support configurable sounds in player --- web/src/routes/t/[timerId]/Timer.svelte | 16 +++++++++------- web/src/types/segment.ts | 7 ++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/web/src/routes/t/[timerId]/Timer.svelte b/web/src/routes/t/[timerId]/Timer.svelte index b59bdf0..17d25a7 100644 --- a/web/src/routes/t/[timerId]/Timer.svelte +++ b/web/src/routes/t/[timerId]/Timer.svelte @@ -7,6 +7,7 @@ calculateTimeInCurrentSegment, getTimerText } from '../../../utils/timer'; + import type { Sound } from 'types/segment'; export let timerData: Timer; export let soundEnabled: boolean; @@ -35,11 +36,12 @@ } }; - const playCurrentSound = (seconds: number) => { + const playCurrentSound = (seconds: number, sounds: Sound[]) => { if (!soundEnabled || !audios) return; - playSoundAt(60, seconds, '/sound/beep.mp3'); - playSoundAt(5, seconds, '/sound/countdown.mp3'); + for (const sound of sounds) { + playSoundAt(sound.trigger_time, seconds, `/sound/${sound.filename}`); + } }; const calculateCurrentSegment: () => { @@ -47,7 +49,7 @@ label: string; seconds: number; color?: string; - sound: boolean; + sounds: Sound[]; currentTime: number; } = () => { const currentTime = performance.now() + timeOffset; @@ -65,14 +67,14 @@ label: currentSegment.label, seconds: Math.floor(effectiveTimeInCurrentSegment / 1000), color: currentSegment.color, - sound: currentSegment.sound, + sounds: currentSegment.sounds, currentTime: currentTime }; }; const update = () => { currentSegment = calculateCurrentSegment(); - const { timerText, label, color, seconds } = currentSegment; + const { timerText, label, color, seconds, sounds } = currentSegment; if (timerSpan !== null) { timerSpan.innerText = timerText; @@ -82,7 +84,7 @@ color ?? 'rgb(var(--color-surface-900))' ); } - playCurrentSound(seconds); + playCurrentSound(seconds, sounds); }; let audios: { [sound: string]: HTMLAudioElement } | undefined; diff --git a/web/src/types/segment.ts b/web/src/types/segment.ts index 5395eb5..70a31df 100644 --- a/web/src/types/segment.ts +++ b/web/src/types/segment.ts @@ -1,7 +1,12 @@ +export interface Sound { + filename: string; + trigger_time: number; +} + export interface Segment { label: string; time: number; - sound: boolean; color?: string; count_to: number; + sounds: Sound[]; } From 6f79cbdac4db859f5eb0d4d3acc574263e649b41 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Mon, 3 Jul 2023 19:15:56 +0200 Subject: [PATCH 04/11] Fix: preload configured sounds correctly --- web/src/routes/t/[timerId]/Timer.svelte | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/web/src/routes/t/[timerId]/Timer.svelte b/web/src/routes/t/[timerId]/Timer.svelte index 17d25a7..d366080 100644 --- a/web/src/routes/t/[timerId]/Timer.svelte +++ b/web/src/routes/t/[timerId]/Timer.svelte @@ -87,6 +87,21 @@ playCurrentSound(seconds, sounds); }; + const getAllSounds = (timer: Timer) => { + let sounds: string[] = []; + + for (const segment of timer.segments) { + for (const sound of segment.sounds) { + const filename = `/sound/${sound.filename}`; + if (!sounds.includes(filename)) { + sounds.push(filename); + } + } + } + + return sounds; + }; + let audios: { [sound: string]: HTMLAudioElement } | undefined; let currentSegment = calculateCurrentSegment(); let backgroundDiv: HTMLElement; @@ -101,7 +116,7 @@ $: { if (soundEnabled) { - audios = preloadSounds(['/sound/beep.mp3', '/sound/countdown.mp3']); + audios = preloadSounds(getAllSounds(timerData)); } else { audios = undefined; } From cf2b86b864aa1f0f92b19f0e85e2114e87a6a520 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Mon, 3 Jul 2023 19:37:08 +0200 Subject: [PATCH 05/11] Feat: manage configurable sounds --- .../manage/[id]/edit/SegmentForm.svelte | 29 ++++++++-- .../routes/manage/[id]/edit/TimerForm.svelte | 4 +- .../manage/create/CreateTimerForm.svelte | 41 ++++++++++---- web/src/utils/sounds.ts | 53 +++++++++++++++++++ 4 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 web/src/utils/sounds.ts diff --git a/web/src/routes/manage/[id]/edit/SegmentForm.svelte b/web/src/routes/manage/[id]/edit/SegmentForm.svelte index 7cfd652..fab32df 100644 --- a/web/src/routes/manage/[id]/edit/SegmentForm.svelte +++ b/web/src/routes/manage/[id]/edit/SegmentForm.svelte @@ -4,10 +4,24 @@ import Fa from 'svelte-fa'; import type { Segment } from 'types/segment'; import TimeInputField from './TimeInputField.svelte'; + import { detectSoundPreset, soundPresets } from 'utils/sounds'; + import HelpPopup from 'components/HelpPopup.svelte'; export let segment: Segment; let clazz: string = ''; export { clazz as class }; + + let soundPreset = detectSoundPreset(segment.sounds); + + const updateSegmentSounds = (preset: string | null) => { + if (!preset) { + segment.sounds = []; + return; + } + segment.sounds = soundPresets[preset]; + }; + + $: updateSegmentSounds(soundPreset);
@@ -32,9 +46,18 @@
- enable sound +