feat: ship a web app (#218)
* feat(orion): add endpoint for link metadata * refactor: cleanup comments * wip: plumbing for building a web app * setup deployment materials for web app * fix(web): favicon
This commit was merged in pull request #218.
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { useRef } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import type { AutoplayPayload } from "@/lib/autoplay-ipc";
|
||||
|
||||
interface AutoplayCardContentProps {
|
||||
payload: AutoplayPayload;
|
||||
onDismiss: () => void;
|
||||
onNavigate: () => void;
|
||||
onEnded: () => void;
|
||||
}
|
||||
|
||||
export function AutoplayCardContent({
|
||||
payload,
|
||||
onDismiss,
|
||||
onNavigate,
|
||||
onEnded,
|
||||
}: AutoplayCardContentProps) {
|
||||
const mediaRef = useRef<HTMLVideoElement | HTMLAudioElement | null>(null);
|
||||
|
||||
const isVideo = payload.mimeType.startsWith("video/");
|
||||
|
||||
const handleClick = () => {
|
||||
mediaRef.current?.pause();
|
||||
onNavigate();
|
||||
};
|
||||
|
||||
const handleClose = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
mediaRef.current?.pause();
|
||||
onDismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full w-full cursor-pointer overflow-hidden bg-black"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{isVideo ? (
|
||||
<>
|
||||
<video
|
||||
ref={mediaRef as React.Ref<HTMLVideoElement>}
|
||||
key={payload.particleId}
|
||||
src={payload.downloadUrl}
|
||||
autoPlay
|
||||
playsInline
|
||||
onEnded={onEnded}
|
||||
className="block h-full w-full object-cover"
|
||||
/>
|
||||
<button
|
||||
className="absolute top-1 right-1 z-10 rounded-full bg-black/50 p-0.5 text-white hover:bg-black/70"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
<div className="absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 py-2">
|
||||
<div className="flex size-6 shrink-0 items-center justify-center rounded-full bg-white/20 text-[10px] font-medium text-white">
|
||||
{payload.senderInitials}
|
||||
</div>
|
||||
<p className="truncate text-xs text-white/80">{payload.senderName}</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="absolute top-1 right-1 z-10 rounded-full bg-black/50 p-0.5 text-white hover:bg-black/70"
|
||||
onClick={handleClose}
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
<div className="flex h-full w-full items-center gap-2 bg-card px-3 py-3">
|
||||
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
|
||||
{payload.senderInitials}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-card-foreground">{payload.senderName}</p>
|
||||
<p className="text-xs text-muted-foreground">Playing audio...</p>
|
||||
</div>
|
||||
</div>
|
||||
<audio
|
||||
ref={mediaRef as React.Ref<HTMLAudioElement>}
|
||||
key={payload.particleId}
|
||||
src={payload.downloadUrl}
|
||||
autoPlay
|
||||
onEnded={onEnded}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from "react";
|
||||
import { platform } from "@/lib/platform";
|
||||
import { useAutoplayPayloadStore } from "@/stores/autoplay-payload-store";
|
||||
import { AutoplayCardContent } from "@/components/autoplay-card-content";
|
||||
|
||||
/**
|
||||
* Bottom-right floating autoplay card used on the web client. The desktop
|
||||
* client uses a separate BrowserWindow (`src/autoplay_window/`), so this
|
||||
* component renders nothing on Electron.
|
||||
*/
|
||||
export function InAppAutoplayCard() {
|
||||
const payload = useAutoplayPayloadStore((s) => s.payload);
|
||||
const setPayload = useAutoplayPayloadStore((s) => s.setPayload);
|
||||
const setPendingNav = useAutoplayPayloadStore((s) => s.setPendingNav);
|
||||
|
||||
const handleDismiss = useCallback(() => setPayload(null), [setPayload]);
|
||||
|
||||
const handleNavigate = useCallback(() => {
|
||||
if (!payload) return;
|
||||
setPayload(null);
|
||||
setPendingNav({ networkId: payload.networkId, streamId: payload.streamId });
|
||||
}, [payload, setPayload, setPendingNav]);
|
||||
|
||||
if (platform.kind !== "web") return null;
|
||||
if (!payload) return null;
|
||||
|
||||
const isVideo = payload.mimeType.startsWith("video/");
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed bottom-4 right-4 z-50 w-80 overflow-hidden rounded-lg shadow-lg"
|
||||
style={{ height: isVideo ? 180 : 64 }}
|
||||
>
|
||||
<AutoplayCardContent
|
||||
payload={payload}
|
||||
onDismiss={handleDismiss}
|
||||
onNavigate={handleNavigate}
|
||||
onEnded={handleDismiss}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { LinkMetadata } from "@/lib/link-metadata";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { platform } from "@/lib/platform";
|
||||
|
||||
interface LinkPreviewCardProps {
|
||||
metadata: LinkMetadata;
|
||||
@@ -12,7 +13,7 @@ interface LinkPreviewCardProps {
|
||||
export function LinkPreviewCard({ metadata, compact }: LinkPreviewCardProps) {
|
||||
const handleOpen = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
window.electronLink.openExternal(metadata.url);
|
||||
platform.link.openExternal(metadata.url);
|
||||
};
|
||||
|
||||
const handleCopy = (e: React.MouseEvent) => {
|
||||
|
||||
@@ -3,20 +3,23 @@ import { Copy, Minus, Square, X } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { platform } from "@/lib/platform";
|
||||
|
||||
export function WindowControls() {
|
||||
const [isMaximized, setIsMaximized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
return window.electronWindow.onMaximizeChange(setIsMaximized);
|
||||
return platform.window.onMaximizeChange(setIsMaximized);
|
||||
}, []);
|
||||
|
||||
if (platform.kind !== "electron") return null;
|
||||
|
||||
const minimize = (
|
||||
<Button
|
||||
key="minimize"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.minimize()}
|
||||
onClick={() => platform.window.minimize()}
|
||||
aria-label="Minimize"
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
@@ -29,7 +32,7 @@ export function WindowControls() {
|
||||
key="maximize"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.maximize()}
|
||||
onClick={() => platform.window.maximize()}
|
||||
aria-label={isMaximized ? "Restore" : "Maximize"}
|
||||
className="dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
@@ -42,7 +45,7 @@ export function WindowControls() {
|
||||
key="close"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={() => window.electronWindow.close()}
|
||||
onClick={() => platform.window.close()}
|
||||
aria-label="Close"
|
||||
className="hover:text-destructive dark:hover:bg-white/10 rounded text-white/50"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user