Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "start at:" option to share menu #943

Merged
merged 13 commits into from
Sep 26, 2023
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ manage:
details:
title: Videodetails
share-direct-link: Via Direktlink teilen
set-time: 'Starten bei: '
LukasKalbertodt marked this conversation as resolved.
Show resolved Hide resolved
copy-direct-link-to-clipboard: Videolink in Zwischenablage kopieren
open-in-editor: Im Videoeditor öffnen
referencing-pages: Referenzierende Seiten
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ manage:
details:
title: Video details
share-direct-link: Share via direct link
set-time: 'Start at: '
copy-direct-link-to-clipboard: Copy video link to clipboard
open-in-editor: Open in video editor
referencing-pages: Referencing pages
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/routes/Embed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Spinner } from "../ui/Spinner";
import { MovingTruck } from "../ui/Waiting";
import { b64regex } from "./util";
import { EmbedQuery } from "./__generated__/EmbedQuery.graphql";
import { PlayerContextProvider } from "../ui/player/PlayerContext";


const query = graphql`
Expand Down Expand Up @@ -60,7 +61,9 @@ export const EmbedVideoRoute = makeRoute(url => {
}} />
</PlayerPlaceholder>
}>
<Embed queryRef={queryRef} />
<PlayerContextProvider>
<Embed queryRef={queryRef} />
</PlayerContextProvider>
</Suspense>
</ErrorBoundary>,
dispose: () => queryRef.dispose(),
Expand Down
114 changes: 82 additions & 32 deletions frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
useForceRerender,
translatedConfig,
currentRef,
secondsToTimeString,
} from "../util";
import { BREAKPOINT_SMALL, BREAKPOINT_MEDIUM } from "../GlobalStyle";
import { Button, LinkButton } from "../ui/Button";
Expand All @@ -38,7 +39,7 @@ import { Link, useRouter } from "../router";
import { useUser } from "../User";
import { b64regex } from "./util";
import { ErrorPage } from "../ui/error";
import { CopyableInput } from "../ui/Input";
import { CopyableInput, InputWithCheckbox, TimeInput } from "../ui/Input";
import { VideoPageInRealmQuery } from "./__generated__/VideoPageInRealmQuery.graphql";
import {
VideoPageEventData$data,
Expand All @@ -62,6 +63,7 @@ import { TrackInfo } from "./manage/Video/TechnicalDetails";
import { COLORS } from "../color";
import { RelativeDate } from "../ui/time";
import { Modal, ModalHandle } from "../ui/Modal";
import { PlayerContextProvider, usePlayerContext } from "../ui/player/PlayerContext";


// ===========================================================================================
Expand Down Expand Up @@ -313,8 +315,10 @@ const VideoPage: React.FC<Props> = ({ eventRef, realmRef, basePath }) => {
return <>
<Breadcrumbs path={breadcrumbs} tail={event.title} />
<script type="application/ld+json">{JSON.stringify(structuredData)}</script>
<InlinePlayer event={event} css={{ margin: "0 auto" }} onEventStateChange={rerender} />
<Metadata id={event.id} event={event} />
<PlayerContextProvider>
<InlinePlayer event={event} css={{ margin: "0 auto" }} onEventStateChange={rerender} />
<Metadata id={event.id} event={event} />
</PlayerContextProvider>

<div css={{ height: 80 }} />

Expand Down Expand Up @@ -530,22 +534,28 @@ const DownloadButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
};

const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
type State = "closed" | "main" | "embed" | "rss";
type MenuState = "closed" | "main" | "embed" | "rss";
/* eslint-disable react/jsx-key */
const entries: [Exclude<State, "closed">, ReactElement][] = [
const entries: [Exclude<MenuState, "closed">, ReactElement][] = [
["main", <LuLink />],
["embed", <FiCode />],
// ["rss", <FiRss />],
];
/* eslint-enable react/jsx-key */

const { t } = useTranslation();
const [state, setState] = useState<State>("closed");
const [menuState, setMenuState] = useState<MenuState>("closed");
const [timestamp, setTimestamp] = useState(0);
const [addLinkTimestamp, setAddLinkTimestamp] = useState(false);
const [addEmbedTimestamp, setAddEmbedTimestamp] = useState(false);
const isDark = useColorScheme().scheme === "dark";
const ref = useRef(null);
const qrModalRef = useRef<ModalHandle>(null);
const { paella, playerIsLoaded } = usePlayerContext();

const isActive = (label: State) => label === state;
const timeStringPattern = /\?t=(\d+h)?(\d+m)?(\d+s)?/;

const isActive = (label: MenuState) => label === menuState;

const tabStyle = {
display: "flex",
Expand Down Expand Up @@ -603,7 +613,7 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
<ProtoButton
disabled={isActive(label)}
key={label}
onClick={() => setState(label)}
onClick={() => setMenuState(label)}
css={tabStyle}
>
{icon}
Expand All @@ -612,7 +622,9 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
))}
</div>;

const ShowQRCodeButton: React.FC<{ target: string; label: State }> = ({ target, label }) => <>
const ShowQRCodeButton: React.FC<{ target: string; label: MenuState }> = (
{ target, label }
) => <>
<Button
onClick={() => currentRef(qrModalRef).open()}
css={{ width: "max-content" }}
Expand All @@ -639,28 +651,48 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
</Modal>
</>;

const inner = match(state, {
const inner = match(menuState, {
"closed": () => null,
"main": () => <>
<CopyableInput
label={t("manage.my-videos.details.copy-direct-link-to-clipboard")}
css={{ fontSize: 14, width: 400 }}
// TODO
value={window.location.href}
/>
<ShowQRCodeButton target={window.location.href} label={state} />
</>,
"main": () => {
let url = window.location.href.replace(timeStringPattern, "");
url += addLinkTimestamp && timestamp
? `?t=${secondsToTimeString(timestamp)}`
: "";

return <>
<div>
<CopyableInput
label={t("manage.my-videos.details.copy-direct-link-to-clipboard")}
css={{ fontSize: 14, width: 400, marginBottom: 6 }}
value={url}
/>
<InputWithCheckbox
checkboxChecked={addLinkTimestamp}
setCheckboxChecked={setAddLinkTimestamp}
label={t("manage.my-videos.details.set-time")}
input={<TimeInput
{...{ timestamp, setTimestamp }}
disabled={!addLinkTimestamp}
/>}
/>
</div>
<ShowQRCodeButton target={window.location.href} label={menuState} />
</>;
},
"embed": () => {
const ar = event.syncedData == null
? [16, 9]
: getPlayerAspectRatio(event.syncedData.tracks);

const target = new URL(location.href);
target.pathname = `/~embed/!v/${event.id.slice(2)}`;
const url = new URL(location.href.replace(timeStringPattern, ""));
url.search = addEmbedTimestamp && timestamp
? `?t=${secondsToTimeString(timestamp)}`
: "";
url.pathname = `/~embed/!v/${event.id.slice(2)}`;

const embedCode = `<iframe ${[
'name="Tobira Player"',
`src="${target}"`,
`src="${url}"`,
"allow=fullscreen",
`style="${[
"border: none;",
Expand All @@ -670,13 +702,24 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
].join(" ")}></iframe>`;

return <>
<CopyableInput
label={t("video.embed.copy-embed-code-to-clipboard")}
value={embedCode}
multiline
css={{ fontSize: 14, width: 400 }}
/>
<ShowQRCodeButton target={embedCode} label={state} />
<div>
<CopyableInput
label={t("video.embed.copy-embed-code-to-clipboard")}
value={embedCode}
multiline
css={{ fontSize: 14, width: 400, height: 75, marginBottom: 6 }}
/>
<InputWithCheckbox
checkboxChecked={addEmbedTimestamp}
setCheckboxChecked={setAddEmbedTimestamp}
label={t("manage.my-videos.details.set-time")}
input={<TimeInput
{...{ timestamp, setTimestamp }}
disabled={!addEmbedTimestamp}
/>}
/>
</div>
<ShowQRCodeButton target={embedCode} label={menuState} />
</>;
},
"rss": () => {
Expand All @@ -693,12 +736,19 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
placement="top"
arrowSize={12}
ariaRole="dialog"
open={state !== "closed"}
onClose={() => setState("closed")}
open={menuState !== "closed"}
onClose={() => setMenuState("closed")}
viewPortMargin={12}
>
<FloatingTrigger>
<Button onClick={() => setState(state => state === "closed" ? "main" : "closed")}>
<Button onClick={() => {
setMenuState(state => state === "closed" ? "main" : "closed");
if (playerIsLoaded) {
paella.current?.player.videoContainer.currentTime().then(res => {
setTimestamp(res);
});
}
}}>
<FiShare2 size={16} />
{t("general.action.share")}
</Button>
Expand Down
19 changes: 16 additions & 3 deletions frontend/src/routes/manage/Video/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useTranslation } from "react-i18next";
import { FiExternalLink } from "react-icons/fi";

import { useState } from "react";
import { Link } from "../../../router";
import { NotAuthorized } from "../../../ui/error";
import { Form } from "../../../ui/Form";
import { CopyableInput, Input, TextArea } from "../../../ui/Input";
import { CopyableInput, Input, InputWithCheckbox, TextArea, TimeInput } from "../../../ui/Input";
import { InputContainer, TitleLabel } from "../../../ui/metadata";
import { isRealUser, useUser } from "../../../User";
import { Breadcrumbs } from "../../../ui/Breadcrumbs";
Expand All @@ -13,6 +14,7 @@ import { AuthorizedEvent, makeManageVideoRoute, PAGE_WIDTH } from "./Shared";
import { ExternalLink } from "../../../relay/auth";
import { buttonStyle } from "../../../ui/Button";
import { COLORS } from "../../../color";
import { secondsToTimeString } from "../../../util";


export const ManageVideoDetailsRoute = makeManageVideoRoute(
Expand Down Expand Up @@ -74,7 +76,13 @@ const Page: React.FC<Props> = ({ event }) => {

const DirectLink: React.FC<Props> = ({ event }) => {
const { t } = useTranslation();
const url = new URL(`/!v/${event.id.slice(2)}`, document.baseURI);
const [timestamp, setTimestamp] = useState(0);
const [checkboxChecked, setCheckboxChecked] = useState(false);

let url = new URL(`/!v/${event.id.slice(2)}`, document.baseURI);
if (timestamp && checkboxChecked) {
url = new URL(url + `?t=${secondsToTimeString(timestamp)}`);
}

return (
<div css={{ marginBottom: 40 }}>
Expand All @@ -84,7 +92,12 @@ const DirectLink: React.FC<Props> = ({ event }) => {
<CopyableInput
label={t("manage.my-videos.details.copy-direct-link-to-clipboard")}
value={url.href}
css={{ width: "100%", fontSize: 14 }}
css={{ width: "100%", fontSize: 14, marginBottom: 6 }}
/>
<InputWithCheckbox
{...{ checkboxChecked, setCheckboxChecked }}
label={t("manage.my-videos.details.set-time")}
input={<TimeInput {...{ timestamp, setTimestamp }} disabled={!checkboxChecked} />}
/>
</div>
);
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/typings/paella-core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ declare module "paella-core" {
*/
public loadManifest(): Promise<void>;

public videoContainer: VideoContainer;

public bindEvent(
event: string,
callback: () => void,
unregisterOnUnload?: boolean
): () => void;

public unload(): Promise<void>;
}

Expand Down Expand Up @@ -46,6 +54,11 @@ declare module "paella-core" {
plugins: Record<string, PluginConfig>;
}

export interface VideoContainer {
setCurrentTime: (t: number) => Promise<void>;
currentTime: () => Promise<number>;
}

export type PluginConfig = Record<string, unknown> & {
enabled: boolean;
};
Expand Down
Loading