diff --git a/backend/src/api/model/search/realm.rs b/backend/src/api/model/search/realm.rs index 11aa02141..d6e9d75c0 100644 --- a/backend/src/api/model/search/realm.rs +++ b/backend/src/api/model/search/realm.rs @@ -21,7 +21,7 @@ impl search::Realm { } fn path(&self) -> &str { - &self.full_path + if self.full_path.is_empty() { "/" } else { &self.full_path } } fn ancestor_names(&self) -> &[Option] { diff --git a/frontend/src/i18n/locales/de.yaml b/frontend/src/i18n/locales/de.yaml index 310da4d17..13444490a 100644 --- a/frontend/src/i18n/locales/de.yaml +++ b/frontend/src/i18n/locales/de.yaml @@ -20,6 +20,8 @@ general: logo-alt: Das Logo von „{{title}}“ no-root-children: Noch keine Seiten ... failed-to-load-thumbnail: Konnte Vorschaubild nicht laden + yes: Ja + no: Nein form: select: no-options: Keine Optionen @@ -140,6 +142,8 @@ video: title: Video einbetten caption: Untertitel manage: Verwalten + start: Anfang + end: Ende series: series: Serie @@ -234,17 +238,28 @@ manage: title: Titel created: Erstellt no-description: Keine Beschreibung - video-details: 'Videodetails: „{{title}}“' - technical-details: Technische Details - opencast-id: Opencast-ID - available-resolutions: Verfügbare Auflösungen page-showing-video-ids: '{{start}}–{{end}} von {{total}}' no-videos-found: Keine Videos gefunden. - share-direct-link: Via Direktlink teilen - open-in-editor: Im Videoeditor öffnen - referencing-pages: Referenzierende Seiten - referencing-pages-explanation: 'Dieses Video wird von den folgenden Seiten referenziert:' - no-referencing-pages: Dieses Video wird von keiner Seite referenziert. + details: + title: Videodetails + share-direct-link: Via Direktlink teilen + open-in-editor: Im Videoeditor öffnen + referencing-pages: Referenzierende Seiten + referencing-pages-explanation: 'Dieses Video wird von den folgenden Seiten referenziert:' + no-referencing-pages: Dieses Video wird von keiner Seite referenziert. + thumbnail: + title: Vorschaubild + acl: + title: Zugangsbeschränkung + technical-details: + title: Technische Details + tracks: Video/Audio-Spuren + unknown-mimetype: Unbekannter MIME-Type + opencast-id: Opencast-ID + further-info: Weitere Informationen + synced: Synchronisiert? + part-of: 'Teil von:' + is-live: Live? are-you-sure: Sind Sie sich sicher? diff --git a/frontend/src/i18n/locales/en.yaml b/frontend/src/i18n/locales/en.yaml index a069e3eb0..64de03125 100644 --- a/frontend/src/i18n/locales/en.yaml +++ b/frontend/src/i18n/locales/en.yaml @@ -19,6 +19,8 @@ general: logo-alt: Logo of “{{title}}” no-root-children: No pages yet ... failed-to-load-thumbnail: Failed to load thumbnail + yes: "Yes" + no: "No" form: select: no-options: No options @@ -137,6 +139,8 @@ video: title: Embed video caption: Caption manage: Manage + start: Start + end: End series: series: Series @@ -227,17 +231,28 @@ manage: title: Title created: Created no-description: No description - video-details: 'Video Details: “{{title}}”' - technical-details: Technical details - opencast-id: Opencast ID - available-resolutions: Available resolutions page-showing-video-ids: '{{start}}–{{end}} of {{total}}' no-videos-found: No videos found. - share-direct-link: Share via direct link - open-in-editor: Open in video editor - referencing-pages: Referencing pages - referencing-pages-explanation: 'This video is referenced on the following pages:' - no-referencing-pages: No pages reference this video. + details: + title: Video details + share-direct-link: Share via direct link + open-in-editor: Open in video editor + referencing-pages: Referencing pages + referencing-pages-explanation: 'This video is referenced on the following pages:' + no-referencing-pages: No pages reference this video. + thumbnail: + title: Thumbnail + acl: + title: Manage access + technical-details: + title: Technical details + tracks: Video/audio tracks + unknown-mimetype: Unknown MIME type + opencast-id: Opencast ID + further-info: Further information + synced: Synchronized? + part-of: 'Part of:' + is-live: Live? are-you-sure: Are you sure? diff --git a/frontend/src/layout/Burger.tsx b/frontend/src/layout/Burger.tsx index f8bf11e93..08cf15aed 100644 --- a/frontend/src/layout/Burger.tsx +++ b/frontend/src/layout/Burger.tsx @@ -1,12 +1,9 @@ -import { ReactNode } from "react"; - - type BurgerMenuProps = { hide: () => void; - children: ReactNode; + items: [] | readonly [JSX.Element] | readonly [JSX.Element, JSX.Element]; }; -export const BurgerMenu: React.FC = ({ hide, children }) => ( +export const BurgerMenu: React.FC = ({ hide, items }) => (
{ if (e.target === e.currentTarget) { @@ -35,11 +32,18 @@ export const BurgerMenu: React.FC = ({ hide, children }) => ( width: "clamp(260px, 75%, 450px)", overflowY: "auto", borderTop: "1px solid var(--grey80)", + gap: 16, "& > *:first-child": { - borderBottom: "1px solid var(--grey80)", + borderBottom: "1px dashed var(--grey80)", + }, + "& > *:last-child": { + borderTop: "1px dashed var(--grey80)", }, }}> - {children} + {items.length > 0 &&
{items[0]}
} + {items.length > 1 && <> +
{items[1]}
+ }
); diff --git a/frontend/src/layout/Navigation.tsx b/frontend/src/layout/Navigation.tsx index 630336192..e864afd0f 100644 --- a/frontend/src/layout/Navigation.tsx +++ b/frontend/src/layout/Navigation.tsx @@ -4,7 +4,13 @@ import { graphql, useFragment } from "react-relay"; import type { NavigationData$key } from "./__generated__/NavigationData.graphql"; import { useTranslation } from "react-i18next"; -import { FOCUS_STYLE_INSET, LinkList, LinkWithIcon, SIDE_BOX_BORDER_RADIUS } from "../ui"; +import { + ellipsisOverflowCss, + FOCUS_STYLE_INSET, + LinkList, + LinkWithIcon, + SIDE_BOX_BORDER_RADIUS, +} from "../ui"; import { MissingRealmName, sortRealms } from "../routes/util"; @@ -98,13 +104,7 @@ type ItemProps = { const Item: React.FC = ({ label, link }) => ( -
{label}
+
{label}
); diff --git a/frontend/src/layout/Root.tsx b/frontend/src/layout/Root.tsx index 1eb4082c5..9477f62ef 100644 --- a/frontend/src/layout/Root.tsx +++ b/frontend/src/layout/Root.tsx @@ -25,16 +25,14 @@ type Props = { export const Root: React.FC = ({ nav, children }) => { const menu = useMenu(); - const navElements = Array.isArray(nav) ? nav : [nav]; + const navElements = Array.isArray(nav) ? nav : [nav] as const; const navExists = navElements.length > 0; return (
{menu.state === "burger" && navExists && ( - menu.close()}> - {navElements.map((elem, i) =>
{elem}
)} -
+ menu.close()} /> )}
document.location.pathname === "/~search"; @@ -190,7 +191,7 @@ const SearchEvent: React.FC = ({ // link should be avoided. const link = hostRealms.length !== 1 ? `/!v/${id.slice(2)}` - : `${hostRealms[0].path}/v/${id.slice(2)}`; + : `${hostRealms[0].path.replace(/^\/$/, "")}/v/${id.slice(2)}`; return ( @@ -212,11 +213,7 @@ const SearchEvent: React.FC = ({

{title}

diff --git a/frontend/src/routes/Video.tsx b/frontend/src/routes/Video.tsx index 8829488d4..f6b340145 100644 --- a/frontend/src/routes/Video.tsx +++ b/frontend/src/routes/Video.tsx @@ -49,6 +49,7 @@ import { NavigationData$key } from "../layout/__generated__/NavigationData.graph import { getEventTimeInfo } from "../util/video"; import { Creators } from "../ui/Video"; import { Description } from "../ui/metadata"; +import { ellipsisOverflowCss } from "../ui"; // =========================================================================================== @@ -378,13 +379,7 @@ const VideoTitle: React.FC = ({ title }) => ( [`@media (max-width: ${BREAKPOINT_MEDIUM}px)`]: { fontSize: 20 }, [`@media (max-width: ${BREAKPOINT_SMALL}px)`]: { fontSize: 18 }, lineHeight: 1.2, - - // Truncate title after two lines - display: "-webkit-box", - WebkitBoxOrient: "vertical", - textOverflow: "ellipsis", - WebkitLineClamp: 2, - overflow: "hidden", + ...ellipsisOverflowCss(2), }} /> ); diff --git a/frontend/src/routes/manage/Video/Details.tsx b/frontend/src/routes/manage/Video/Details.tsx new file mode 100644 index 000000000..f37f9c855 --- /dev/null +++ b/frontend/src/routes/manage/Video/Details.tsx @@ -0,0 +1,156 @@ +import { useTranslation } from "react-i18next"; +import { FiExternalLink } from "react-icons/fi"; + +import { Link } from "../../../router"; +import { NotAuthorized } from "../../../ui/error"; +import { Form } from "../../../ui/Form"; +import { CopyableInput, Input, TextArea } from "../../../ui/Input"; +import { InputContainer, TitleLabel } from "../../../ui/metadata"; +import { useUser } from "../../../User"; +import { LinkButton } from "../../../ui/Button"; +import CONFIG from "../../../config"; +import { Breadcrumbs } from "../../../ui/Breadcrumbs"; +import { PageTitle } from "../../../layout/header/ui"; +import { AuthorizedEvent, makeManageVideoRoute, PAGE_WIDTH } from "./Shared"; + + +export const ManageVideoDetailsRoute = makeManageVideoRoute( + "details", + "", + event => , +); + +type Props = { + event: AuthorizedEvent; +}; + +const Page: React.FC = ({ event }) => { + const { t } = useTranslation(); + + const breadcrumbs = [ + { label: t("manage.management"), link: "/~manage" }, + { label: t("manage.my-videos.title"), link: "/~manage/videos" }, + ]; + + const user = useUser(); + if (user === "none" || user === "unknown") { + return ; + } + const editorUrl = `${CONFIG.opencast.editorUrl}?mediaPackageId=${event.opencastId}`; + + return <> + + +
+ +
+ {user.canUseEditor && event.canWrite && ( + + {t("manage.my-videos.details.open-in-editor")} + + )} + + +
+
+
+ +
+ ; +}; + +const DirectLink: React.FC = ({ event }) => { + const { t } = useTranslation(); + const url = new URL(`/!v/${event.id.slice(2)}`, document.baseURI); + + return ( +
+
+ {t("manage.my-videos.details.share-direct-link") + ":"} +
+ +
+ ); +}; + +/** Shows the `created` and `updated` timestamps. */ +const UpdatedCreatedInfo: React.FC = ({ event }) => { + const { t, i18n } = useTranslation(); + const created = new Date(event.created).toLocaleString(i18n.language); + const updated = event.syncedData?.updated == null + ? null + : new Date(event.syncedData.updated).toLocaleString(i18n.language); + + return ( +
+ + {updated && } +
+ ); +}; + +type DateValueProps = { + label: string; + value: string; +}; + +const DateValue: React.FC = ({ label, value }) => ( + + {label + ":"} + {value} + +); + +const MetadataSection: React.FC = ({ event }) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + +