(null);
+
+ useEffect(() => {
+ // Subscribe to state updates.
+ return player.current!.subscribe(({ paused, viewType }) => {
+ // console.log('is paused?', '->', state.paused);
+ // console.log('is audio view?', '->', state.viewType === 'audio');
+ });
+ }, []);
+
+ function onProviderChange(
+ provider: MediaProviderAdapter | null,
+ nativeEvent: MediaProviderChangeEvent
+ ) {
+ // We can configure provider's here.
+ if (isHLSProvider(provider)) {
+ provider.config = {};
+ }
+ }
+
+ // We can listen for the `can-play` event to be notified when the player is ready.
+ function onCanPlay(
+ detail: MediaCanPlayDetail,
+ nativeEvent: MediaCanPlayEvent
+ ) {
+ // ...
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/buttons.tsx b/src/components/buttons.tsx
new file mode 100644
index 0000000..f897fe2
--- /dev/null
+++ b/src/components/buttons.tsx
@@ -0,0 +1,150 @@
+import * as Tooltip from "@radix-ui/react-tooltip";
+import {
+ FullscreenButton,
+ MuteButton,
+ PIPButton,
+ PlayButton,
+ useMediaState,
+} from "@vidstack/react";
+import {
+ Minimize as FullscreenExitIcon,
+ Maximize as FullscreenIcon,
+ VolumeX as MuteIcon,
+ PauseIcon,
+ PictureInPictureIcon as PictureInPictureExitIcon,
+ PictureInPicture2 as PictureInPictureIcon,
+ PlayIcon,
+ Volume2 as VolumeHighIcon,
+ Volume1 as VolumeLowIcon,
+} from "lucide-react";
+
+export interface MediaButtonProps {
+ tooltipSide?: Tooltip.TooltipContentProps["side"];
+ tooltipAlign?: Tooltip.TooltipContentProps["align"];
+ tooltipOffset?: number;
+}
+
+export const buttonClass =
+ "group ring-media-focus relative inline-flex h-10 w-10 cursor-pointer items-center justify-center rounded-md outline-none ring-inset hover:bg-white/20 focus-visible:ring-4 aria-disabled:hidden";
+
+export const tooltipClass =
+ "animate-out fade-out slide-out-to-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in data-[state=delayed-open]:slide-in-from-bottom-4 z-10 rounded-sm bg-black/90 px-2 py-0.5 text-sm font-medium text-white parent-data-[open]:hidden";
+
+export function Play({
+ tooltipOffset = 0,
+ tooltipSide = "top",
+ tooltipAlign = "center",
+}: MediaButtonProps) {
+ const isPaused = useMediaState("paused");
+ return (
+
+
+
+ {isPaused ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isPaused ? "Play" : "Pause"}
+
+
+ );
+}
+
+export function Mute({
+ tooltipOffset = 0,
+ tooltipSide = "top",
+ tooltipAlign = "center",
+}: MediaButtonProps) {
+ const volume = useMediaState("volume"),
+ isMuted = useMediaState("muted");
+ return (
+
+
+
+ {isMuted || volume == 0 ? (
+
+ ) : volume < 0.5 ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isMuted ? "Unmute" : "Mute"}
+
+
+ );
+}
+
+export function PIP({
+ tooltipOffset = 0,
+ tooltipSide = "top",
+ tooltipAlign = "center",
+}: MediaButtonProps) {
+ const isActive = useMediaState("pictureInPicture");
+ return (
+
+
+
+ {isActive ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isActive ? "Exit PIP" : "Enter PIP"}
+
+
+ );
+}
+
+export function Fullscreen({
+ tooltipOffset = 0,
+ tooltipSide = "top",
+ tooltipAlign = "center",
+}: MediaButtonProps) {
+ const isActive = useMediaState("fullscreen");
+ return (
+
+
+
+ {isActive ? (
+
+ ) : (
+
+ )}
+
+
+
+ {isActive ? "Exit Fullscreen" : "Enter Fullscreen"}
+
+
+ );
+}
diff --git a/src/components/sliders.tsx b/src/components/sliders.tsx
new file mode 100644
index 0000000..bb36f75
--- /dev/null
+++ b/src/components/sliders.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useState } from "react";
+
+import {
+ formatTime,
+ Thumbnail,
+ useMediaRemote,
+ useMediaState,
+ useSliderPreview,
+} from "@vidstack/react";
+import * as Slider from "@radix-ui/react-slider";
+
+export function Volume() {
+ const volume = useMediaState("volume"),
+ canSetVolume = useMediaState("canSetVolume"),
+ remote = useMediaRemote();
+
+ if (!canSetVolume) return null;
+
+ return (
+ {
+ if (value) remote.changeVolume(value / 100);
+ }}
+ >
+
+
+
+
+
+ );
+}
+
+export interface TimeSliderProps {
+ thumbnails?: string;
+}
+
+export function Time({ thumbnails }: TimeSliderProps) {
+ const time = useMediaState("currentTime"),
+ canSeek = useMediaState("canSeek"),
+ duration = useMediaState("duration"),
+ seeking = useMediaState("seeking"),
+ remote = useMediaRemote(),
+ [value, setValue] = useState(0),
+ { previewRootRef, previewRef, previewValue } = useSliderPreview({
+ clamp: true,
+ offset: 6,
+ orientation: "horizontal",
+ }),
+ previewTime = (previewValue / 100) * duration;
+
+ // Keep slider value in-sync with playback.
+ useEffect(() => {
+ if (seeking) return;
+ setValue((time / duration) * 100);
+ }, [time, duration]);
+
+ return (
+ {
+ if (value) {
+ setValue(value);
+ remote.seeking((value / 100) * duration);
+ }
+ }}
+ onValueCommit={([value]) => {
+ if (value) remote.seek((value / 100) * duration);
+ }}
+ >
+
+
+
+
+
+
+ {/* Preview */}
+
+ {thumbnails ? (
+
+
+
+ ) : null}
+ {formatTime(previewTime)}
+
+
+ );
+}
diff --git a/src/components/time-group.tsx b/src/components/time-group.tsx
new file mode 100644
index 0000000..4054079
--- /dev/null
+++ b/src/components/time-group.tsx
@@ -0,0 +1,11 @@
+import { Time } from "@vidstack/react";
+
+export function TimeGroup() {
+ return (
+
+ );
+}
diff --git a/src/components/title.tsx b/src/components/title.tsx
new file mode 100644
index 0000000..ee8e739
--- /dev/null
+++ b/src/components/title.tsx
@@ -0,0 +1,10 @@
+import { ChapterTitle } from "@vidstack/react";
+
+export function Title() {
+ return (
+
+ |
+
+
+ );
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..bd0c391
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
diff --git a/src/pages/share/[videoId].tsx b/src/pages/share/[videoId].tsx
index 237019f..f5b2085 100644
--- a/src/pages/share/[videoId].tsx
+++ b/src/pages/share/[videoId].tsx
@@ -15,6 +15,7 @@ import { useAtom } from "jotai";
import recordVideoModalOpen from "~/atoms/recordVideoModalOpen";
import VideoRecordModal from "~/components/VideoRecordModal";
import defaultProfileIcon from "~/assets/default profile icon.jpg";
+import VideoPlayer from "~/components/VideoPlayer";
const VideoList: NextPage = () => {
const router = useRouter();
@@ -133,38 +134,42 @@ const VideoList: NextPage = () => {
{video?.video_url && (
<>
-
+
+ {/**/}
>
)}
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b5c61c9..3c85e03 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,3 +1,12 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+@layer base {
+ :root {
+ --radius: 0.5rem
+ }
+}
+
+.controlsShadow {
+ box-shadow: inset 0 calc(-9 * clamp(8px, 14px, 11px)) calc(3 * clamp(8px, 14px, 11px)) calc(-6 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35), inset 0 calc(-18 * clamp(8px, 14px, 11px)) calc(6 * clamp(8px, 14px, 11px)) calc(-12 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35), inset 0 calc(-27 * clamp(8px, 14px, 11px)) calc(9 * clamp(8px, 14px, 11px)) calc(-18 * clamp(8px, 14px, 11px)) hsl(0 0% 0% / 0.35)
+}
\ No newline at end of file
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 5cc8997..533f9e2 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,6 +1,7 @@
import { type Config } from "tailwindcss";
export default {
+ darkMode: ["class"],
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
@@ -10,15 +11,50 @@ export default {
},
keyframes: {
marquee: {
- "0%": { transform: "translateX(0%)" },
- "100%": { transform: "translateX(-100%)" },
+ "0%": {
+ transform: "translateX(0%)",
+ },
+ "100%": {
+ transform: "translateX(-100%)",
+ },
},
marquee2: {
- "0%": { transform: "translateX(100%)" },
- "100%": { transform: "translateX(0%)" },
+ "0%": {
+ transform: "translateX(100%)",
+ },
+ "100%": {
+ transform: "translateX(0%)",
+ },
},
},
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ colors: {},
},
},
- plugins: [require("tailwindcss-radix")()],
+ plugins: [
+ require("tailwindcss-radix")(),
+ require("tailwindcss-animate"),
+ require("@vidstack/react/tailwind.cjs")({
+ prefix: "media",
+ }),
+ customVariants,
+ ],
} satisfies Config;
+
+function customVariants({
+ addVariant,
+ matchVariant,
+}: {
+ addVariant: any;
+ matchVariant: any;
+}) {
+ // Strict version of `.group` to help with nesting.
+ matchVariant("parent-data", (value: any) => `.parent[data-${value}] > &`);
+
+ addVariant("hocus", ["&:hover", "&:focus-visible"]);
+ addVariant("group-hocus", [".group:hover &", ".group:focus-visible &"]);
+}