From 025782447b6ff3ab2a8667d85a7cad26be94201a Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 21 Nov 2022 14:03:17 +0100 Subject: [PATCH 01/10] Slightly improve separation of both parts in the burger menu --- frontend/src/layout/Burger.tsx | 18 +++++++++++------- frontend/src/layout/Root.tsx | 6 ++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/frontend/src/layout/Burger.tsx b/frontend/src/layout/Burger.tsx index f8bf11e93..06edbbcb7 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: [] | [JSX.Element] | [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/Root.tsx b/frontend/src/layout/Root.tsx index 1eb4082c5..89ea8c8e9 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 [JSX.Element]; const navExists = navElements.length > 0; return (
{menu.state === "burger" && navExists && ( - menu.close()}> - {navElements.map((elem, i) =>
{elem}
)} -
+ menu.close()} /> )}
Date: Thu, 24 Nov 2022 15:30:10 +0100 Subject: [PATCH 02/10] Extract CSS logic for "multiline text-overflow: ellipsis" into function --- frontend/src/layout/Navigation.tsx | 16 ++++++++-------- frontend/src/routes/Search.tsx | 7 ++----- frontend/src/routes/Video.tsx | 9 ++------- frontend/src/ui/Blocks/Series.tsx | 15 ++++----------- frontend/src/ui/index.tsx | 9 +++++++++ frontend/src/ui/metadata.tsx | 7 ++----- 6 files changed, 27 insertions(+), 36 deletions(-) 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/routes/Search.tsx b/frontend/src/routes/Search.tsx index c3aeb7d28..f8ef2b2dc 100644 --- a/frontend/src/routes/Search.tsx +++ b/frontend/src/routes/Search.tsx @@ -15,6 +15,7 @@ import { Card } from "../ui/Card"; import { PageTitle } from "../layout/header/ui"; import { BreadcrumbsContainer, BreadcrumbSeparator } from "../ui/Breadcrumbs"; import { MissingRealmName } from "./util"; +import { ellipsisOverflowCss } from "../ui"; export const isSearchActive = (): boolean => document.location.pathname === "/~search"; @@ -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/ui/Blocks/Series.tsx b/frontend/src/ui/Blocks/Series.tsx index 275037698..5f5ccebe9 100644 --- a/frontend/src/ui/Blocks/Series.tsx +++ b/frontend/src/ui/Blocks/Series.tsx @@ -18,6 +18,7 @@ import { Card } from "../Card"; import { FiPlay } from "react-icons/fi"; import { keyframes } from "@emotion/react"; import { Description } from "../metadata"; +import { ellipsisOverflowCss } from ".."; type SharedProps = { @@ -186,13 +187,9 @@ const SeriesBlockContainer: React.FC = ({ title, chil transform: "translateY(-50%)", fontSize: 19, margin: "0 8px", - - display: "-webkit-inline-box", - WebkitBoxOrient: "vertical", - textOverflow: "ellipsis", - WebkitLineClamp: 3, - overflow: "hidden", lineHeight: 1.3, + ...ellipsisOverflowCss(3), + display: "-webkit-inline-box", }}>{title}
} {children} @@ -265,12 +262,8 @@ const GridTile: React.FC = ({ event, basePath, active }) => { }}/>}
{event.title}
=> ({ + overflow: "hidden", + textOverflow: "ellipsis", + display: "-webkit-box", + WebkitBoxOrient: "vertical", + WebkitLineClamp: lines, +}); diff --git a/frontend/src/ui/metadata.tsx b/frontend/src/ui/metadata.tsx index cbbf55082..775a2a127 100644 --- a/frontend/src/ui/metadata.tsx +++ b/frontend/src/ui/metadata.tsx @@ -1,5 +1,6 @@ import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; +import { ellipsisOverflowCss } from "."; export const TitleLabel: React.FC<{ htmlFor: string }> = ({ htmlFor }) => { @@ -48,11 +49,7 @@ export const SmallDescription: React.FC = ({ text, lines ...sharedStyle, color: "var(--grey40)", maxWidth: 800, - display: "-webkit-box", - WebkitBoxOrient: "vertical", - textOverflow: "ellipsis", - WebkitLineClamp: lines, - overflow: "hidden", + ...ellipsisOverflowCss(lines), }}>{text}
; } }; From 392adc89e0c89282e17b888a3c003570f5e2c91b Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 21 Nov 2022 18:36:09 +0100 Subject: [PATCH 03/10] Make `LinkWithIcon` font bold when active This was only used in one place where it was configured like that anyway. We might want to improve the style of this in the future still. --- frontend/src/routes/manage/index.tsx | 12 +----------- frontend/src/ui/index.tsx | 1 + 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/frontend/src/routes/manage/index.tsx b/frontend/src/routes/manage/index.tsx index 0a070e334..94bac75a6 100644 --- a/frontend/src/routes/manage/index.tsx +++ b/frontend/src/routes/manage/index.tsx @@ -140,18 +140,8 @@ export const ManageNav: React.FC = ({ active }) => { ]; /* eslint-enable react/jsx-key */ - // TODO: we probably want a better style for active items - const activeStyle = { - fontWeight: "bold" as const, - }; const items = entries.map(([path, label, icon]) => ( - + {icon} {label} diff --git a/frontend/src/ui/index.tsx b/frontend/src/ui/index.tsx index ff23e6c93..41cf60ddd 100644 --- a/frontend/src/ui/index.tsx +++ b/frontend/src/ui/index.tsx @@ -107,6 +107,7 @@ export const LinkWithIcon: React.FC = ({ "&:hover": hoverActiveStyle, ...active && { color: "var(--nav-color-darker)", + fontWeight: "bold", "&": hoverActiveStyle, }, }; From 2faa88c077769a8dacce43b317849a73d4a8890c Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 21 Nov 2022 18:44:04 +0100 Subject: [PATCH 04/10] Restructure "manage single video" page into a multi-page UI We will still add a lot more stuff and we can't possibly cram it all on one page. This is similar to how YouTube does it, for example. Currently, there are only 2 sub pages, but there are already commented out entries for two more. Most notably, this commit: - Moves the "technical details" part to a new sub page - Some new technical details are shown - Adds a nav element to switch between different sub pages - Said nav element also contains the thumbnail and title of the video - As such, the thumbnail on the actual manage page is removed and the updated/created timestamps are moved elsewhere on the page. --- frontend/src/i18n/locales/de.yaml | 23 +- frontend/src/i18n/locales/en.yaml | 23 +- frontend/src/router.tsx | 6 +- frontend/src/routes/manage/Video/Details.tsx | 154 ++++++++++ frontend/src/routes/manage/Video/Shared.tsx | 197 ++++++++++++ frontend/src/routes/manage/Video/Single.tsx | 289 ------------------ .../routes/manage/Video/TechnicalDetails.tsx | 162 ++++++++++ 7 files changed, 555 insertions(+), 299 deletions(-) create mode 100644 frontend/src/routes/manage/Video/Details.tsx create mode 100644 frontend/src/routes/manage/Video/Shared.tsx delete mode 100644 frontend/src/routes/manage/Video/Single.tsx create mode 100644 frontend/src/routes/manage/Video/TechnicalDetails.tsx diff --git a/frontend/src/i18n/locales/de.yaml b/frontend/src/i18n/locales/de.yaml index 310da4d17..4c1c9cdc2 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,10 +238,6 @@ 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 @@ -245,6 +245,21 @@ manage: 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 + 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 + other-info: Andere 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..a1807cc02 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: Anfang + end: Ende series: series: Series @@ -227,10 +231,6 @@ 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 @@ -238,6 +238,21 @@ manage: 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 + 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 + other-info: Other information + synced: Synchronized? + part-of: 'Part of:' + is-live: Live? are-you-sure: Are you sure? diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 0b04eb93d..a7f146329 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -10,11 +10,12 @@ import { RealmRoute } from "./routes/Realm"; import { DirectOpencastVideoRoute, DirectVideoRoute, VideoRoute } from "./routes/Video"; import { DirectSeriesOCRoute, DirectSeriesRoute } from "./routes/Series"; import { ManageVideosRoute } from "./routes/manage/Video"; -import { ManageSingleVideoRoute } from "./routes/manage/Video/Single"; import { UploadRoute } from "./routes/Upload"; import { SearchRoute } from "./routes/Search"; import { InvalidUrlRoute } from "./routes/InvalidUrl"; import { BlockEmbedRoute, EmbedVideoRoute } from "./routes/Embed"; +import { ManageVideoDetailsRoute } from "./routes/manage/Video/Details"; +import { ManageVideoTechnicalDetailsRoute } from "./routes/manage/Video/TechnicalDetails"; @@ -41,7 +42,8 @@ const { DirectSeriesOCRoute, ManageRoute, ManageVideosRoute, - ManageSingleVideoRoute, + ManageVideoDetailsRoute, + ManageVideoTechnicalDetailsRoute, ManageRealmRoute, UploadRoute, AddChildRoute, diff --git a/frontend/src/routes/manage/Video/Details.tsx b/frontend/src/routes/manage/Video/Details.tsx new file mode 100644 index 000000000..50fe47364 --- /dev/null +++ b/frontend/src/routes/manage/Video/Details.tsx @@ -0,0 +1,154 @@ +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.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.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 ( +
+ + + + + + + +