Skip to content

Commit

Permalink
Add playlist direct routes & make VideoListBlock work for playlists
Browse files Browse the repository at this point in the history
  • Loading branch information
LukasKalbertodt committed Apr 10, 2024
1 parent 76e7524 commit a7b1948
Show file tree
Hide file tree
Showing 9 changed files with 427 additions and 125 deletions.
15 changes: 13 additions & 2 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ not-found:
page-not-found: Seite nicht gefunden
video-not-found: Video nicht gefunden
series-not-found: Serie nicht gefunden
playlist-not-found: Playlist nicht gefunden
page-explanation: >
Die von Ihnen gewünschte Seite existiert nicht. Sie wurde möglicherweise
gelöscht oder umbenannt.
Expand All @@ -65,6 +66,9 @@ not-found:
series-explanation: >
Die von Ihnen gewünschte Serie existiert nicht. Sie wurde möglicherweise
gelöscht oder verschoben.
playlist-explanation: >
Die von Ihnen gewünschte Playlist existiert nicht. Sie wurde möglicherweise
gelöscht oder verschoben.
url-typo: >
Falls Sie die Adresse/URL manuell eingegeben haben, prüfen Sie diese auf
Fehler.
Expand Down Expand Up @@ -176,8 +180,6 @@ series:
series: Serie
deleted: Gelöschte Serie
deleted-series-block: Die hier referenzierte Serie wurde gelöscht.
no-events: Diese Serie enthält keine Videos oder Sie sind nicht berechtigt, diese zu sehen.
upcoming-live-streams: "Anstehende Livestreams ({{count}})"
entry-of-series-thumbnail: "Vorschlaubild für Teil von „{{series}}“"
videos:
heading: Videos
Expand All @@ -186,9 +188,17 @@ series:
text: >
Die Daten der gewünschten Serie wurden leider noch nicht vollständig übertragen. Dies sollte
in Kürze automatisch passieren. Versuchen Sie es in wenigen Minuten noch einmal!
videolist-block:
upcoming-live-streams: "Anstehende Livestreams ({{count}})"
missing-video: Video nicht gefunden
unauthorized: Fehlende Berechtigung
hidden-items_one: 'Ein Video wurde nicht gefunden oder Sie haben keinen Zugriff darauf.'
hidden-items_other: '{{count}} Videos wurden nicht gefunden oder Sie haben keinen Zugriff darauf.'
settings:
order: Reihenfolge
order-label: Video Reihenfolge auswählen
original: Wie Playlist
new-to-old: Neueste zuerst
old-to-new: Älteste zuerst
a-z: A bis Z
Expand Down Expand Up @@ -580,6 +590,7 @@ manage:
api-remote-errors:
view:
event: Sie sind nicht autorisiert, dieses Video zu sehen.
playlist: Sie sind nicht autorisiert, diese Playlist zu sehen.
upload:
not-logged-in: Sie müssen angemeldet sein, um Videos hochzuladen.
not-authorized: $t(upload.not-authorized)
Expand Down
16 changes: 14 additions & 2 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ not-found:
page-not-found: Page not found
video-not-found: Video not found
series-not-found: Series not found
playlist-not-found: Playlist not found
page-explanation: >
The page you want to visit does not exist. It might have been removed or
renamed.
Expand All @@ -64,6 +65,9 @@ not-found:
series-explanation: >
The series you want to view does not exist. It might have been removed or
moved.
playlist-explanation: >
The playlist you want to view does not exist. It might have been removed or
moved.
url-typo: >
If you entered the address/URL manually, please double check it for
spelling mistakes.
Expand Down Expand Up @@ -173,8 +177,6 @@ series:
series: Series
deleted: Deleted series
deleted-series-block: The series referenced here was deleted.
no-events: This series does not contain any events, or you might not be authorized to see them.
upcoming-live-streams: "Upcoming live streams ({{count}})"
entry-of-series-thumbnail: "Thumbnail for entry of “{{series}}”"
videos:
heading: Videos
Expand All @@ -183,9 +185,18 @@ series:
text: >
The data of the requested series has not been fully transferred yet. This should
happen automatically soon. Try again in a few minutes.
videolist-block:
upcoming-live-streams: "Upcoming live streams ({{count}})"
missing-video: Video not found
unauthorized: Missing permissions
hidden-items_one: 'One video is missing or requires additional permissions to view.'
hidden-items_other: '{{count}} videos are missing or require additional permissions to view.'
no-videos: No videos
settings:
order: Order
order-label: Choose video order
original: Playlist order
new-to-old: Newest first
old-to-new: Oldest first
a-z: A to Z
Expand Down Expand Up @@ -554,6 +565,7 @@ manage:
api-remote-errors:
view:
event: You are not authorized to view this video.
playlist: You are not authorized to view this playlist.
upload:
not-logged-in: You have to be logged in to upload videos.
not-authorized: $t(upload.not-authorized)
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ManageVideoDetailsRoute } from "./routes/manage/Video/Details";
import { ManageVideoTechnicalDetailsRoute } from "./routes/manage/Video/TechnicalDetails";
import React from "react";
import { ManageVideoAccessRoute } from "./routes/manage/Video/Access";
import { DirectPlaylistOCRoute, DirectPlaylistRoute } from "./routes/Playlist";



Expand All @@ -42,6 +43,8 @@ const {
DirectOpencastVideoRoute,
DirectSeriesRoute,
DirectSeriesOCRoute,
DirectPlaylistRoute,
DirectPlaylistOCRoute,
ManageRoute,
ManageVideosRoute,
ManageVideoAccessRoute,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/routes/NotFound.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const query = graphql`
`;

type Props = {
kind: "page" | "video" | "series";
kind: "page" | "video" | "series" | "playlist";
};

export const NotFound: React.FC<Props> = ({ kind }) => {
Expand All @@ -41,6 +41,7 @@ export const NotFound: React.FC<Props> = ({ kind }) => {
"page": () => t("not-found.page-not-found"),
"video": () => t("not-found.video-not-found"),
"series": () => t("not-found.series-not-found"),
"playlist": () => t("not-found.playlist-not-found"),
});

// Ideally our backend would respond with 404 here, but that's not
Expand All @@ -64,6 +65,7 @@ export const NotFound: React.FC<Props> = ({ kind }) => {
"page": () => t("not-found.page-explanation"),
"video": () => t("not-found.video-explanation"),
"series": () => t("not-found.series-explanation"),
"playlist": () => t("not-found.playlist-explanation"),
})}
{t("not-found.url-typo")}
</p>
Expand Down
153 changes: 153 additions & 0 deletions frontend/src/routes/Playlist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { graphql, readInlineData, useFragment } from "react-relay";
import { useTranslation } from "react-i18next";
import { unreachable } from "@opencast/appkit";

import { loadQuery } from "../relay";
import { makeRoute } from "../rauta";
import { RootLoader } from "../layout/Root";
import { Nav } from "../layout/Navigation";
import { PageTitle } from "../layout/header/ui";
import { keyOfId, playlistId } from "../util";
import { NotFound } from "./NotFound";
import { b64regex } from "./util";
import { Breadcrumbs } from "../ui/Breadcrumbs";
import { PlaylistByOpencastIdQuery } from "./__generated__/PlaylistByOpencastIdQuery.graphql";
import { PlaylistRouteData$key } from "./__generated__/PlaylistRouteData.graphql";
import { PlaylistByIdQuery } from "./__generated__/PlaylistByIdQuery.graphql";
import { ErrorPage } from "../ui/error";
import { VideoListBlock, videoListEventFragment } from "../ui/Blocks/VideoList";
import { VideoListEventData$key } from "../ui/Blocks/__generated__/VideoListEventData.graphql";


export const DirectPlaylistOCRoute = makeRoute({
url: ({ ocID }: { ocID: string }) => `/!s/:${ocID}`,
match: url => {
const regex = new RegExp("^/!p/:([^/]+)$", "u");
const matches = regex.exec(url.pathname);

if (!matches) {
return null;
}


const opencastId = decodeURIComponent(matches[1]);
const query = graphql`
query PlaylistByOpencastIdQuery($id: String!) {
... UserData
playlist: playlistByOpencastId(id: $id) { ...PlaylistRouteData }
rootRealm { ... NavigationData }
}
`;
const queryRef = loadQuery<PlaylistByOpencastIdQuery>(query, { id: opencastId });


return {
render: () => <RootLoader
{...{ query, queryRef }}
noindex
nav={data => <Nav fragRef={data.rootRealm} />}
render={result => <PlaylistPage playlistFrag={result.playlist} />}
/>,
dispose: () => queryRef.dispose(),
};
},
});

export const DirectPlaylistRoute = makeRoute({
url: ({ seriesId }: { seriesId: string }) => `/!s/${keyOfId(seriesId)}`,
match: url => {
const regex = new RegExp(`^/!s/(${b64regex}+)$`, "u");
const matches = regex.exec(url.pathname);

if (!matches) {
return null;
}


const id = decodeURIComponent(matches[1]);
const query = graphql`
query PlaylistByIdQuery($id: ID!) {
... UserData
playlist: playlistById(id: $id) { ...PlaylistRouteData }
rootRealm { ... NavigationData }
}
`;
const queryRef = loadQuery<PlaylistByIdQuery>(query, { id: playlistId(id) });


return {
render: () => <RootLoader
{...{ query, queryRef }}
noindex
nav={data => <Nav fragRef={data.rootRealm} />}
render={result => <PlaylistPage playlistFrag={result.playlist} />}
/>,
dispose: () => queryRef.dispose(),
};
},
});

const fragment = graphql`
fragment PlaylistRouteData on Playlist {
__typename
... on NotAllowed { dummy } # workaround
... on AuthorizedPlaylist {
title
description
entries {
__typename
...on AuthorizedEvent { id, ...VideoListEventData }
...on Missing { dummy }
...on NotAllowed { dummy }
}
}
}
`;

type PlaylistPageProps = {
playlistFrag?: PlaylistRouteData$key | null;
};

const PlaylistPage: React.FC<PlaylistPageProps> = ({ playlistFrag }) => {
const { t } = useTranslation();
const playlist = useFragment(fragment, playlistFrag ?? null);

if (!playlist) {
return <NotFound kind="playlist" />;
}

if (playlist.__typename === "NotAllowed") {
return <ErrorPage title={t("api-remote-errors.view.playlist")} />;
}
if (playlist.__typename !== "AuthorizedPlaylist") {
return unreachable();
}

const items = playlist.entries.map(entry => {
if (entry.__typename === "AuthorizedEvent") {
const out = readInlineData<VideoListEventData$key>(videoListEventFragment, entry);
return out;
} else if (entry.__typename === "Missing") {
return "missing";
} else if (entry.__typename === "NotAllowed") {
return "unauthorized";
} else {
return unreachable();
}
});

return <div css={{ display: "flex", flexDirection: "column" }}>
<Breadcrumbs path={[]} tail={playlist.title} />
<PageTitle title={playlist.title} />
<p css={{ maxWidth: "90ch" }}>{playlist.description}</p>
<div css={{ marginTop: 12 }}>
<VideoListBlock
allowOriginalOrder={true}
initialOrder="ORIGINAL"
title={t("series.videos.heading")}
basePath="/!v"
items={items}
/>
</div>
</div>;
};
15 changes: 11 additions & 4 deletions frontend/src/ui/Blocks/Series.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay";
import { graphql, readInlineData, useFragment } from "react-relay";

import { isSynced } from "../../util";
import type { Fields } from "../../relay";
Expand All @@ -11,7 +11,8 @@ import {
SeriesBlockSeriesData$key,
} from "./__generated__/SeriesBlockSeriesData.graphql";
import { Card } from "../Card";
import { VideoListBlock, VideoListBlockContainer } from "./VideoList";
import { VideoListBlock, VideoListBlockContainer, videoListEventFragment } from "./VideoList";
import { VideoListEventData$key } from "./__generated__/VideoListEventData.graphql";


// ==============================================================================================
Expand Down Expand Up @@ -79,6 +80,9 @@ type Props = SharedFromSeriesProps & {

const SeriesBlock: React.FC<Props> = ({ series, ...props }) => {
const { t } = useTranslation();
const events = series.events.map(event => (
readInlineData<VideoListEventData$key>(videoListEventFragment, event)
));

if (!isSynced(series)) {
const { title, layout } = props;
Expand All @@ -89,11 +93,14 @@ const SeriesBlock: React.FC<Props> = ({ series, ...props }) => {

return <VideoListBlock
initialLayout={props.layout}
initialOrder={props.order}
initialOrder={
(props.order === "%future added value" ? undefined : props.order) ?? "NEW_TO_OLD"
}
allowOriginalOrder={false}
title={props.title ?? (props.showTitle ? series.title : undefined)}
description={(props.showMetadata && series.syncedData.description) || undefined}
activeEventId={props.activeEventId}
basePath={props.basePath}
eventsFrag={series.events}
items={events}
/>;
};

0 comments on commit a7b1948

Please sign in to comment.