Skip to content

Commit

Permalink
Add custom input for adding timestamp to video url
Browse files Browse the repository at this point in the history
  • Loading branch information
owi92 committed Sep 24, 2023
1 parent f7fe88b commit 32c7460
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 56 deletions.
19 changes: 13 additions & 6 deletions frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,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 @@ -63,7 +63,6 @@ import { TrackInfo } from "./manage/Video/TechnicalDetails";
import { COLORS } from "../color";
import { RelativeDate } from "../ui/time";
import { Modal, ModalHandle } from "../ui/Modal";
import { TimePicker } from "./manage/Video/Details";
import { PlayerContextProvider, usePlayerContext } from "../ui/player/PlayerContext";


Expand Down Expand Up @@ -676,10 +675,14 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
css={{ fontSize: 14, width: 400 }}
value={url}
/>
<TimePicker
{...{ timestamp, setTimestamp }}
<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} />
Expand Down Expand Up @@ -713,10 +716,14 @@ const ShareButton: React.FC<{ event: SyncedEvent }> = ({ event }) => {
multiline
css={{ fontSize: 14, width: 400, height: 75 }}
/>
<TimePicker
{...{ timestamp, setTimestamp }}
<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} />
Expand Down
48 changes: 6 additions & 42 deletions frontend/src/routes/manage/Video/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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 Down Expand Up @@ -73,41 +73,6 @@ const Page: React.FC<Props> = ({ event }) => {
</>;
};

type TimePickerProps = {
timestamp: string;
setTimestamp: (newTime: string) => void;
checkboxChecked: boolean;
setCheckboxChecked: (newValue: boolean) => void;
}

export const TimePicker: React.FC<TimePickerProps> = (
{ timestamp, setTimestamp, checkboxChecked, setCheckboxChecked }
) => {
const { t } = useTranslation();

return <>
<input
type="checkbox"
checked={checkboxChecked}
onChange={() => setCheckboxChecked(!checkboxChecked)}
css={{ margin: "0 4px" }}
/>
<label css={{ color: COLORS.neutral90, fontSize: 14 }}>
{t("manage.my-videos.details.set-time")}
</label>
<input
disabled={!checkboxChecked}
value={timestamp}
onChange={e => setTimestamp(e.target.value)}
css={{
border: 0,
backgroundColor: "transparent",
fontSize: 14,
}}
/>
</>;
};

const DirectLink: React.FC<Props> = ({ event }) => {
const { t } = useTranslation();
const [timestamp, setTimestamp] = useState<string>("0m0s");
Expand All @@ -128,12 +93,11 @@ const DirectLink: React.FC<Props> = ({ event }) => {
value={url.href}
css={{ width: "100%", fontSize: 14 }}
/>
<TimePicker {...{
timestamp,
setTimestamp,
checkboxChecked,
setCheckboxChecked,
}} />
<InputWithCheckbox
{...{ checkboxChecked, setCheckboxChecked }}
label={t("manage.my-videos.details.set-time")}
input={<TimeInput {...{ timestamp, setTimestamp }} disabled={!checkboxChecked} />}
/>
</div>
);
};
Expand Down
89 changes: 88 additions & 1 deletion frontend/src/ui/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useId, useState } from "react";
import React, { Fragment, ReactNode, useId, useState } from "react";
import { FiCheck, FiCopy } from "react-icons/fi";
import { WithTooltip } from "@opencast/appkit";

Expand Down Expand Up @@ -50,6 +50,93 @@ export const TextArea = React.forwardRef<HTMLTextAreaElement, TextAreaProps>(
),
);

type InputWithCheckboxProps = {
checkboxChecked: boolean;
setCheckboxChecked: (newValue: boolean) => void;
label: string;
input: ReactNode;
}

/** Checkbox with a label to enable/disable an adjacent input */
export const InputWithCheckbox: React.FC<InputWithCheckboxProps> = (
{ checkboxChecked, setCheckboxChecked, label, input }
) => <div css={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
<input
type="checkbox"
checked={checkboxChecked}
onChange={() => setCheckboxChecked(!checkboxChecked)}
css={{ margin: "0 4px" }}
/>
<label css={{ color: COLORS.neutral90, fontSize: 14 }}>
{label}
</label>
{input}
</div>;

type TimeInputProps = {
timestamp: string;
setTimestamp: (newTime: string) => void;
disabled: boolean;
}

/** A custom three-part input for time inputs split into hours, minutes and seconds */
export const TimeInput: React.FC<TimeInputProps> = ({ timestamp, setTimestamp, disabled }) => {
const timeParts = (/(\d+h)?(\d+m)?(\d+s)?/).exec(timestamp)?.slice(1) ?? [];
const [hours, minutes, seconds] = timeParts
.map(part => part ? parseInt(part.replace(/\D/g, "")) : 0);

const handleTimeChange = (newValue: number, type: TimeUnit) => {
if (isNaN(newValue)) {
return;
}

const cappedValue = Math.min(newValue, 59);
const newTimestamp = `${type === "h" ? cappedValue : hours}h`
+ `${type === "m" ? cappedValue : minutes}m`
+ `${type === "s" ? cappedValue : seconds}s`;

setTimestamp(newTimestamp);
};

type TimeUnit = "h" | "m" | "s";
const entries: [number, TimeUnit][] = [
[hours, "h"],
[minutes, "m"],
[seconds, "s"],
];

return (
<div css={{ color: disabled ? COLORS.neutral70 : COLORS.neutral90 }}>
{entries.map(([time, unit]) => <Fragment key={`${unit}-input`}>
<input
{...{ disabled }}
value={time}
maxLength={2}
onChange={e => handleTimeChange(Number(e.target.value), unit)}
css={{
width: time > 9 ? 24 : "2ch",
lineHeight: 1,
padding: 0,
border: 0,
textAlign: "center",
borderRadius: 4,
outline: `1px solid ${COLORS.neutral20}`,
outlineOffset: "-2px",
userSelect: "all",
...focusStyle({ inset: true }),
":disabled": {
textAlign: "right",
backgroundColor: "transparent",
outline: "none",
},
}}
/>
<span>{unit}</span>
</Fragment>)}
</div>
);
};

export type SelectProps = React.ComponentPropsWithoutRef<"select"> & {
error?: boolean;
};
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,14 @@ export const timeStringToSeconds = (timeString: string): number => {
return hours + minutes + seconds;
};

/**
* Formats the given number of seconds as string containing hours, minutes and seconds,
* e.g. "0h2m4s".
*/
export const secondsToTimeString = (seconds: number): string => {
type TimeUnit = "h" | "m" | "s";
const formatTime = (time: number, unit: TimeUnit): string =>
unit === "h" && time === 0 ? "" : time.toString().padStart(2, "0") + unit;

const hours = formatTime(Math.floor(seconds / 3600), "h");
const minutes = formatTime(Math.floor((seconds % 3600) / 60), "m");
const remainingSeconds = formatTime(Math.floor(seconds % 60), "s");
const hours = Math.floor(seconds / 3600) + "h";
const minutes = Math.floor((seconds % 3600) / 60) + "m";
const remainingSeconds = Math.floor(seconds % 60) + "s";

return hours + minutes + remainingSeconds;
};
Expand Down

0 comments on commit 32c7460

Please sign in to comment.