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
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}
LukasKalbertodt marked this conversation as resolved.
Show resolved Hide resolved
</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) ?? [];
LukasKalbertodt marked this conversation as resolved.
Show resolved Hide resolved
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
LukasKalbertodt marked this conversation as resolved.
Show resolved Hide resolved
{...{ 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