Skip to content

Commit

Permalink
Merge pull request #459 from charlielee/issue-419
Browse files Browse the repository at this point in the history
Option to switch status bar time between frames and seconds
  • Loading branch information
charlielee committed May 9, 2023
2 parents 4532b5d + 2386d1d commit 98c16f3
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 41 deletions.
2 changes: 2 additions & 0 deletions src/common/UserPreferences.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
export interface UserPreferences {
playCaptureSound: boolean;
shortPlayLength: number;
showTimestampInSeconds: boolean;
workingDirectory: string | undefined;
}

export const defaultUserPreferences: UserPreferences = {
playCaptureSound: true,
shortPlayLength: 6,
showTimestampInSeconds: false,
workingDirectory: undefined,
};
74 changes: 74 additions & 0 deletions src/common/timeUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { buildStartTimeCode, secondsToTimeCode } from "./timeUtils";

describe("secondsToTimeCode", () => {
it("should convert to correct time codes when show decimal is true", () => {
expect(secondsToTimeCode(0, true)).toBe("00:00.000");
expect(secondsToTimeCode(0.01, true)).toBe("00:00.010");
expect(secondsToTimeCode(0.999, true)).toBe("00:00.999");
expect(secondsToTimeCode(0.9999, true)).toBe("00:01.000");
expect(secondsToTimeCode(1, true)).toBe("00:01.000");
expect(secondsToTimeCode(1.1, true)).toBe("00:01.100");
expect(secondsToTimeCode(1.6664, true)).toBe("00:01.666");
expect(secondsToTimeCode(1.6665, true)).toBe("00:01.667");
expect(secondsToTimeCode(59, true)).toBe("00:59.000");
expect(secondsToTimeCode(60, true)).toBe("01:00.000");
expect(secondsToTimeCode(61, true)).toBe("01:01.000");
expect(secondsToTimeCode(600, true)).toBe("10:00.000");
expect(secondsToTimeCode(601, true)).toBe("10:01.000");
expect(secondsToTimeCode(601.789, true)).toBe("10:01.789");
expect(secondsToTimeCode(5999, true)).toBe("99:59.000");
expect(secondsToTimeCode(6000, true)).toBe("100:00.000");
});

it("should convert to correct time codes when show decimal is false", () => {
expect(secondsToTimeCode(0, false)).toBe("00:00");
expect(secondsToTimeCode(0.01, false)).toBe("00:00");
expect(secondsToTimeCode(0.999, false)).toBe("00:01");
expect(secondsToTimeCode(0.9999, false)).toBe("00:01");
expect(secondsToTimeCode(1, false)).toBe("00:01");
expect(secondsToTimeCode(1.1, false)).toBe("00:01");
expect(secondsToTimeCode(1.6664, false)).toBe("00:02");
expect(secondsToTimeCode(1.6665, false)).toBe("00:02");
expect(secondsToTimeCode(59, false)).toBe("00:59");
expect(secondsToTimeCode(60, false)).toBe("01:00");
expect(secondsToTimeCode(61, false)).toBe("01:01");
expect(secondsToTimeCode(600, false)).toBe("10:00");
expect(secondsToTimeCode(601, false)).toBe("10:01");
expect(secondsToTimeCode(601.789, false)).toBe("10:02");
expect(secondsToTimeCode(5999, false)).toBe("99:59");
expect(secondsToTimeCode(6000, false)).toBe("100:00");
});

it("should convert to time code with decimal by default", () => {
expect(secondsToTimeCode(60)).toBe("01:00.000");
});
});

describe("buildStartTimeCode", () => {
it("should build correct time codes for frame positions", () => {
// 15 FPS
expect(buildStartTimeCode(0, 15, true)).toBe("00:00.000");
expect(buildStartTimeCode(1, 15, true)).toBe("00:00.067");
expect(buildStartTimeCode(14, 15, true)).toBe("00:00.933");
expect(buildStartTimeCode(15, 15, true)).toBe("00:01.000");
expect(buildStartTimeCode(16, 15, true)).toBe("00:01.067");
expect(buildStartTimeCode(999, 15, true)).toBe("01:06.600");
expect(buildStartTimeCode(9999, 15, true)).toBe("11:06.600");
// 12 FPS
expect(buildStartTimeCode(0, 12, true)).toBe("00:00.000");
expect(buildStartTimeCode(1, 12, true)).toBe("00:00.083");
expect(buildStartTimeCode(11, 12, true)).toBe("00:00.917");
expect(buildStartTimeCode(12, 12, true)).toBe("00:01.000");
expect(buildStartTimeCode(13, 12, true)).toBe("00:01.083");
expect(buildStartTimeCode(999, 12, true)).toBe("01:23.250");
expect(buildStartTimeCode(9999, 12, true)).toBe("13:53.250");
});

it("should build time code for frame position when showDecimal is false", () => {
expect(buildStartTimeCode(16, 15, false)).toBe("00:01");
});

it("should build time code for frame position with decimal by default", () => {
expect(buildStartTimeCode(16, 15)).toBe("00:01.067");
});
});
24 changes: 24 additions & 0 deletions src/common/timeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { FrameRate, TimelineIndex } from "./Flavors";
import { zeroPad } from "./utils";

export const secondsToTimeCode = (seconds: number, showDecimal = true) => {
const roundedSeconds = seconds.toFixed(showDecimal ? 3 : 0);
const [fullSeconds, millisecondComponent] = roundedSeconds.split(".");

const minuteComponent = Math.floor(parseInt(fullSeconds, 10) / 60);
const secondComponent = parseInt(fullSeconds, 10) % 60;
const paddedMinutesAndSeconds = `${zeroPad(minuteComponent, 2)}:${zeroPad(secondComponent, 2)}`;

return showDecimal
? `${paddedMinutesAndSeconds}.${millisecondComponent}`
: paddedMinutesAndSeconds;
};

export const buildStartTimeCode = (
framePosition: TimelineIndex,
frameRate: FrameRate,
showDecimal?: boolean
) => {
const timeInSeconds = framePosition / frameRate;
return secondsToTimeCode(timeInSeconds, showDecimal);
};
7 changes: 0 additions & 7 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,6 @@ export const zeroPad = (value: number, length: number) => {
return `${zeros.substring(0, zeros.length - value.toString().length)}${value}`;
};

export const secondsToTimeCode = (seconds: number) => {
const minuteComponent = (seconds - (seconds % 60)) / 60;
const secondComponent = seconds % 60;

return [zeroPad(minuteComponent, 2), zeroPad(secondComponent, 2)].join(":");
};

export const stringToArray = (items: string) =>
// Regexes are to handle arguments in quotes
// https://stackoverflow.com/a/56119602
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/animator/Animator/Animator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import AnimationToolbarWithContext from "../AnimationToolbar/AnimationToolbar";
import CaptureButtonToolbarWithContext from "../CaptureButtonToolbar/CaptureButtonToolbar";
import CaptureSidebarBlock from "../CaptureSidebarBlock/CaptureSidebarBlock";
import Preview from "../Preview/Preview";
import StatusToolbarWithContext from "../StatusToolbar/StatusToolbar";
import StatusToolbar from "../StatusToolbar/StatusToolbar";
import Timeline from "../Timeline/Timeline";
import TitleToolbar from "../TitleToolbar/TitleToolbar";

Expand All @@ -25,7 +25,7 @@ const Animator = ({ take }: AnimatorWithProviderProps): JSX.Element => {
<TitleToolbar take={take} />
<PageBody>
<Content>
<StatusToolbarWithContext take={take} />
<StatusToolbar take={take} />
<Preview take={take} />
<CaptureButtonToolbarWithContext />
<AnimationToolbarWithContext />
Expand Down
26 changes: 5 additions & 21 deletions src/renderer/components/animator/StatusToolbar/StatusToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,19 @@
import { TimelineIndex } from "../../../../common/Flavors";
import { Take } from "../../../../common/project/Take";
import PlaybackContext from "../../../context/PlaybackContext/PlaybackContext";
import { getTrackLength } from "../../../services/project/projectCalculator";
import Toolbar from "../../common/Toolbar/Toolbar";
import ToolbarItem, { ToolbarItemAlign } from "../../common/ToolbarItem/ToolbarItem";
import "./StatusToolbar.css";
import StatusToolbarTimestampWithContext from "./StatusToolbarTimestamp/StatusToolbarTimestamp";

interface StatusToolbarWithContextProps {
interface StatusToolbarProps {
take: Take;
}

interface StatusToolbarProps extends StatusToolbarWithContextProps {
timelineIndex: TimelineIndex | undefined;
}

const StatusToolbar = ({ take, timelineIndex }: StatusToolbarProps): JSX.Element => (
const StatusToolbar = ({ take }: StatusToolbarProps): JSX.Element => (
<Toolbar className="status-toolbar">
<ToolbarItem align={ToolbarItemAlign.CENTER}>
<div>
Frame{" "}
{timelineIndex === undefined ? getTrackLength(take.frameTrack) + 1 : timelineIndex + 1} of{" "}
{getTrackLength(take.frameTrack)}
</div>
<StatusToolbarTimestampWithContext take={take} />
</ToolbarItem>
</Toolbar>
);

const StatusToolbarWithContext = (props: StatusToolbarWithContextProps) => (
<PlaybackContext.Consumer>
{(value) => <StatusToolbar timelineIndex={value.timelineIndex} {...props} />}
</PlaybackContext.Consumer>
);

export default StatusToolbarWithContext;
export default StatusToolbar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.status-toolbar-timestamp {
font-family: monospace;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useState } from "react";
import { TimelineIndex } from "../../../../../common/Flavors";
import { Take } from "../../../../../common/project/Take";
import PlaybackContext from "../../../../context/PlaybackContext/PlaybackContext";
import { getTrackLength } from "../../../../services/project/projectCalculator";
import Button from "../../../common/Button/Button";
import { ButtonColor } from "../../../common/Button/ButtonColor";
import { buildStartTimeCode } from "../../../../../common/timeUtils";
import "./StatusToolbarTimestamp.css";
import { useSelector } from "react-redux";
import { RootState } from "../../../../redux/store";

interface StatusToolbarTimestampWithContextProps {
take: Take;
}

interface StatusToolbarTimestampProps extends StatusToolbarTimestampWithContextProps {
timelineIndex: TimelineIndex | undefined;
}

const StatusToolbarTimestamp = ({
take,
timelineIndex,
}: StatusToolbarTimestampProps): JSX.Element => {
const { showTimestampInSeconds } = useSelector((state: RootState) => state.app.userPreferences);
const [showInSeconds, setShowInSeconds] = useState(showTimestampInSeconds);

const trackLength = getTrackLength(take.frameTrack);
const secondsText = [
buildStartTimeCode(timelineIndex ?? (trackLength as TimelineIndex), take.frameRate),
"/",
buildStartTimeCode(trackLength as TimelineIndex, take.frameRate),
].join(" ");
const frameText = [
"Frame",
timelineIndex === undefined ? trackLength + 1 : timelineIndex + 1,
"of",
trackLength,
].join(" ");

return (
<Button
className="status-toolbar-timestamp"
label={showInSeconds ? secondsText : frameText}
title={showInSeconds ? "Show timestamp in frames" : "Show timestamp in seconds"}
onClick={() => setShowInSeconds((prevState) => !prevState)}
color={ButtonColor.TRANSPARENT}
/>
);
};

const StatusToolbarTimestampWithContext = (props: StatusToolbarTimestampWithContextProps) => (
<PlaybackContext.Consumer>
{(value) => <StatusToolbarTimestamp timelineIndex={value.timelineIndex} {...props} />}
</PlaybackContext.Consumer>
);

export default StatusToolbarTimestampWithContext;
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
import { secondsToTimeCode } from "../../../../../common/utils";
import { FrameCount, FrameRate, TimelineIndex } from "../../../../../common/Flavors";
import { buildStartTimeCode } from "../../../../../common/timeUtils";
import "./TimelinePosition.css";

interface TimelinePositionProps {
frameRate: number;
totalFrames: number;
frameRate: FrameRate;
totalFrames: FrameCount;
}

const TimelinePosition = ({ frameRate, totalFrames }: TimelinePositionProps): JSX.Element => {
const frameCount = totalFrames < 30 ? 30 : totalFrames;
const frameCount: FrameCount = totalFrames < 30 ? 30 : totalFrames;
const frameNumbers = [...Array(frameCount).keys()];

const buildTimeCode = (framePosition: number) => {
const timeInSeconds = framePosition / frameRate;
return secondsToTimeCode(timeInSeconds);
};

return (
<div className="timeline-position">
<div className="timeline-position__inner">
{frameNumbers.map((i) => (
{frameNumbers.map((i: TimelineIndex) => (
<div className="timeline-position__marker" key={i}>
{i % frameRate === 0 && buildTimeCode(i)}
{i % frameRate === 0 && buildStartTimeCode(i, frameRate, false)}
<br />
{i + 1}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ const PreferencesModal = (): JSX.Element => {
validateOnChange
/>
</InputGroup>

<InputGroup row>
<InputSwitch
id="preferencesShowTimestampInSeconds"
checked={userPreferences.showTimestampInSeconds}
onChange={() =>
dispatch(
editUserPreferences({
showTimestampInSeconds: !userPreferences.showTimestampInSeconds,
})
)
}
/>
<InputLabel inputId="preferencesShowTimestampInSeconds">
Show timestamp in seconds on startup
</InputLabel>
</InputGroup>
</ContentBlock>
</Content>
</PageBody>
Expand Down

0 comments on commit 98c16f3

Please sign in to comment.