mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
Merge branch 'master' into wip/migratedb
This commit is contained in:
+1
-1
@@ -7,6 +7,6 @@ body,
|
|||||||
html,
|
html,
|
||||||
.App {
|
.App {
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
overflow: hidden;
|
overflow: clip;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,12 +58,3 @@ export const BatchOperation = atom(null, (get, set, payload) => {
|
|||||||
|
|
||||||
localStorage.setItem(PATH, JSON.stringify(options));
|
localStorage.setItem(PATH, JSON.stringify(options));
|
||||||
});
|
});
|
||||||
|
|
||||||
// change collapsed in all items
|
|
||||||
export const setAll = async (items, isCollapsed) => {
|
|
||||||
// clear storage
|
|
||||||
localStorage.removeItem(PATH);
|
|
||||||
|
|
||||||
// call batch
|
|
||||||
BatchOperation({ items: items, isCollapsed: isCollapsed });
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const useFetch = (namespace, fn) => {
|
|||||||
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
const { data, status, isError, refetch } = useQuery(namespace, fn, {
|
||||||
refetchInterval: refetchIntervalMs,
|
refetchInterval: refetchIntervalMs,
|
||||||
cacheTime: Infinity,
|
cacheTime: Infinity,
|
||||||
|
notifyOnChangeProps: 'tracked',
|
||||||
});
|
});
|
||||||
|
|
||||||
return { data, status, isError, refetch };
|
return { data, status, isError, refetch };
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ export default function LockIconBtn(props) {
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
size={props.size || 'xs'}
|
size={props.size || 'xs'}
|
||||||
icon={<FiTarget />}
|
icon={<FiTarget />}
|
||||||
colorScheme='pink'
|
color={active ? 'pink.100' : 'pink.300'}
|
||||||
color={'pink.300'}
|
borderColor={active ? undefined : 'pink.300'}
|
||||||
|
backgroundColor={active ? 'pink.300' : undefined}
|
||||||
variant={active ? 'solid' : 'outline'}
|
variant={active ? 'solid' : 'outline'}
|
||||||
onClick={clickhandler}
|
onClick={clickhandler}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export default function RollIconBtn(props) {
|
|||||||
width={120}
|
width={120}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useInterval } from 'app/hooks/useInterval';
|
|||||||
export default function Paginator(props) {
|
export default function Paginator(props) {
|
||||||
const { events, selectedId } = props;
|
const { events, selectedId } = props;
|
||||||
const LIMIT_PER_PAGE = props.limit || 8;
|
const LIMIT_PER_PAGE = props.limit || 8;
|
||||||
const SCROLL_TIME = props.time * 1000 || 5000;
|
const SCROLL_TIME = props.time * 1000 || 10000;
|
||||||
const SCROLL_PAST = false;
|
const SCROLL_PAST = false;
|
||||||
const [numEvents, setNumEvents] = useState(0);
|
const [numEvents, setNumEvents] = useState(0);
|
||||||
const [page, setPage] = useState([]);
|
const [page, setPage] = useState([]);
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ export default function EditableText(props) {
|
|||||||
>
|
>
|
||||||
<EditablePreview
|
<EditablePreview
|
||||||
color={text === '' ? '#666' : 'inherit'}
|
color={text === '' ? '#666' : 'inherit'}
|
||||||
maxWidth='20em'
|
maxWidth='75%'
|
||||||
/>
|
/>
|
||||||
<EditableInput overflowX='hidden' maxWidth='20em' />
|
<EditableInput overflowX='hidden' maxWidth='75%' />
|
||||||
</Editable>
|
</Editable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -34,12 +34,12 @@ export default function EditableTimer(props) {
|
|||||||
if (value === '') return false;
|
if (value === '') return false;
|
||||||
|
|
||||||
// Time now and time submitedVal
|
// Time now and time submitedVal
|
||||||
const original = stringFromMillis(time, false);
|
const original = stringFromMillis(time + delay, false);
|
||||||
|
|
||||||
// check if time is different from before
|
// check if time is different from before
|
||||||
if (value === original) return false;
|
if (value === original) return false;
|
||||||
|
|
||||||
// conver to millis object
|
// convert to millis object
|
||||||
const millis = timeStringToMillis(value, timeFormat);
|
const millis = timeStringToMillis(value, timeFormat);
|
||||||
|
|
||||||
// validate with parent
|
// validate with parent
|
||||||
@@ -51,14 +51,8 @@ export default function EditableTimer(props) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const showOriginal = () => {
|
|
||||||
setValue(stringFromMillis(time, false));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Editable
|
<Editable
|
||||||
onFocus={() => showOriginal}
|
|
||||||
onEdit={() => showOriginal}
|
|
||||||
onChange={(v) => setValue(v)}
|
onChange={(v) => setValue(v)}
|
||||||
onSubmit={(v) => validateValue(v)}
|
onSubmit={(v) => validateValue(v)}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
@@ -41,10 +41,7 @@ const Transport = ({ selectedId, playbackControl }) => {
|
|||||||
<div className={style.playbackContainer}>
|
<div className={style.playbackContainer}>
|
||||||
<PrevIconBtn clickhandler={() => playbackControl('previous')} />
|
<PrevIconBtn clickhandler={() => playbackControl('previous')} />
|
||||||
<NextIconBtn clickhandler={() => playbackControl('next')} />
|
<NextIconBtn clickhandler={() => playbackControl('next')} />
|
||||||
<UnloadIconBtn
|
<UnloadIconBtn clickhandler={() => playbackControl('unload')} />
|
||||||
clickhandler={() => playbackControl('unload')}
|
|
||||||
disabled={!selectedId}
|
|
||||||
/>
|
|
||||||
<ReloadIconButton
|
<ReloadIconButton
|
||||||
clickhandler={() => playbackControl('reload')}
|
clickhandler={() => playbackControl('reload')}
|
||||||
disabled={!selectedId}
|
disabled={!selectedId}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export default function PlaybackControl() {
|
|||||||
<div className={style.mainContainer}>
|
<div className={style.mainContainer}>
|
||||||
<PlaybackTimer
|
<PlaybackTimer
|
||||||
timer={timer}
|
timer={timer}
|
||||||
|
playback={playback}
|
||||||
handleIncrement={(amount) => socket.emit('increment-timer', amount)}
|
handleIncrement={(amount) => socket.emit('increment-timer', amount)}
|
||||||
/>
|
/>
|
||||||
<PlaybackButtons
|
<PlaybackButtons
|
||||||
|
|||||||
@@ -35,18 +35,19 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-around;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indRoll,
|
.indRoll,
|
||||||
.indDelay,
|
.indDelay,
|
||||||
.indNegative {
|
.indNegative {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.indRoll,
|
.indRoll,
|
||||||
|
.indRollActive,
|
||||||
.indDelay {
|
.indDelay {
|
||||||
|
margin: 0 auto;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 0.8em;
|
width: 0.8em;
|
||||||
height: 0.8em;
|
height: 0.8em;
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
.indNegativeActive {
|
.indNegativeActive {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
height: 0.3em
|
height: 0.3em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indNegativeActive {
|
.indNegativeActive {
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ const areEqual = (prevProps, nextProps) => {
|
|||||||
return (
|
return (
|
||||||
prevProps.timer.running === nextProps.timer.running &&
|
prevProps.timer.running === nextProps.timer.running &&
|
||||||
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
prevProps.timer.expectedFinish === nextProps.timer.expectedFinish &&
|
||||||
prevProps.timer.startedAt === nextProps.timer.startedAt
|
prevProps.timer.startedAt === nextProps.timer.startedAt &&
|
||||||
|
prevProps.playback === nextProps.playback
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const PlaybackTimer = ({ timer, handleIncrement }) => {
|
const PlaybackTimer = (props) => {
|
||||||
|
const { timer, playback, handleIncrement } = props;
|
||||||
const started = stringFromMillis(timer.startedAt, true);
|
const started = stringFromMillis(timer.startedAt, true);
|
||||||
const finish = stringFromMillis(timer.expectedFinish, true);
|
const finish = stringFromMillis(timer.expectedFinish, true);
|
||||||
const isNegative = timer.running < 0;
|
const isNegative = timer.running < 0;
|
||||||
const isDelayed = false;
|
const isDelayed = false;
|
||||||
const isRolling = false;
|
const isRolling = playback === 'roll';
|
||||||
|
|
||||||
const incrementProps = {
|
const incrementProps = {
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
@@ -31,7 +33,7 @@ const PlaybackTimer = ({ timer, handleIncrement }) => {
|
|||||||
<>
|
<>
|
||||||
<div className={style.timeContainer}>
|
<div className={style.timeContainer}>
|
||||||
<div className={style.indicators}>
|
<div className={style.indicators}>
|
||||||
<div className={style.indRoll} />
|
<div className={isRolling ? style.indRollActive : style.indRoll} />
|
||||||
<div
|
<div
|
||||||
className={isNegative ? style.indNegativeActive : style.indNegative}
|
className={isNegative ? style.indNegativeActive : style.indNegative}
|
||||||
/>
|
/>
|
||||||
@@ -49,16 +51,32 @@ const PlaybackTimer = ({ timer, handleIncrement }) => {
|
|||||||
<span className={style.time}>{finish}</span>
|
<span className={style.time}>{finish}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={style.btn}>
|
<div className={style.btn}>
|
||||||
<Button {...incrementProps} onClick={() => handleIncrement(-1)}>
|
<Button
|
||||||
|
{...incrementProps}
|
||||||
|
disabled={isRolling}
|
||||||
|
onClick={() => handleIncrement(-1)}
|
||||||
|
>
|
||||||
-1
|
-1
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...incrementProps} onClick={() => handleIncrement(1)}>
|
<Button
|
||||||
|
{...incrementProps}
|
||||||
|
disabled={isRolling}
|
||||||
|
onClick={() => handleIncrement(1)}
|
||||||
|
>
|
||||||
+1
|
+1
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...incrementProps} onClick={() => handleIncrement(-5)}>
|
<Button
|
||||||
|
{...incrementProps}
|
||||||
|
disabled={isRolling}
|
||||||
|
onClick={() => handleIncrement(-5)}
|
||||||
|
>
|
||||||
-5
|
-5
|
||||||
</Button>
|
</Button>
|
||||||
<Button {...incrementProps} onClick={() => handleIncrement(5)}>
|
<Button
|
||||||
|
{...incrementProps}
|
||||||
|
disabled={isRolling}
|
||||||
|
onClick={() => handleIncrement(5)}
|
||||||
|
>
|
||||||
+5
|
+5
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,11 +18,12 @@
|
|||||||
/* 2/3 window, hide previews */
|
/* 2/3 window, hide previews */
|
||||||
@media (max-width: 1250px) and (min-height: 700px) {
|
@media (max-width: 1250px) and (min-height: 700px) {
|
||||||
.mainContainer {
|
.mainContainer {
|
||||||
|
height: 100%;
|
||||||
grid-template-rows: 1fr 1fr;
|
grid-template-rows: 1fr 1fr;
|
||||||
grid-template-columns: 48em 1fr 1fr;
|
grid-template-columns: 48em 1fr 1fr;
|
||||||
grid-template-areas:
|
/* grid-template-areas:
|
||||||
'even play sett'
|
'even play sett'
|
||||||
'even mess sett';
|
'even mess sett'; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@@ -40,6 +41,11 @@
|
|||||||
'play';
|
'play';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messages,
|
||||||
|
.playback {
|
||||||
|
min-width: 31em;
|
||||||
|
}
|
||||||
|
|
||||||
.editor,
|
.editor,
|
||||||
.info,
|
.info,
|
||||||
.settings {
|
.settings {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function ActionButtons(props) {
|
|||||||
aria-label='Options'
|
aria-label='Options'
|
||||||
size='xs'
|
size='xs'
|
||||||
icon={<FiZap />}
|
icon={<FiZap />}
|
||||||
_expanded={{ bg: 'pink.300', color: 'white' }}
|
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
backgroundColor={'orange.200'}
|
backgroundColor={'orange.200'}
|
||||||
color={'orange.500'}
|
color={'orange.500'}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import { SelectCollapse, HandleCollapse } from 'app/context/collapseAtom';
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
|
|
||||||
const ExpandedBlock = (props) => {
|
const ExpandedBlock = (props) => {
|
||||||
const { provided, data, next, delay, delayValue, actionHandler } = props;
|
const { provided, data, eventIndex, next, delay, delayValue, actionHandler } =
|
||||||
|
props;
|
||||||
|
|
||||||
const oscid = data.id.length > 4 ? '...' : data.id;
|
const oscid = data.id.length > 4 ? '...' : data.id;
|
||||||
|
|
||||||
@@ -80,7 +81,9 @@ const ExpandedBlock = (props) => {
|
|||||||
actionHandler('update', { field: 'note', value: v })
|
actionHandler('update', { field: 'note', value: v })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className={style.oscLabel}>{`OSC ID: ${oscid}`}</span>
|
<span className={style.oscLabel}>
|
||||||
|
{`/ontime/goto ${eventIndex + 1} << OSC >> /ontime/gotoid ${oscid}`}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Icon
|
<Icon
|
||||||
className={style.more}
|
className={style.more}
|
||||||
@@ -156,7 +159,7 @@ const CollapsedBlock = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function EventBlock(props) {
|
export default function EventBlock(props) {
|
||||||
const { data, selected, delay, index, actionHandler } = props;
|
const { data, selected, delay, index, eventIndex, actionHandler } = props;
|
||||||
|
|
||||||
// const [collapsed, setCollapsed] = useState(checkLocalStorage(data.id));
|
// const [collapsed, setCollapsed] = useState(checkLocalStorage(data.id));
|
||||||
|
|
||||||
@@ -200,6 +203,7 @@ export default function EventBlock(props) {
|
|||||||
) : (
|
) : (
|
||||||
<ExpandedBlock
|
<ExpandedBlock
|
||||||
provided={provided}
|
provided={provided}
|
||||||
|
eventIndex={eventIndex}
|
||||||
data={data}
|
data={data}
|
||||||
next={props.next}
|
next={props.next}
|
||||||
delay={delay}
|
delay={delay}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function EventList(props) {
|
|||||||
},
|
},
|
||||||
'Alt+ArrowUp': () => {
|
'Alt+ArrowUp': () => {
|
||||||
if (cursor == null) setCursor(0);
|
if (cursor == null) setCursor(0);
|
||||||
else if (cursor >= 0) setCursor(cursor - 1);
|
else if (cursor > 0) setCursor(cursor - 1);
|
||||||
},
|
},
|
||||||
'Alt+KeyE': (event) => {
|
'Alt+KeyE': (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -49,7 +49,7 @@ export default function EventList(props) {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, [cursor, events.length, eventsHandler]);
|
}, [cursor, events, eventsHandler]);
|
||||||
|
|
||||||
// handle incoming messages
|
// handle incoming messages
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -62,7 +62,6 @@ export default function EventList(props) {
|
|||||||
// Handle playstate
|
// Handle playstate
|
||||||
socket.on('selected', (data) => {
|
socket.on('selected', (data) => {
|
||||||
setSelected(data);
|
setSelected(data);
|
||||||
console.log('debug cursor setted', data);
|
|
||||||
});
|
});
|
||||||
socket.on('next-id', (data) => {
|
socket.on('next-id', (data) => {
|
||||||
setNext(data);
|
setNext(data);
|
||||||
@@ -80,13 +79,20 @@ export default function EventList(props) {
|
|||||||
if (cursorSettings !== 'locked' || selected == null) return;
|
if (cursorSettings !== 'locked' || selected == null) return;
|
||||||
if (selected.index == null) return;
|
if (selected.index == null) return;
|
||||||
|
|
||||||
setCursor(selected.index);
|
let eventIndex = -1;
|
||||||
}, [selected, cursorSettings]);
|
let gotoIndex = -1;
|
||||||
|
for (const e of events) {
|
||||||
|
gotoIndex++;
|
||||||
|
if (e.type === 'event') eventIndex++;
|
||||||
|
if (eventIndex === selected.index) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCursor(gotoIndex);
|
||||||
|
}, [events, selected, cursorSettings]);
|
||||||
|
|
||||||
// attach scroll to cursor
|
// attach scroll to cursor
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cursor == null || cursorRef.current == null) return;
|
if (cursor == null || cursorRef.current == null) return;
|
||||||
console.log('debug cursor scrolling');
|
|
||||||
|
|
||||||
cursorRef.current.scrollIntoView({
|
cursorRef.current.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
@@ -117,8 +123,7 @@ export default function EventList(props) {
|
|||||||
|
|
||||||
console.log('EventList: events in event list', events);
|
console.log('EventList: events in event list', events);
|
||||||
let cumulativeDelay = 0;
|
let cumulativeDelay = 0;
|
||||||
|
let eventIndex = -1;
|
||||||
console.log('debug selected', selected);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.eventContainer}>
|
<div className={style.eventContainer}>
|
||||||
@@ -131,20 +136,28 @@ export default function EventList(props) {
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{events.map((e, index) => {
|
{events.map((e, index) => {
|
||||||
let isCursor = cursor === index;
|
if (index === 0) {
|
||||||
if (index === 0) cumulativeDelay = 0;
|
cumulativeDelay = 0;
|
||||||
|
eventIndex = -1;
|
||||||
|
}
|
||||||
if (e.type === 'delay' && e.duration != null) {
|
if (e.type === 'delay' && e.duration != null) {
|
||||||
cumulativeDelay += e.duration;
|
cumulativeDelay += e.duration;
|
||||||
} else if (e.type === 'block') cumulativeDelay = 0;
|
} else if (e.type === 'block') {
|
||||||
|
cumulativeDelay = 0;
|
||||||
|
} else if (e.type === 'event') {
|
||||||
|
eventIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={isCursor ? cursorRef : undefined}
|
ref={cursor === index ? cursorRef : undefined}
|
||||||
key={e.id}
|
key={e.id}
|
||||||
className={isCursor ? style.cursor : undefined}
|
className={cursor === index ? style.cursor : undefined}
|
||||||
>
|
>
|
||||||
<EventListItem
|
<EventListItem
|
||||||
type={e.type}
|
type={e.type}
|
||||||
index={index}
|
index={index}
|
||||||
|
eventIndex={eventIndex}
|
||||||
data={e}
|
data={e}
|
||||||
selected={selected?.id === e.id}
|
selected={selected?.id === e.id}
|
||||||
next={next === e.id}
|
next={next === e.id}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const EventListItem = (props) => {
|
|||||||
const {
|
const {
|
||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
|
eventIndex,
|
||||||
data,
|
data,
|
||||||
selected,
|
selected,
|
||||||
next,
|
next,
|
||||||
@@ -71,6 +72,7 @@ const EventListItem = (props) => {
|
|||||||
return (
|
return (
|
||||||
<EventBlock
|
<EventBlock
|
||||||
index={index}
|
index={index}
|
||||||
|
eventIndex={eventIndex}
|
||||||
data={data}
|
data={data}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
next={next}
|
next={next}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function MenuActionButtons(props) {
|
|||||||
aria-label='Options'
|
aria-label='Options'
|
||||||
size={props.size || 'xs'}
|
size={props.size || 'xs'}
|
||||||
icon={<FiZap />}
|
icon={<FiZap />}
|
||||||
_expanded={{ bg: 'pink.300', color: 'white' }}
|
_expanded={{ bg: 'orange.300', color: 'white' }}
|
||||||
_focus={{ boxShadow: 'none' }}
|
_focus={{ boxShadow: 'none' }}
|
||||||
backgroundColor={'orange.200'}
|
backgroundColor={'orange.200'}
|
||||||
color={'orange.500'}
|
color={'orange.500'}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const withSocket = (Component) => {
|
|||||||
});
|
});
|
||||||
const [timer, setTimer] = useState({
|
const [timer, setTimer] = useState({
|
||||||
clock: null,
|
clock: null,
|
||||||
currentSeconds: null,
|
running: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
expectedFinish: null,
|
expectedFinish: null,
|
||||||
});
|
});
|
||||||
@@ -221,7 +221,7 @@ const withSocket = (Component) => {
|
|||||||
// get clock string
|
// get clock string
|
||||||
const timeManager = {
|
const timeManager = {
|
||||||
...timer,
|
...timer,
|
||||||
finished: timer.running <= 0 && timer.startedAt,
|
finished: playback === 'start' && timer.running <= 0 && timer.startedAt,
|
||||||
clock: stringFromMillis(timer.clock),
|
clock: stringFromMillis(timer.clock),
|
||||||
playstate: playback,
|
playstate: playback,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function StageManager(props) {
|
|||||||
// Format messages
|
// Format messages
|
||||||
const showPubl = publ.text !== '' && publ.visible;
|
const showPubl = publ.text !== '' && publ.visible;
|
||||||
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||||
if (time.running < 0) stageTimer = '-' + stageTimer;
|
if (time.running < 0) stageTimer = `-${stageTimer}`;
|
||||||
|
|
||||||
// motion
|
// motion
|
||||||
const titleVariants = {
|
const titleVariants = {
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ export default function PresenterView(props) {
|
|||||||
document.title = 'ontime - Speaker Screen';
|
document.title = 'ontime - Speaker Screen';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const showOverlay = pres.text !== '' && pres.visible;
|
const showOverlay = pres.text !== '' && pres.visible;
|
||||||
const isPlaying = time.playstate === 'start';
|
const isPlaying = time.playstate !== 'pause';
|
||||||
|
const normalisedTime = Math.max(time.running, 0);
|
||||||
|
|
||||||
// motion
|
// motion
|
||||||
const titleVariants = {
|
const titleVariants = {
|
||||||
@@ -61,7 +60,7 @@ export default function PresenterView(props) {
|
|||||||
<div className={style.finished}>TIME UP</div>
|
<div className={style.finished}>TIME UP</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
<div className={isPlaying ? style.countdown : style.countdownPaused}>
|
||||||
<Countdown time={time.currentSeconds} hideZeroHours />
|
<Countdown time={normalisedTime} hideZeroHours />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -73,7 +72,7 @@ export default function PresenterView(props) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MyProgressBar
|
<MyProgressBar
|
||||||
now={time.currentSeconds}
|
now={normalisedTime}
|
||||||
complete={time.durationSeconds}
|
complete={time.durationSeconds}
|
||||||
showElapsed
|
showElapsed
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -51,11 +51,8 @@ export default function Pip(props) {
|
|||||||
// Format messages
|
// Format messages
|
||||||
const showInfo =
|
const showInfo =
|
||||||
general.backstageInfo !== '' && general.backstageInfo != null;
|
general.backstageInfo !== '' && general.backstageInfo != null;
|
||||||
|
let stageTimer = formatDisplay(Math.abs(time.running), true);
|
||||||
const stageTimer =
|
if (time.running < 0) stageTimer = `-${stageTimer}`;
|
||||||
time.currentSeconds != null && !isNaN(time.currentSeconds)
|
|
||||||
? formatDisplay(time.currentSeconds, true)
|
|
||||||
: '';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={style.container__gray}>
|
<div className={style.container__gray}>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
"lowdb": "2.1.0",
|
"lowdb": "2.1.0",
|
||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"nanoid": "^3.1.22",
|
"nanoid": "^3.1.22",
|
||||||
"node-osc": "^6.0.1",
|
"node-osc": "6.0.2",
|
||||||
"passport": "~0.4.1",
|
"passport": "~0.4.1",
|
||||||
"passport-local": "~1.0.0",
|
"passport-local": "~1.0.0",
|
||||||
"socket.io": "^4.0.0"
|
"socket.io": "^4.0.0"
|
||||||
|
|||||||
@@ -109,8 +109,29 @@ export class EventTimer extends Timer {
|
|||||||
|
|
||||||
update() {
|
update() {
|
||||||
// if there is nothing selected, no nothing
|
// if there is nothing selected, no nothing
|
||||||
if (this.selectedEventId == null) return;
|
if (this.selectedEventId == null && this.state !== 'roll') return;
|
||||||
super.update();
|
|
||||||
|
// only implement roll here
|
||||||
|
if (this.state !== 'roll') {
|
||||||
|
super.update();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get current time
|
||||||
|
const now = this._getCurrentTime();
|
||||||
|
this.clock = now;
|
||||||
|
|
||||||
|
if (this.selectedEventId && this.current > 0) {
|
||||||
|
// update timer as usual
|
||||||
|
this.current = this._finishAt - now;
|
||||||
|
} else {
|
||||||
|
// look for event if none is loaded
|
||||||
|
if (this.current <= 0 || this.secondaryTimer <= 0) this.rollLoad();
|
||||||
|
|
||||||
|
// count to next event
|
||||||
|
// TODO: replace with proper counter
|
||||||
|
if (this.secondaryTimer != null) this.secondaryTimer -= 1000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
@@ -140,10 +161,11 @@ export class EventTimer extends Timer {
|
|||||||
else if (payload === 'next') this.next();
|
else if (payload === 'next') this.next();
|
||||||
else if (payload === 'reload') this.reload();
|
else if (payload === 'reload') this.reload();
|
||||||
else if (payload === 'unload') this.unload();
|
else if (payload === 'unload') this.unload();
|
||||||
|
else if (payload === 'roll') this.roll();
|
||||||
|
|
||||||
// Not yet implemented
|
// TODO: Cleanup
|
||||||
// else if (payload === 'roll') this.roll();
|
// here tdo this.broadcastState;
|
||||||
|
// remove broadcast from functions
|
||||||
this.broadcastThis('playstate', this.state);
|
this.broadcastThis('playstate', this.state);
|
||||||
this.broadcastThis('selected-id', this.selectedEventId);
|
this.broadcastThis('selected-id', this.selectedEventId);
|
||||||
this.broadcastThis('titles', this.titles);
|
this.broadcastThis('titles', this.titles);
|
||||||
@@ -472,7 +494,7 @@ export class EventTimer extends Timer {
|
|||||||
|
|
||||||
if (eventIndex === -1) return;
|
if (eventIndex === -1) return;
|
||||||
this.pause();
|
this.pause();
|
||||||
this.loadEvent(eventIndex, true, true);
|
this.loadEvent(eventIndex, 'load', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads a given event
|
// Loads a given event
|
||||||
@@ -522,18 +544,12 @@ export class EventTimer extends Timer {
|
|||||||
if (e == null) return;
|
if (e == null) return;
|
||||||
|
|
||||||
// private title is always current
|
// private title is always current
|
||||||
this.titles.titleNow = e.title;
|
|
||||||
this.titles.subtitleNow = e.subtitle;
|
|
||||||
this.titles.presenterNow = e.presenter;
|
|
||||||
this.selectedEventId = e.id;
|
|
||||||
|
|
||||||
// check if current is also public
|
// check if current is also public
|
||||||
if (e.isPublic) {
|
if (e.isPublic) {
|
||||||
this.titlesPublic.titleNow = e.title;
|
this._loadThisTitles(e, 'now');
|
||||||
this.titlesPublic.subtitleNow = e.subtitle;
|
|
||||||
this.titlesPublic.presenterNow = e.presenter;
|
|
||||||
this.selectedPublicEventId = e.id;
|
|
||||||
} else {
|
} else {
|
||||||
|
this._loadThisTitles(e, 'now-private');
|
||||||
|
|
||||||
// assume there is no public event
|
// assume there is no public event
|
||||||
this.titlesPublic.titleNow = null;
|
this.titlesPublic.titleNow = null;
|
||||||
this.titlesPublic.subtitleNow = null;
|
this.titlesPublic.subtitleNow = null;
|
||||||
@@ -549,16 +565,77 @@ export class EventTimer extends Timer {
|
|||||||
this._eventlist[i].type === 'event' &&
|
this._eventlist[i].type === 'event' &&
|
||||||
this._eventlist[i].isPublic
|
this._eventlist[i].isPublic
|
||||||
) {
|
) {
|
||||||
this.titlesPublic.titleNow = this._eventlist[i].title;
|
this._loadThisTitles(this._eventlist[i], 'now-public');
|
||||||
this.titlesPublic.subtitleNow = this._eventlist[i].subtitle;
|
|
||||||
this.titlesPublic.presenterNow = this._eventlist[i].presenter;
|
|
||||||
this.selectedPublicEventId = this._eventlist[i].id;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_loadThisTitles(e, type) {
|
||||||
|
if (e == null) return;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
// now, load to both public and private
|
||||||
|
case 'now':
|
||||||
|
// public
|
||||||
|
this.titlesPublic.titleNow = e.title;
|
||||||
|
this.titlesPublic.subtitleNow = e.subtitle;
|
||||||
|
this.titlesPublic.presenterNow = e.presenter;
|
||||||
|
this.selectedPublicEventId = e.id;
|
||||||
|
|
||||||
|
// private
|
||||||
|
this.titles.titleNow = e.title;
|
||||||
|
this.titles.subtitleNow = e.subtitle;
|
||||||
|
this.titles.presenterNow = e.presenter;
|
||||||
|
this.selectedEventId = e.id;
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'now-public':
|
||||||
|
this.titlesPublic.titleNow = e.title;
|
||||||
|
this.titlesPublic.subtitleNow = e.subtitle;
|
||||||
|
this.titlesPublic.presenterNow = e.presenter;
|
||||||
|
this.selectedPublicEventId = e.id;
|
||||||
|
break;
|
||||||
|
case 'now-private':
|
||||||
|
this.titles.titleNow = e.title;
|
||||||
|
this.titles.subtitleNow = e.subtitle;
|
||||||
|
this.titles.presenterNow = e.presenter;
|
||||||
|
this.selectedEventId = e.id;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// next, load to both public and private
|
||||||
|
case 'next':
|
||||||
|
// public
|
||||||
|
this.titlesPublic.titleNext = e.title;
|
||||||
|
this.titlesPublic.subtitleNext = e.subtitle;
|
||||||
|
this.titlesPublic.presenterNext = e.presenter;
|
||||||
|
this.nextPublicEventId = e.id;
|
||||||
|
|
||||||
|
// private
|
||||||
|
this.titles.titleNext = e.title;
|
||||||
|
this.titles.subtitleNext = e.subtitle;
|
||||||
|
this.titles.presenterNext = e.presenter;
|
||||||
|
this.nextEventId = e.id;
|
||||||
|
break;
|
||||||
|
case 'next-public':
|
||||||
|
this.titlesPublic.titleNext = e.title;
|
||||||
|
this.titlesPublic.subtitleNext = e.subtitle;
|
||||||
|
this.titlesPublic.presenterNext = e.presenter;
|
||||||
|
this.nextPublicEventId = e.id;
|
||||||
|
break;
|
||||||
|
case 'next-private':
|
||||||
|
this.titles.titleNext = e.title;
|
||||||
|
this.titles.subtitleNext = e.subtitle;
|
||||||
|
this.titles.presenterNext = e.presenter;
|
||||||
|
this.nextEventId = e.id;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_loadTitlesNext() {
|
_loadTitlesNext() {
|
||||||
// maybe there is nothing to load
|
// maybe there is nothing to load
|
||||||
if (this.selectedEventIndex == null) return;
|
if (this.selectedEventIndex == null) return;
|
||||||
@@ -583,19 +660,13 @@ export class EventTimer extends Timer {
|
|||||||
if (this._eventlist[i].type === 'event') {
|
if (this._eventlist[i].type === 'event') {
|
||||||
// if we have not set private
|
// if we have not set private
|
||||||
if (!nextPrivate) {
|
if (!nextPrivate) {
|
||||||
this.titles.titleNext = this._eventlist[i].title;
|
this._loadThisTitles(this._eventlist[i], 'next-private');
|
||||||
this.titles.subtitleNext = this._eventlist[i].subtitle;
|
|
||||||
this.titles.presenterNext = this._eventlist[i].presenter;
|
|
||||||
this.nextEventId = this._eventlist[i].id;
|
|
||||||
nextPrivate = true;
|
nextPrivate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if event is public
|
// if event is public
|
||||||
if (this._eventlist[i].isPublic) {
|
if (this._eventlist[i].isPublic) {
|
||||||
this.titlesPublic.titleNext = this._eventlist[i].title;
|
this._loadThisTitles(this._eventlist[i], 'next-public');
|
||||||
this.titlesPublic.subtitleNext = this._eventlist[i].subtitle;
|
|
||||||
this.titlesPublic.presenterNext = this._eventlist[i].presenter;
|
|
||||||
this.nextPublicEventId = this._eventlist[i].id;
|
|
||||||
nextPublic = true;
|
nextPublic = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,6 +713,7 @@ export class EventTimer extends Timer {
|
|||||||
state = ${this.state}
|
state = ${this.state}
|
||||||
current = ${this.current}
|
current = ${this.current}
|
||||||
duration = ${this.duration}
|
duration = ${this.duration}
|
||||||
|
secondaryTimer = ${this.secondaryTimer}
|
||||||
|
|
||||||
Events
|
Events
|
||||||
------------------------------
|
------------------------------
|
||||||
@@ -730,10 +802,77 @@ export class EventTimer extends Timer {
|
|||||||
this.broadcastState();
|
this.broadcastState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rollLoad() {
|
||||||
|
const now = this._getCurrentTime();
|
||||||
|
this._resetTimers(true);
|
||||||
|
this._resetSelection();
|
||||||
|
|
||||||
|
let foundNow = null;
|
||||||
|
let nextIndex = null;
|
||||||
|
let nextStart = null;
|
||||||
|
|
||||||
|
// loop through events, look for where we should be
|
||||||
|
for (const [index, e] of this._eventlist.entries()) {
|
||||||
|
if (!foundNow) {
|
||||||
|
if (e.timeStart <= now && now < e.timeEnd) {
|
||||||
|
// set flag
|
||||||
|
foundNow = true;
|
||||||
|
|
||||||
|
// set timers
|
||||||
|
this._startedAt = e.timeStart;
|
||||||
|
this._finishAt = e.timeEnd;
|
||||||
|
this.duration = e.timeEnd - e.timeStart;
|
||||||
|
this.current = e.timeEnd - now;
|
||||||
|
|
||||||
|
// set selection
|
||||||
|
this.selectedEventId = e.id;
|
||||||
|
this.selectedEventIndex = index;
|
||||||
|
|
||||||
|
// set titles
|
||||||
|
this._loadTitlesNow();
|
||||||
|
|
||||||
|
// skip this entry for next
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// check how far the start is from now
|
||||||
|
let wait = e.timeStart - now;
|
||||||
|
if (wait > 0) {
|
||||||
|
if (nextStart == null || wait < nextStart) {
|
||||||
|
nextStart = wait;
|
||||||
|
nextIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing to play next, unload
|
||||||
|
if (!foundNow && !nextIndex) {
|
||||||
|
this.unload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex) {
|
||||||
|
// load titles
|
||||||
|
this._loadThisTitles(nextIndex, 'next');
|
||||||
|
|
||||||
|
if (!foundNow) {
|
||||||
|
// timer counts to nextStart
|
||||||
|
this.secondaryTimer = nextStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
roll() {
|
roll() {
|
||||||
console.log('roll: not yet implemented');
|
// do we need to change
|
||||||
return false;
|
if (this.state === 'roll') return;
|
||||||
|
|
||||||
|
// load into event
|
||||||
|
this.rollLoad();
|
||||||
|
|
||||||
|
// set state
|
||||||
this.state = 'roll';
|
this.state = 'roll';
|
||||||
|
|
||||||
|
this.broadcastState();
|
||||||
}
|
}
|
||||||
|
|
||||||
previous() {
|
previous() {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export class Timer {
|
|||||||
clock = null;
|
clock = null;
|
||||||
duration = null;
|
duration = null;
|
||||||
current = null;
|
current = null;
|
||||||
|
secondaryTimer = null;
|
||||||
_finishAt = null;
|
_finishAt = null;
|
||||||
_finishedAt = null;
|
_finishedAt = null;
|
||||||
_startedAt = null;
|
_startedAt = null;
|
||||||
@@ -50,6 +51,9 @@ export class Timer {
|
|||||||
// check playstate
|
// check playstate
|
||||||
switch (this.state) {
|
switch (this.state) {
|
||||||
case 'start':
|
case 'start':
|
||||||
|
// ensure we have a start time
|
||||||
|
if (this._startedAt == null) this._startedAt = now;
|
||||||
|
|
||||||
// update current timer
|
// update current timer
|
||||||
this.current =
|
this.current =
|
||||||
this._startedAt + this.duration + this._pausedTotal - now;
|
this._startedAt + this.duration + this._pausedTotal - now;
|
||||||
@@ -110,8 +114,10 @@ export class Timer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetTimers() {
|
_resetTimers(total = false) {
|
||||||
|
if (total) this.duration = null;
|
||||||
this.current = this.duration;
|
this.current = this.duration;
|
||||||
|
this.secondaryTimer = null;
|
||||||
this._finishAt = null;
|
this._finishAt = null;
|
||||||
this._finishedAt = null;
|
this._finishedAt = null;
|
||||||
this._startedAt = null;
|
this._startedAt = null;
|
||||||
@@ -132,7 +138,7 @@ export class Timer {
|
|||||||
return {
|
return {
|
||||||
clock: this.clock,
|
clock: this.clock,
|
||||||
running: Timer.toSeconds(this.current),
|
running: Timer.toSeconds(this.current),
|
||||||
currentSeconds: Timer.toSeconds(Math.max(this.current, 0)),
|
secondary: Timer.toSeconds(this.secondaryTimer),
|
||||||
durationSeconds: Timer.toSeconds(this.duration),
|
durationSeconds: Timer.toSeconds(this.duration),
|
||||||
expectedFinish: this._getExpectedFinish(),
|
expectedFinish: this._getExpectedFinish(),
|
||||||
startedAt: this._startedAt,
|
startedAt: this._startedAt,
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export const initiateOSC = (config) => {
|
|||||||
console.log(`OSC Server is listening on port ${config.port}`);
|
console.log(`OSC Server is listening on port ${config.port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// error
|
||||||
|
oscServer.on('error', console.error);
|
||||||
|
|
||||||
oscServer.on('message', function (msg) {
|
oscServer.on('message', function (msg) {
|
||||||
// message should look like /ontime/{path}/{args} where
|
// message should look like /ontime/{path}/{args} where
|
||||||
// ontime: fixed message for app
|
// ontime: fixed message for app
|
||||||
@@ -62,7 +65,8 @@ export const initiateOSC = (config) => {
|
|||||||
console.log('calling goto with', args);
|
console.log('calling goto with', args);
|
||||||
try {
|
try {
|
||||||
let eventIndex = parseInt(args);
|
let eventIndex = parseInt(args);
|
||||||
if (isNaN(eventIndex) || eventIndex <= 0) return;
|
if (isNaN(eventIndex) || eventIndex <= 0 || eventIndex == null)
|
||||||
|
return;
|
||||||
global.timer.loadEvent(eventIndex - 1, undefined, true);
|
global.timer.loadEvent(eventIndex - 1, undefined, true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error calling goto: ', error);
|
console.log('error calling goto: ', error);
|
||||||
@@ -70,8 +74,9 @@ export const initiateOSC = (config) => {
|
|||||||
break;
|
break;
|
||||||
case 'gotoid':
|
case 'gotoid':
|
||||||
console.log('calling gotoid with', args);
|
console.log('calling gotoid with', args);
|
||||||
|
if (args == null) return;
|
||||||
try {
|
try {
|
||||||
global.timer.loadEventById(args.toLowerCase());
|
global.timer.loadEventById(args.toString().toLowerCase());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('error calling goto: ', error);
|
console.log('error calling goto: ', error);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -438,10 +438,10 @@ negotiator@0.6.2:
|
|||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||||
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
|
||||||
|
|
||||||
node-osc@^6.0.1:
|
node-osc@6.0.2:
|
||||||
version "6.0.1"
|
version "6.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.0.1.tgz#6f2c39996c64460435b678dc91c128d3a9711031"
|
resolved "https://registry.yarnpkg.com/node-osc/-/node-osc-6.0.2.tgz#185d387928b186a84e8d0eebcae9e54a8bf50729"
|
||||||
integrity sha512-UZBQGH4vVTw3Ub5/xcnhlrhL6bpaYz3hhNjO4eup4tknyP5qOi2GVVnEmEn/qaTgTvs7SVyytj3OHg9JWRD4Aw==
|
integrity sha512-RW8PRLMh4H14veC5gZVUTcRkrk5pXd+b4vmitWL55c9ZI+5yEvMXkrokfnRH0u7DYveqw4h+uCPIUDifY/vjfA==
|
||||||
dependencies:
|
dependencies:
|
||||||
osc-min "^1.1.1"
|
osc-min "^1.1.1"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user