mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Add keyboard shortcuts (#637)
* Add documentation * Fix manual styling * Add dialog for setting Movie images * Mention manual in README
This commit is contained in:
parent
3157d748bc
commit
bfeb7d1824
23 changed files with 698 additions and 30 deletions
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
See a demo [here](https://vimeo.com/275537038) (password is stashapp).
|
||||
|
||||
An in-app manual is available, and the manual pages can be viewed [here](https://github.com/stashapp/stash/tree/develop/ui/v2.5/src/docs/en).
|
||||
|
||||
# Docker install
|
||||
|
||||
Follow [this README.md in the docker directory.](docker/production/README.md)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "^5.13.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||
"@types/mousetrap": "^1.6.3",
|
||||
"apollo-cache": "^1.3.4",
|
||||
"apollo-cache-inmemory": "^1.6.5",
|
||||
"apollo-client": "^2.6.8",
|
||||
|
|
@ -50,6 +51,7 @@
|
|||
"jimp": "^0.12.1",
|
||||
"localforage": "1.7.3",
|
||||
"lodash": "^4.17.15",
|
||||
"mousetrap": "^1.6.5",
|
||||
"query-string": "6.12.1",
|
||||
"react": "16.13.1",
|
||||
"react-apollo": "^3.1.5",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import ReactMarkdown from "react-markdown";
|
|||
|
||||
const markup = `
|
||||
### ✨ New Features
|
||||
* Add various keyboard shortcuts (see manual).
|
||||
* Support deleting multiple scenes.
|
||||
* Add in-app help manual.
|
||||
* Add support for custom served folders.
|
||||
* Add support for parent/child studios.
|
||||
|
||||
### 🎨 Improvements
|
||||
* Add dialog when pasting movie images.
|
||||
* Allow click and click-drag selection after selecting scene.
|
||||
* Added multi-scene edit dialog.
|
||||
* Moved images to separate tables, increasing performance.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import Galleries from "src/docs/en/Galleries.md";
|
|||
import Scraping from "src/docs/en/Scraping.md";
|
||||
import Contributing from "src/docs/en/Contributing.md";
|
||||
import SceneFilenameParser from "src/docs/en/SceneFilenameParser.md";
|
||||
import KeyboardShortcuts from "src/docs/en/KeyboardShortcuts.md";
|
||||
import Help from "src/docs/en/Help.md";
|
||||
import { Page } from "./Page";
|
||||
|
||||
|
|
@ -32,7 +33,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||
},
|
||||
{
|
||||
key: "Interface.md",
|
||||
title: "Interface",
|
||||
title: "Interface Options",
|
||||
content: Interface,
|
||||
},
|
||||
{
|
||||
|
|
@ -68,6 +69,11 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||
title: "Metadata Scraping",
|
||||
content: Scraping,
|
||||
},
|
||||
{
|
||||
key: "KeyboardShortcuts.md",
|
||||
title: "Keyboard Shortcuts",
|
||||
content: KeyboardShortcuts,
|
||||
},
|
||||
{
|
||||
key: "Contributing.md",
|
||||
title: "Contributing",
|
||||
|
|
@ -116,7 +122,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||
id="manual-tabs"
|
||||
>
|
||||
<Row>
|
||||
<Col lg={3} className="mb-3 mb-lg-0">
|
||||
<Col lg={3} className="mb-3 mb-lg-0 manual-toc">
|
||||
<Nav variant="pills" className="flex-column">
|
||||
{content.map((c) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -17,22 +17,24 @@
|
|||
color: $text-color;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.manual .manual-content {
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
|
||||
.indent-1 {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.manual .manual-content,
|
||||
.manual .manual-toc {
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.manual .modal-body {
|
||||
overflow-y: auto;
|
||||
|
||||
.manual-content {
|
||||
.manual-content,
|
||||
.manual-toc {
|
||||
max-height: inherit;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||
|
||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
||||
|
||||
// configure keyboard shortcuts
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("f", () => setIsOpen(true));
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("f");
|
||||
};
|
||||
});
|
||||
|
||||
// Configure if we are editing an existing criterion
|
||||
useEffect(() => {
|
||||
if (!props.editingCriterion) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import _, { debounce } from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||
import {
|
||||
Badge,
|
||||
|
|
@ -19,6 +19,7 @@ import { Icon } from "src/components/Shared";
|
|||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useFocus } from "src/utils";
|
||||
import { AddFilter } from "./AddFilter";
|
||||
|
||||
interface IListFilterOperation {
|
||||
|
|
@ -27,6 +28,7 @@ interface IListFilterOperation {
|
|||
}
|
||||
|
||||
interface IListFilterProps {
|
||||
subComponent?: boolean;
|
||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||
zoomIndex?: number;
|
||||
onChangeZoom?: (zoomIndex: number) => void;
|
||||
|
|
@ -40,10 +42,14 @@ interface IListFilterProps {
|
|||
}
|
||||
|
||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||
const minZoom = 0;
|
||||
const maxZoom = 3;
|
||||
|
||||
export const ListFilter: React.FC<IListFilterProps> = (
|
||||
props: IListFilterProps
|
||||
) => {
|
||||
const [queryRef, setQueryFocus] = useFocus();
|
||||
|
||||
const searchCallback = debounce((value: string) => {
|
||||
const newFilter = _.cloneDeep(props.filter);
|
||||
newFilter.searchTerm = value;
|
||||
|
|
@ -55,6 +61,81 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
Criterion | undefined
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("/", (e) => {
|
||||
setQueryFocus();
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
Mousetrap.bind("r", () => onReshuffleRandomSort());
|
||||
Mousetrap.bind("v g", () => {
|
||||
if (props.filter.displayModeOptions.includes(DisplayMode.Grid)) {
|
||||
onChangeDisplayMode(DisplayMode.Grid);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("v l", () => {
|
||||
if (props.filter.displayModeOptions.includes(DisplayMode.List)) {
|
||||
onChangeDisplayMode(DisplayMode.List);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("v w", () => {
|
||||
if (props.filter.displayModeOptions.includes(DisplayMode.Wall)) {
|
||||
onChangeDisplayMode(DisplayMode.Wall);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("+", () => {
|
||||
if (
|
||||
props.onChangeZoom &&
|
||||
props.zoomIndex !== undefined &&
|
||||
props.zoomIndex < maxZoom
|
||||
) {
|
||||
props.onChangeZoom(props.zoomIndex + 1);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("-", () => {
|
||||
if (
|
||||
props.onChangeZoom &&
|
||||
props.zoomIndex !== undefined &&
|
||||
props.zoomIndex > minZoom
|
||||
) {
|
||||
props.onChangeZoom(props.zoomIndex - 1);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("s a", () => onSelectAll());
|
||||
Mousetrap.bind("s n", () => onSelectNone());
|
||||
|
||||
if (!props.subComponent && props.itemsSelected) {
|
||||
Mousetrap.bind("e", () => {
|
||||
if (props.onEdit) {
|
||||
props.onEdit();
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind("d d", () => {
|
||||
if (props.onDelete) {
|
||||
props.onDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("/");
|
||||
Mousetrap.unbind("r");
|
||||
Mousetrap.unbind("v g");
|
||||
Mousetrap.unbind("v l");
|
||||
Mousetrap.unbind("v w");
|
||||
Mousetrap.unbind("+");
|
||||
Mousetrap.unbind("-");
|
||||
Mousetrap.unbind("s a");
|
||||
Mousetrap.unbind("s n");
|
||||
|
||||
if (!props.subComponent && props.itemsSelected) {
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const val = event.currentTarget.value;
|
||||
|
||||
|
|
@ -322,9 +403,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
<Form.Control
|
||||
className="zoom-slider d-none d-sm-inline-flex ml-3"
|
||||
type="range"
|
||||
min={0}
|
||||
max={3}
|
||||
defaultValue={1}
|
||||
min={minZoom}
|
||||
max={maxZoom}
|
||||
value={props.zoomIndex}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
||||
}
|
||||
|
|
@ -369,6 +450,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
<ButtonGroup className="mr-3 my-1">
|
||||
<InputGroup className="mr-2">
|
||||
<FormControl
|
||||
ref={queryRef}
|
||||
placeholder="Search..."
|
||||
defaultValue={props.filter.searchTerm}
|
||||
onInput={onChangeQuery}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
import { Nav, Navbar, Button } from "react-bootstrap";
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||
import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
|
||||
import { SessionUtils } from "src/utils";
|
||||
|
||||
import { Icon } from "src/components/Shared";
|
||||
|
|
@ -90,6 +90,7 @@ const menuItems: IMenuItem[] = [
|
|||
];
|
||||
|
||||
export const MainNavbar: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
|
|
@ -120,7 +121,14 @@ export const MainNavbar: React.FC = () => {
|
|||
};
|
||||
}, [expanded]);
|
||||
|
||||
const path =
|
||||
function goto(page: string) {
|
||||
history.push(page);
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
const newPath =
|
||||
location.pathname === "/performers"
|
||||
? "/performers/new"
|
||||
: location.pathname === "/studios"
|
||||
|
|
@ -129,16 +137,49 @@ export const MainNavbar: React.FC = () => {
|
|||
? "/movies/new"
|
||||
: null;
|
||||
const newButton =
|
||||
path === null ? (
|
||||
newPath === null ? (
|
||||
""
|
||||
) : (
|
||||
<Link to={path}>
|
||||
<Link to={newPath}>
|
||||
<Button variant="primary">
|
||||
<FormattedMessage id="new" defaultMessage="New" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("?", () => setShowManual(!showManual));
|
||||
Mousetrap.bind("g s", () => goto("/scenes"));
|
||||
Mousetrap.bind("g v", () => goto("/movies"));
|
||||
Mousetrap.bind("g k", () => goto("/scenes/markers"));
|
||||
Mousetrap.bind("g l", () => goto("/galleries"));
|
||||
Mousetrap.bind("g p", () => goto("/performers"));
|
||||
Mousetrap.bind("g u", () => goto("/studios"));
|
||||
Mousetrap.bind("g t", () => goto("/tags"));
|
||||
Mousetrap.bind("g z", () => goto("/settings"));
|
||||
|
||||
if (newPath) {
|
||||
Mousetrap.bind("n", () => history.push(newPath));
|
||||
}
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("?");
|
||||
Mousetrap.unbind("g s");
|
||||
Mousetrap.unbind("g v");
|
||||
Mousetrap.unbind("g k");
|
||||
Mousetrap.unbind("g l");
|
||||
Mousetrap.unbind("g p");
|
||||
Mousetrap.unbind("g u");
|
||||
Mousetrap.unbind("g t");
|
||||
Mousetrap.unbind("g z");
|
||||
|
||||
if (newPath) {
|
||||
Mousetrap.unbind("n");
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function maybeRenderLogout() {
|
||||
if (SessionUtils.isLoggedIn()) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
StudioSelect,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { Table, Form } from "react-bootstrap";
|
||||
import { Table, Form, Modal as BSModal, Button } from "react-bootstrap";
|
||||
import {
|
||||
TableUtils,
|
||||
ImageUtils,
|
||||
|
|
@ -34,6 +34,7 @@ export const Movie: React.FC = () => {
|
|||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing movie state
|
||||
const [frontImage, setFrontImage] = useState<string | undefined>(undefined);
|
||||
|
|
@ -57,6 +58,10 @@ export const Movie: React.FC = () => {
|
|||
undefined
|
||||
);
|
||||
|
||||
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
// Network state
|
||||
const { data, error, loading } = useFindMovie(id);
|
||||
const [updateMovie] = useMovieUpdate(getMovieInput() as GQL.MovieUpdateInput);
|
||||
|
|
@ -67,6 +72,42 @@ export const Movie: React.FC = () => {
|
|||
|
||||
const intl = useIntl();
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
Mousetrap.bind("r 0", () => setRating(NaN));
|
||||
Mousetrap.bind("r 1", () => setRating(1));
|
||||
Mousetrap.bind("r 2", () => setRating(2));
|
||||
Mousetrap.bind("r 3", () => setRating(3));
|
||||
Mousetrap.bind("r 4", () => setRating(4));
|
||||
Mousetrap.bind("r 5", () => setRating(5));
|
||||
// Mousetrap.bind("u", (e) => {
|
||||
// setStudioFocus()
|
||||
// e.preventDefault();
|
||||
// });
|
||||
Mousetrap.bind("s s", () => onSave());
|
||||
}
|
||||
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
Mousetrap.unbind("r 0");
|
||||
Mousetrap.unbind("r 1");
|
||||
Mousetrap.unbind("r 2");
|
||||
Mousetrap.unbind("r 3");
|
||||
Mousetrap.unbind("r 4");
|
||||
Mousetrap.unbind("r 5");
|
||||
// Mousetrap.unbind("u");
|
||||
Mousetrap.unbind("s s");
|
||||
}
|
||||
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
|
||||
function updateMovieEditState(state: Partial<GQL.MovieDataFragment>) {
|
||||
setName(state.name ?? undefined);
|
||||
setAliases(state.aliases ?? undefined);
|
||||
|
|
@ -98,8 +139,21 @@ export const Movie: React.FC = () => {
|
|||
}, [data, updateMovieData]);
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setImagePreview(imageData);
|
||||
setFrontImage(imageData);
|
||||
setImageClipboard(imageData);
|
||||
setIsImageAlertOpen(true);
|
||||
}
|
||||
|
||||
function setImageFromClipboard(isFrontImage: boolean) {
|
||||
if (isFrontImage) {
|
||||
setImagePreview(imageClipboard);
|
||||
setFrontImage(imageClipboard);
|
||||
} else {
|
||||
setBackImagePreview(imageClipboard);
|
||||
setBackImage(imageClipboard);
|
||||
}
|
||||
|
||||
setImageClipboard(undefined);
|
||||
setIsImageAlertOpen(false);
|
||||
}
|
||||
|
||||
function onBackImageLoad(imageData: string) {
|
||||
|
|
@ -107,11 +161,7 @@ export const Movie: React.FC = () => {
|
|||
setBackImage(imageData);
|
||||
}
|
||||
|
||||
const encodingFrontImage = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
const encodingBackImage = ImageUtils.usePasteImage(
|
||||
onBackImageLoad,
|
||||
isEditing
|
||||
);
|
||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||
|
||||
if (!isNew && !isEditing) {
|
||||
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
||||
|
|
@ -198,13 +248,50 @@ export const Movie: React.FC = () => {
|
|||
);
|
||||
}
|
||||
|
||||
function renderImageAlert() {
|
||||
return (
|
||||
<BSModal
|
||||
show={isImageAlertOpen}
|
||||
onHide={() => setIsImageAlertOpen(false)}
|
||||
>
|
||||
<BSModal.Body>
|
||||
<p>Select image to set</p>
|
||||
</BSModal.Body>
|
||||
<BSModal.Footer>
|
||||
<div>
|
||||
<Button
|
||||
className="mr-2"
|
||||
variant="secondary"
|
||||
onClick={() => setIsImageAlertOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => setImageFromClipboard(false)}
|
||||
>
|
||||
Back Image
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-2"
|
||||
onClick={() => setImageFromClipboard(true)}
|
||||
>
|
||||
Front Image
|
||||
</Button>
|
||||
</div>
|
||||
</BSModal.Footer>
|
||||
</BSModal>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: CSS class
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="movie-details col">
|
||||
{isNew && <h2>Add Movie</h2>}
|
||||
<div className="logo w-100">
|
||||
{encodingFrontImage || encodingBackImage ? (
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -312,6 +399,7 @@ export const Movie: React.FC = () => {
|
|||
</div>
|
||||
)}
|
||||
{renderDeleteAlert()}
|
||||
{renderImageAlert()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export const Performer: React.FC = () => {
|
|||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [activeTabKey, setActiveTabKey] = useState("details");
|
||||
|
||||
const { data, error } = useFindPerformer(id);
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
|
@ -49,6 +51,23 @@ export const Performer: React.FC = () => {
|
|||
|
||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("a", () => setActiveTabKey("details"));
|
||||
Mousetrap.bind("e", () => setActiveTabKey("edit"));
|
||||
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
||||
Mousetrap.bind("o", () => setActiveTabKey("operations"));
|
||||
Mousetrap.bind("f", () => setFavorite(!performer.favorite));
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("a");
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("c");
|
||||
Mousetrap.unbind("f");
|
||||
Mousetrap.unbind("o");
|
||||
};
|
||||
});
|
||||
|
||||
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
|
||||
return <LoadingIndicator />;
|
||||
|
||||
|
|
@ -100,9 +119,18 @@ export const Performer: React.FC = () => {
|
|||
}
|
||||
|
||||
const renderTabs = () => (
|
||||
<Tabs defaultActiveKey="details" id="performer-details" unmountOnExit>
|
||||
<Tabs
|
||||
activeKey={activeTabKey}
|
||||
onSelect={(k: string) => setActiveTabKey(k)}
|
||||
id="performer-details"
|
||||
unmountOnExit
|
||||
>
|
||||
<Tab eventKey="details" title="Details">
|
||||
<PerformerDetailsPanel performer={performer} isEditing={false} />
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
isEditing={false}
|
||||
isVisible={activeTabKey === "details"}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="scenes" title="Scenes">
|
||||
<PerformerScenesPanel performer={performer} />
|
||||
|
|
@ -111,6 +139,7 @@ export const Performer: React.FC = () => {
|
|||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
isEditing
|
||||
isVisible={activeTabKey === "edit"}
|
||||
isNew={isNew}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
|
|
@ -227,6 +256,7 @@ export const Performer: React.FC = () => {
|
|||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
isEditing
|
||||
isVisible
|
||||
isNew={isNew}
|
||||
onDelete={onDelete}
|
||||
onSave={onSave}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ interface IPerformerDetails {
|
|||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
isNew?: boolean;
|
||||
isEditing?: boolean;
|
||||
isVisible: boolean;
|
||||
onSave?: (
|
||||
performer:
|
||||
| Partial<GQL.PerformerCreateInput>
|
||||
|
|
@ -46,6 +47,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
performer,
|
||||
isNew,
|
||||
isEditing,
|
||||
isVisible,
|
||||
onSave,
|
||||
onDelete,
|
||||
onImageChange,
|
||||
|
|
@ -162,6 +164,29 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
setImage(imageData);
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isEditing && isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
onSave?.(getPerformerInput());
|
||||
});
|
||||
|
||||
if (!isNew) {
|
||||
Mousetrap.bind("d d", () => {
|
||||
setIsDeleteAlertOpen(true);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("s s");
|
||||
|
||||
if (!isNew) {
|
||||
Mousetrap.unbind("d d");
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setImage(undefined);
|
||||
updatePerformerEditState(performer);
|
||||
|
|
|
|||
|
|
@ -18,9 +18,23 @@ export const PerformerList: React.FC = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const addKeybinds = (
|
||||
result: FindPerformersQueryResult,
|
||||
filter: ListFilterModel
|
||||
) => {
|
||||
Mousetrap.bind("p r", () => {
|
||||
getRandom(result, filter);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("p r");
|
||||
};
|
||||
};
|
||||
|
||||
const listData = usePerformersList({
|
||||
otherOperations,
|
||||
renderContent,
|
||||
addKeybinds,
|
||||
});
|
||||
|
||||
async function getRandom(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { LoadingIndicator, Icon } from "src/components/Shared";
|
|||
import { useToast } from "src/hooks";
|
||||
import { ScenePlayer } from "src/components/ScenePlayer";
|
||||
import { TextUtils, JWUtils } from "src/utils";
|
||||
import * as Mousetrap from "mousetrap";
|
||||
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
||||
import { SceneEditPanel } from "./SceneEditPanel";
|
||||
|
|
@ -38,6 +39,8 @@ export const Scene: React.FC = () => {
|
|||
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
|
||||
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
||||
|
||||
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
|
|
@ -177,7 +180,10 @@ export const Scene: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Tab.Container defaultActiveKey="scene-details-panel">
|
||||
<Tab.Container
|
||||
activeKey={activeTabKey}
|
||||
onSelect={(k) => setActiveTabKey(k)}
|
||||
>
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
|
|
@ -224,7 +230,11 @@ export const Scene: React.FC = () => {
|
|||
<SceneDetailPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-markers-panel" title="Markers">
|
||||
<SceneMarkersPanel scene={scene} onClickMarker={onClickMarker} />
|
||||
<SceneMarkersPanel
|
||||
scene={scene}
|
||||
onClickMarker={onClickMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-movie-panel" title="Movies">
|
||||
<SceneMoviePanel scene={scene} />
|
||||
|
|
@ -245,6 +255,7 @@ export const Scene: React.FC = () => {
|
|||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-edit-panel" title="Edit">
|
||||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onUpdate={(newScene) => setScene(newScene)}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
|
|
@ -255,6 +266,23 @@ export const Scene: React.FC = () => {
|
|||
);
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("a", () => setActiveTabKey("scene-details-panel"));
|
||||
Mousetrap.bind("e", () => setActiveTabKey("scene-edit-panel"));
|
||||
Mousetrap.bind("k", () => setActiveTabKey("scene-markers-panel"));
|
||||
Mousetrap.bind("f", () => setActiveTabKey("scene-file-info-panel"));
|
||||
Mousetrap.bind("o", () => onIncrementClick());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("a");
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("k");
|
||||
Mousetrap.unbind("f");
|
||||
Mousetrap.unbind("o");
|
||||
};
|
||||
});
|
||||
|
||||
if (loading || !scene || !data?.findScene) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { RatingStars } from "./RatingStars";
|
|||
|
||||
interface IProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
isVisible: boolean;
|
||||
onUpdate: (scene: GQL.SceneDataFragment) => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
|
@ -65,6 +66,48 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||
|
||||
const [updateScene] = useSceneUpdate(getSceneInput());
|
||||
|
||||
useEffect(() => {
|
||||
if (props.isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
onSave();
|
||||
});
|
||||
Mousetrap.bind("d d", () => {
|
||||
props.onDelete();
|
||||
});
|
||||
|
||||
// numeric keypresses get caught by jwplayer, so blur the element
|
||||
// if the rating sequence is started
|
||||
Mousetrap.bind("r", () => {
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
Mousetrap.bind("0", () => setRating(NaN));
|
||||
Mousetrap.bind("1", () => setRating(1));
|
||||
Mousetrap.bind("2", () => setRating(2));
|
||||
Mousetrap.bind("3", () => setRating(3));
|
||||
Mousetrap.bind("4", () => setRating(4));
|
||||
Mousetrap.bind("5", () => setRating(5));
|
||||
|
||||
setTimeout(() => {
|
||||
Mousetrap.unbind("0");
|
||||
Mousetrap.unbind("1");
|
||||
Mousetrap.unbind("2");
|
||||
Mousetrap.unbind("3");
|
||||
Mousetrap.unbind("4");
|
||||
Mousetrap.unbind("5");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("s s");
|
||||
Mousetrap.unbind("d d");
|
||||
|
||||
Mousetrap.unbind("r");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newQueryableScrapers = (
|
||||
Scrapers?.data?.listSceneScrapers ?? []
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||
|
|
@ -7,6 +7,7 @@ import { SceneMarkerForm } from "./SceneMarkerForm";
|
|||
|
||||
interface ISceneMarkersPanelProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
isVisible: boolean;
|
||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +19,17 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
|||
GQL.SceneMarkerDataFragment
|
||||
>();
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (props.isVisible) {
|
||||
Mousetrap.bind("n", () => onOpenEditor());
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("n");
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
||||
setIsEditorOpen(true);
|
||||
setEditingMarker(marker ?? undefined);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,19 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||
},
|
||||
];
|
||||
|
||||
const addKeybinds = (
|
||||
result: FindScenesQueryResult,
|
||||
filter: ListFilterModel
|
||||
) => {
|
||||
Mousetrap.bind("p r", () => {
|
||||
playRandom(result, filter);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("p r");
|
||||
};
|
||||
};
|
||||
|
||||
const listData = useScenesList({
|
||||
zoomable: true,
|
||||
otherOperations,
|
||||
|
|
@ -40,6 +53,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||
renderDeleteDialog: renderDeleteScenesDialog,
|
||||
subComponent,
|
||||
filterHook,
|
||||
addKeybinds,
|
||||
});
|
||||
|
||||
async function playRandom(
|
||||
|
|
|
|||
|
|
@ -18,9 +18,23 @@ export const SceneMarkerList: React.FC = () => {
|
|||
},
|
||||
];
|
||||
|
||||
const addKeybinds = (
|
||||
result: FindSceneMarkersQueryResult,
|
||||
filter: ListFilterModel
|
||||
) => {
|
||||
Mousetrap.bind("p r", () => {
|
||||
playRandom(result, filter);
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("p r");
|
||||
};
|
||||
};
|
||||
|
||||
const listData = useSceneMarkersList({
|
||||
otherOperations,
|
||||
renderContent,
|
||||
addKeybinds,
|
||||
});
|
||||
|
||||
async function playRandom(
|
||||
|
|
|
|||
|
|
@ -68,6 +68,25 @@ export const Studio: React.FC = () => {
|
|||
setStudio(studioData);
|
||||
}
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
if (isEditing) {
|
||||
Mousetrap.bind("s s", () => onSave());
|
||||
}
|
||||
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
Mousetrap.unbind("s s");
|
||||
}
|
||||
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("d d");
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data && data.findStudio) {
|
||||
setImage(undefined);
|
||||
|
|
|
|||
155
ui/v2.5/src/docs/en/KeyboardShortcuts.md
Normal file
155
ui/v2.5/src/docs/en/KeyboardShortcuts.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Keyboard Shortcuts
|
||||
|
||||
## Global shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `?` | Display manual |
|
||||
|
||||
### Global Navigation
|
||||
|
||||
| Keyboard sequence | Target page |
|
||||
|-------------------|--------|
|
||||
| `g s` | Scenes |
|
||||
| `g v` | Movies |
|
||||
| `g k` | Markers |
|
||||
| `g l` | Galleries |
|
||||
| `g p` | Performers |
|
||||
| `g u` | Studios |
|
||||
| `g t` | Tags |
|
||||
| `g z` | Settings |
|
||||
|
||||
## Query page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `/` | Focus search field |
|
||||
| `f` | Show Add Filter dialog |
|
||||
| `r` | Reshuffle if sorted by random |
|
||||
| `v g` | Set view to grid |
|
||||
| `v l` | Set view to list |
|
||||
| `v w` | Set view to wall |
|
||||
| `+` | Increase zoom slider |
|
||||
| `-` | Decrease zoom slider |
|
||||
| `←` | Previous page of results |
|
||||
| `→` | Next page of results |
|
||||
| `Shift + ←` | Go to current results page -10 |
|
||||
| `Shift + →` | Go to current results page +10 |
|
||||
| `Ctrl + Home` | Go to first page of results |
|
||||
| `Ctrl + End` | Go to last page of results |
|
||||
| `s a` | Select all on page |
|
||||
| `s n` | Unselect all |
|
||||
| `e` | Edit selected |
|
||||
| `d d` | Delete selected |
|
||||
|
||||
## Scenes page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `p r` | Play random scene |
|
||||
|
||||
## Scene page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `a` | Details tab |
|
||||
| `k` | Markers tab |
|
||||
| `f` | File info tab |
|
||||
| `e` | Edit tab |
|
||||
| `o` | Increment O-Counter |
|
||||
|
||||
### Scene Markers tab shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `n` | Display Create Markers dialog |
|
||||
|
||||
### Edit Scene tab shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `r {1-5}` | Set rating |
|
||||
| `r 0` | Unset rating |
|
||||
| `s s` | Save Scene |
|
||||
| `d d` | Delete Scene |
|
||||
| `Ctrl + v` | Paste Scene cover |
|
||||
|
||||
[//]: # "Commented until implementation is dealt with"
|
||||
[//]: # "(| `l` | Focus Gallery selector |)"
|
||||
[//]: # "(| `u` | Focus Studio selector |)"
|
||||
[//]: # "(| `p` | Focus Performers selector |)"
|
||||
[//]: # "(| `v` | Focus Movies selector |)"
|
||||
[//]: # "(| `t` | Focus Tags selector |)"
|
||||
|
||||
## Movies Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `n` | New Movie |
|
||||
|
||||
## Movie Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `e` | Edit Movie |
|
||||
| `s s` | Save Movie |
|
||||
| `d d` | Delete Movie |
|
||||
| `r {1-5}` | Set rating (in edit mode) |
|
||||
| `r 0` | Unset rating (in edit mode) |
|
||||
| `Ctrl + v` | Paste Movie image |
|
||||
|
||||
[//]: # "Commented until implementation is dealt with"
|
||||
[//]: # "(| `u` | Focus Studio selector (in edit mode) |)"
|
||||
|
||||
## Markers Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `p r` | Play random marker |
|
||||
|
||||
## Performers Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `n` | New Performer |
|
||||
| `p r` | Open random Performer |
|
||||
|
||||
## Performer Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `a` | Details tab |
|
||||
| `c` | Scenes tab |
|
||||
| `e` | Edit tab |
|
||||
| `o` | Operations tab |
|
||||
| `f` | Toggle favourite |
|
||||
|
||||
### Edit Performer tab shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `s s` | Save Performer |
|
||||
| `d d` | Delete Performer |
|
||||
| `Ctrl + v` | Paste Performer image |
|
||||
|
||||
## Studios Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `n` | New Studio |
|
||||
|
||||
## Studio Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `e` | Edit Studio |
|
||||
| `s s` | Save Studio |
|
||||
| `d d` | Delete Studio |
|
||||
| `Ctrl + v` | Paste Studio image |
|
||||
|
||||
## Tags Page shortcuts
|
||||
|
||||
| Keyboard sequence | Action |
|
||||
|-------------------|--------|
|
||||
| `n` | New Tag |
|
||||
|
||||
|
|
@ -69,6 +69,11 @@ interface IListHookOptions<T, E> {
|
|||
selected: E[],
|
||||
onClose: (confirmed: boolean) => void
|
||||
) => JSX.Element | undefined;
|
||||
addKeybinds?: (
|
||||
result: T,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => () => void;
|
||||
}
|
||||
|
||||
interface IDataItem {
|
||||
|
|
@ -112,6 +117,53 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
const totalCount = options.getCount(result);
|
||||
const items = options.getData(result);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("right", () => {
|
||||
const maxPage = totalCount / filter.itemsPerPage;
|
||||
if (filter.currentPage < maxPage) {
|
||||
onChangePage(filter.currentPage + 1);
|
||||
}
|
||||
});
|
||||
Mousetrap.bind("left", () => {
|
||||
if (filter.currentPage > 1) {
|
||||
onChangePage(filter.currentPage - 1);
|
||||
}
|
||||
});
|
||||
|
||||
Mousetrap.bind("shift+right", () => {
|
||||
const maxPage = totalCount / filter.itemsPerPage + 1;
|
||||
onChangePage(Math.min(maxPage, filter.currentPage + 10));
|
||||
});
|
||||
Mousetrap.bind("shift+left", () => {
|
||||
onChangePage(Math.max(1, filter.currentPage - 10));
|
||||
});
|
||||
Mousetrap.bind("ctrl+end", () => {
|
||||
const maxPage = totalCount / filter.itemsPerPage + 1;
|
||||
onChangePage(maxPage);
|
||||
});
|
||||
Mousetrap.bind("ctrl+home", () => {
|
||||
onChangePage(1);
|
||||
});
|
||||
|
||||
let unbindExtras: () => void;
|
||||
if (options.addKeybinds) {
|
||||
unbindExtras = options.addKeybinds(result, filter, selectedIds);
|
||||
}
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("right");
|
||||
Mousetrap.unbind("left");
|
||||
Mousetrap.unbind("shift+right");
|
||||
Mousetrap.unbind("shift+left");
|
||||
Mousetrap.unbind("ctrl+end");
|
||||
Mousetrap.unbind("ctrl+home");
|
||||
|
||||
if (unbindExtras) {
|
||||
unbindExtras();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const updateInterfaceConfig = useCallback(
|
||||
(updatedFilter: ListFilterModel) => {
|
||||
setInterfaceState((config) => {
|
||||
|
|
@ -354,6 +406,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||
const template = (
|
||||
<div>
|
||||
<ListFilter
|
||||
subComponent={options.subComponent}
|
||||
onFilterUpdate={updateQueryParams}
|
||||
onSelectAll={onSelectAll}
|
||||
onSelectNone={onSelectNone}
|
||||
|
|
|
|||
16
ui/v2.5/src/utils/focus.ts
Normal file
16
ui/v2.5/src/utils/focus.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { useRef } from "react";
|
||||
|
||||
const useFocus = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const htmlElRef = useRef<any>();
|
||||
const setFocus = () => {
|
||||
const currentEl = htmlElRef.current;
|
||||
if (currentEl) {
|
||||
currentEl.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return [htmlElRef, setFocus] as const;
|
||||
};
|
||||
|
||||
export default useFocus;
|
||||
|
|
@ -9,3 +9,4 @@ export { default as JWUtils } from "./jwplayer";
|
|||
export { default as SessionUtils } from "./session";
|
||||
export { default as flattenMessages } from "./flattenMessages";
|
||||
export { default as getISOCountry } from "./country";
|
||||
export { default as useFocus } from "./focus";
|
||||
|
|
|
|||
|
|
@ -2860,6 +2860,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
|
||||
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
|
||||
|
||||
"@types/mousetrap@^1.6.3":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/mousetrap/-/mousetrap-1.6.3.tgz#3159a01a2b21c9155a3d8f85588885d725dc987d"
|
||||
integrity sha512-13gmo3M2qVvjQrWNseqM3+cR6S2Ss3grbR2NZltgMq94wOwqJYQdgn8qzwDshzgXqMlSUtyPZjysImmktu22ew==
|
||||
|
||||
"@types/node@*":
|
||||
version "13.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.2.tgz#fe94285bf5e0782e1a9e5a8c482b1c34465fa385"
|
||||
|
|
@ -10198,6 +10203,11 @@ moment@~2.25.0:
|
|||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.1.tgz#1cb546dca1eccdd607c9324747842200b683465d"
|
||||
integrity sha512-nRKMf9wDS4Fkyd0C9LXh2FFXinD+iwbJ5p/lh3CHitW9kZbRbJ8hCruiadiIXZVbeAqKZzqcTvHnK3mRhFjb6w==
|
||||
|
||||
mousetrap@^1.6.5:
|
||||
version "1.6.5"
|
||||
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.6.5.tgz#8a766d8c272b08393d5f56074e0b5ec183485bf9"
|
||||
integrity sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==
|
||||
|
||||
move-concurrently@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||
|
|
|
|||
Loading…
Reference in a new issue