kaas
This commit is contained in:
parent
fbc040c317
commit
826a30f95f
19 changed files with 430 additions and 170 deletions
13
src/features/player/controls/playState.module.css
Normal file
13
src/features/player/controls/playState.module.css
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
68
src/features/player/controls/seekBar.module.css
Normal file
68
src/features/player/controls/seekBar.module.css
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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(":");
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue