diff options
author | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2022-06-03 19:04:53 +0300 |
---|---|---|
committer | Alexander Kiryukhin <a.kiryukhin@mail.ru> | 2022-06-03 19:04:53 +0300 |
commit | 0e7b36c2d443306325f17bb8850f5bb6176202bf (patch) | |
tree | 86629d4fe05d73f2d77dad423cc37d5a612430f3 /src/components | |
parent | 15d75cdc37e1459f7d11d004005d4305a6377ffd (diff) |
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/App.js | 97 | ||||
-rw-r--r-- | src/components/TimePicker.js | 22 | ||||
-rw-r--r-- | src/components/Timer.js | 62 | ||||
-rw-r--r-- | src/components/Timers.js | 41 |
4 files changed, 222 insertions, 0 deletions
diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 0000000..4688bbb --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,97 @@ +import { useEffect, useState } from 'react'; +import { CopyTwoTone, PlusCircleFilled, ClearOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Form, Input, Layout, message, Popconfirm, Row, Switch } from 'antd'; + +import useInterval from '../hooks/interval'; +import useLocalStorage from '../hooks/local'; +import { copyTextToClipboard } from '../utils/clipboard'; +import { configFromLink } from '../utils/configFromLink'; +import { defaultTimer } from '../utils/timerTemplate'; +import { updateTimers } from '../utils/updateTimers'; + +import Timers from './Timers'; + +const { Footer } = Layout; + +function App() { + const [browserNotifications, setNotifications] = useLocalStorage("notifications", false); // доступны ли нативные уведомления + const [sound, setSound] = useLocalStorage("sound", true); // включен ли звук + const [timers, setTimers] = useLocalStorage("timers", configFromLink()); // хранилище таймеров + const [url, setUrl] = useState(""); // генерируемая ссылка на кофигурацию + + // Действия при открытии приложения: + // 1. Инициализация конфигурации из адреса + // 2. Запрос разрешений на уведомления + useEffect(() => { + if (window.location.hash !== "") { + setTimers(configFromLink()); + window.location.hash = ""; + } + Notification.requestPermission().then((result) => { + if (result === 'granted') { + setNotifications(true); + } else { + setNotifications(false); + } + }); + // eslint-disable-next-line + }, []); + + // Формирование ссылки на конфигурацию при обновлении таймеров + useEffect(() => { + const d = timers.map(t => [t.name, t.initialTime]); + setUrl(btoa(unescape(encodeURIComponent(JSON.stringify(d))))); + }, [timers]); + + // Обновление таймеров + useInterval(updateTimers.bind(this, timers, setTimers, sound, browserNotifications), 100, true); // интервал опроса таймеров - 100мс потому что если вкладка с таймерами в фоне, браузер начинает заметлять работу js + + return <Layout> + <Row> + <Col xs={24} sm={12} md={12} lg={8} xl={6}> + <Card + title={<strong><img alt="Мультитаймер" src="/logo192.png" style={{ height: 32, width: 32 }} /> Мультитаймер</strong>} + bodyStyle={{ padding: 4 }} + style={{ padding: 4, margin: 4 }} + > + <Form layout='vertical'> + <Form.Item> + <Button type={'primary'} block onClick={() => setTimers([...timers, { ...defaultTimer }])} icon={<PlusCircleFilled />}>Добавить таймер</Button> + </Form.Item> + <Form.Item label="Звук таймера"> + <Switch checkedChildren="Включен" unCheckedChildren="Отключен" checked={sound} onChange={x => setSound(x)} /> + </Form.Item> + <Form.Item label="Ссылка на конфигурацию"> + <Input + readOnly + value={`https://timer.neonxp.dev/#${url}`} + addonAfter={<CopyTwoTone onClick={() => { + copyTextToClipboard(`https://timer.neonxp.dev/#${url}`) + message.success("Скопировано в буфер обмена") + }} />} + /> + </Form.Item> + <Form.Item> + <Popconfirm + title="Очистить таймеры?" + onConfirm={() => setTimers([])} + okText="Да" + cancelText="Нет"> + <Button type={'ghost'} block icon={<ClearOutlined />}>Очистить</Button> + </Popconfirm> + </Form.Item> + </Form> + </Card> + </Col> + <Timers + items={timers} + setTimers={setTimers} + /> + </Row> + <Footer> + Сделал в 2022г <a href="https://neonxp.dev/">Александр NeonXP Кирюхин</a>. + </Footer> + </Layout>; +} + +export default App; diff --git a/src/components/TimePicker.js b/src/components/TimePicker.js new file mode 100644 index 0000000..0419088 --- /dev/null +++ b/src/components/TimePicker.js @@ -0,0 +1,22 @@ +import { InputNumber } from "antd"; + +const TimePicker = ({ setTime, time, disabled }) => { + const seconds = parseInt(Math.floor(time) % 60); + const minutes = parseInt(Math.floor(time / 60) % 60); + const hours = parseInt(Math.floor(time / 3600)); + const updateHours = (hours) => { + setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds)); + } + const updateMinutes = (minutes) => { + setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds)); + } + const updateSeconds = (seconds) => { + setTime(parseInt(hours)*3600+parseInt(minutes)*60+parseInt(seconds)); + } + return <> + <InputNumber disabled={disabled} type="number" min={0} max={24} style={{width: '90px'}} value={hours} onChange={x => {updateHours(x)}} addonAfter={`ч`} /> + <InputNumber disabled={disabled} type="number" min={0} max={59} style={{width: '90px'}} value={minutes} onChange={x => {updateMinutes(x)}} addonAfter={`м`} /> + <InputNumber disabled={disabled} type="number" min={0} max={59} style={{width: '90px'}} value={seconds} onChange={x => {updateSeconds(x)}} addonAfter={`с`} /> + </>; +} +export default TimePicker; diff --git a/src/components/Timer.js b/src/components/Timer.js new file mode 100644 index 0000000..1333040 --- /dev/null +++ b/src/components/Timer.js @@ -0,0 +1,62 @@ +import { DeleteTwoTone, PlayCircleFilled, StopFilled, LeftCircleFilled, RightCircleFilled, DownCircleFilled, UpCircleFilled, EditFilled } from "@ant-design/icons"; +import { Button, Card, Col, Input, Progress, Space, Popconfirm, Grid } from "antd"; +import ButtonGroup from "antd/lib/button/button-group"; +import { useState } from "react"; +import TimePicker from "./TimePicker"; + +const { useBreakpoint } = Grid; + +const Timer = ({ name, started, start, stop, setName, initialTime, time, setTime, remove, index, first, last, moveTimer }) => { + const [edit, setEdit] = useState(false); + const toggleEdit = () => { + setEdit(!edit); + } + const screens = useBreakpoint(); + let seconds = ("0" + (Math.floor(time) % 60)).slice(-2); + let minutes = ("0" + (Math.floor(time / 60) % 60)).slice(-2); + let hours = ("0" + Math.floor(time / 3600)).slice(-2); + return <Col xs={24} sm={12} md={12} lg={8} xl={6}> + <Card + index={index} + title={ + edit + ? (<Input value={name} onChange={ev => setName(ev.target.value)} />) + : (<div style={{ height: 32, overflow: 'hidden', textOverflow: 'ellipsis', width: '100%' }}>{name}</div>) + } + actions={[ + <Button type='primary' block title='Запуск' onClick={start} disabled={started || initialTime === 0} icon={<PlayCircleFilled />}>Запуск</Button>, + <Button type='danger' block title='Стоп' onClick={stop} disabled={!started} icon={<StopFilled />}>Стоп</Button>, + ]} + bodyStyle={{ padding: 4 }} + style={{ padding: 4, margin: 4 }} + > + <Space align="baseline" style={{ justifyContent: "flex-end", display: "flex" }}> + {edit + ? (<ButtonGroup> + <Popconfirm + title="Удалить таймер?" + onConfirm={remove} + okText="Да" + cancelText="Нет"> + <Button icon={<DeleteTwoTone />}></Button> + </Popconfirm> + <Button onClick={toggleEdit} icon={<EditFilled />}></Button> + </ButtonGroup>) + : (<ButtonGroup> + <Button disabled={first} onClick={() => moveTimer(-1)} icon={screens.sm ? <LeftCircleFilled /> : <UpCircleFilled />}></Button> + <Button disabled={last} onClick={() => moveTimer(1)} icon={screens.sm ? <RightCircleFilled /> : <DownCircleFilled />}></Button> + <Button onClick={toggleEdit} icon={<EditFilled />}></Button> + </ButtonGroup>)} + </Space> + <Space direction="vertical" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> + {screens.sm + ? (<Progress type="circle" status="active" percent={100 - (time / initialTime * 100)} format={percent => `${hours}:${minutes}:${seconds}`} width={200} />) + : (<Progress size="large" status="active" percent={100 - (time / initialTime * 100)} format={percent => `${hours}:${minutes}:${seconds}`} width={200} style={{ width: "200px" }} />) + } + <TimePicker disabled={started} time={initialTime} setTime={setTime} /> + </Space> + </Card> + </Col>; +} + +export default Timer; diff --git a/src/components/Timers.js b/src/components/Timers.js new file mode 100644 index 0000000..10b2557 --- /dev/null +++ b/src/components/Timers.js @@ -0,0 +1,41 @@ +import Timer from "./Timer"; + +const Timers = ({ items, setTimers }) => { + const setTime = (idx, time) => { + setTimers(items.map((t, id) => id !== idx ? t : { ...t, time: time, initialTime: time })); + }; + const initialTimer = (idx) => { + setTimers(items.map((t, id) => id !== idx ? t : { ...t, started: true, time: t.initialTime, startedAt: (new Date()).getTime() })); + }; + const stopTimer = (idx) => { + setTimers(items.map((t, id) => id !== idx ? t : { ...t, started: false })); + }; + const setTimerName = (idx, name) => { + setTimers(items.map((t, id) => id !== idx ? t : { ...t, name })); + }; + const deleteTimer = (idx) => { + setTimers(items.filter((t, id) => id !== idx)); + }; + const moveTimer = (idx, offset) => { + [items[idx], items[idx + offset]] = [items[idx + offset], items[idx]]; + setTimers(items); + }; + return items.map((t, idx) => <Timer + key={`item-${idx}`} + index={idx} + first={idx === 0} + last={idx === (items.length-1)} + name={t.name} + started={t.started} + time={t.time || 0} + initialTime={t.initialTime || 0} + setTime={(time) => setTime(idx, time)} + start={() => initialTimer(idx)} + stop={() => stopTimer(idx)} + setName={(name) => setTimerName(idx, name)} + remove={() => deleteTimer(idx)} + moveTimer={(offset) => moveTimer(idx, offset)} + />); +} + +export default Timers; |