From 04d75c386fc388226f143b4de86431efcaaa6ae3 Mon Sep 17 00:00:00 2001 From: MrSphay Date: Fri, 15 May 2026 22:06:33 +0200 Subject: [PATCH] Enable room queue controls --- .../migration.sql | 11 ++ prisma/schema.prisma | 2 + src/app/globals.css | 50 ++++++ src/app/rooms/[slug]/page.tsx | 42 +++-- src/components/room-console.tsx | 105 +++++++++--- src/lib/media-actions.ts | 152 +++++++++++++++++- src/lib/media.ts | 5 + src/lib/room-actions.ts | 73 ++++++++- 8 files changed, 406 insertions(+), 34 deletions(-) create mode 100644 prisma/migrations/20260515200500_queue_controls_and_thumbnails/migration.sql diff --git a/prisma/migrations/20260515200500_queue_controls_and_thumbnails/migration.sql b/prisma/migrations/20260515200500_queue_controls_and_thumbnails/migration.sql new file mode 100644 index 0000000..5341efb --- /dev/null +++ b/prisma/migrations/20260515200500_queue_controls_and_thumbnails/migration.sql @@ -0,0 +1,11 @@ +ALTER TABLE "MediaSource" ADD COLUMN "thumbnailUrl" TEXT; +ALTER TABLE "MediaSource" ADD COLUMN "queuePosition" INTEGER NOT NULL DEFAULT 0; + +WITH ordered AS ( + SELECT "id", ROW_NUMBER() OVER (PARTITION BY "roomId" ORDER BY "createdAt" ASC, "id" ASC) AS position + FROM "MediaSource" +) +UPDATE "MediaSource" +SET "queuePosition" = ordered.position +FROM ordered +WHERE "MediaSource"."id" = ordered."id"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8704b95..e907a51 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -134,7 +134,9 @@ model MediaSource { provider MediaProvider originalUrl String playbackUrl String + thumbnailUrl String? title String? + queuePosition Int @default(0) createdAt DateTime @default(now()) room Room @relation(fields: [roomId], references: [id], onDelete: Cascade) submitter User? @relation(fields: [submitterId], references: [id], onDelete: SetNull) diff --git a/src/app/globals.css b/src/app/globals.css index e6d8fdf..5870dc5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -302,6 +302,28 @@ textarea { font-size: 14px; font-weight: 700; cursor: pointer; + transition: + transform 140ms ease, + border-color 140ms ease, + background 140ms ease, + color 140ms ease, + box-shadow 140ms ease; +} + +.button:hover:not(:disabled), +.icon-button:hover:not(:disabled), +.rail-tabs button:hover:not(:disabled), +.tab:hover { + transform: translateY(-1px); + border-color: color-mix(in srgb, var(--accent-2) 45%, var(--border)); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.10); +} + +.button:active:not(:disabled), +.icon-button:active:not(:disabled), +.rail-tabs button:active:not(:disabled), +.tab:active { + transform: translateY(0) scale(0.98); } .button.primary { @@ -352,6 +374,13 @@ textarea { color: var(--text); } +.inline-check { + display: flex !important; + align-items: center; + gap: 8px !important; + color: var(--text) !important; +} + .stack { display: grid; gap: 12px; @@ -688,6 +717,27 @@ textarea { font-weight: 800; } +.queue-thumbnail { + display: grid; + width: 72px; + aspect-ratio: 16 / 9; + flex: 0 0 auto; + place-items: center; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 7px; + background: #05070a; + color: var(--accent-2); + font-size: 12px; + font-weight: 900; +} + +.queue-thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; +} + .queue-row .row-title, .timeline-item .row-title { min-width: 0; diff --git a/src/app/rooms/[slug]/page.tsx b/src/app/rooms/[slug]/page.tsx index 2d0dc0e..bbf685c 100644 --- a/src/app/rooms/[slug]/page.tsx +++ b/src/app/rooms/[slug]/page.tsx @@ -3,26 +3,35 @@ import { RoomConsole } from "@/components/room-console"; import { StatusBadge } from "@/components/status-badge"; import { PageHeader, StatusDot } from "@/components/ui"; import { canEnterRoom } from "@/lib/access"; +import { normalizeMediaUrl } from "@/lib/media"; import { prisma } from "@/lib/prisma"; import { getShellContext } from "@/lib/shell"; import { requireCurrentUser, userIsAdmin } from "@/lib/session"; import { requireInitialSetup } from "@/lib/setup"; import { redirect } from "next/navigation"; +import Link from "next/link"; export const dynamic = "force-dynamic"; -export default async function RoomPage({ params }: { params: Promise<{ slug: string }> }) { +export default async function RoomPage({ + params, + searchParams +}: { + params: Promise<{ slug: string }>; + searchParams: Promise<{ rail?: string; saved?: string; error?: string }>; +}) { await requireInitialSetup(); const user = await requireCurrentUser(); const isAdmin = userIsAdmin(user); const { slug } = await params; + const { rail = "Activity", saved, error } = await searchParams; const roomSlug = decodeURIComponent(slug); const room = await prisma.room.findUnique({ where: { slug: roomSlug }, include: { owner: true, members: { include: { user: true } }, - mediaSources: { include: { submitter: true }, orderBy: { createdAt: "desc" }, take: 20 } + mediaSources: { include: { submitter: true }, orderBy: [{ queuePosition: "asc" }, { createdAt: "asc" }], take: 40 } } }); @@ -76,8 +85,8 @@ export default async function RoomPage({ params }: { params: Promise<{ slug: str } actions={ <> - - + Invite + Settings } /> @@ -87,16 +96,23 @@ export default async function RoomPage({ params }: { params: Promise<{ slug: str roomName={room.name} roomVisibility={room.visibility} ownerName={room.owner?.displayName || room.owner?.username || "Unassigned"} + initialRail={rail} + savedState={saved} + errorState={error} currentUser={user.displayName || user.username} - queue={room.mediaSources.map((item) => ({ - id: item.id, - title: item.title || item.originalUrl, - provider: item.provider, - originalUrl: item.originalUrl, - playbackUrl: item.playbackUrl, - by: item.submitter?.displayName || item.submitter?.username || "Unknown", - createdAt: formatDate(item.createdAt) - }))} + queue={room.mediaSources.map((item) => { + const normalized = normalizeMediaUrl(item.originalUrl); + return { + id: item.id, + title: item.title || item.originalUrl, + provider: item.provider, + originalUrl: item.originalUrl, + playbackUrl: item.playbackUrl, + thumbnailUrl: item.thumbnailUrl || normalized.thumbnailUrl, + by: item.submitter?.displayName || item.submitter?.username || "Unknown", + createdAt: formatDate(item.createdAt) + }; + })} participants={[ ...(room.owner ? [ diff --git a/src/components/room-console.tsx b/src/components/room-console.tsx index 894bcc0..e303b1e 100644 --- a/src/components/room-console.tsx +++ b/src/components/room-console.tsx @@ -1,10 +1,11 @@ "use client"; import { useEffect, useMemo, useRef, useState } from "react"; -import { ChevronDown, ChevronUp, MessageSquareText, Pause, Play, Radio, Settings2, ShieldCheck, SkipForward, Trash2 } from "lucide-react"; +import { ChevronDown, ChevronUp, MessageSquareText, Pause, Play, Radio, Settings2, ShieldCheck, SkipForward, Trash2, UserPlus } from "lucide-react"; import { io } from "socket.io-client"; import { normalizeMediaUrl } from "@/lib/media"; -import { addMediaToRoom } from "@/lib/media-actions"; +import { addMediaToRoom, moveMediaDown, moveMediaUp, removeMediaFromRoom, setCurrentMedia } from "@/lib/media-actions"; +import { addRoomMember, updateRoomSettings } from "@/lib/room-actions"; import { Avatar } from "./avatar"; import { StatusBadge } from "./status-badge"; import { EmptyState, Panel, StatusDot } from "./ui"; @@ -45,6 +46,7 @@ type QueueItem = { provider: string; originalUrl: string; playbackUrl: string; + thumbnailUrl?: string | null; by: string; createdAt: string; }; @@ -65,6 +67,9 @@ export function RoomConsole({ roomName, roomVisibility, ownerName, + initialRail = "Activity", + savedState, + errorState, currentUser, queue = [], participants = [] @@ -74,13 +79,16 @@ export function RoomConsole({ roomName: string; roomVisibility: string; ownerName: string; + initialRail?: string; + savedState?: string; + errorState?: string; currentUser: string; queue?: QueueItem[]; participants?: Participant[]; }) { const [connected, setConnected] = useState(false); const [source, setSource] = useState(""); - const [rail, setRail] = useState("Activity"); + const [rail, setRail] = useState(["Activity", "Chat", "Invite", "Settings"].includes(initialRail) ? initialRail : "Activity"); const [activeQueueItem, setActiveQueueItem] = useState(null); const iframeRef = useRef(null); const videoRef = useRef(null); @@ -150,6 +158,10 @@ export function RoomConsole({ }, 800); } + function submitAndPlay(item: QueueItem) { + playQueueItem(item); + } + return (
@@ -217,23 +229,38 @@ export function RoomConsole({ {queue.map((item, index) => (
{index + 1} +
+ {item.thumbnailUrl ? : {item.provider.slice(0, 2)}} +
{item.title} {item.provider} by {item.by} - {item.createdAt}
- - - - +
submitAndPlay(item)}> + + +
+
+ + +
+
+ + +
+
+ + +
))} @@ -265,7 +292,7 @@ export function RoomConsole({