mirror of https://github.com/stashapp/stash.git
Linting
This commit is contained in:
parent
0cb61d14be
commit
c31205c47f
File diff suppressed because one or more lines are too long
|
@ -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>
|
||||
<span className={levelClass(logEntry.level)}>{level}</span>
|
||||
<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>
|
||||
<span className={levelClass(props.logEntry.level)}>{level}</span>
|
||||
<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}/>
|
||||
)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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()}><</a>
|
||||
<Button variant="link" className="scrubber-button" id="scrubber-back" onClick={() => goBack()}><</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()}>></a>
|
||||
<Button className="scrubber-button" id="scrubber-forward" onClick={() => goForward()}>></Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue