mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-20 06:29:07 +00:00
Merge branch 'master' into feat/electron
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
# This is a basic workflow to help you get started with Actions
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
# Controls when the action will run.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Pattern matched against refs/tags
|
||||||
|
tags:
|
||||||
|
# Push events to every tag not containing /
|
||||||
|
- '*'
|
||||||
|
|
||||||
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||||
|
jobs:
|
||||||
|
# build a file for windows
|
||||||
|
buildwin:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
CI: ''
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
|
||||||
|
# install and build react
|
||||||
|
- name: Install React dependencies
|
||||||
|
run: yarn install
|
||||||
|
working-directory: ./client
|
||||||
|
- name: Build React App
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./client
|
||||||
|
|
||||||
|
# install and build electron
|
||||||
|
- name: Install Electron + nodejs dependencies
|
||||||
|
run: yarn install
|
||||||
|
working-directory: ./server
|
||||||
|
- name: Build Electron App
|
||||||
|
run: yarn dist-win
|
||||||
|
working-directory: ./server
|
||||||
|
|
||||||
|
# release
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: ./server/dist/ontime-win64.exe
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# build a file for mac
|
||||||
|
buildmac:
|
||||||
|
# The type of runner that the job will run on
|
||||||
|
runs-on: macOS-latest
|
||||||
|
env:
|
||||||
|
CI: ''
|
||||||
|
|
||||||
|
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||||
|
steps:
|
||||||
|
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: '14.x'
|
||||||
|
|
||||||
|
# install and build react
|
||||||
|
- name: Install React dependencies
|
||||||
|
run: yarn install
|
||||||
|
working-directory: ./client
|
||||||
|
- name: Build React App
|
||||||
|
run: yarn build
|
||||||
|
working-directory: ./client
|
||||||
|
|
||||||
|
# install and build electron
|
||||||
|
- name: Install Electron + nodejs dependencies
|
||||||
|
run: yarn install
|
||||||
|
working-directory: ./server
|
||||||
|
- name: Build Electron App
|
||||||
|
run: yarn dist-win
|
||||||
|
working-directory: ./server
|
||||||
|
|
||||||
|
# release
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
files: ./server/dist/ontime-macOS.dmg
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
[](https://opensource.org/licenses/MIT)
|
# Initial page
|
||||||
|
|
||||||
# License
|
|
||||||
This project is licensed under the terms of the MIT license
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Table of contents
|
||||||
|
|
||||||
|
* [Initial page](README.md)
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
"react-fast-compare": "^3.2.0",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.2.0",
|
||||||
"react-qr-code": "^1.1.1",
|
"react-qr-code": "^1.1.1",
|
||||||
"react-query": "^3.13.5",
|
"react-query": "^3.13.5",
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import styles from './MyProgressBar.module.css';
|
|||||||
|
|
||||||
export default function MyProgressBar(props) {
|
export default function MyProgressBar(props) {
|
||||||
const { now, complete, showElapsed } = props;
|
const { now, complete, showElapsed } = props;
|
||||||
const percentComplete = showElapsed
|
|
||||||
? clamp((100 - (now * 100) / complete), 0, 100)
|
let percentComplete = showElapsed ? 0 : 100;
|
||||||
: clamp((now * 100) / complete, 0, 100)
|
|
||||||
|
if (now != null && complete != null) {
|
||||||
|
percentComplete = showElapsed
|
||||||
|
? clamp(100 - (now * 100) / complete, 0, 100)
|
||||||
|
: clamp((now * 100) / complete, 0, 100);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.progress}>
|
<div className={showElapsed ? styles.progress : styles.progressCountdown}>
|
||||||
<div
|
<div
|
||||||
className={styles.progressBar}
|
className={styles.progressBar}
|
||||||
style={{ width: `${percentComplete}%` }}
|
style={{ width: `${percentComplete}%` }}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
.progress {
|
.progress,
|
||||||
|
.progressCountdown {
|
||||||
height: 2vh;
|
height: 2vh;
|
||||||
background-color: rgba(255, 255, 255, 0.13);
|
background-color: rgba(255, 255, 255, 0.13);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
background-color: rgba(255, 255, 255, 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressCountdown {
|
||||||
|
background-color: #ff7597;
|
||||||
|
}
|
||||||
|
|
||||||
.progressBar {
|
.progressBar {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2vh;
|
height: 2vh;
|
||||||
background-color: rgba(255, 255, 255, 0.79);
|
background-color: rgb(255, 255, 255);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: 1s linear;
|
transition: 1s linear;
|
||||||
transition-property: width;
|
transition-property: width;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export default function Paginator(props) {
|
|||||||
const [page, setPage] = useState([]);
|
const [page, setPage] = useState([]);
|
||||||
const [pages, setPages] = useState(0);
|
const [pages, setPages] = useState(0);
|
||||||
const [selPage, setSelPage] = useState(0);
|
const [selPage, setSelPage] = useState(0);
|
||||||
const [selected, setSelected] = useState(-1);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (events == null) return;
|
if (events == null) return;
|
||||||
@@ -33,18 +32,6 @@ export default function Paginator(props) {
|
|||||||
// if array is completely in past, show depending on SCROLL_PAST
|
// if array is completely in past, show depending on SCROLL_PAST
|
||||||
}, [events, selPage, LIMIT_PER_PAGE]);
|
}, [events, selPage, LIMIT_PER_PAGE]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// maybe nothing is selected
|
|
||||||
if (events == null || selectedId == null) return;
|
|
||||||
|
|
||||||
// which one is selected
|
|
||||||
const s = events.filter((e) => e.id === selectedId);
|
|
||||||
|
|
||||||
if (s.length < 1) return;
|
|
||||||
|
|
||||||
setSelected(s[0].id);
|
|
||||||
}, [events, selectedId]);
|
|
||||||
|
|
||||||
// every SCROLL_TIME go to the next array
|
// every SCROLL_TIME go to the next array
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
if (numEvents > LIMIT_PER_PAGE) {
|
if (numEvents > LIMIT_PER_PAGE) {
|
||||||
@@ -68,9 +55,8 @@ export default function Paginator(props) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={style.entries}>
|
<div className={style.entries}>
|
||||||
{page.map((e) => {
|
{page.map((e) => {
|
||||||
if (selectedState === 1) selectedState = 2;
|
if (e.id === selectedId) selectedState = 1;
|
||||||
if (e.id === selected) selectedState = 1;
|
else if (selectedState === 1) selectedState = 2;
|
||||||
else if (e.id > selected) selectedState = 2;
|
|
||||||
return (
|
return (
|
||||||
<TodayItem
|
<TodayItem
|
||||||
key={e.id}
|
key={e.id}
|
||||||
@@ -78,6 +64,7 @@ export default function Paginator(props) {
|
|||||||
timeStart={e.timeStart}
|
timeStart={e.timeStart}
|
||||||
timeEnd={e.timeEnd}
|
timeEnd={e.timeEnd}
|
||||||
title={e.title}
|
title={e.title}
|
||||||
|
backstageEvent={!e.isPublic}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -26,11 +26,13 @@
|
|||||||
margin-left: 0.5vw;
|
margin-left: 0.5vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entryStart,
|
.entryTimes {
|
||||||
.entryEnd {
|
|
||||||
font-family: 'Open Sans', sans-serif;
|
font-family: 'Open Sans', sans-serif;
|
||||||
min-width: 5vw;
|
font-size: 1.4vw;
|
||||||
|
|
||||||
|
min-width: 10vw;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
|
letter-spacing: 0.035em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entryTitle {
|
.entryTitle {
|
||||||
@@ -40,26 +42,42 @@
|
|||||||
.entryPast,
|
.entryPast,
|
||||||
.entryNow,
|
.entryNow,
|
||||||
.entryFuture {
|
.entryFuture {
|
||||||
background-color: rgba(255, 255, 255, 0.03);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 1.3vw;
|
font-size: 1.3vw;
|
||||||
padding: 0.2vh 1vw;
|
padding: 0.2vh 1vw;
|
||||||
margin-bottom: 1.5vh;
|
margin-bottom: 1.5vh;
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entryPast {
|
.entryPast {
|
||||||
color: #aaa;
|
background-color: rgba(255, 255, 255, 0.01);
|
||||||
font-size: 1.2vw;
|
font-size: 1.2vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entryPast > .entryTitle {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
.entryFuture {
|
.entryFuture {
|
||||||
|
background-color: rgba(255, 255, 255, 0.03);
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entryNow {
|
.entryNow {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
line-height: 5vh;
|
line-height: 5vh;
|
||||||
background-color: rgba(255, 255, 255, 0.09);
|
background-color: rgba(255, 255, 255, 0.07);
|
||||||
border-bottom: 0.5vh solid #ff7597aa;
|
border-bottom: 0.5vh solid #ff7597aa;
|
||||||
margin-left: -0.5vw;
|
margin-left: -0.5em;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backstageInd {
|
||||||
|
color: #ff7597aa;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: -0.5vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backstageInd::before {
|
||||||
|
content: '*';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { stringFromMillis } from 'common/dateConfig';
|
import { stringFromMillis } from 'common/dateConfig';
|
||||||
import style from './Paginator.module.css';
|
import style from './Paginator.module.css';
|
||||||
export default function TodayItem(props) {
|
export default function TodayItem(props) {
|
||||||
const { selected, timeStart, timeEnd, title } = props;
|
const { selected, timeStart, timeEnd, title, backstageEvent } = props;
|
||||||
|
|
||||||
// Format timers
|
// Format timers
|
||||||
const start = stringFromMillis(timeStart, false) || '';
|
const start = stringFromMillis(timeStart, false) || '';
|
||||||
@@ -13,9 +13,13 @@ export default function TodayItem(props) {
|
|||||||
else if (selected === 2) selectStyle = style.entryFuture;
|
else if (selected === 2) selectStyle = style.entryFuture;
|
||||||
return (
|
return (
|
||||||
<div className={selectStyle}>
|
<div className={selectStyle}>
|
||||||
<div className={style.entryStart}>{start}</div>
|
<div
|
||||||
<div className={style.entryEnd}>{end}</div>
|
className={`${style.entryTimes} ${
|
||||||
|
backstageEvent ? style.backstage : undefined
|
||||||
|
}`}
|
||||||
|
>{`${start} · ${end}`}</div>
|
||||||
<div className={style.entryTitle}>{title}</div>
|
<div className={style.entryTitle}>{title}</div>
|
||||||
|
{backstageEvent && <div className={style.backstageInd}></div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
|
||||||
|
import { lazy, useEffect } from 'react';
|
||||||
|
import { Heading } from '@chakra-ui/layout';
|
||||||
import { Box } from '@chakra-ui/layout';
|
import { Box } from '@chakra-ui/layout';
|
||||||
import PlaybackControl from '../control/PlaybackControl';
|
import NumberedText from 'common/components/text/NumberedText';
|
||||||
import MessageControl from '../control/MessageControl';
|
|
||||||
import styles from './Editor.module.css';
|
import styles from './Editor.module.css';
|
||||||
import EventListWrapper from './list/EventListWrapper';
|
|
||||||
import { useDisclosure } from '@chakra-ui/hooks';
|
import { useDisclosure } from '@chakra-ui/hooks';
|
||||||
import SettingsModal from '../modals/SettingsModal';
|
import SettingsModal from '../modals/SettingsModal';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import MenuBar from 'features/menu/MenuBar';
|
import MenuBar from 'features/menu/MenuBar';
|
||||||
|
|
||||||
|
const EventListWrapper = lazy(() =>
|
||||||
|
import('features/editors/list/EventListWrapper')
|
||||||
|
);
|
||||||
|
const PlaybackControl = lazy(() => import('features/control/PlaybackControl'));
|
||||||
|
const MessageControl = lazy(() => import('features/control/MessageControl'));
|
||||||
|
|
||||||
export default function Editor() {
|
export default function Editor() {
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
|
|
||||||
|
|||||||
@@ -71,11 +71,7 @@ export default function PresenterView(props) {
|
|||||||
isPlaying ? style.progressContainer : style.progressContainerPaused
|
isPlaying ? style.progressContainer : style.progressContainerPaused
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MyProgressBar
|
<MyProgressBar now={normalisedTime} complete={time.durationSeconds} />
|
||||||
now={normalisedTime}
|
|
||||||
complete={time.durationSeconds}
|
|
||||||
showElapsed
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export default function LowerClean(props) {
|
|||||||
|
|
||||||
// calculate transition times
|
// calculate transition times
|
||||||
useEffect(() => {});
|
useEffect(() => {});
|
||||||
|
|
||||||
// Format messages
|
// Format messages
|
||||||
const showLowerMessage = lower.text !== '' && lower.visible;
|
const showLowerMessage = lower.text !== '' && lower.visible;
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export default function LowerLines(props) {
|
|||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
|
}, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setShowLower(title.showNow);
|
||||||
|
}, [title.showNow]);
|
||||||
|
|
||||||
// Format messages
|
// Format messages
|
||||||
const showLowerMessage = lower.text !== '' && lower.visible;
|
const showLowerMessage = lower.text !== '' && lower.visible;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import LowerClean from './LowerClean';
|
import LowerClean from './LowerClean';
|
||||||
import LowerLines from './LowerLines';
|
import LowerLines from './LowerLines';
|
||||||
|
const isEqual = require('react-fast-compare');
|
||||||
|
|
||||||
export default function Lower(props) {
|
const areEqual = (prevProps, nextProps) => {
|
||||||
|
return (
|
||||||
|
isEqual(prevProps.title, nextProps.title) &&
|
||||||
|
isEqual(prevProps.lower && nextProps.lower)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Lower = (props) => {
|
||||||
|
const { title } = props;
|
||||||
|
const [titles, setTitles] = useState({
|
||||||
|
titleNow: '',
|
||||||
|
titleNext: '',
|
||||||
|
subtitleNow: '',
|
||||||
|
subtitleNext: '',
|
||||||
|
presenterNow: '',
|
||||||
|
presenterNext: '',
|
||||||
|
showNow: false,
|
||||||
|
showNext: false,
|
||||||
|
});
|
||||||
const [preset, setPreset] = useState(1);
|
const [preset, setPreset] = useState(1);
|
||||||
const [lowerOptions, setLowerOptions] = useState({});
|
const [lowerOptions, setLowerOptions] = useState({});
|
||||||
|
|
||||||
@@ -11,6 +30,32 @@ export default function Lower(props) {
|
|||||||
document.title = 'ontime - Lower Thirds';
|
document.title = 'ontime - Lower Thirds';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// reload if data changes
|
||||||
|
useEffect(() => {
|
||||||
|
// clear titles if necessary
|
||||||
|
// will trigger an animation out in the component
|
||||||
|
let timeout = null;
|
||||||
|
if (
|
||||||
|
title?.titleNow !== titles?.titleNow ||
|
||||||
|
title?.subtitleNow !== titles?.subtitleNow ||
|
||||||
|
title?.presenterNow !== titles?.presenterNow
|
||||||
|
) {
|
||||||
|
setTitles((t) => ({ ...t, showNow: false }));
|
||||||
|
|
||||||
|
const transitionTime = 2000;
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setTitles(title);
|
||||||
|
}, transitionTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeout != null) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [title.titleNow, title.subtitleNow, title.presenterNow]);
|
||||||
|
|
||||||
// TODO: sanitize data
|
// TODO: sanitize data
|
||||||
// getting config from URL: preset, size, transition, bg, text, key
|
// getting config from URL: preset, size, transition, bg, text, key
|
||||||
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
|
// eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
|
||||||
@@ -77,10 +122,18 @@ export default function Lower(props) {
|
|||||||
|
|
||||||
switch (preset) {
|
switch (preset) {
|
||||||
case 0:
|
case 0:
|
||||||
return <LowerClean {...props} options={lowerOptions} />;
|
return (
|
||||||
|
<LowerClean lower={props.lower} title={titles} options={lowerOptions} />
|
||||||
|
);
|
||||||
case 1:
|
case 1:
|
||||||
return <LowerLines {...props} options={lowerOptions} />;
|
return (
|
||||||
|
<LowerLines lower={props.lower} title={titles} options={lowerOptions} />
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return <LowerLines {...props} options={lowerOptions} />;
|
return (
|
||||||
|
<LowerLines lower={props.lower} title={titles} options={lowerOptions} />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default memo(Lower, areEqual);
|
||||||
|
|||||||
+1
-1
@@ -10078,7 +10078,7 @@ react-error-overlay@^6.0.9:
|
|||||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||||
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
||||||
|
|
||||||
react-fast-compare@3.2.0:
|
react-fast-compare@3.2.0, react-fast-compare@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|||||||
Reference in New Issue
Block a user