show stream state in stream-view
This commit is contained in:
@@ -23,6 +23,8 @@ interface ComposeOverlayProps {
|
|||||||
targetPath?: ParticlePath;
|
targetPath?: ParticlePath;
|
||||||
onActiveChange?: (active: boolean) => void;
|
onActiveChange?: (active: boolean) => void;
|
||||||
onParticleCreated?: (particleId: string) => void;
|
onParticleCreated?: (particleId: string) => void;
|
||||||
|
/** When true, composing is blocked (e.g. stream is closed). */
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@ export function ComposeOverlay({
|
|||||||
targetPath,
|
targetPath,
|
||||||
onActiveChange,
|
onActiveChange,
|
||||||
onParticleCreated,
|
onParticleCreated,
|
||||||
|
disabled,
|
||||||
}: ComposeOverlayProps) {
|
}: ComposeOverlayProps) {
|
||||||
const [step, setStep] = useState<ComposeStep>("idle");
|
const [step, setStep] = useState<ComposeStep>("idle");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -56,6 +59,8 @@ export function ComposeOverlay({
|
|||||||
// Refs for synchronous reads in keyboard handlers
|
// Refs for synchronous reads in keyboard handlers
|
||||||
const stepRef = useRef(step);
|
const stepRef = useRef(step);
|
||||||
const recordStartRef = useRef(0);
|
const recordStartRef = useRef(0);
|
||||||
|
const disabledRef = useRef(disabled);
|
||||||
|
disabledRef.current = disabled;
|
||||||
|
|
||||||
const setStepSync = useCallback((next: ComposeStep) => {
|
const setStepSync = useCallback((next: ComposeStep) => {
|
||||||
stepRef.current = next;
|
stepRef.current = next;
|
||||||
@@ -341,6 +346,13 @@ export function ComposeOverlay({
|
|||||||
|
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case "idle": {
|
case "idle": {
|
||||||
|
if (disabledRef.current) {
|
||||||
|
if ((e.key === "`" && !e.repeat) || e.key === "t" || e.key === "T") {
|
||||||
|
e.preventDefault();
|
||||||
|
toast.info("This stream is closed");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (e.key === "`" && !e.repeat) {
|
if (e.key === "`" && !e.repeat) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
recordStartRef.current = Date.now();
|
recordStartRef.current = Date.now();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from "@/components/ui/context-menu";
|
} from "@/components/ui/context-menu";
|
||||||
|
import { CircleCheckBig, CircleDot } from "lucide-react";
|
||||||
import { updateStreamStatus } from "@/lib/firestore-particles";
|
import { updateStreamStatus } from "@/lib/firestore-particles";
|
||||||
import { toFirestoreDocPath, particlePath } from "@/lib/particle-path";
|
import { toFirestoreDocPath, particlePath } from "@/lib/particle-path";
|
||||||
import type { StreamParticle } from "@/hooks/use-stream-particles";
|
import type { StreamParticle } from "@/hooks/use-stream-particles";
|
||||||
@@ -27,7 +28,17 @@ export function StreamContextMenu({ particle, networkId, children }: StreamConte
|
|||||||
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
<ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem onSelect={toggleStatus}>
|
<ContextMenuItem onSelect={toggleStatus}>
|
||||||
{isOpen ? "Close stream" : "Open stream"}
|
{isOpen ? (
|
||||||
|
<>
|
||||||
|
<CircleCheckBig className="size-4" />
|
||||||
|
Close stream
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CircleDot className="size-4 text-green-500" />
|
||||||
|
Open stream
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
import { apiClient } from "@/api/client";
|
import { apiClient } from "@/api/client";
|
||||||
import type { Particle } from "@/api/types";
|
import type { Particle } from "@/api/types";
|
||||||
import { parseParticlePath, type ParticlePath } from "@/lib/particle-path";
|
import { parseParticlePath, particlePath, toFirestoreDocPath, type ParticlePath } from "@/lib/particle-path";
|
||||||
import { ComposeOverlay } from "@/features/compose/compose-overlay";
|
import { ComposeOverlay } from "@/features/compose/compose-overlay";
|
||||||
import { PlaybackPageIndicator } from "@/features/particles/playback-page-indicator";
|
import { PlaybackPageIndicator } from "@/features/particles/playback-page-indicator";
|
||||||
import { MediaParticleView } from "@/features/particles/media-particle-view";
|
import { MediaParticleView } from "@/features/particles/media-particle-view";
|
||||||
@@ -14,7 +14,14 @@ import ControlsIndicator from "@/features/compose/controls-indicator";
|
|||||||
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";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Settings } from "lucide-react";
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Settings, CircleCheckBig, CircleDot, EllipsisVertical } from "lucide-react";
|
||||||
|
import { updateStreamStatus } from "@/lib/firestore-particles";
|
||||||
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
|
||||||
import { WindowControls } from "@/components/window-controls";
|
import { WindowControls } from "@/components/window-controls";
|
||||||
import { RelativeTimestamp } from "@/components/relative-timestamp";
|
import { RelativeTimestamp } from "@/components/relative-timestamp";
|
||||||
@@ -248,6 +255,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
networkId={networkId}
|
networkId={networkId}
|
||||||
targetPath={path}
|
targetPath={path}
|
||||||
onActiveChange={setComposeActive}
|
onActiveChange={setComposeActive}
|
||||||
|
disabled={streamParticle.status === "closed"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -314,6 +322,7 @@ export function StreamView({ path, streamParticle }: StreamViewProps) {
|
|||||||
targetPath={path}
|
targetPath={path}
|
||||||
onActiveChange={setComposeActive}
|
onActiveChange={setComposeActive}
|
||||||
onParticleCreated={onLocalParticleCreated}
|
onParticleCreated={onLocalParticleCreated}
|
||||||
|
disabled={streamParticle.status === "closed"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* BottomBar */}
|
{/* BottomBar */}
|
||||||
@@ -461,14 +470,48 @@ function TopBar({ networkId, particle, streamParticle }: { networkId: string; pa
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
{streamParticle.status === "closed" && (
|
||||||
variant="ghost"
|
<span className="no-drag flex items-center gap-1 rounded-full bg-white/10 px-2 py-0.5 text-xs text-muted-foreground backdrop-blur-sm">
|
||||||
size="sm"
|
<CircleCheckBig className="size-3" />
|
||||||
className="no-drag text-muted-foreground"
|
Closed
|
||||||
onClick={() => navigate("/settings")}
|
</span>
|
||||||
>
|
)}
|
||||||
<Settings className="size-3.5" />
|
|
||||||
</Button>
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="no-drag text-muted-foreground"
|
||||||
|
>
|
||||||
|
<EllipsisVertical className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={async () => {
|
||||||
|
const docPath = toFirestoreDocPath(particlePath(networkId, [streamParticle.id]));
|
||||||
|
await updateStreamStatus(docPath, streamParticle.status === "open" ? "closed" : "open");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{streamParticle.status === "open" ? (
|
||||||
|
<>
|
||||||
|
<CircleCheckBig className="size-4" />
|
||||||
|
Close stream
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CircleDot className="size-4 text-green-500" />
|
||||||
|
Open stream
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={() => navigate("/settings")}>
|
||||||
|
<Settings className="size-4" />
|
||||||
|
Settings
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user