This commit is contained in:
Infinite 2020-01-18 21:04:19 +01:00
parent 0cb61d14be
commit c31205c47f
10 changed files with 250 additions and 298 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState } from "react";
import { Form, Col } from 'react-bootstrap';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
@ -22,6 +22,29 @@ function convertTime(logEntry: GQL.LogEntryDataFragment) {
return dateStr;
}
function levelClass(level : string) {
return level.toLowerCase().trim();
}
interface ILogElementProps {
logEntry : LogEntry
}
const LogElement: React.FC<ILogElementProps> = ({ logEntry }) => {
// pad to maximum length of level enum
var level = logEntry.level.padEnd(GQL.LogLevel.Progress.length);
return (
<>
<span>{logEntry.time}</span>&nbsp;
<span className={levelClass(logEntry.level)}>{level}</span>&nbsp;
<span>{logEntry.message}</span>
<br/>
</>
);
}
class LogEntry {
public time: string;
public level: string;
@ -40,120 +63,29 @@ class LogEntry {
}
}
// maximum number of log entries to display. Subsequent entries will truncate
// the list, dropping off the oldest entries first.
const MAX_LOG_ENTRIES = 200;
const logLevels = ["Debug", "Info", "Warning", "Error"];
export const SettingsLogsPanel: React.FC = () => {
const { data, error } = StashService.useLoggingSubscribe();
const { data: existingData } = StashService.useLogs();
const logEntries = useRef<LogEntry[]>([]);
const [logLevel, setLogLevel] = useState<string>("Info");
const [filteredLogEntries, setFilteredLogEntries] = useState<LogEntry[]>([]);
const lastUpdate = useRef<number>(0);
const updateTimeout = useRef<NodeJS.Timeout>();
// maximum number of log entries to display. Subsequent entries will truncate
// the list, dropping off the oldest entries first.
const MAX_LOG_ENTRIES = 200;
const oldData = (existingData?.logs ?? []).map(e => new LogEntry(e));
const newData = (data?.loggingSubscribe ?? []).map(e => new LogEntry(e));
function truncateLogEntries(entries : LogEntry[]) {
entries.length = Math.min(entries.length, MAX_LOG_ENTRIES);
}
const filteredLogEntries = [...newData.reverse(), ...oldData]
.filter(filterByLogLevel).slice(0, MAX_LOG_ENTRIES);
function prependLogEntries(toPrepend : LogEntry[]) {
var newLogEntries = toPrepend.concat(logEntries.current);
truncateLogEntries(newLogEntries);
logEntries.current = newLogEntries;
}
function appendLogEntries(toAppend : LogEntry[]) {
var newLogEntries = logEntries.current.concat(toAppend);
truncateLogEntries(newLogEntries);
logEntries.current = newLogEntries;
}
useEffect(() => {
if (!data) { return; }
// append data to the logEntries
var convertedData = data.loggingSubscribe.map(convertLogEntry);
// filter subscribed data as it comes in, otherwise we'll end up
// truncating stuff that wasn't filtered out
convertedData = convertedData.filter(filterByLogLevel)
// put newest entries at the top
convertedData.reverse();
prependLogEntries(convertedData);
updateFilteredEntries();
}, [data]);
useEffect(() => {
if (!existingData || !existingData.logs) { return; }
var convertedData = existingData.logs.map(convertLogEntry);
appendLogEntries(convertedData);
updateFilteredEntries();
}, [existingData]);
function updateFilteredEntries() {
if (!updateTimeout.current) {
console.log("Updating after timeout");
}
updateTimeout.current = undefined;
var filteredEntries = logEntries.current.filter(filterByLogLevel);
setFilteredLogEntries(filteredEntries);
lastUpdate.current = new Date().getTime();
}
useEffect(() => {
updateFilteredEntries();
}, [logLevel]);
function convertLogEntry(logEntry : GQL.LogEntryDataFragment) {
return new LogEntry(logEntry);
}
function levelClass(level : string) {
return level.toLowerCase().trim();
}
interface ILogElementProps {
logEntry : LogEntry
}
function LogElement(props : ILogElementProps) {
// pad to maximum length of level enum
var level = props.logEntry.level.padEnd(GQL.LogLevel.Progress.length);
return (
<>
<span>{props.logEntry.time}</span>&nbsp;
<span className={levelClass(props.logEntry.level)}>{level}</span>&nbsp;
<span>{props.logEntry.message}</span>
<br/>
</>
);
}
function maybeRenderError() {
if (error) {
return (
<>
<span className={"error"}>Error connecting to log server: {error.message}</span><br/>
</>
);
}
}
const logLevels = ["Debug", "Info", "Warning", "Error"];
const maybeRenderError = error
? <div className={"error"}>Error connecting to log server: {error.message}</div>
: '';
function filterByLogLevel(logEntry : LogEntry) {
if (logLevel === "Debug") {
if (logLevel === "Debug")
return true;
}
var logLevelIndex = logLevels.indexOf(logLevel);
var levelIndex = logLevels.indexOf(logEntry.level);
@ -179,7 +111,7 @@ export const SettingsLogsPanel: React.FC = () => {
</Col>
</Form.Row>
<div className="logs">
{maybeRenderError()}
{maybeRenderError}
{filteredLogEntries.map((logEntry) =>
<LogElement logEntry={logEntry} key={logEntry.id}/>
)}

View File

@ -118,13 +118,14 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
<InputGroup>
<Form.Control
disabled={props.disabled}
defaultValue={value}
value={value}
onChange={(e : any) => setValue(e.target.value)}
onBlur={() => props.onValueChange(stringToSeconds(value))}
placeholder="hh:mm:ss"
/>
<InputGroup.Append>
{maybeRenderReset()}
{ maybeRenderReset() }
{ renderButtons() }
</InputGroup.Append>
</InputGroup>
</Form.Group>

View File

@ -52,7 +52,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
defaultValue={currentDirectory}
/>
<InputGroup.Append>
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : undefined}
{(!data || !data.directories || loading) ? <Spinner animation="border" variant="light" /> : ''}
</InputGroup.Append>
</InputGroup>
@ -75,7 +75,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
{renderDialog()}
<Form.Group>
{selectedDirectories.map((path) => {
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
return <div key={path}>{path} <Button variant="link" onClick={() => onRemoveDirectory(path)}>Remove</Button></div>;
})}
</Form.Group>

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState } from "react";
import { Button, Form, Spinner, Table } from 'react-bootstrap';
import _ from "lodash";
import { useParams, useHistory } from 'react-router-dom';
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
@ -48,7 +47,7 @@ export const Performer: React.FC = () => {
const Scrapers = StashService.useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.ListPerformerScrapersListPerformerScrapers[]>([]);
const { data, error, loading } = StashService.useFindPerformer(id);
const { data, error } = StashService.useFindPerformer(id);
const updatePerformer = StashService.usePerformerUpdate(getPerformerInput() as GQL.PerformerUpdateInput);
const createPerformer = StashService.usePerformerCreate(getPerformerInput() as GQL.PerformerCreateInput);
const deletePerformer = StashService.usePerformerDestroy(getPerformerInput() as GQL.PerformerDestroyInput);
@ -75,19 +74,16 @@ export const Performer: React.FC = () => {
}
useEffect(() => {
setIsLoading(loading);
if (!data || !data.findPerformer || error)
return;
setPerformer(data.findPerformer);
setIsLoading(false);
if(data?.findPerformer)
setPerformer(data.findPerformer);
}, [data]);
useEffect(() => {
setImagePreview(performer.image_path);
setImage(undefined);
updatePerformerEditState(performer);
if (!isNew) {
setIsEditing(false);
}
setIsEditing(false);
}, [performer]);
function onImageLoad(this: FileReader) {

View File

@ -52,8 +52,8 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (props: ISce
<div key={marker.id}>
<hr />
<div>
<a onClick={() => onClickMarker(marker)}>{marker.title}</a>
{!isEditorOpen ? <a style={{float: "right"}} onClick={() => onOpenEditor(marker)}>Edit</a> : undefined}
<Button variant="link" onClick={() => onClickMarker(marker)}>{marker.title}</Button>
{!isEditorOpen ? <Button variant="link" style={{float: "right"}} onClick={() => onOpenEditor(marker)}>Edit</Button> : ''}
</div>
<div>
{TextUtils.secondsToTimestamp(marker.seconds)}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useCallback } from "react";
import { Badge, Button, Card, Collapse, Dropdown, DropdownButton, Form, Table, Spinner } from 'react-bootstrap';
import _ from "lodash";
import { StashService } from "src/core/StashService";
@ -255,10 +255,28 @@ const builtInRecipes = [
}
];
const initialParserInput = {
pattern: "{title}.{ext}",
ignoreWords: [],
whitespaceCharacters: "._",
capitalizeTitle: true,
page: 1,
pageSize: 20,
findClicked: false
};
const initialShowFieldsState = new Map<string, boolean>([
["Title", true],
["Date", true],
["Performers", true],
["Tags", true],
["Studio", true]
]);
export const SceneFilenameParser: React.FC = () => {
const Toast = useToast();
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput());
const [parserInput, setParserInput] = useState<IParserInput>(initialParserInput);
const [allTitleSet, setAllTitleSet] = useState<boolean>(false);
const [allDateSet, setAllDateSet] = useState<boolean>(false);
@ -266,7 +284,7 @@ export const SceneFilenameParser: React.FC = () => {
const [allTagSet, setAllTagSet] = useState<boolean>(false);
const [allStudioSet, setAllStudioSet] = useState<boolean>(false);
const [showFields, setShowFields] = useState<Map<string, boolean>>(initialShowFieldsState());
const [showFields, setShowFields] = useState<Map<string, boolean>>(initialShowFieldsState);
const [totalItems, setTotalItems] = useState<number>(0);
@ -275,71 +293,75 @@ export const SceneFilenameParser: React.FC = () => {
const updateScenes = StashService.useScenesUpdate(getScenesUpdateData());
function initialParserInput() {
return {
pattern: "{title}.{ext}",
ignoreWords: [],
whitespaceCharacters: "._",
capitalizeTitle: true,
page: 1,
pageSize: 20,
findClicked: false
};
}
const determineFieldsToHide = useCallback(() => {
var pattern = parserInput.pattern;
var titleSet = pattern.includes("{title}");
var dateSet = pattern.includes("{date}") ||
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
ParserField.fullDateFields.some((f) => {
return pattern.includes("{" + f.field + "}");
});
var performerSet = pattern.includes("{performer}");
var tagSet = pattern.includes("{tag}");
var studioSet = pattern.includes("{studio}");
function initialShowFieldsState() {
return new Map<string, boolean>([
["Title", true],
["Date", true],
["Performers", true],
["Tags", true],
["Studio", true]
const newShowFields = new Map<string, boolean>([
["Title", titleSet],
["Date", dateSet],
["Performers", performerSet],
["Tags", tagSet],
["Studio", studioSet]
]);
}
function getParserFilter() {
return {
q: parserInput.pattern,
page: parserInput.page,
per_page: parserInput.pageSize,
sort: "path",
direction: GQL.SortDirectionEnum.Asc,
};
}
setShowFields(newShowFields);
}, [parserInput]);
function getParserInput() {
return {
ignoreWords: parserInput.ignoreWords,
whitespaceCharacters: parserInput.whitespaceCharacters,
capitalizeTitle: parserInput.capitalizeTitle
};
}
const parseResults = useCallback((results : GQL.ParseSceneFilenamesResults[]) => {
if (results) {
var result = results.map((r) => {
return new SceneParserResult(r);
}).filter((r) => !!r) as SceneParserResult[];
async function onFind() {
setParserResult([]);
setIsLoading(true);
try {
const response = await StashService.queryParseSceneFilenames(getParserFilter(), getParserInput());
let result = response.data.parseSceneFilenames;
if (result) {
parseResults(result.results);
setTotalItems(result.count);
}
} catch (err) {
Toast.error(err);
setParserResult(result);
determineFieldsToHide();
}
setIsLoading(false);
}
}, [determineFieldsToHide]);
useEffect(() => {
if(parserInput.findClicked) {
onFind();
setParserResult([]);
setIsLoading(true);
const parserFilter = {
q: parserInput.pattern,
page: parserInput.page,
per_page: parserInput.pageSize,
sort: "path",
direction: GQL.SortDirectionEnum.Asc,
};
const parserInputData = {
ignoreWords: parserInput.ignoreWords,
whitespaceCharacters: parserInput.whitespaceCharacters,
capitalizeTitle: parserInput.capitalizeTitle
};
StashService.queryParseSceneFilenames(parserFilter, parserInputData)
.then((response) => {
let result = response.data.parseSceneFilenames;
if (result) {
parseResults(result.results);
setTotalItems(result.count);
}
})
.catch((err) => (
Toast.error(err)
))
.finally(() => (
setIsLoading(false)
));
}
}, [parserInput]);
}, [parserInput, parseResults, Toast]);
function onPageSizeChanged(newSize : number) {
var newInput = _.clone(parserInput);
@ -380,38 +402,6 @@ export const SceneFilenameParser: React.FC = () => {
setIsLoading(false);
}
function parseResults(results : GQL.ParseSceneFilenamesResults[]) {
if (results) {
var result = results.map((r) => {
return new SceneParserResult(r);
}).filter((r) => !!r) as SceneParserResult[];
setParserResult(result);
determineFieldsToHide();
}
}
function determineFieldsToHide() {
var pattern = parserInput.pattern;
var titleSet = pattern.includes("{title}");
var dateSet = pattern.includes("{date}") ||
pattern.includes("{dd}") || // don't worry about other partial date fields since this should be implied
ParserField.fullDateFields.some((f) => {
return pattern.includes("{" + f.field + "}");
});
var performerSet = pattern.includes("{performer}");
var tagSet = pattern.includes("{tag}");
var studioSet = pattern.includes("{studio}");
var showFieldsCopy = _.clone(showFields);
showFieldsCopy.set("Title", titleSet);
showFieldsCopy.set("Date", dateSet);
showFieldsCopy.set("Performers", performerSet);
showFieldsCopy.set("Tags", tagSet);
showFieldsCopy.set("Studio", studioSet);
setShowFields(showFieldsCopy);
}
useEffect(() => {
var newAllTitleSet = !parserResult.some((r) => {
return !r.title.set;
@ -429,21 +419,11 @@ export const SceneFilenameParser: React.FC = () => {
return !r.studioId.set;
});
if (newAllTitleSet !== allTitleSet) {
setAllTitleSet(newAllTitleSet);
}
if (newAllDateSet !== allDateSet) {
setAllDateSet(newAllDateSet);
}
if (newAllPerformerSet !== allPerformerSet) {
setAllTagSet(newAllPerformerSet);
}
if (newAllTagSet !== allTagSet) {
setAllTagSet(newAllTagSet);
}
if (newAllStudioSet !== allStudioSet) {
setAllStudioSet(newAllStudioSet);
}
setAllTitleSet(newAllTitleSet);
setAllDateSet(newAllDateSet);
setAllTagSet(newAllPerformerSet);
setAllTagSet(newAllTagSet);
setAllStudioSet(newAllStudioSet);
}, [parserResult]);
function onSelectAllTitleSet(selected : boolean) {
@ -746,7 +726,7 @@ export const SceneFilenameParser: React.FC = () => {
const elements = parserResult.originalValue
? Array.isArray(parserResult.originalValue)
? parserResult.originalValue.map((el:HasName) => el.name)
: parserResult.originalValue.name
: [parserResult.originalValue.name]
: [];
return (

View File

@ -1,4 +1,5 @@
import React, { CSSProperties, useEffect, useRef, useState } from "react";
import React, { CSSProperties, useEffect, useRef, useState, useCallback } from "react";
import { Button } from 'react-bootstrap';
import axios from "axios";
import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils";
@ -20,6 +21,47 @@ interface ISceneSpriteItem {
h: number;
}
async function fetchSpriteInfo(vttPath: string) {
const response = await axios.get<string>(vttPath, {responseType: "text"});
if (response.status !== 200) {
console.log(response.statusText);
}
// TODO: This is gnarly
const lines = response.data.split("\n");
if (lines.shift() !== "WEBVTT") { return; }
if (lines.shift() !== "") { return; }
let item: ISceneSpriteItem = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
const newSpriteItems: ISceneSpriteItem[] = [];
while (lines.length) {
const line = lines.shift();
if (line === undefined) { continue; }
if (line.includes("#") && line.includes("=") && line.includes(",")) {
const size = line.split("#")[1].split("=")[1].split(",");
item.x = Number(size[0]);
item.y = Number(size[1]);
item.w = Number(size[2]);
item.h = Number(size[3]);
newSpriteItems.push(item);
item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
} else if (line.includes(" --> ")) {
const times = line.split(" --> ");
const start = times[0].split(":");
item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);
const end = times[1].split(":");
item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
}
}
return newSpriteItems;
}
export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props: IScenePlayerScrubberProps) => {
const contentEl = useRef<HTMLDivElement>(null);
const positionIndicatorEl = useRef<HTMLDivElement>(null);
@ -30,8 +72,8 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
const velocity = useRef(0);
const _position = useRef(0);
function getPostion() { return _position.current; }
function setPosition(newPostion: number, shouldEmit: boolean = true) {
const getPosition = useCallback(() => _position.current, []);
const setPosition = useCallback((newPostion: number, shouldEmit: boolean = true) => {
if (!scrubberSliderEl.current || !positionIndicatorEl.current) { return; }
if (shouldEmit) { props.onScrolled(); }
@ -52,10 +94,9 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
(newPostion - midpointOffset) / (bounds - (midpointOffset * 2)) * scrubberSliderEl.current.clientWidth
);
positionIndicatorEl.current.style.transform = `translateX(${indicatorPosition}px)`;
}
}, [props]);
const [spriteItems, setSpriteItems] = useState<ISceneSpriteItem[]>([]);
const [delayedRender, setDelayedRender] = useState(false);
useEffect(() => {
if (!scrubberSliderEl.current) { return; }
@ -63,7 +104,12 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
}, [scrubberSliderEl]);
useEffect(() => {
fetchSpriteInfo();
if (!props.scene.paths.vtt)
return;
fetchSpriteInfo(props.scene.paths.vtt).then((sprites) => {
if(sprites)
setSpriteItems(sprites);
});
}, [props.scene]);
useEffect(() => {
@ -74,7 +120,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
(scrubberSliderEl.current.scrollWidth * percentage) - (scrubberSliderEl.current.clientWidth / 2)
) * -1;
setPosition(position, false);
}, [props.position]);
}, [props.position, props.scene.file.duration, setPosition]);
useEffect(() => {
window.addEventListener("mouseup", onMouseUp, false);
@ -85,19 +131,21 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
useEffect(() => {
if (!contentEl.current) { return; }
contentEl.current.addEventListener("mousedown", onMouseDown, false);
const el = contentEl.current;
el.addEventListener("mousedown", onMouseDown, false);
return () => {
if (!contentEl.current) { return; }
contentEl.current.removeEventListener("mousedown", onMouseDown);
if (!el) { return; }
el.removeEventListener("mousedown", onMouseDown);
};
});
useEffect(() => {
if (!contentEl.current) { return; }
contentEl.current.addEventListener("mousemove", onMouseMove, false);
const el = contentEl.current;
el.addEventListener("mousemove", onMouseMove, false);
return () => {
if (!contentEl.current) { return; }
contentEl.current.removeEventListener("mousemove", onMouseMove);
if (!el) { return; }
el.removeEventListener("mousemove", onMouseMove);
};
});
@ -125,7 +173,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
if (!!seekSeconds) { props.onSeek(seekSeconds); }
} else if (Math.abs(velocity.current) > 25) {
const newPosition = getPostion() + (velocity.current * 10);
const newPosition = getPosition() + (velocity.current * 10);
setPosition(newPosition);
velocity.current = 0;
}
@ -148,7 +196,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
const movement = event.movementX;
velocity.current = movement;
const newPostion = getPostion() + delta;
const newPostion = getPosition() + delta;
setPosition(newPostion);
lastMouseEvent.current = event;
}
@ -160,61 +208,16 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
function goBack() {
if (!scrubberSliderEl.current) { return; }
const newPosition = getPostion() + scrubberSliderEl.current.clientWidth;
const newPosition = getPosition() + scrubberSliderEl.current.clientWidth;
setPosition(newPosition);
}
function goForward() {
if (!scrubberSliderEl.current) { return; }
const newPosition = getPostion() - scrubberSliderEl.current.clientWidth;
const newPosition = getPosition() - scrubberSliderEl.current.clientWidth;
setPosition(newPosition);
}
async function fetchSpriteInfo() {
if (!props.scene || !props.scene.paths.vtt) { return; }
const response = await axios.get<string>(props.scene.paths.vtt, {responseType: "text"});
if (response.status !== 200) {
console.log(response.statusText);
}
// TODO: This is gnarly
const lines = response.data.split("\n");
if (lines.shift() !== "WEBVTT") { return; }
if (lines.shift() !== "") { return; }
let item: ISceneSpriteItem = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
const newSpriteItems: ISceneSpriteItem[] = [];
while (lines.length) {
const line = lines.shift();
if (line === undefined) { continue; }
if (line.includes("#") && line.includes("=") && line.includes(",")) {
const size = line.split("#")[1].split("=")[1].split(",");
item.x = Number(size[0]);
item.y = Number(size[1]);
item.w = Number(size[2]);
item.h = Number(size[3]);
newSpriteItems.push(item);
item = {start: 0, end: 0, x: 0, y: 0, w: 0, h: 0};
} else if (line.includes(" --> ")) {
const times = line.split(" --> ");
const start = times[0].split(":");
item.start = (+start[0]) * 60 * 60 + (+start[1]) * 60 + (+start[2]);
const end = times[1].split(":");
item.end = (+end[0]) * 60 * 60 + (+end[1]) * 60 + (+end[2]);
}
}
setSpriteItems(newSpriteItems);
// TODO: Very hacky. Need to wait for the scroll width to update from the image loading.
setTimeout(() => {
setDelayedRender(true);
}, 100);
}
function renderTags() {
function getTagStyle(i: number): CSSProperties {
if (!scrubberSliderEl.current ||
@ -296,7 +299,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
return (
<div className="scrubber-wrapper">
<a className="scrubber-button" id="scrubber-back" onClick={() => goBack()}>&lt;</a>
<Button variant="link" className="scrubber-button" id="scrubber-back" onClick={() => goBack()}>&lt;</Button>
<div ref={contentEl} className="scrubber-content">
<div className="scrubber-tags-background" />
<div ref={positionIndicatorEl} id="scrubber-position-indicator" />
@ -310,7 +313,7 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = (props:
</div>
</div>
</div>
<a className="scrubber-button" id="scrubber-forward" onClick={() => goForward()}>&gt;</a>
<Button className="scrubber-button" id="scrubber-forward" onClick={() => goForward()}>&gt;</Button>
</div>
);
};

View File

@ -1,4 +1,4 @@
import React, { useState, useContext, createContext } from 'react';
import React, { useEffect, useState, useContext, createContext } from 'react';
import { Toast } from 'react-bootstrap';
interface IToast {
@ -52,19 +52,28 @@ export const ToastProvider: React.FC = ({children}) => {
)
}
const useToasts = () => {
const setToast = useContext(ToastContext);
function createHookObject(toastFunc: (toast:IToast) => void) {
return {
success: setToast,
success: toastFunc,
error: (error: Error) => {
console.error(error.message);
setToast({
toastFunc({
variant: 'danger',
header: 'Error',
content: error.message ?? error.toString()
});
}
};
}
}
const useToasts = () => {
const setToast = useContext(ToastContext);
const [hookObject, setHookObject] = useState(createHookObject(setToast));
useEffect(() => (
setHookObject(createHookObject(setToast))
), [setToast]);
return hookObject;
}
export default useToasts;

View File

@ -4463,6 +4463,13 @@ escodegen@^1.11.0, escodegen@^1.9.1:
optionalDependencies:
source-map "~0.6.1"
eslint-config-prettier@^6.9.0:
version "6.9.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.9.0.tgz#430d24822e82f7deb1e22a435bfa3999fae4ad64"
integrity sha512-k4E14HBtcLv0uqThaI6I/n1LEqROp8XaPu6SO9Z32u5NlGRC07Enu1Bh2KEFw4FNHbekH8yzbIU9kUGxbiGmCA==
dependencies:
get-stdin "^6.0.0"
eslint-config-react-app@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-5.1.0.tgz#a37b3f2d4f56f856f93277281ef52bd791273e63"
@ -4536,6 +4543,13 @@ eslint-plugin-jsx-a11y@6.2.3:
has "^1.0.3"
jsx-ast-utils "^2.2.1"
eslint-plugin-prettier@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.2.tgz#432e5a667666ab84ce72f945c72f77d996a5c9ba"
integrity sha512-GlolCC9y3XZfv3RQfwGew7NnuFDKsfI4lbvRK+PIIo23SFH+LemGs4cKwzAaRa+Mdb+lQO/STaIayno8T5sJJA==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^1.6.1:
version "1.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz#6210b6d5a37205f0b92858f895a4e827020a7d04"
@ -4859,6 +4873,11 @@ fast-deep-equal@^2.0.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^2.0.2:
version "2.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@ -5311,6 +5330,11 @@ get-stdin@^4.0.1:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=
get-stdin@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b"
integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==
get-stream@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
@ -9640,6 +9664,13 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@1.16.4:
version "1.16.4"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.4.tgz#73e37e73e018ad2db9c76742e2647e21790c9717"