This commit is contained in:
Chris Kruining 2025-05-27 16:15:56 +02:00
parent fbc040c317
commit 826a30f95f
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
19 changed files with 430 additions and 170 deletions

View file

@ -0,0 +1,13 @@
.play {
font-size: var(--size-7);
text-shadow: 0 0 .5rem #000;
aspect-ratio: 1;
background-color: transparent;
border-radius: var(--radius-2);
transition: background-color .2s var(--ease-in-out-1);
&:hover {
background-color: rgba(from var(--gray-2) r g b / .25);
}
}

View file

@ -1,15 +1,23 @@
import { Component, createMemo } from "solid-js";
import { Component, Show } from "solid-js";
import { useVideo } from "../context";
import { FaSolidPause, FaSolidPlay } from "solid-icons/fa";
import css from "./playState.module.css";
export const PlayState: Component<{}> = (props) => {
const video = useVideo();
const icon = createMemo(() => {
return {
playing: "⏵",
paused: "⏸",
}[video.state()];
});
return <button onclick={(e) => video.togglePlayState()}>{icon()}</button>;
return (
<button
class={css.play}
onclick={(e) =>
video.state.setState((last) =>
last === "playing" ? "paused" : "playing"
)
}
>
<Show when={video.state.state() === "playing"} fallback={<FaSolidPlay />}>
<FaSolidPause />
</Show>
</button>
);
};

View file

@ -0,0 +1,68 @@
.container {
position: relative;
display: block grid;
grid: auto var(--size-2) / auto auto;
place-content: space-between;
gap: var(--size-2);
}
.time {
grid-area: 1 / 1;
}
.duration {
grid-area: 1 / 2;
}
.bar {
--_v: calc(1% * attr(data-value type(<number>), 0));
grid-area: 2 / span 2;
position: absolute;
inline-size: 100%;
block-size: 100%;
z-index: 1;
appearance: none;
background: linear-gradient(var(--blue-3)) top left / var(--_v) 100% no-repeat transparent;
border-radius: var(--radius-round);
&::-webkit-slider-thumb {
appearance: none;
display: block;
inline-size: var(--size-3);
block-size: var(--size-3);
background-color: var(--blue-7);
border-radius: var(--radius-round);
box-shadow: var(--shadow-2);
/* No clue why this offset is what works... */
margin-top: -.8rem;
}
}
.buffered {
grid-area: 2 / span 2;
position: absolute;
inline-size: 100%;
block-size: 100%;
appearance: none;
background: transparent;
&::-webkit-progress-bar {
background-color: rgba(from var(--gray-4) r g b / .5);
border-radius: var(--radius-round);
}
&::-webkit-progress-value {
background-color: rgba(from var(--gray-2) r g b / .75);
border-radius: var(--radius-round);
}
&::-moz-progress-bar {
background-color: rgba(from var(--surface-4) r g b / .5);
border-radius: var(--radius-round);
}
}

View file

@ -1,19 +1,32 @@
import { Component } from "solid-js";
import { useVideo } from "../context";
import css from "./seekBar.module.css";
interface SeekBarProps {
video: HTMLVideoElement | undefined;
}
interface SeekBarProps {}
export const SeekBar: Component<SeekBarProps> = () => {
const video = useVideo();
return (
<>
<div class={css.container}>
<span class={css.time}>{formatTime(video.currentTime())}</span>
<span class={css.duration}>{formatTime(video.duration())}</span>
<input
class={css.bar}
list="chapters"
type="range"
max={duration().toFixed(0)}
value={currentTime().toFixed(0)}
oninput={(e) => setTime(e.target.valueAsNumber)}
step="1"
max={video.duration().toFixed(2)}
value={video.currentTime().toFixed(2)}
data-value={((video.currentTime() / video.duration()) * 100).toFixed(2)}
oninput={(e) => video.setTime(e.target.valueAsNumber)}
step="0.01"
/>
<progress
class={css.buffered}
max={video.duration().toFixed(2)}
value={video.buffered().toFixed(2)}
/>
<datalist id="chapters">
@ -21,6 +34,20 @@ export const SeekBar: Component<SeekBarProps> = () => {
<option value="200">Chapter 2</option>
<option value="300">Chapter 3</option>
</datalist>
</>
</div>
);
};
const formatTime = (subject: number) => {
if (Number.isNaN(subject)) {
return "";
}
const hours = Math.floor(subject / 3600);
const minutes = Math.floor((subject % 3600) / 60);
const seconds = Math.floor(subject % 60);
const sections = hours !== 0 ? [hours, minutes, seconds] : [minutes, seconds];
return sections.map((section) => String(section).padStart(2, "0")).join(":");
};

View file

@ -1,3 +1,18 @@
.container {
display: block grid;
grid: 100% / auto 1fr;
& > button {
font-size: var(--size-7);
text-shadow: 0 0 .5rem #000;
aspect-ratio: 1;
background-color: transparent;
border-radius: var(--radius-2);
transition: background-color .2s var(--ease-in-out-1);
&:hover {
background-color: rgba(from var(--gray-2) r g b / .25);
}
}
}

View file

@ -1,8 +1,7 @@
import { Component, createEffect, createSignal, Show } from "solid-js";
import { Component, Show } from "solid-js";
import css from "./volume.module.css";
import { createStore, unwrap } from "solid-js/store";
import { trackDeep } from "@solid-primitives/deep";
import { useVideo } from "../context";
import { FaSolidVolumeOff, FaSolidVolumeXmark } from "solid-icons/fa";
interface VolumeProps {
value: number;
@ -13,31 +12,23 @@ interface VolumeProps {
export const Volume: Component<VolumeProps> = (props) => {
const video = useVideo();
const [state, setState] = createStore({
volume: props.value,
muted: props.muted ?? false,
});
createEffect(() => {
props.onInput?.(unwrap(trackDeep(state)));
});
return (
<div class={css.container}>
<button onClick={() => setState("muted", (m) => !m)}>
<Show when={state.muted} fallback="mute">
unmute
<button onClick={() => video.volume.setMuted((m) => !m)}>
<Show when={video.volume.muted()} fallback={<FaSolidVolumeOff />}>
<FaSolidVolumeXmark />
</Show>
</button>
<input
type="range"
value={state.volume}
value={video.volume.value()}
min="0"
max="1"
step="0.01"
onInput={(e) =>
setState({ muted: false, volume: e.target.valueAsNumber })
}
onInput={(e) => {
video.volume.setValue(e.target.valueAsNumber);
video.volume.setMuted(false);
}}
/>
</div>
);