feat(don): add volume slider to moderator

This commit is contained in:
2026-06-28 23:49:47 +07:00 Unverified
parent aa6e7809f0
commit 409fcfd656
5 changed files with 54 additions and 18 deletions
+5 -13
View File
@@ -13,7 +13,7 @@ import {
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import posthog from "posthog-js"; import posthog from "posthog-js";
import { useEffect, useRef } from "react"; import { useEffect } from "react";
import { useEllipsisVisible } from "react-hook-text-overflow"; import { useEllipsisVisible } from "react-hook-text-overflow";
import { DataTable } from "@/components/tantable"; import { DataTable } from "@/components/tantable";
import { SimpleTooltip } from "@/components/tooltip"; import { SimpleTooltip } from "@/components/tooltip";
@@ -183,23 +183,15 @@ export function DonateAdminPage({
} }
// component cuz simplicity sake // component cuz simplicity sake
export function DonateWatcher({ sfx = false }: { sfx?: boolean }) { export function DonateWatcher() {
const player = useRef<HTMLAudioElement | null>(null);
const router = useRouter(); const router = useRouter();
useEffect(() => { useEffect(() => {
if (!router) return; if (!router) return;
return sse.donate.subMany({ return sse.donate.subMany({
ping: () => { ping: router.refresh.bind(router),
router.refresh(); update: router.refresh.bind(router),
player.current?.play();
},
update: () => router.refresh(),
}).clean; }).clean;
}, [router]); }, [router]);
if (sfx) return undefined;
return (
<audio src="/assets/donate-mod-sfx.wav" preload="auto" ref={player} />
);
else return undefined;
} }
+37 -2
View File
@@ -1,5 +1,7 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Slider } from "@/components/ui/slider";
import { shared } from "@/lib/comms";
import { sse } from "@/lib/db/sse-endpoints"; import { sse } from "@/lib/db/sse-endpoints";
import { ExternalLink, RadioTower } from "lucide-react"; import { ExternalLink, RadioTower } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
@@ -39,10 +41,22 @@ export function PIP(props: ComponentProps<typeof ReactPIP>) {
export function ClientTracker() { export function ClientTracker() {
const [connected, setConnected] = useState(0); const [connected, setConnected] = useState(0);
const player = useRef<HTMLAudioElement>(null);
const tracking = useRef(0); const tracking = useRef(0);
shared.signal(
"moderator.volume",
(ev) => player.current && (player.current.volume = ev ?? 1),
);
useEffect( useEffect(
() => sse.donate.sub("heartbeat", () => tracking.current++).clean, () =>
player.current
? sse.donate.subMany({
heartbeat: () => tracking.current++,
ping: player.current.play.bind(player.current),
}).clean
: undefined,
[], [],
); );
useEffect(() => { useEffect(() => {
@@ -54,7 +68,28 @@ export function ClientTracker() {
}, []); }, []);
return ( return (
<Button variant="outline" disabled size="sm"> <Button variant="outline" disabled size="sm">
<RadioTower /> Connected: {connected} <RadioTower /> {connected}
<audio
src="/assets/donate-mod-sfx.wav"
preload="auto"
ref={player}
hidden
/>
</Button> </Button>
); );
} }
export function VolumeChanger() {
const [volume, setVolume] = shared.state("moderator.volume");
return (
<div className="px-5 py-1">
<Slider
max={1}
step={0.01}
value={[volume ?? 0.5]}
onValueChange={(v) => setVolume(v[0])}
/>
</div>
);
}
+4 -3
View File
@@ -11,7 +11,7 @@ import { Spinner } from "@/components/ui/spinner";
import { ActionButton } from "@/components/action-button"; import { ActionButton } from "@/components/action-button";
import { reloadWidget } from "../api"; import { reloadWidget } from "../api";
import { markDone } from "@/app/widget/donate/api"; import { markDone } from "@/app/widget/donate/api";
import { ClientTracker, PIP } from "./client"; import { ClientTracker, PIP, VolumeChanger } from "./client";
export default async function DonateModeratorPage() { export default async function DonateModeratorPage() {
if (!(await adminCheck())) if (!(await adminCheck()))
@@ -32,7 +32,7 @@ export default async function DonateModeratorPage() {
return ( return (
<PIP width={400} height={600}> <PIP width={400} height={600}>
<div className="dark flex w-full max-w-100 flex-col gap-1 bg-background p-1 text-foreground scheme-dark"> <div className="dark flex w-full max-w-100 flex-col gap-1 rounded-md bg-background p-1 text-foreground scheme-dark">
<span>Donate Moderator</span> <span>Donate Moderator</span>
{sub.image ? ( {sub.image ? (
<div className="relative flex aspect-square w-full max-w-100 items-center justify-center overflow-hidden rounded-lg border bg-black/50"> <div className="relative flex aspect-square w-full max-w-100 items-center justify-center overflow-hidden rounded-lg border bg-black/50">
@@ -67,7 +67,8 @@ export default async function DonateModeratorPage() {
</ActionButton> </ActionButton>
<ClientTracker /> <ClientTracker />
</div> </div>
<DonateWatcher sfx /> <VolumeChanger />
<DonateWatcher />
</div> </div>
</PIP> </PIP>
); );
+7
View File
@@ -168,6 +168,13 @@ export const DocumentPictureInPicture = forwardRef<
div.style.width = "100%"; div.style.width = "100%";
div.style.height = "100%"; div.style.height = "100%";
div.style.background = "black";
div.style.backgroundImage = "radial-gradient(#222 1px, transparent 1px)";
div.style.backgroundSize = "24px 24px";
div.style.height = "100svh";
div.style.display = "flex";
div.style.justifyContent = "center";
div.style.alignItems = "center";
win.document.body.appendChild(div); win.document.body.appendChild(div);
+1
View File
@@ -28,6 +28,7 @@ type SharedStates = Partial<{
warning: "showcase" | "nf" | "guide" | "private"; warning: "showcase" | "nf" | "guide" | "private";
"warning.uid": string; "warning.uid": string;
"warning.src": "submit" | "input"; "warning.src": "submit" | "input";
"moderator.volume": number;
_raw: Record<string, unknown>; _raw: Record<string, unknown>;
}>; }>;