Path: blob/master/src/packages/frontend/editors/stopwatch/stopwatch.tsx
1691 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6The stopwatch and timer component7*/89import {10DeleteTwoTone,11EditTwoTone,12PauseCircleTwoTone,13PlayCircleTwoTone,14StopTwoTone,15} from "@ant-design/icons";16import { Button, Col, Modal, Row, TimePicker, Tooltip } from "antd";17import type { Dayjs } from "dayjs";18import dayjs from "dayjs";19import { CSSProperties, useEffect, useState } from "react";2021import { redux, useForceUpdate } from "@cocalc/frontend/app-framework";22import { Icon } from "@cocalc/frontend/components/icon";23import MarkdownInput from "@cocalc/frontend/editors/markdown-input/multimode";24import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";25import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context";26import { webapp_client } from "@cocalc/frontend/webapp-client";27import { TimerState } from "./actions";28import { TimeAmount } from "./time";2930function assertNever(x: never): never {31throw new Error("Unexpected object: " + x);32}3334interface StopwatchProps {35state: TimerState; // 'paused' or 'running' or 'stopped'36time: number; // when entered this state37countdown?: number; // if given, this is a countdown timer, counting down from this many seconds.38clickButton?: (str: string) => void;39setLabel?: (str: string) => void;40setCountdown?: (time: number) => void; // time in seconds41compact?: boolean;42label?: string; // a text label43noLabel?: boolean; // show no label at all44noDelete?: boolean; // do not show delete button45noButtons?: boolean; // hide ALL buttons46total?: number; // total time accumulated before entering current state47style?: CSSProperties;48timeStyle?: CSSProperties;49readOnly?: boolean; // can't change, and won't display something when timer goes off!50}5152export default function Stopwatch(props: StopwatchProps) {53const [editingLabel, setEditingLabel] = useState<boolean>(false);54const [editingTime, setEditingTime] = useState<boolean>(false);55const update = useForceUpdate();56const frame = useFrameContext();5758useEffect(() => {59const interval = setInterval(update, 1000);60return () => clearInterval(interval);61}, []);6263function renderStartButton() {64return (65<Tooltip66title={`Start the ${67props.countdown != null ? "countdown timer" : "stopwatch"68}`}69mouseEnterDelay={1}70>71<Button72icon={<PlayCircleTwoTone />}73onClick={() => props.clickButton?.("start")}74style={!props.compact ? { width: "8em" } : undefined}75>76{!props.compact ? "Start" : undefined}77</Button>78</Tooltip>79);80}8182function renderResetButton() {83return (84<Tooltip85mouseEnterDelay={1}86title={87<>88Reset the{" "}89{props.countdown != null ? "countdown timer" : "stopwatch"} to{" "}90{props.countdown != null ? (91<TimeAmount compact amount={props.countdown * 1000} />92) : (93"0"94)}95</>96}97>98<Button99icon={<StopTwoTone />}100onClick={() => props.clickButton?.("reset")}101>102{!props.compact ? "Reset" : undefined}103</Button>104</Tooltip>105);106}107108function renderEditTimeButton() {109const { setCountdown } = props;110if (setCountdown == null) return;111return (112<div>113<Tooltip title="Edit countdown timer" mouseEnterDelay={1}>114<Button icon={<EditTwoTone />} onClick={() => setEditingTime(true)}>115{!props.compact ? "Edit" : undefined}116</Button>117</Tooltip>118{editingTime ? (119<TimePicker120open121defaultValue={getCountdownMoment(props.countdown)}122onChange={(time) => {123if (time != null) {124setCountdown(125time.second() + time.minute() * 60 + time.hour() * 60 * 60,126);127// timeout so the setcountdown can fully propagate through flux; needed for whiteboard128setTimeout(() => props.clickButton?.("reset"), 0);129}130}}131showNow={false}132onOpenChange={(open) => {133if (!open) {134setEditingTime(false);135}136}}137/>138) : undefined}139</div>140);141}142143function renderDeleteButton() {144if (props.compact || props.noDelete) return;145return (146<Tooltip147mouseEnterDelay={1}148title={`Delete this ${149props.countdown != null ? "countdown timer" : "stopwatch"150}`}151>152<Button153icon={<DeleteTwoTone />}154onClick={() => props.clickButton?.("delete")}155>156{!props.compact ? "Delete" : undefined}157</Button>158</Tooltip>159);160}161162function renderPauseButton() {163return (164<Tooltip mouseEnterDelay={1} title="Pause the stopwatch">165<Button166icon={<PauseCircleTwoTone />}167onClick={() => props.clickButton?.("pause")}168style={!props.compact ? { width: "8em" } : undefined}169>170{!props.compact ? "Pause" : undefined}171</Button>172</Tooltip>173);174}175176function getRemainingMs(): number {177let amount: number = 0;178switch (props.state) {179case "stopped":180break;181case "paused":182amount = props.total || 0;183break;184case "running":185amount =186(props.total || 0) + (webapp_client.server_time() - props.time);187break;188default:189assertNever(props.state);190}191192if (props.countdown != null) {193// it's a countdown timer.194amount = Math.max(0, 1000 * props.countdown - amount);195}196return amount;197}198199function renderTime() {200const amount = getRemainingMs();201return (202<>203<TimeAmount204key={"time"}205amount={amount}206compact={props.compact}207showIcon={props.compact}208countdown={props.countdown}209style={{210...props.timeStyle,211...(props.countdown && amount == 0212? {213background: "#b71c1c",214borderRadius: "3px",215marginRight: "15px",216color: "white",217}218: undefined),219}}220/>221{props.countdown && amount == 0 && !props.readOnly && (222<Modal223title={224<>225<Icon name="hourglass-half" /> A Countdown Timer in "226{frame.path}" is Finished227</>228}229open230onOk={() => {231props.clickButton?.("reset");232redux233.getProjectActions(frame.project_id)234?.open_file({ path: frame.path });235}}236onCancel={() => {237props.clickButton?.("reset");238}}239>240{props.label && <StaticMarkdown value={props.label} />}241</Modal>242)}243</>244);245}246247function renderLabel() {248if (editingLabel) {249return renderEditingLabel();250}251return (252<div253key="show-label"254style={{255fontSize: "16px",256marginTop: "25px",257width: "100%",258color: props.label ? "#444" : "#999",259borderBottom: "1px solid #999",260marginBottom: "10px",261}}262onClick={() => setEditingLabel(true)}263>264{props.label ? <StaticMarkdown value={props.label} /> : "Label"}265</div>266);267}268269function renderEditingLabel() {270return (271<div272key="edit-label"273style={{274fontSize: "25px",275marginTop: "25px",276width: "100%",277}}278>279<MarkdownInput280autoFocus281height="150px"282value={props.label ? props.label : ""}283onChange={(value) => {284props.setLabel?.(value);285}}286onShiftEnter={() => setEditingLabel(false)}287onBlur={() => setEditingLabel(false)}288/>289</div>290);291}292293function renderActionButtons() {294switch (props.state) {295case "stopped":296return (297<Button.Group>298{renderStartButton()}299{renderEditTimeButton()}300</Button.Group>301);302case "paused":303return (304<Button.Group>305{renderStartButton()}306{renderResetButton()}307{renderEditTimeButton()}308</Button.Group>309);310case "running":311return (312<Button.Group>313{renderPauseButton()}314{renderResetButton()}315</Button.Group>316);317default:318assertNever(props.state);319// TS doesn't have strong enough type inference here??320return <div />;321}322}323324function renderButtons() {325return (326<div key="buttons">327{renderActionButtons()}328<div style={{ float: "right" }}>{renderDeleteButton()}</div>329</div>330);331}332333function renderFullSize() {334if (props.noLabel) {335return (336<div337style={{338borderBottom: "1px solid #666",339background: "#efefef",340padding: "15px",341...props.style,342}}343>344<div>{renderTime()}</div>345<div>{renderButtons()}</div>346</div>347);348}349return (350<div351style={{352borderBottom: "1px solid #666",353background: "#efefef",354padding: "15px",355}}356>357<div style={{ float: "right", fontSize: "24px", color: "#666" }}>358{props.countdown != null ? (359<Tooltip title="Countdown Timer" mouseEnterDelay={1}>360<Icon name="hourglass-half" />361</Tooltip>362) : (363<Tooltip title="Stopwatch" mouseEnterDelay={1}>364<Icon name="stopwatch" />365</Tooltip>366)}367</div>368<Row>369<Col sm={12} md={12}>370{renderTime()}371</Col>372<Col sm={12} md={12}>373{renderLabel()}374</Col>375</Row>376{!props.noButtons && !props.readOnly && (377<Row style={{ marginTop: "5px" }}>378<Col md={24}>{renderButtons()}</Col>379</Row>380)}381</div>382);383}384385if (props.compact) {386return (387<div style={{ display: "flex" }}>388{renderTime()}389{!props.noButtons && !props.readOnly && (390<div style={{ marginTop: "3px", marginLeft: "5px" }}>391{renderButtons()}392</div>393)}394</div>395);396} else {397return renderFullSize();398}399}400401export function getCountdownMoment(countdown: number | undefined): Dayjs {402let amount = Math.round(countdown ?? 0);403const m = dayjs();404m.second(amount % 60);405amount = (amount - (amount % 60)) / 60;406m.minute(amount % 60);407amount = (amount - (amount % 60)) / 60;408m.hour(amount);409return m;410}411412413