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