mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03: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:
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
See a demo [here](https://vimeo.com/275537038) (password is stashapp).
|
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
|
# Docker install
|
||||||
|
|
||||||
Follow [this README.md in the docker directory.](docker/production/README.md)
|
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-regular-svg-icons": "^5.13.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
"@fortawesome/free-solid-svg-icons": "^5.13.0",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.9",
|
"@fortawesome/react-fontawesome": "^0.1.9",
|
||||||
|
"@types/mousetrap": "^1.6.3",
|
||||||
"apollo-cache": "^1.3.4",
|
"apollo-cache": "^1.3.4",
|
||||||
"apollo-cache-inmemory": "^1.6.5",
|
"apollo-cache-inmemory": "^1.6.5",
|
||||||
"apollo-client": "^2.6.8",
|
"apollo-client": "^2.6.8",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"jimp": "^0.12.1",
|
"jimp": "^0.12.1",
|
||||||
"localforage": "1.7.3",
|
"localforage": "1.7.3",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
|
"mousetrap": "^1.6.5",
|
||||||
"query-string": "6.12.1",
|
"query-string": "6.12.1",
|
||||||
"react": "16.13.1",
|
"react": "16.13.1",
|
||||||
"react-apollo": "^3.1.5",
|
"react-apollo": "^3.1.5",
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import ReactMarkdown from "react-markdown";
|
|||||||
|
|
||||||
const markup = `
|
const markup = `
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Add various keyboard shortcuts (see manual).
|
||||||
* Support deleting multiple scenes.
|
* Support deleting multiple scenes.
|
||||||
* Add in-app help manual.
|
* Add in-app help manual.
|
||||||
* Add support for custom served folders.
|
* Add support for custom served folders.
|
||||||
* Add support for parent/child studios.
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Add dialog when pasting movie images.
|
||||||
* Allow click and click-drag selection after selecting scene.
|
* Allow click and click-drag selection after selecting scene.
|
||||||
* Added multi-scene edit dialog.
|
* Added multi-scene edit dialog.
|
||||||
* Moved images to separate tables, increasing performance.
|
* 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 Scraping from "src/docs/en/Scraping.md";
|
||||||
import Contributing from "src/docs/en/Contributing.md";
|
import Contributing from "src/docs/en/Contributing.md";
|
||||||
import SceneFilenameParser from "src/docs/en/SceneFilenameParser.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 Help from "src/docs/en/Help.md";
|
||||||
import { Page } from "./Page";
|
import { Page } from "./Page";
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "Interface.md",
|
key: "Interface.md",
|
||||||
title: "Interface",
|
title: "Interface Options",
|
||||||
content: Interface,
|
content: Interface,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,6 +69,11 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||||||
title: "Metadata Scraping",
|
title: "Metadata Scraping",
|
||||||
content: Scraping,
|
content: Scraping,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "KeyboardShortcuts.md",
|
||||||
|
title: "Keyboard Shortcuts",
|
||||||
|
content: KeyboardShortcuts,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "Contributing.md",
|
key: "Contributing.md",
|
||||||
title: "Contributing",
|
title: "Contributing",
|
||||||
@@ -116,7 +122,7 @@ export const Manual: React.FC<IManualProps> = ({ show, onClose }) => {
|
|||||||
id="manual-tabs"
|
id="manual-tabs"
|
||||||
>
|
>
|
||||||
<Row>
|
<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">
|
<Nav variant="pills" className="flex-column">
|
||||||
{content.map((c) => {
|
{content.map((c) => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -17,22 +17,24 @@
|
|||||||
color: $text-color;
|
color: $text-color;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.manual .manual-content {
|
|
||||||
max-height: calc(100vh - 10rem);
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.indent-1 {
|
.indent-1 {
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.manual .manual-content,
|
||||||
|
.manual .manual-toc {
|
||||||
|
max-height: calc(100vh - 10rem);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.manual .modal-body {
|
.manual .modal-body {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.manual-content {
|
.manual-content,
|
||||||
|
.manual-toc {
|
||||||
max-height: inherit;
|
max-height: inherit;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
|
|
||||||
const valueStage = useRef<CriterionValue>(criterion.value);
|
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
|
// Configure if we are editing an existing criterion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.editingCriterion) {
|
if (!props.editingCriterion) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _, { debounce } from "lodash";
|
import _, { debounce } from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { SortDirectionEnum } from "src/core/generated-graphql";
|
import { SortDirectionEnum } from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
@@ -19,6 +19,7 @@ import { Icon } from "src/components/Shared";
|
|||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
|
import { useFocus } from "src/utils";
|
||||||
import { AddFilter } from "./AddFilter";
|
import { AddFilter } from "./AddFilter";
|
||||||
|
|
||||||
interface IListFilterOperation {
|
interface IListFilterOperation {
|
||||||
@@ -27,6 +28,7 @@ interface IListFilterOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IListFilterProps {
|
interface IListFilterProps {
|
||||||
|
subComponent?: boolean;
|
||||||
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
onFilterUpdate: (newFilter: ListFilterModel) => void;
|
||||||
zoomIndex?: number;
|
zoomIndex?: number;
|
||||||
onChangeZoom?: (zoomIndex: number) => void;
|
onChangeZoom?: (zoomIndex: number) => void;
|
||||||
@@ -40,10 +42,14 @@ interface IListFilterProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
const PAGE_SIZE_OPTIONS = ["20", "40", "60", "120"];
|
||||||
|
const minZoom = 0;
|
||||||
|
const maxZoom = 3;
|
||||||
|
|
||||||
export const ListFilter: React.FC<IListFilterProps> = (
|
export const ListFilter: React.FC<IListFilterProps> = (
|
||||||
props: IListFilterProps
|
props: IListFilterProps
|
||||||
) => {
|
) => {
|
||||||
|
const [queryRef, setQueryFocus] = useFocus();
|
||||||
|
|
||||||
const searchCallback = debounce((value: string) => {
|
const searchCallback = debounce((value: string) => {
|
||||||
const newFilter = _.cloneDeep(props.filter);
|
const newFilter = _.cloneDeep(props.filter);
|
||||||
newFilter.searchTerm = value;
|
newFilter.searchTerm = value;
|
||||||
@@ -55,6 +61,81 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
Criterion | undefined
|
Criterion | undefined
|
||||||
>(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>) {
|
function onChangePageSize(event: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
const val = event.currentTarget.value;
|
const val = event.currentTarget.value;
|
||||||
|
|
||||||
@@ -322,9 +403,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
className="zoom-slider d-none d-sm-inline-flex ml-3"
|
className="zoom-slider d-none d-sm-inline-flex ml-3"
|
||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={minZoom}
|
||||||
max={3}
|
max={maxZoom}
|
||||||
defaultValue={1}
|
value={props.zoomIndex}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
||||||
}
|
}
|
||||||
@@ -369,6 +450,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
<ButtonGroup className="mr-3 my-1">
|
<ButtonGroup className="mr-3 my-1">
|
||||||
<InputGroup className="mr-2">
|
<InputGroup className="mr-2">
|
||||||
<FormControl
|
<FormControl
|
||||||
|
ref={queryRef}
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
defaultValue={props.filter.searchTerm}
|
defaultValue={props.filter.searchTerm}
|
||||||
onInput={onChangeQuery}
|
onInput={onChangeQuery}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { Nav, Navbar, Button } from "react-bootstrap";
|
import { Nav, Navbar, Button } from "react-bootstrap";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { LinkContainer } from "react-router-bootstrap";
|
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 { SessionUtils } from "src/utils";
|
||||||
|
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
@@ -90,6 +90,7 @@ const menuItems: IMenuItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const MainNavbar: React.FC = () => {
|
export const MainNavbar: React.FC = () => {
|
||||||
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const [showManual, setShowManual] = useState(false);
|
const [showManual, setShowManual] = useState(false);
|
||||||
@@ -120,7 +121,14 @@ export const MainNavbar: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [expanded]);
|
}, [expanded]);
|
||||||
|
|
||||||
const path =
|
function goto(page: string) {
|
||||||
|
history.push(page);
|
||||||
|
if (document.activeElement instanceof HTMLElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPath =
|
||||||
location.pathname === "/performers"
|
location.pathname === "/performers"
|
||||||
? "/performers/new"
|
? "/performers/new"
|
||||||
: location.pathname === "/studios"
|
: location.pathname === "/studios"
|
||||||
@@ -129,16 +137,49 @@ export const MainNavbar: React.FC = () => {
|
|||||||
? "/movies/new"
|
? "/movies/new"
|
||||||
: null;
|
: null;
|
||||||
const newButton =
|
const newButton =
|
||||||
path === null ? (
|
newPath === null ? (
|
||||||
""
|
""
|
||||||
) : (
|
) : (
|
||||||
<Link to={path}>
|
<Link to={newPath}>
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
<FormattedMessage id="new" defaultMessage="New" />
|
<FormattedMessage id="new" defaultMessage="New" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</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() {
|
function maybeRenderLogout() {
|
||||||
if (SessionUtils.isLoggedIn()) {
|
if (SessionUtils.isLoggedIn()) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
StudioSelect,
|
StudioSelect,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Table, Form } from "react-bootstrap";
|
import { Table, Form, Modal as BSModal, Button } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
TableUtils,
|
TableUtils,
|
||||||
ImageUtils,
|
ImageUtils,
|
||||||
@@ -34,6 +34,7 @@ export const Movie: React.FC = () => {
|
|||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing movie state
|
// Editing movie state
|
||||||
const [frontImage, setFrontImage] = useState<string | undefined>(undefined);
|
const [frontImage, setFrontImage] = useState<string | undefined>(undefined);
|
||||||
@@ -57,6 +58,10 @@ export const Movie: React.FC = () => {
|
|||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const { data, error, loading } = useFindMovie(id);
|
const { data, error, loading } = useFindMovie(id);
|
||||||
const [updateMovie] = useMovieUpdate(getMovieInput() as GQL.MovieUpdateInput);
|
const [updateMovie] = useMovieUpdate(getMovieInput() as GQL.MovieUpdateInput);
|
||||||
@@ -67,6 +72,42 @@ export const Movie: React.FC = () => {
|
|||||||
|
|
||||||
const intl = useIntl();
|
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>) {
|
function updateMovieEditState(state: Partial<GQL.MovieDataFragment>) {
|
||||||
setName(state.name ?? undefined);
|
setName(state.name ?? undefined);
|
||||||
setAliases(state.aliases ?? undefined);
|
setAliases(state.aliases ?? undefined);
|
||||||
@@ -98,8 +139,21 @@ export const Movie: React.FC = () => {
|
|||||||
}, [data, updateMovieData]);
|
}, [data, updateMovieData]);
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
function onImageLoad(imageData: string) {
|
||||||
setImagePreview(imageData);
|
setImageClipboard(imageData);
|
||||||
setFrontImage(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) {
|
function onBackImageLoad(imageData: string) {
|
||||||
@@ -107,11 +161,7 @@ export const Movie: React.FC = () => {
|
|||||||
setBackImage(imageData);
|
setBackImage(imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encodingFrontImage = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
||||||
const encodingBackImage = ImageUtils.usePasteImage(
|
|
||||||
onBackImageLoad,
|
|
||||||
isEditing
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isNew && !isEditing) {
|
if (!isNew && !isEditing) {
|
||||||
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
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
|
// TODO: CSS class
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="movie-details col">
|
<div className="movie-details col">
|
||||||
{isNew && <h2>Add Movie</h2>}
|
{isNew && <h2>Add Movie</h2>}
|
||||||
<div className="logo w-100">
|
<div className="logo w-100">
|
||||||
{encodingFrontImage || encodingBackImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -312,6 +399,7 @@ export const Movie: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderDeleteAlert()}
|
{renderDeleteAlert()}
|
||||||
|
{renderImageAlert()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const Performer: React.FC = () => {
|
|||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const [activeTabKey, setActiveTabKey] = useState("details");
|
||||||
|
|
||||||
const { data, error } = useFindPerformer(id);
|
const { data, error } = useFindPerformer(id);
|
||||||
const [updatePerformer] = usePerformerUpdate();
|
const [updatePerformer] = usePerformerUpdate();
|
||||||
const [createPerformer] = usePerformerCreate();
|
const [createPerformer] = usePerformerCreate();
|
||||||
@@ -49,6 +51,23 @@ export const Performer: React.FC = () => {
|
|||||||
|
|
||||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
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)
|
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
|
|
||||||
@@ -100,9 +119,18 @@ export const Performer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderTabs = () => (
|
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">
|
<Tab eventKey="details" title="Details">
|
||||||
<PerformerDetailsPanel performer={performer} isEditing={false} />
|
<PerformerDetailsPanel
|
||||||
|
performer={performer}
|
||||||
|
isEditing={false}
|
||||||
|
isVisible={activeTabKey === "details"}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title="Scenes">
|
||||||
<PerformerScenesPanel performer={performer} />
|
<PerformerScenesPanel performer={performer} />
|
||||||
@@ -111,6 +139,7 @@ export const Performer: React.FC = () => {
|
|||||||
<PerformerDetailsPanel
|
<PerformerDetailsPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isEditing
|
isEditing
|
||||||
|
isVisible={activeTabKey === "edit"}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
@@ -227,6 +256,7 @@ export const Performer: React.FC = () => {
|
|||||||
<PerformerDetailsPanel
|
<PerformerDetailsPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isEditing
|
isEditing
|
||||||
|
isVisible
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface IPerformerDetails {
|
|||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
|
isVisible: boolean;
|
||||||
onSave?: (
|
onSave?: (
|
||||||
performer:
|
performer:
|
||||||
| Partial<GQL.PerformerCreateInput>
|
| Partial<GQL.PerformerCreateInput>
|
||||||
@@ -46,6 +47,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
performer,
|
performer,
|
||||||
isNew,
|
isNew,
|
||||||
isEditing,
|
isEditing,
|
||||||
|
isVisible,
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
onImageChange,
|
onImageChange,
|
||||||
@@ -162,6 +164,29 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
setImage(imageData);
|
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(() => {
|
useEffect(() => {
|
||||||
setImage(undefined);
|
setImage(undefined);
|
||||||
updatePerformerEditState(performer);
|
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({
|
const listData = usePerformersList({
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
|
addKeybinds,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getRandom(
|
async function getRandom(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { LoadingIndicator, Icon } from "src/components/Shared";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { ScenePlayer } from "src/components/ScenePlayer";
|
import { ScenePlayer } from "src/components/ScenePlayer";
|
||||||
import { TextUtils, JWUtils } from "src/utils";
|
import { TextUtils, JWUtils } from "src/utils";
|
||||||
|
import * as Mousetrap from "mousetrap";
|
||||||
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||||
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
||||||
import { SceneEditPanel } from "./SceneEditPanel";
|
import { SceneEditPanel } from "./SceneEditPanel";
|
||||||
@@ -38,6 +39,8 @@ export const Scene: React.FC = () => {
|
|||||||
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
|
const [decrementO] = useSceneDecrementO(scene?.id ?? "0");
|
||||||
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
const [resetO] = useSceneResetO(scene?.id ?? "0");
|
||||||
|
|
||||||
|
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
|
||||||
|
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
const queryParams = queryString.parse(location.search);
|
const queryParams = queryString.parse(location.search);
|
||||||
@@ -177,7 +180,10 @@ export const Scene: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Container defaultActiveKey="scene-details-panel">
|
<Tab.Container
|
||||||
|
activeKey={activeTabKey}
|
||||||
|
onSelect={(k) => setActiveTabKey(k)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Nav variant="tabs" className="mr-auto">
|
<Nav variant="tabs" className="mr-auto">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
@@ -224,7 +230,11 @@ export const Scene: React.FC = () => {
|
|||||||
<SceneDetailPanel scene={scene} />
|
<SceneDetailPanel scene={scene} />
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
<Tab.Pane eventKey="scene-markers-panel" title="Markers">
|
<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>
|
||||||
<Tab.Pane eventKey="scene-movie-panel" title="Movies">
|
<Tab.Pane eventKey="scene-movie-panel" title="Movies">
|
||||||
<SceneMoviePanel scene={scene} />
|
<SceneMoviePanel scene={scene} />
|
||||||
@@ -245,6 +255,7 @@ export const Scene: React.FC = () => {
|
|||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
<Tab.Pane eventKey="scene-edit-panel" title="Edit">
|
<Tab.Pane eventKey="scene-edit-panel" title="Edit">
|
||||||
<SceneEditPanel
|
<SceneEditPanel
|
||||||
|
isVisible={activeTabKey === "scene-edit-panel"}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
onUpdate={(newScene) => setScene(newScene)}
|
onUpdate={(newScene) => setScene(newScene)}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
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) {
|
if (loading || !scene || !data?.findScene) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { RatingStars } from "./RatingStars";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
|
isVisible: boolean;
|
||||||
onUpdate: (scene: GQL.SceneDataFragment) => void;
|
onUpdate: (scene: GQL.SceneDataFragment) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
@@ -65,6 +66,48 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
|
|
||||||
const [updateScene] = useSceneUpdate(getSceneInput());
|
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(() => {
|
useEffect(() => {
|
||||||
const newQueryableScrapers = (
|
const newQueryableScrapers = (
|
||||||
Scrapers?.data?.listSceneScrapers ?? []
|
Scrapers?.data?.listSceneScrapers ?? []
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||||
@@ -7,6 +7,7 @@ import { SceneMarkerForm } from "./SceneMarkerForm";
|
|||||||
|
|
||||||
interface ISceneMarkersPanelProps {
|
interface ISceneMarkersPanelProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
|
isVisible: boolean;
|
||||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +19,17 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
|||||||
GQL.SceneMarkerDataFragment
|
GQL.SceneMarkerDataFragment
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
// set up hotkeys
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.isVisible) {
|
||||||
|
Mousetrap.bind("n", () => onOpenEditor());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("n");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
||||||
setIsEditorOpen(true);
|
setIsEditorOpen(true);
|
||||||
setEditingMarker(marker ?? undefined);
|
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({
|
const listData = useScenesList({
|
||||||
zoomable: true,
|
zoomable: true,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
@@ -40,6 +53,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
renderDeleteDialog: renderDeleteScenesDialog,
|
renderDeleteDialog: renderDeleteScenesDialog,
|
||||||
subComponent,
|
subComponent,
|
||||||
filterHook,
|
filterHook,
|
||||||
|
addKeybinds,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function playRandom(
|
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({
|
const listData = useSceneMarkersList({
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
|
addKeybinds,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function playRandom(
|
async function playRandom(
|
||||||
|
|||||||
@@ -68,6 +68,25 @@ export const Studio: React.FC = () => {
|
|||||||
setStudio(studioData);
|
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(() => {
|
useEffect(() => {
|
||||||
if (data && data.findStudio) {
|
if (data && data.findStudio) {
|
||||||
setImage(undefined);
|
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[],
|
selected: E[],
|
||||||
onClose: (confirmed: boolean) => void
|
onClose: (confirmed: boolean) => void
|
||||||
) => JSX.Element | undefined;
|
) => JSX.Element | undefined;
|
||||||
|
addKeybinds?: (
|
||||||
|
result: T,
|
||||||
|
filter: ListFilterModel,
|
||||||
|
selectedIds: Set<string>
|
||||||
|
) => () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDataItem {
|
interface IDataItem {
|
||||||
@@ -112,6 +117,53 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
const totalCount = options.getCount(result);
|
const totalCount = options.getCount(result);
|
||||||
const items = options.getData(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(
|
const updateInterfaceConfig = useCallback(
|
||||||
(updatedFilter: ListFilterModel) => {
|
(updatedFilter: ListFilterModel) => {
|
||||||
setInterfaceState((config) => {
|
setInterfaceState((config) => {
|
||||||
@@ -354,6 +406,7 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
const template = (
|
const template = (
|
||||||
<div>
|
<div>
|
||||||
<ListFilter
|
<ListFilter
|
||||||
|
subComponent={options.subComponent}
|
||||||
onFilterUpdate={updateQueryParams}
|
onFilterUpdate={updateQueryParams}
|
||||||
onSelectAll={onSelectAll}
|
onSelectAll={onSelectAll}
|
||||||
onSelectNone={onSelectNone}
|
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 SessionUtils } from "./session";
|
||||||
export { default as flattenMessages } from "./flattenMessages";
|
export { default as flattenMessages } from "./flattenMessages";
|
||||||
export { default as getISOCountry } from "./country";
|
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"
|
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
|
||||||
integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=
|
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@*":
|
"@types/node@*":
|
||||||
version "13.1.2"
|
version "13.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-13.1.2.tgz#fe94285bf5e0782e1a9e5a8c482b1c34465fa385"
|
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"
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.25.1.tgz#1cb546dca1eccdd607c9324747842200b683465d"
|
||||||
integrity sha512-nRKMf9wDS4Fkyd0C9LXh2FFXinD+iwbJ5p/lh3CHitW9kZbRbJ8hCruiadiIXZVbeAqKZzqcTvHnK3mRhFjb6w==
|
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:
|
move-concurrently@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
|
||||||
|
|||||||
Reference in New Issue
Block a user