From c6bccd358ea86a8a5b344e3c40ec7e7b113eabc3 Mon Sep 17 00:00:00 2001 From: Quick <31828688+Quick104@users.noreply.github.com> Date: Tue, 26 May 2026 18:24:39 -0400 Subject: [PATCH] feat(web): show metadata tools to curators --- web/src/pages/ItemDetail/EpisodeContent.tsx | 24 ++++++++++------ web/src/pages/ItemDetail/MovieContent.tsx | 28 ++++++++++++------- web/src/pages/ItemDetail/SeasonContent.tsx | 24 ++++++++++------ web/src/pages/ItemDetail/SeriesContent.tsx | 28 ++++++++++++------- .../pages/ItemDetail/components/ActionBar.tsx | 18 ++++++++---- 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/web/src/pages/ItemDetail/EpisodeContent.tsx b/web/src/pages/ItemDetail/EpisodeContent.tsx index aaa380f1..18df321d 100644 --- a/web/src/pages/ItemDetail/EpisodeContent.tsx +++ b/web/src/pages/ItemDetail/EpisodeContent.tsx @@ -35,6 +35,7 @@ import { type EpisodeNavigationState, } from "./itemDetailLayout"; import { getWatchedActionLabel } from "./watchedState"; +import { canCurateMetadata as canCurateMetadataForUser } from "@/lib/permissions"; function formatDuration(minutes: number): string { if (minutes <= 0) return ""; @@ -49,6 +50,7 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e useAmbientColor(item.backdrop_thumbhash); const { user } = useAuth(); const isAdmin = user?.role === "admin"; + const canCurateMetadata = canCurateMetadataForUser(user); const { profile: currentProfile } = useCurrentProfile(); const [editOpen, setEditOpen] = useState(false); const [downloadOpen, setDownloadOpen] = useState(false); @@ -294,12 +296,15 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e watchedLabel={getWatchedActionLabel(item)} onToggleWatched={() => watchedMutation.mutate(!(item.user_data?.played ?? false))} isUpdatingWatched={watchedMutation.isPending} - onRefresh={(mode) => - refreshMetadataMutation.mutate({ - item, - mode, - onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), - }) + onRefresh={ + canCurateMetadata + ? (mode) => + refreshMetadataMutation.mutate({ + item, + mode, + onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), + }) + : undefined } isRefreshing={refreshMetadataMutation.isPending} onRedetectIntro={ @@ -307,7 +312,8 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e } isRedetectingIntro={redetectIntroMutation.isPending} isAdmin={isAdmin} - onEditMetadata={isAdmin ? () => setEditOpen(true) : undefined} + canCurateMetadata={canCurateMetadata} + onEditMetadata={canCurateMetadata ? () => setEditOpen(true) : undefined} versions={item.versions ?? []} playbackVariants={item.playback_variants} selectedVersion={selectedVersion} @@ -367,7 +373,9 @@ export default function EpisodeContent({ item }: { item: ItemDetail & { type: "e {item.crew && item.crew.length > 0 && } - {isAdmin && } + {canCurateMetadata && ( + + )} toggleWatchlistMutation.mutate(inWatchlist)} inWatchlist={inWatchlist} - onRefresh={(mode) => - refreshMetadataMutation.mutate({ - item, - mode, - onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), - }) + onRefresh={ + canCurateMetadata + ? (mode) => + refreshMetadataMutation.mutate({ + item, + mode, + onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), + }) + : undefined } isRefreshing={refreshMetadataMutation.isPending} isAdmin={isAdmin} - onEditMetadata={isAdmin ? () => setEditOpen(true) : undefined} - onMatchItem={isAdmin ? () => setMatchOpen(true) : undefined} + canCurateMetadata={canCurateMetadata} + onEditMetadata={canCurateMetadata ? () => setEditOpen(true) : undefined} + onMatchItem={canCurateMetadata ? () => setMatchOpen(true) : undefined} versions={item.versions} playbackVariants={item.playback_variants} selectedVersion={selectedVersion} @@ -307,8 +313,10 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov ) )} - {isAdmin && } - {isAdmin && ( + {canCurateMetadata && ( + + )} + {canCurateMetadata && ( watchedMutation.mutate(!(item.user_data?.played ?? false))} isUpdatingWatched={watchedMutation.isPending} - onRefresh={(mode) => - refreshMetadataMutation.mutate({ - item, - mode, - onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), - }) + onRefresh={ + canCurateMetadata + ? (mode) => + refreshMetadataMutation.mutate({ + item, + mode, + onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), + }) + : undefined } isRefreshing={refreshMetadataMutation.isPending} isAdmin={isAdmin} - onEditMetadata={isAdmin ? () => setEditOpen(true) : undefined} + canCurateMetadata={canCurateMetadata} + onEditMetadata={canCurateMetadata ? () => setEditOpen(true) : undefined} /> } /> @@ -144,7 +150,9 @@ export default function SeasonContent({ item }: { item: ItemDetail & { type: "se )} - {isAdmin && } + {canCurateMetadata && ( + + )} ); } diff --git a/web/src/pages/ItemDetail/SeriesContent.tsx b/web/src/pages/ItemDetail/SeriesContent.tsx index de46882a..638acc45 100644 --- a/web/src/pages/ItemDetail/SeriesContent.tsx +++ b/web/src/pages/ItemDetail/SeriesContent.tsx @@ -25,12 +25,14 @@ import ActionBar from "./components/ActionBar"; import { SeasonCarouselSkeleton, RecommendationGridSkeleton } from "./components/SectionSkeletons"; import { getSeasonDisplayTitle, resolveSeriesPrimaryAction } from "./itemDetailLayout"; import { getWatchedActionLabel } from "./watchedState"; +import { canCurateMetadata as canCurateMetadataForUser } from "@/lib/permissions"; export default function SeriesContent({ item }: { item: ItemDetail & { type: "series" } }) { const navigate = useNavigate(); useAmbientColor(item.backdrop_thumbhash); const { user } = useAuth(); const isAdmin = user?.role === "admin"; + const canCurateMetadata = canCurateMetadataForUser(user); const isFavorite = item.user_state?.is_favorite ?? false; const inWatchlist = item.user_state?.in_watchlist ?? false; @@ -156,17 +158,21 @@ export default function SeriesContent({ item }: { item: ItemDetail & { type: "se isFavorite={isFavorite} onToggleWatchlist={() => toggleWatchlistMutation.mutate(inWatchlist)} inWatchlist={inWatchlist} - onRefresh={(mode) => - refreshMetadataMutation.mutate({ - item, - mode, - onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), - }) + onRefresh={ + canCurateMetadata + ? (mode) => + refreshMetadataMutation.mutate({ + item, + mode, + onReplaced: (contentID) => navigate(`/item/${contentID}`, { replace: true }), + }) + : undefined } isRefreshing={refreshMetadataMutation.isPending} isAdmin={isAdmin} - onEditMetadata={isAdmin ? () => setEditOpen(true) : undefined} - onMatchItem={isAdmin ? () => setMatchOpen(true) : undefined} + canCurateMetadata={canCurateMetadata} + onEditMetadata={canCurateMetadata ? () => setEditOpen(true) : undefined} + onMatchItem={canCurateMetadata ? () => setMatchOpen(true) : undefined} rating={item.user_rating ?? null} onRatingChange={handleRatingChange} /> @@ -213,8 +219,10 @@ export default function SeriesContent({ item }: { item: ItemDetail & { type: "se ) )} - {isAdmin && } - {isAdmin && ( + {canCurateMetadata && ( + + )} + {canCurateMetadata && ( void; onMatchItem?: () => void; isAdmin?: boolean; + canCurateMetadata?: boolean; versions?: FileVersion[]; playbackVariants?: PlaybackVariant[]; selectedVersion?: FileVersion | null; @@ -119,6 +120,7 @@ export default function ActionBar({ onEditMetadata, onMatchItem, isAdmin = false, + canCurateMetadata = false, versions, playbackVariants, selectedVersion, @@ -223,6 +225,10 @@ export default function ActionBar({ const hasOverflowActions = Boolean( restartHref || onToggleWatchlist || onDownload || onSearchSubtitles, ); + const hasAdminActions = Boolean(isAdmin && (contentId || onRedetectIntro)); + const hasMetadataActions = Boolean( + canCurateMetadata && (onRefresh || onEditMetadata || onMatchItem), + ); const formattedResumeTime = formatPlaybackTime(resumePositionSeconds ?? 0); const percentComplete = @@ -370,10 +376,10 @@ export default function ActionBar({ Search Subtitles )} - {isAdmin && ( + {(hasAdminActions || hasMetadataActions) && ( <> {hasOverflowActions && } - {contentId && ( + {isAdmin && contentId && ( navigate(`/admin/history?media_item_id=${encodeURIComponent(contentId)}`) @@ -382,7 +388,7 @@ export default function ActionBar({ View Play History )} - {onRefresh && ( + {canCurateMetadata && onRefresh && ( { @@ -393,19 +399,19 @@ export default function ActionBar({ Refresh Metadata )} - {onRedetectIntro && ( + {isAdmin && onRedetectIntro && ( Re-detect Intro Markers )} - {onEditMetadata && ( + {canCurateMetadata && onEditMetadata && ( Edit Metadata )} - {onMatchItem && ( + {canCurateMetadata && onMatchItem && ( Match Item