feat: toggle video / audio with keybinding
This commit is contained in:
@@ -6,27 +6,28 @@ export function VideoAudioToggle() {
|
|||||||
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<span
|
||||||
<kbd
|
role="button"
|
||||||
role="button"
|
onClick={() =>
|
||||||
onClick={() =>
|
setRecordingMode(recordingMode === "video" ? "audio" : "video")
|
||||||
setRecordingMode(recordingMode === "video" ? "audio" : "video")
|
}
|
||||||
}
|
title={
|
||||||
title={
|
recordingMode === "video" ? "Switch to audio-only (V)" : "Switch to video (V)"
|
||||||
recordingMode === "video" ? "Switch to audio-only" : "Switch to video"
|
}
|
||||||
}
|
className="cursor-pointer transition-colors hover:text-white/80"
|
||||||
className="cursor-pointer rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs transition-colors hover:text-white/80"
|
>
|
||||||
>
|
<kbd className="rounded bg-white/10 px-1.5 py-0.5 font-mono text-xs">
|
||||||
{recordingMode === "video" ? (
|
V
|
||||||
<>
|
</kbd>{" "}
|
||||||
<Video className="inline size-3" /> Video
|
{recordingMode === "video" ? (
|
||||||
</>
|
<>
|
||||||
) : (
|
<Video className="inline size-3" /> video
|
||||||
<>
|
</>
|
||||||
<Mic className="inline size-3" /> Audio
|
) : (
|
||||||
</>
|
<>
|
||||||
)}
|
<Mic className="inline size-3" /> audio
|
||||||
</kbd>
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TextParticleView } from "@/features/particles/text-particle-view";
|
|||||||
import { FallbackParticleView } from "@/features/particles/fallback-particle-view";
|
import { FallbackParticleView } from "@/features/particles/fallback-particle-view";
|
||||||
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarGroup } from "@/components/ui/avatar";
|
||||||
import { VideoAudioToggle } from "@/components/video-audio-toggle";
|
import { VideoAudioToggle } from "@/components/video-audio-toggle";
|
||||||
|
import { useMediaSettingsStore } from "@/stores/media-settings-store";
|
||||||
import { KeybindingsOverlay, type KeybindingGroup } from "@/components/keybindings-overlay";
|
import { KeybindingsOverlay, type KeybindingGroup } from "@/components/keybindings-overlay";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { useNetwork, useNetworks } from "@/hooks/use-networks";
|
import { useNetwork, useNetworks } from "@/hooks/use-networks";
|
||||||
@@ -132,6 +133,7 @@ const STREAM_VIEW_KEYBINDINGS: KeybindingGroup[] = [
|
|||||||
{ keys: ["Hold", "`"], description: "Reply" },
|
{ keys: ["Hold", "`"], description: "Reply" },
|
||||||
{ keys: ["S"], description: "Screen record" },
|
{ keys: ["S"], description: "Screen record" },
|
||||||
{ keys: ["T"], description: "Text compose" },
|
{ keys: ["T"], description: "Text compose" },
|
||||||
|
{ keys: ["V"], description: "Toggle video / audio" },
|
||||||
{ keys: ["H"], description: "Join huddle" },
|
{ keys: ["H"], description: "Join huddle" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -174,6 +176,8 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
usePrefetchAdjacentMedia(children, currentIndex);
|
usePrefetchAdjacentMedia(children, currentIndex);
|
||||||
|
|
||||||
const authedUser = useAuthStore((s) => s.user);
|
const authedUser = useAuthStore((s) => s.user);
|
||||||
|
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
|
||||||
|
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
||||||
const network = useNetwork(networkId);
|
const network = useNetwork(networkId);
|
||||||
const presenceBySegment = usePresencePositions(
|
const presenceBySegment = usePresencePositions(
|
||||||
streamParticle.playback_markers,
|
streamParticle.playback_markers,
|
||||||
@@ -284,6 +288,10 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
navigate(`/${networkId}`);
|
navigate(`/${networkId}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "v":
|
||||||
|
e.preventDefault();
|
||||||
|
setRecordingMode(recordingMode === "video" ? "audio" : "video");
|
||||||
|
break;
|
||||||
case "?":
|
case "?":
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowKeybindings((v) => !v);
|
setShowKeybindings((v) => !v);
|
||||||
@@ -316,7 +324,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
window.removeEventListener("keyup", handleKeyUp);
|
window.removeEventListener("keyup", handleKeyUp);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[composeActive, next, prev, pause, resume, navigate, networkId, streamParticle.id, setShowKeybindings, handleToggleReaction],
|
[composeActive, next, prev, pause, resume, navigate, networkId, streamParticle.id, setShowKeybindings, handleToggleReaction, recordingMode, setRecordingMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click-to-navigate: left 30% = prev, right 70% = next
|
// Click-to-navigate: left 30% = prev, right 70% = next
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useMediaSettingsStore } from "@/stores/media-settings-store";
|
||||||
|
|
||||||
interface UseStreamKeyboardNavOptions {
|
interface UseStreamKeyboardNavOptions {
|
||||||
streams: Array<{ id: string }>;
|
streams: Array<{ id: string }>;
|
||||||
@@ -12,6 +13,8 @@ export function useStreamKeyboardNav({
|
|||||||
onNavigate,
|
onNavigate,
|
||||||
}: UseStreamKeyboardNavOptions) {
|
}: UseStreamKeyboardNavOptions) {
|
||||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
const recordingMode = useMediaSettingsStore((s) => s.recordingMode);
|
||||||
|
const setRecordingMode = useMediaSettingsStore((s) => s.setRecordingMode);
|
||||||
|
|
||||||
// Initialize selection when streams first load; clear if streams become empty.
|
// Initialize selection when streams first load; clear if streams become empty.
|
||||||
// Do NOT reset on every Firestore update — that would scroll the list to the top.
|
// Do NOT reset on every Firestore update — that would scroll the list to the top.
|
||||||
@@ -61,6 +64,13 @@ export function useStreamKeyboardNav({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// V: toggle video / audio
|
||||||
|
if (e.key === "v" || e.key === "V") {
|
||||||
|
e.preventDefault();
|
||||||
|
setRecordingMode(recordingMode === "video" ? "audio" : "video");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Arrow keys: move selection
|
// Arrow keys: move selection
|
||||||
let delta: number | null = null;
|
let delta: number | null = null;
|
||||||
|
|
||||||
@@ -81,7 +91,7 @@ export function useStreamKeyboardNav({
|
|||||||
// components (ToggleGroup, etc.) consume them for their own navigation.
|
// components (ToggleGroup, etc.) consume them for their own navigation.
|
||||||
window.addEventListener("keydown", handleKeyDown, true);
|
window.addEventListener("keydown", handleKeyDown, true);
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
return () => window.removeEventListener("keydown", handleKeyDown, true);
|
||||||
}, [enabled, streams, onNavigate]);
|
}, [enabled, streams, onNavigate, recordingMode, setRecordingMode]);
|
||||||
|
|
||||||
return { selectedIndex };
|
return { selectedIndex };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user