diff --git a/frontend/src/i18n/locales/de.yaml b/frontend/src/i18n/locales/de.yaml index f38ec9b0b..6891c5167 100644 --- a/frontend/src/i18n/locales/de.yaml +++ b/frontend/src/i18n/locales/de.yaml @@ -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. @@ -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. @@ -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 @@ -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 @@ -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) diff --git a/frontend/src/i18n/locales/en.yaml b/frontend/src/i18n/locales/en.yaml index 1385cf14b..497c55849 100644 --- a/frontend/src/i18n/locales/en.yaml +++ b/frontend/src/i18n/locales/en.yaml @@ -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. @@ -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. @@ -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 @@ -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 @@ -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) diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 28dd2e609..bb469d993 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -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"; @@ -42,6 +43,8 @@ const { DirectOpencastVideoRoute, DirectSeriesRoute, DirectSeriesOCRoute, + DirectPlaylistRoute, + DirectPlaylistOCRoute, ManageRoute, ManageVideosRoute, ManageVideoAccessRoute, diff --git a/frontend/src/routes/NotFound.tsx b/frontend/src/routes/NotFound.tsx index 0711554f9..b1d199523 100644 --- a/frontend/src/routes/NotFound.tsx +++ b/frontend/src/routes/NotFound.tsx @@ -32,7 +32,7 @@ const query = graphql` `; type Props = { - kind: "page" | "video" | "series"; + kind: "page" | "video" | "series" | "playlist"; }; export const NotFound: React.FC = ({ kind }) => { @@ -41,6 +41,7 @@ export const NotFound: React.FC = ({ 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 @@ -64,6 +65,7 @@ export const NotFound: React.FC = ({ 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")}

diff --git a/frontend/src/routes/Playlist.tsx b/frontend/src/routes/Playlist.tsx new file mode 100644 index 000000000..3ec078728 --- /dev/null +++ b/frontend/src/routes/Playlist.tsx @@ -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(query, { id: opencastId }); + + + return { + render: () =>