feat(web): show metadata tools to curators
This commit is contained in:
@@ -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 && <CrewList crew={item.crew} />}
|
||||
</div>
|
||||
{isAdmin && <EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />}
|
||||
{canCurateMetadata && (
|
||||
<EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />
|
||||
)}
|
||||
<DownloadVersionPicker
|
||||
open={downloadOpen}
|
||||
onOpenChange={setDownloadOpen}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { selectDefaultPlaybackVariantVersion } from "./components/versionRanking
|
||||
import { RecommendationGridSkeleton } from "./components/SectionSkeletons";
|
||||
import { resolveLeafPrimaryAction } from "./itemDetailLayout";
|
||||
import { getWatchedActionLabel } from "./watchedState";
|
||||
import { canCurateMetadata as canCurateMetadataForUser } from "@/lib/permissions";
|
||||
|
||||
function formatDuration(minutes: number): string {
|
||||
if (minutes <= 0) return "";
|
||||
@@ -42,6 +43,7 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov
|
||||
useAmbientColor(item.backdrop_thumbhash);
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
const canCurateMetadata = canCurateMetadataForUser(user);
|
||||
const { profile: currentProfile } = useCurrentProfile();
|
||||
|
||||
const isFavorite = item.user_state?.is_favorite ?? false;
|
||||
@@ -238,17 +240,21 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov
|
||||
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}
|
||||
versions={item.versions}
|
||||
playbackVariants={item.playback_variants}
|
||||
selectedVersion={selectedVersion}
|
||||
@@ -307,8 +313,10 @@ export default function MovieContent({ item }: { item: ItemDetail & { type: "mov
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && <EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />}
|
||||
{isAdmin && (
|
||||
{canCurateMetadata && (
|
||||
<EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />
|
||||
)}
|
||||
{canCurateMetadata && (
|
||||
<MatchItemDialog
|
||||
key={item.content_id}
|
||||
item={item}
|
||||
|
||||
@@ -15,6 +15,7 @@ import DetailBreadcrumb from "./components/DetailBreadcrumb";
|
||||
import SeasonEpisodeGrid from "./components/SeasonEpisodeGrid";
|
||||
import type { EpisodeNavigationState } from "./itemDetailLayout";
|
||||
import { getWatchedActionLabel } from "./watchedState";
|
||||
import { canCurateMetadata as canCurateMetadataForUser } from "@/lib/permissions";
|
||||
|
||||
function seasonLabel(seasonNumber: number, title?: string) {
|
||||
if (title) return title;
|
||||
@@ -27,6 +28,7 @@ export default function SeasonContent({ item }: { item: ItemDetail & { type: "se
|
||||
useAmbientColor(item.backdrop_thumbhash);
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
const canCurateMetadata = canCurateMetadataForUser(user);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const watchedMutation = useWatchedStateMutation(item);
|
||||
const refreshMetadataMutation = useRefreshItemMetadata();
|
||||
@@ -103,16 +105,20 @@ export default function SeasonContent({ item }: { item: ItemDetail & { type: "se
|
||||
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}
|
||||
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
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && <EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />}
|
||||
{canCurateMetadata && (
|
||||
<EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{isAdmin && <EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />}
|
||||
{isAdmin && (
|
||||
{canCurateMetadata && (
|
||||
<EditMetadataDialog item={item} open={editOpen} onOpenChange={setEditOpen} />
|
||||
)}
|
||||
{canCurateMetadata && (
|
||||
<MatchItemDialog
|
||||
key={item.content_id}
|
||||
item={item}
|
||||
|
||||
@@ -71,6 +71,7 @@ interface ActionBarProps {
|
||||
onEditMetadata?: () => 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
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && (
|
||||
{(hasAdminActions || hasMetadataActions) && (
|
||||
<>
|
||||
{hasOverflowActions && <DropdownMenuSeparator />}
|
||||
{contentId && (
|
||||
{isAdmin && contentId && (
|
||||
<DropdownMenuItem
|
||||
onSelect={() =>
|
||||
navigate(`/admin/history?media_item_id=${encodeURIComponent(contentId)}`)
|
||||
@@ -382,7 +388,7 @@ export default function ActionBar({
|
||||
View Play History
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onRefresh && (
|
||||
{canCurateMetadata && onRefresh && (
|
||||
<DropdownMenuItem
|
||||
disabled={isRefreshing}
|
||||
onSelect={() => {
|
||||
@@ -393,19 +399,19 @@ export default function ActionBar({
|
||||
Refresh Metadata
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onRedetectIntro && (
|
||||
{isAdmin && onRedetectIntro && (
|
||||
<DropdownMenuItem disabled={isRedetectingIntro} onSelect={onRedetectIntro}>
|
||||
<RefreshCw className={`size-4 ${isRedetectingIntro ? "animate-spin" : ""}`} />
|
||||
Re-detect Intro Markers
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onEditMetadata && (
|
||||
{canCurateMetadata && onEditMetadata && (
|
||||
<DropdownMenuItem onSelect={onEditMetadata}>
|
||||
<Pencil className="size-4" />
|
||||
Edit Metadata
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onMatchItem && (
|
||||
{canCurateMetadata && onMatchItem && (
|
||||
<DropdownMenuItem onSelect={onMatchItem}>
|
||||
<Search className="size-4" />
|
||||
Match Item
|
||||
|
||||
Reference in New Issue
Block a user