mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Styling
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
"stylelint-order"
|
"stylelint-order"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"indentation": 2,
|
||||||
"at-rule-empty-line-before": [ "always", {
|
"at-rule-empty-line-before": [ "always", {
|
||||||
except: ["after-same-name", "first-nested" ],
|
except: ["after-same-name", "first-nested" ],
|
||||||
ignore: ["after-comment"],
|
ignore: ["after-comment"],
|
||||||
@@ -46,7 +47,6 @@
|
|||||||
"function-parentheses-space-inside": "never-single-line",
|
"function-parentheses-space-inside": "never-single-line",
|
||||||
"function-url-quotes": "always",
|
"function-url-quotes": "always",
|
||||||
"function-whitespace-after": "always",
|
"function-whitespace-after": "always",
|
||||||
"indentation": 4,
|
|
||||||
"length-zero-no-unit": true,
|
"length-zero-no-unit": true,
|
||||||
"max-empty-lines": 1,
|
"max-empty-lines": 1,
|
||||||
"max-nesting-depth": 3,
|
"max-nesting-depth": 3,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import Scenes from "./components/scenes/scenes";
|
|||||||
import { Settings } from "./components/Settings/Settings";
|
import { Settings } from "./components/Settings/Settings";
|
||||||
import { Stats } from "./components/Stats";
|
import { Stats } from "./components/Stats";
|
||||||
import Studios from "./components/Studios/Studios";
|
import Studios from "./components/Studios/Studios";
|
||||||
import Tags from "./components/Tags/Tags";
|
import { TagList } from "./components/Tags/TagList";
|
||||||
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
import { SceneFilenameParser } from "./components/scenes/SceneFilenameParser";
|
||||||
|
|
||||||
library.add(fas);
|
library.add(fas);
|
||||||
@@ -20,8 +20,8 @@ library.add(fas);
|
|||||||
export const App: React.FC = () => (
|
export const App: React.FC = () => (
|
||||||
<div className="bp3-dark">
|
<div className="bp3-dark">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<MainNavbar />
|
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<MainNavbar />
|
||||||
<div className="main">
|
<div className="main">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={Stats} />
|
<Route exact path="/" component={Stats} />
|
||||||
@@ -29,7 +29,7 @@ export const App: React.FC = () => (
|
|||||||
{/* <Route path="/scenes/:id" component={Scene} /> */}
|
{/* <Route path="/scenes/:id" component={Scene} /> */}
|
||||||
<Route path="/galleries" component={Galleries} />
|
<Route path="/galleries" component={Galleries} />
|
||||||
<Route path="/performers" component={Performers} />
|
<Route path="/performers" component={Performers} />
|
||||||
<Route path="/tags" component={Tags} />
|
<Route path="/tags" component={TagList} />
|
||||||
<Route path="/studios" component={Studios} />
|
<Route path="/studios" component={Studios} />
|
||||||
<Route path="/settings" component={Settings} />
|
<Route path="/settings" component={Settings} />
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Table, Spinner } from "react-bootstrap";
|
import { Button, Table } from "react-bootstrap";
|
||||||
|
import { LoadingIndicator } from 'src/components/Shared';
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
|
|
||||||
export const SettingsAboutPanel: React.FC = () => {
|
export const SettingsAboutPanel: React.FC = () => {
|
||||||
@@ -26,10 +27,8 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
|
|
||||||
function maybeRenderLatestVersion() {
|
function maybeRenderLatestVersion() {
|
||||||
if (
|
if (
|
||||||
!dataLatest ||
|
!dataLatest?.latestversion.shorthash ||
|
||||||
!dataLatest.latestversion ||
|
!dataLatest?.latestversion.url
|
||||||
!dataLatest.latestversion.shorthash ||
|
|
||||||
!dataLatest.latestversion.url
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -149,12 +148,12 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
{!data || loading ? <Spinner animation="border" variant="light" /> : ""}
|
{!data || loading ? <LoadingIndicator inline /> : ""}
|
||||||
{error && <span>{error.message}</span>}
|
{error && <span>{error.message}</span>}
|
||||||
{errorLatest && <span>{errorLatest.message}</span>}
|
{errorLatest && <span>{errorLatest.message}</span>}
|
||||||
{renderVersion()}
|
{renderVersion()}
|
||||||
{!dataLatest || loadingLatest || networkStatus === 4 ? (
|
{!dataLatest || loadingLatest || networkStatus === 4 ? (
|
||||||
<Spinner animation="border" variant="light" />
|
<LoadingIndicator inline />
|
||||||
) : (
|
) : (
|
||||||
<>{renderLatestVersion()}</>
|
<>{renderLatestVersion()}</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form, InputGroup, Spinner } from "react-bootstrap";
|
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon, LoadingIndicator } from "src/components/Shared";
|
||||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||||
|
|
||||||
export const SettingsConfigurationPanel: React.FC = () => {
|
export const SettingsConfigurationPanel: React.FC = () => {
|
||||||
@@ -154,7 +154,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
|
|
||||||
if (error) return <h1>{error.message}</h1>;
|
if (error) return <h1>{error.message}</h1>;
|
||||||
if (!data?.configuration || loading)
|
if (!data?.configuration || loading)
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <LoadingIndicator />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form, Spinner } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { LoadingIndicator } from 'src/components/Shared';
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
<>
|
<>
|
||||||
{config.error ? <h1>{config.error.message}</h1> : ""}
|
{config.error ? <h1>{config.error.message}</h1> : ""}
|
||||||
{!config?.data?.configuration || config.loading ? (
|
{!config?.data?.configuration || config.loading ? (
|
||||||
<Spinner animation="border" variant="light" />
|
<LoadingIndicator />
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
|
||||||
Modal,
|
Modal,
|
||||||
Nav,
|
|
||||||
Navbar,
|
|
||||||
OverlayTrigger,
|
|
||||||
Popover
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils } from "src/utils";
|
import { ImageInput } from 'src/components/Shared';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
performer?: Partial<GQL.PerformerDataFragment>;
|
performer?: Partial<GQL.PerformerDataFragment>;
|
||||||
@@ -22,102 +16,46 @@ interface IProps {
|
|||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onAutoTag?: () => void;
|
onAutoTag?: () => void;
|
||||||
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
|
|
||||||
// TODO: only for performers. make generic
|
|
||||||
scrapers?: Pick<GQL.Scraper, "id" | "name">[];
|
|
||||||
onDisplayScraperDialog?: (scraper: Pick<GQL.Scraper, "id" | "name">) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
function renderEditButton() {
|
function renderEditButton() {
|
||||||
if (props.isNew) {
|
if (props.isNew) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button variant="primary" onClick={() => props.onToggleEdit()}>
|
<Button variant="primary" className="edit" onClick={() => props.onToggleEdit()}>
|
||||||
{props.isEditing ? "Cancel" : "Edit"}
|
{props.isEditing ? "Cancel" : "Edit"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSaveButton() {
|
function renderSaveButton() {
|
||||||
if (!props.isEditing) {
|
if (!props.isEditing) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button variant="success" onClick={() => props.onSave()}>
|
<Button variant="success" className="save" onClick={() => props.onSave()}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteButton() {
|
function renderDeleteButton() {
|
||||||
if (props.isNew || props.isEditing) {
|
if (props.isNew || props.isEditing) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Button variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>
|
<Button variant="danger" className="delete" onClick={() => setIsDeleteAlertOpen(true)}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImageInput() {
|
|
||||||
if (!props.isEditing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form.Group controlId="cover-file">
|
|
||||||
<Form.Label>Choose image...</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
type="file"
|
|
||||||
accept=".jpg,.jpeg,.png"
|
|
||||||
onChange={props.onImageChange}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScraperMenu() {
|
|
||||||
if (!props.performer || !props.isEditing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const popover = (
|
|
||||||
<Popover id="scraper-popover">
|
|
||||||
<Popover.Content>
|
|
||||||
<div>
|
|
||||||
{props.scrapers
|
|
||||||
? props.scrapers.map(s => (
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={() => props.onDisplayScraperDialog?.(s)}
|
|
||||||
>
|
|
||||||
{s.name}
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</Popover.Content>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OverlayTrigger trigger="click" placement="bottom" overlay={popover}>
|
|
||||||
<Button>Scrape with...</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAutoTagButton() {
|
function renderAutoTagButton() {
|
||||||
if (props.isNew || props.isEditing) {
|
if (props.isNew || props.isEditing) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.onAutoTag) {
|
if (props.onAutoTag) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (props.onAutoTag) {
|
if (props.onAutoTag) {
|
||||||
props.onAutoTag();
|
props.onAutoTag();
|
||||||
@@ -130,19 +68,6 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderScenesButton() {
|
|
||||||
if (props.isEditing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let linkSrc: string = "#";
|
|
||||||
if (props.performer) {
|
|
||||||
linkSrc = NavUtils.makePerformerScenesUrl(props.performer);
|
|
||||||
} else if (props.studio) {
|
|
||||||
linkSrc = NavUtils.makeStudioScenesUrl(props.studio);
|
|
||||||
}
|
|
||||||
return <Link to={linkSrc}>Scenes</Link>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
const name = props?.studio?.name ?? props?.performer?.name;
|
const name = props?.studio?.name ?? props?.performer?.name;
|
||||||
|
|
||||||
@@ -165,20 +90,13 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="details-edit">
|
||||||
|
{renderEditButton()}
|
||||||
|
<ImageInput isEditing={props.isEditing} onImageChange={props.onImageChange} />
|
||||||
|
{renderAutoTagButton()}
|
||||||
|
{renderSaveButton()}
|
||||||
|
{renderDeleteButton()}
|
||||||
{renderDeleteAlert()}
|
{renderDeleteAlert()}
|
||||||
<Navbar bg="dark">
|
</div>
|
||||||
<Nav className="mr-auto ml-auto">
|
|
||||||
{renderEditButton()}
|
|
||||||
{renderScraperMenu()}
|
|
||||||
{renderImageInput()}
|
|
||||||
{renderSaveButton()}
|
|
||||||
|
|
||||||
{renderAutoTagButton()}
|
|
||||||
{renderScenesButton()}
|
|
||||||
{renderDeleteButton()}
|
|
||||||
</Nav>
|
|
||||||
</Navbar>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
function renderButtons() {
|
function renderButtons() {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup vertical>
|
<ButtonGroup vertical>
|
||||||
<Button disabled={props.disabled} onClick={() => increment()}>
|
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => increment()}>
|
||||||
<Icon icon="chevron-up" />
|
<Icon icon="chevron-up" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button disabled={props.disabled} onClick={() => decrement()}>
|
<Button variant="secondary" className="duration-button" disabled={props.disabled} onClick={() => decrement()}>
|
||||||
<Icon icon="chevron-down" />
|
<Icon icon="chevron-down" />
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
@@ -53,7 +53,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
function maybeRenderReset() {
|
function maybeRenderReset() {
|
||||||
if (props.onReset) {
|
if (props.onReset) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => onReset()}>
|
<Button variant="secondary" onClick={onReset}>
|
||||||
<Icon icon="clock" />
|
<Icon icon="clock" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, InputGroup, Form, Modal, Spinner } from "react-bootstrap";
|
import { Button, InputGroup, Form, Modal } from "react-bootstrap";
|
||||||
|
import { LoadingIndicator } from 'src/components/Shared';
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -55,7 +56,7 @@ export const FolderSelect: React.FC<IProps> = (props: IProps) => {
|
|||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{!data || !data.directories || loading ? (
|
{!data || !data.directories || loading ? (
|
||||||
<Spinner animation="border" variant="light" />
|
<LoadingIndicator inline />
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
|
|||||||
23
ui/v2.5/src/components/Shared/ImageInput.tsx
Normal file
23
ui/v2.5/src/components/Shared/ImageInput.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Button, Form } from 'react-bootstrap';
|
||||||
|
|
||||||
|
interface IImageInput {
|
||||||
|
isEditing: boolean;
|
||||||
|
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageInput: React.FC<IImageInput> = ({ isEditing, onImageChange }) => {
|
||||||
|
if (!isEditing) return <div />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Label className="image-input">
|
||||||
|
<Button variant="secondary">Browse for image...</Button>
|
||||||
|
<Form.Control
|
||||||
|
type="file"
|
||||||
|
onChange={onImageChange}
|
||||||
|
accept=".jpg,.jpeg,.png"
|
||||||
|
/>
|
||||||
|
</Form.Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Spinner } from "react-bootstrap";
|
import { Spinner } from "react-bootstrap";
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
interface ILoadingProps {
|
interface ILoadingProps {
|
||||||
message: string;
|
message?: string;
|
||||||
|
inline?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLASSNAME = "LoadingIndicator";
|
const CLASSNAME = "LoadingIndicator";
|
||||||
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
|
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
|
||||||
|
|
||||||
const LoadingIndicator: React.FC<ILoadingProps> = ({ message }) => (
|
const LoadingIndicator: React.FC<ILoadingProps> = ({ message, inline = false }) => (
|
||||||
<div className={CLASSNAME}>
|
<div className={cx(CLASSNAME, { inline }) }>
|
||||||
<Spinner animation="border" role="status">
|
<Spinner animation="border" role="status">
|
||||||
<span className="sr-only">Loading...</span>
|
<span className="sr-only">Loading...</span>
|
||||||
</Spinner>
|
</Spinner>
|
||||||
<h4 className={CLASSNAME_MESSAGE}>{message}</h4>
|
<h4 className={CLASSNAME_MESSAGE}>{message ?? "Loading..."}</h4>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useCallback } from "react";
|
import React, { useState, useCallback, CSSProperties } from "react";
|
||||||
import Select, { ValueType } from "react-select";
|
import Select, { ValueType } from "react-select";
|
||||||
import CreatableSelect from "react-select/creatable";
|
import CreatableSelect from "react-select/creatable";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
@@ -38,6 +38,8 @@ interface ISelectProps {
|
|||||||
isClearable?: boolean,
|
isClearable?: boolean,
|
||||||
onInputChange?: (input: string) => void;
|
onInputChange?: (input: string) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
showDropdown?: boolean;
|
||||||
|
groupHeader?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISceneGallerySelect {
|
interface ISceneGallerySelect {
|
||||||
@@ -120,6 +122,8 @@ export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = pr
|
|||||||
items={items}
|
items={items}
|
||||||
initialIds={[]}
|
initialIds={[]}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
|
className="select-suggest"
|
||||||
|
showDropdown={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -147,10 +151,13 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = props => {
|
|||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
items={items}
|
items={items}
|
||||||
initialIds={initialIds}
|
initialIds={initialIds}
|
||||||
|
placeholder="Marker title..."
|
||||||
|
className="select-suggest"
|
||||||
|
showDropdown={false}
|
||||||
|
groupHeader="Previously used titles..."
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = props =>
|
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = props =>
|
||||||
props.type === "performers" ? (
|
props.type === "performers" ? (
|
||||||
<PerformerSelect {...(props as IFilterProps)} />
|
<PerformerSelect {...(props as IFilterProps)} />
|
||||||
@@ -304,44 +311,28 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||||||
creatable = false,
|
creatable = false,
|
||||||
isMulti = false,
|
isMulti = false,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
placeholder
|
placeholder,
|
||||||
|
showDropdown = true,
|
||||||
|
groupHeader
|
||||||
}) => {
|
}) => {
|
||||||
const defaultValue =
|
const defaultValue =
|
||||||
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
||||||
|
|
||||||
|
const options = groupHeader ? [{
|
||||||
|
label: groupHeader,
|
||||||
|
options: items
|
||||||
|
}] : items;
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
control: (provided:any) => ({
|
option: (provided:CSSProperties) => ({
|
||||||
...provided,
|
...provided,
|
||||||
background: '#394b59',
|
color: "#000"
|
||||||
borderColor: 'rgba(16,22,26,.4)'
|
})
|
||||||
}),
|
|
||||||
singleValue: (provided:any) => ({
|
|
||||||
...provided,
|
|
||||||
color: 'f5f8fa',
|
|
||||||
}),
|
|
||||||
placeholder: (provided:any) => ({
|
|
||||||
...provided,
|
|
||||||
color: 'f5f8fa',
|
|
||||||
}),
|
|
||||||
menu: (provided:any) => ({
|
|
||||||
...provided,
|
|
||||||
color: 'f5f8fa',
|
|
||||||
background: '#394b59',
|
|
||||||
borderColor: 'rgba(16,22,26,.4)',
|
|
||||||
zIndex: 3
|
|
||||||
}),
|
|
||||||
option: (provided:any, state:any ) => (
|
|
||||||
state.isFocused ? { ...provided, backgroundColor: '#137cbd' } : provided
|
|
||||||
),
|
|
||||||
multiValueRemove: (provided:any, state:any) => (
|
|
||||||
{ ...provided, color: 'black' }
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
options: items,
|
options,
|
||||||
value: selectedOptions,
|
value: selectedOptions,
|
||||||
styles,
|
|
||||||
className,
|
className,
|
||||||
onChange,
|
onChange,
|
||||||
isMulti,
|
isMulti,
|
||||||
@@ -351,7 +342,11 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||||||
placeholder,
|
placeholder,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
isLoading,
|
isLoading,
|
||||||
components: { IndicatorSeparator: () => null }
|
styles,
|
||||||
|
components: {
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
...(!showDropdown && { DropdownIndicator: () => null })
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return creatable ? (
|
return creatable ? (
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ export { DurationInput } from "./DurationInput";
|
|||||||
export { TagLink } from "./TagLink";
|
export { TagLink } from "./TagLink";
|
||||||
export { HoverPopover } from "./HoverPopover";
|
export { HoverPopover } from "./HoverPopover";
|
||||||
export { default as LoadingIndicator } from "./LoadingIndicator";
|
export { default as LoadingIndicator } from "./LoadingIndicator";
|
||||||
|
export { ImageInput } from './ImageInput';
|
||||||
|
|||||||
@@ -1,17 +1,49 @@
|
|||||||
.LoadingIndicator {
|
.LoadingIndicator {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 70vh;
|
height: 70vh;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
&-message {
|
&-message {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-border {
|
.spinner-border {
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.inline {
|
||||||
|
height: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-edit {
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete,
|
||||||
|
.save {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-suggest {
|
||||||
|
&:hover {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-button {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
line-height: 10px;
|
||||||
|
margin-left: 0 !important;
|
||||||
|
padding: 1px 7px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ interface IProps {
|
|||||||
|
|
||||||
export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
export const StudioCard: React.FC<IProps> = ({ studio }) => {
|
||||||
return (
|
return (
|
||||||
<Card className="col-4">
|
<Card className="studio-card">
|
||||||
<Link
|
<Link
|
||||||
to={`/studios/${studio.id}`}
|
to={`/studios/${studio.id}`}
|
||||||
className="studio previewable image"
|
className="studio previewable image"
|
||||||
style={{ backgroundImage: `url(${studio.image_path})` }}
|
style={{ backgroundImage: `url(${studio.image_path})` }}
|
||||||
/>
|
/>
|
||||||
<div className="card-section">
|
<div className="card-section">
|
||||||
<h4 className="text-truncate">{studio.name}</h4>
|
<h5 className="text-truncate">{studio.name}</h5>
|
||||||
<span>{studio.scene_count} scenes.</span>
|
<span>{studio.scene_count} scenes.</span>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
/* eslint-disable react/no-this-in-sfc */
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
|
||||||
import { Form, Spinner, Table } from "react-bootstrap";
|
import { Table } from "react-bootstrap";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ImageUtils, TableUtils } from "src/utils";
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared";
|
import { DetailsEditNavbar, Modal, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
import { StudioScenesPanel } from './StudioScenesPanel';
|
||||||
|
|
||||||
export const Studio: React.FC = () => {
|
export const Studio: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
@@ -18,17 +20,16 @@ export const Studio: React.FC = () => {
|
|||||||
|
|
||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||||
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing studio state
|
// Editing studio state
|
||||||
const [image, setImage] = useState<string | undefined>(undefined);
|
const [image, setImage] = useState<string>();
|
||||||
const [name, setName] = useState<string | undefined>(undefined);
|
const [name, setName] = useState<string>();
|
||||||
const [url, setUrl] = useState<string | undefined>(undefined);
|
const [url, setUrl] = useState<string>();
|
||||||
|
|
||||||
// Studio state
|
// Studio state
|
||||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||||
const [imagePreview, setImagePreview] = useState<string | undefined>(
|
const [imagePreview, setImagePreview] = useState<string>();
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data, error, loading } = StashService.useFindStudio(id);
|
const { data, error, loading } = StashService.useFindStudio(id);
|
||||||
const [updateStudio] = StashService.useStudioUpdate(
|
const [updateStudio] = StashService.useStudioUpdate(
|
||||||
@@ -71,7 +72,7 @@ export const Studio: React.FC = () => {
|
|||||||
|
|
||||||
if (!isNew && !isEditing) {
|
if (!isNew && !isEditing) {
|
||||||
if (!data?.findStudio || loading)
|
if (!data?.findStudio || loading)
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <LoadingIndicator />;
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +99,10 @@ export const Studio: React.FC = () => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const result = await createStudio();
|
const result = await createStudio();
|
||||||
if (result.data?.studioCreate?.id)
|
if (result.data?.studioCreate?.id) {
|
||||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -107,9 +110,7 @@ export const Studio: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag() {
|
async function onAutoTag() {
|
||||||
if (!studio || !studio.id) {
|
if (!studio.id) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
await StashService.queryMetadataAutoTag({ studios: [studio.id] });
|
await StashService.queryMetadataAutoTag({ studios: [studio.id] });
|
||||||
Toast.success({ content: "Started auto tagging" });
|
Toast.success({ content: "Started auto tagging" });
|
||||||
@@ -133,52 +134,57 @@ export const Studio: React.FC = () => {
|
|||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: CSS class
|
function renderDeleteAlert() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={isDeleteAlertOpen}
|
||||||
|
icon="trash-alt"
|
||||||
|
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
||||||
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
|
>
|
||||||
|
<p>Are you sure you want to delete {studio.name ?? 'studio'}?</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="columns is-multiline no-spacing">
|
<div className="row">
|
||||||
<div className="column is-half details-image-container">
|
<div className={cx('studio-details', { 'col-4': !isNew, 'col-8': isNew})}>
|
||||||
<img className="studio" alt={name} src={imagePreview} />
|
{ isNew && <h2>Add Studio</h2> }
|
||||||
</div>
|
<img className="logo" alt={name} src={imagePreview} />
|
||||||
<div className="column is-half details-detail-container">
|
<Table id="performer-details" style={{ width: "100%" }}>
|
||||||
|
<tbody>
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Name",
|
||||||
|
value: studio.name ?? '',
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setName
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "URL",
|
||||||
|
value: url,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setUrl
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
studio={studio}
|
studio={studio}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={() => {
|
onToggleEdit={() => setIsEditing(!isEditing)}
|
||||||
setIsEditing(!isEditing);
|
|
||||||
updateStudioEditState(studio);
|
|
||||||
}}
|
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onDelete={onDelete}
|
|
||||||
onAutoTag={onAutoTag}
|
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChangeHandler}
|
||||||
|
onAutoTag={onAutoTag}
|
||||||
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
<h1>
|
|
||||||
{!isEditing ? (
|
|
||||||
<span>{studio.name}</span>
|
|
||||||
) : (
|
|
||||||
<Form.Group controlId="studio-name">
|
|
||||||
<Form.Label>Name</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
defaultValue={studio.name || ""}
|
|
||||||
placeholder="Name"
|
|
||||||
onChange={(event: any) => setName(event.target.value)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
)}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<Table style={{ width: "100%" }}>
|
|
||||||
<tbody>
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "URL",
|
|
||||||
value: studio.url ?? undefined,
|
|
||||||
isEditing,
|
|
||||||
onChange: (val: string) => setUrl(val)
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
|
{ !isNew && (
|
||||||
|
<div className="col-8">
|
||||||
|
<StudioScenesPanel studio={studio} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{renderDeleteAlert()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { SceneList } from "../../scenes/SceneList";
|
||||||
|
|
||||||
|
interface IStudioScenesPanel {
|
||||||
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({
|
||||||
|
studio
|
||||||
|
}) => {
|
||||||
|
function filterHook(filter: ListFilterModel) {
|
||||||
|
const studioValue = { id: studio.id!, label: studio.name! };
|
||||||
|
// if studio is already present, then we modify it, otherwise add
|
||||||
|
let studioCriterion = filter.criteria.find(c => {
|
||||||
|
return c.type === "studios";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (
|
||||||
|
studioCriterion &&
|
||||||
|
(studioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
|
studioCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||||
|
) {
|
||||||
|
// add the studio if not present
|
||||||
|
if (
|
||||||
|
!studioCriterion.value.find((p: any) => {
|
||||||
|
return p.id === studio.id;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
studioCriterion.value.push(studioValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
|
} else {
|
||||||
|
// overwrite
|
||||||
|
studioCriterion = new StudiosCriterion();
|
||||||
|
studioCriterion.value = [studioValue];
|
||||||
|
filter.criteria.push(studioCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SceneList subComponent filterHook={filterHook} />;
|
||||||
|
};
|
||||||
@@ -14,9 +14,9 @@ export const StudioList: React.FC = () => {
|
|||||||
result: FindStudiosQueryResult,
|
result: FindStudiosQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel
|
||||||
) {
|
) {
|
||||||
if (!result.data || !result.data.findStudios) {
|
if (!result.data?.findStudios)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
if (filter.displayMode === DisplayMode.Grid) {
|
if (filter.displayMode === DisplayMode.Grid) {
|
||||||
return (
|
return (
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
|
|||||||
8
ui/v2.5/src/components/Studios/styles.scss
Normal file
8
ui/v2.5/src/components/Studios/styles.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.studio-details {
|
||||||
|
padding-left: 4rem;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
margin: 4rem 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Form, Spinner } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { NavUtils } from "src/utils";
|
import { NavUtils } from "src/utils";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { Icon, Modal, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
export const TagList: React.FC = () => {
|
export const TagList: React.FC = () => {
|
||||||
@@ -93,34 +93,36 @@ export const TagList: React.FC = () => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data?.allTags) return <Spinner animation="border" variant="light" />;
|
if (!data?.allTags)
|
||||||
|
return <LoadingIndicator />;
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
|
|
||||||
const tagElements = data.allTags.map(tag => {
|
const tagElements = data.allTags.map(tag => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div key={tag.id} className="tag-list-row">
|
||||||
{deleteAlert}
|
<Button variant="link" onClick={() => setEditingTag(tag)}>
|
||||||
<div key={tag.id} className="tag-list-row">
|
{tag.name}
|
||||||
<Button variant="link" onClick={() => setEditingTag(tag)}>
|
</Button>
|
||||||
{tag.name}
|
<div style={{ float: "right" }}>
|
||||||
</Button>
|
<Button variant="secondary" onClick={() => onAutoTag(tag)}>Auto Tag</Button>
|
||||||
<div style={{ float: "right" }}>
|
<Button variant="secondary">
|
||||||
<Button onClick={() => onAutoTag(tag)}>Auto Tag</Button>
|
|
||||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
||||||
Scenes: {tag.scene_count}
|
Scenes: {tag.scene_count}
|
||||||
</Link>
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary">
|
||||||
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)}>
|
<Link to={NavUtils.makeTagSceneMarkersUrl(tag)}>
|
||||||
Markers: {tag.scene_marker_count}
|
Markers: {tag.scene_marker_count}
|
||||||
</Link>
|
</Link>
|
||||||
<span>
|
</Button>
|
||||||
Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}
|
<span>
|
||||||
</span>
|
Total: {(tag.scene_count || 0) + (tag.scene_marker_count || 0)}
|
||||||
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
|
</span>
|
||||||
<Icon icon="trash-alt" color="danger" />
|
<Button variant="danger" onClick={() => setDeletingTag(tag)}>
|
||||||
</Button>
|
<Icon icon="trash-alt" color="danger" />
|
||||||
</div>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,6 +156,7 @@ export const TagList: React.FC = () => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
{tagElements}
|
{tagElements}
|
||||||
|
{deleteAlert}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Route, Switch } from "react-router-dom";
|
|
||||||
import { TagList } from "./TagList";
|
|
||||||
|
|
||||||
const Tags = () => (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/tags" component={TagList} />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Tags;
|
|
||||||
28
ui/v2.5/src/components/Tags/styles.scss
Normal file
28
ui/v2.5/src/components/Tags/styles.scss
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
#tag-list-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 50vw;
|
||||||
|
|
||||||
|
a,
|
||||||
|
.btn {
|
||||||
|
color: $text-color;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list-row {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 10px;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list-row:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,35 +1,38 @@
|
|||||||
.wall-overlay {
|
.wall-overlay {
|
||||||
background-color: rgba(0,0,0,.8);
|
background-color: rgba(0, 0, 0, .8);
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1;
|
left: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
transition: transform .5s ease-in-out;
|
transition: transform .5s ease-in-out;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity .5s ease-in-out;
|
transition: opacity .5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity .5s ease-in-out;
|
transition: opacity .5s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visible-unanimated {
|
.visible-unanimated {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-unanimated {
|
.hidden-unanimated {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.double-scale {
|
.double-scale {
|
||||||
position: absolute;
|
|
||||||
z-index: 2;
|
|
||||||
transform: scale(2);
|
|
||||||
background-color: black;
|
background-color: black;
|
||||||
|
position: absolute;
|
||||||
|
transform: scale(2);
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.double-scale img {
|
.double-scale img {
|
||||||
@@ -38,60 +41,61 @@
|
|||||||
|
|
||||||
.scene-wall-item-container {
|
.scene-wall-item-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transition: transform .5s;
|
justify-content: center;
|
||||||
max-height: 253px;
|
max-height: 253px;
|
||||||
|
position: relative;
|
||||||
|
transition: transform .5s;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-wall-item-container video {
|
.scene-wall-item-container video {
|
||||||
|
height: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-wall-item-text-container {
|
.scene-wall-item-text-container {
|
||||||
position: absolute;
|
background: linear-gradient(rgba(255, 255, 255, .25), rgba(255, 255, 255, .65));
|
||||||
font-weight: 700;
|
|
||||||
color: #444;
|
|
||||||
padding: 5px;
|
|
||||||
width: 100%;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
color: #444;
|
||||||
|
font-weight: 700;
|
||||||
left: 0;
|
left: 0;
|
||||||
background: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.65));
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
& span {
|
& span {
|
||||||
line-height: 1;
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-wall-item-blur {
|
.scene-wall-item-blur {
|
||||||
position: absolute;
|
|
||||||
top: -5px;
|
|
||||||
left: -5px;
|
|
||||||
right: -5px;
|
|
||||||
bottom: -5px;
|
bottom: -5px;
|
||||||
|
left: -5px;
|
||||||
|
position: absolute;
|
||||||
|
right: -5px;
|
||||||
|
top: -5px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall-item video, .wall-item img {
|
.wall-item video,
|
||||||
width: 100%;
|
.wall-item img {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall-item {
|
.wall-item {
|
||||||
width: 20%;
|
|
||||||
padding: 0 !important;
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
padding: 0 !important;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
width: 20%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const WallItem: React.FC<IWallItemProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let linkSrc: string = "#";
|
let linkSrc: string = "#";
|
||||||
if (props.clickHandler) {
|
if (!props.clickHandler) {
|
||||||
if (props.scene) {
|
if (props.scene) {
|
||||||
linkSrc = `/scenes/${props.scene.id}`;
|
linkSrc = `/scenes/${props.scene.id}`;
|
||||||
} else if (props.sceneMarker) {
|
} else if (props.sceneMarker) {
|
||||||
@@ -100,7 +100,6 @@ export const WallItem: React.FC<IWallItemProps> = (
|
|||||||
setPreviewPath(props.scene.paths.webp || "");
|
setPreviewPath(props.scene.paths.webp || "");
|
||||||
setScreenshotPath(props.scene.paths.screenshot || "");
|
setScreenshotPath(props.scene.paths.screenshot || "");
|
||||||
setTitle(props.scene.title || "");
|
setTitle(props.scene.title || "");
|
||||||
// tags = props.scene.tags.map((tag) => (<span key={tag.id}>{tag.name}</span>));
|
|
||||||
}
|
}
|
||||||
}, [props.sceneMarker, props.scene]);
|
}, [props.sceneMarker, props.scene]);
|
||||||
|
|
||||||
@@ -128,7 +127,7 @@ export const WallItem: React.FC<IWallItemProps> = (
|
|||||||
onMouseMove={() => debouncedOnMouseEnter.current()}
|
onMouseMove={() => debouncedOnMouseEnter.current()}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
<Link onClick={() => onClick()} to={linkSrc}>
|
<Link onClick={onClick} to={linkSrc}>
|
||||||
<video
|
<video
|
||||||
src={videoPath}
|
src={videoPath}
|
||||||
poster={screenshotPath}
|
poster={screenshotPath}
|
||||||
|
|||||||
@@ -117,17 +117,15 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form.Control
|
||||||
<Form.Control
|
as="select"
|
||||||
as="select"
|
onChange={onChangedModifierSelect}
|
||||||
onChange={onChangedModifierSelect}
|
value={criterion.modifier}
|
||||||
value={criterion.modifier}
|
>
|
||||||
>
|
{criterion.modifierOptions.map(c => (
|
||||||
{criterion.modifierOptions.map(c => (
|
<option key={c.value} value={c.value}>{c.label}</option>
|
||||||
<option value={c.value}>{c.label}</option>
|
))}
|
||||||
))}
|
</Form.Control>
|
||||||
</Form.Control>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,10 +153,13 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
return (
|
return (
|
||||||
<FilterSelect
|
<FilterSelect
|
||||||
type={type}
|
type={type}
|
||||||
|
isMulti
|
||||||
onSelect={items => {
|
onSelect={items => {
|
||||||
criterion.value = items.map(i => ({ id: i.id, label: i.name! }));
|
const newCriterion = _.cloneDeep(criterion);
|
||||||
|
newCriterion.value = items.map(i => ({ id: i.id, label: i.name! }));
|
||||||
|
setCriterion(newCriterion);
|
||||||
}}
|
}}
|
||||||
initialIds={criterion.value.map((labeled: any) => labeled.id)}
|
ids={criterion.value.map((labeled: any) => labeled.id)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -171,7 +172,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
value={criterion.value}
|
value={criterion.value}
|
||||||
>
|
>
|
||||||
{criterion.options.map(c => (
|
{criterion.options.map(c => (
|
||||||
<option value={c}>{c}</option>
|
<option key={c} value={c}>{c}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
);
|
);
|
||||||
@@ -215,7 +216,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
value={criterion.type}
|
value={criterion.type}
|
||||||
>
|
>
|
||||||
{props.filter.criterionOptions.map(c => (
|
{props.filter.criterionOptions.map(c => (
|
||||||
<option value={c.value}>{c.label}</option>
|
<option key={c.value} value={c.value}>{c.label}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -134,13 +134,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
}
|
}
|
||||||
return props.filter.displayModeOptions.map(option => (
|
return props.filter.displayModeOptions.map(option => (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
|
key={option}
|
||||||
overlay={
|
overlay={
|
||||||
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
key={option}
|
|
||||||
active={props.filter.displayMode === option}
|
active={props.filter.displayMode === option}
|
||||||
onClick={() => onChangeDisplayMode(option)}
|
onClick={() => onChangeDisplayMode(option)}
|
||||||
>
|
>
|
||||||
@@ -180,7 +180,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
function renderSelectAll() {
|
function renderSelectAll() {
|
||||||
if (props.onSelectAll) {
|
if (props.onSelectAll) {
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item onClick={() => onSelectAll()}>Select All</Dropdown.Item>
|
<Dropdown.Item key="select-all" onClick={() => onSelectAll()}>Select All</Dropdown.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
function renderSelectNone() {
|
function renderSelectNone() {
|
||||||
if (props.onSelectNone) {
|
if (props.onSelectNone) {
|
||||||
return (
|
return (
|
||||||
<Dropdown.Item onClick={() => onSelectNone()}>
|
<Dropdown.Item key="select-none" onClick={() => onSelectNone()}>
|
||||||
Select None
|
Select None
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
@@ -201,7 +201,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
if (props.otherOperations) {
|
if (props.otherOperations) {
|
||||||
props.otherOperations.forEach(o => {
|
props.otherOperations.forEach(o => {
|
||||||
options.push(
|
options.push(
|
||||||
<Dropdown.Item onClick={o.onClick}>{o.text}</Dropdown.Item>
|
<Dropdown.Item key={o.text} onClick={o.onClick}>{o.text}</Dropdown.Item>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -232,6 +232,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
type="range"
|
type="range"
|
||||||
min={0}
|
min={0}
|
||||||
max={3}
|
max={3}
|
||||||
|
defaultValue={1}
|
||||||
onChange={(event: any) =>
|
onChange={(event: any) =>
|
||||||
onChangeZoom(Number.parseInt(event.target.value, 10))
|
onChangeZoom(Number.parseInt(event.target.value, 10))
|
||||||
}
|
}
|
||||||
@@ -246,7 +247,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
<div className="filter-container">
|
<div className="filter-container">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value={props.filter.searchTerm}
|
defaultValue={props.filter.searchTerm}
|
||||||
onChange={onChangeQuery}
|
onChange={onChangeQuery}
|
||||||
className="filter-item"
|
className="filter-item"
|
||||||
style={{ width: "inherit" }}
|
style={{ width: "inherit" }}
|
||||||
@@ -258,7 +259,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
className="filter-item"
|
className="filter-item"
|
||||||
>
|
>
|
||||||
{PAGE_SIZE_OPTIONS.map(s => (
|
{PAGE_SIZE_OPTIONS.map(s => (
|
||||||
<option value={s}>{s}</option>
|
<option value={s} key={s}>{s}</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<ButtonGroup className="filter-item">
|
<ButtonGroup className="filter-item">
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if(pages.length <= 1)
|
||||||
|
return <div />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonGroup className="filter-container pagination">
|
<ButtonGroup className="filter-container pagination">
|
||||||
<Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}>
|
<Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
.pagination {
|
.pagination {
|
||||||
.btn {
|
.btn {
|
||||||
|
border-left: 1px solid $body-bg;
|
||||||
|
border-right: 1px solid $body-bg;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
padding-left: 15px;
|
padding-left: 15px;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
border-left: 1px solid $body-bg;
|
|
||||||
border-right: 1px solid $body-bg;
|
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="grid-item">
|
<Card className="performer-card">
|
||||||
<Link
|
<Link
|
||||||
to={`/performers/${props.performer.id}`}
|
to={`/performers/${props.performer.id}`}
|
||||||
className="performer previewable image"
|
className="performer previewable image"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/* eslint-disable react/no-this-in-sfc */
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Spinner, Tabs, Tab } from "react-bootstrap";
|
import { Button, Tabs, Tab } from "react-bootstrap";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
|
import cx from 'classnames'
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import Lightbox from "react-images";
|
import Lightbox from "react-images";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
|
||||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||||
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
@@ -49,7 +49,7 @@ export const Performer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
|
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <LoadingIndicator />;
|
||||||
|
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
|
|
||||||
@@ -163,42 +163,46 @@ export const Performer: React.FC = () => {
|
|||||||
onSave(performer);
|
onSave(performer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIcons() {
|
const renderIcons = () => (
|
||||||
function maybeRenderURL(url?: string, icon: IconName = "link") {
|
<span className="name-icons">
|
||||||
if (performer.url) {
|
<Button
|
||||||
return (
|
className={cx('minimal', performer.favorite ? "favorite" : "not-favorite")}
|
||||||
<Button>
|
onClick={() => setFavorite(!performer.favorite)}
|
||||||
<a href={performer.url}>
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon="heart" />
|
||||||
</a>
|
</Button>
|
||||||
</Button>
|
{ performer.url && (
|
||||||
);
|
<Button className="minimal">
|
||||||
}
|
<a href={performer.url} className="link" target="_blank" rel="noopener noreferrer">
|
||||||
}
|
<Icon icon="link" />
|
||||||
|
</a>
|
||||||
return (
|
</Button>
|
||||||
<>
|
)}
|
||||||
<span className="name-icons">
|
{ performer.twitter && (
|
||||||
<Button
|
<Button className="minimal">
|
||||||
className={performer.favorite ? "favorite" : "not-favorite"}
|
<a href={`https://www.twitter.com/${performer.twitter}`} className="twitter" target="_blank" rel="noopener noreferrer">
|
||||||
onClick={() => setFavorite(!performer.favorite)}
|
<Icon icon="dove" />
|
||||||
>
|
</a>
|
||||||
<Icon icon="heart" />
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
{maybeRenderURL(performer.url ?? undefined)}
|
{ performer.instagram && (
|
||||||
{/* TODO - render instagram and twitter links with icons */}
|
<Button className="minimal">
|
||||||
</span>
|
<a href={`https://www.instagram.com/${performer.instagram}`} className="instagram" target="_blank" rel="noopener noreferrer">
|
||||||
</>
|
<Icon icon="camera" />
|
||||||
);
|
</a>
|
||||||
}
|
</Button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
function renderNewView() {
|
function renderNewView() {
|
||||||
return (
|
return (
|
||||||
<div className="columns is-multiline no-spacing">
|
<div className="row new-view">
|
||||||
<div className="column is-half details-image-container">
|
<div className="col-4">
|
||||||
<img className="performer" src={imagePreview} alt="Performer" />
|
<img className="photo" src={imagePreview} alt="Performer" />
|
||||||
</div>
|
</div>
|
||||||
<div className="column is-half details-detail-container">
|
<div className="col-6">
|
||||||
|
<h2>Create Performer</h2>
|
||||||
{renderTabs()}
|
{renderTabs()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -207,47 +211,38 @@ export const Performer: React.FC = () => {
|
|||||||
|
|
||||||
const photos = [{ src: imagePreview || "", caption: "Image" }];
|
const photos = [{ src: imagePreview || "", caption: "Image" }];
|
||||||
|
|
||||||
function openLightbox() {
|
|
||||||
setLightboxIsOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeLightbox() {
|
|
||||||
setLightboxIsOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
return renderNewView();
|
return renderNewView();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div id="performer-page" className="row">
|
||||||
<div id="performer-page">
|
<div className="image-container col-4 offset-1">
|
||||||
<div className="details-image-container">
|
<Button variant="link" onClick={() => setLightboxIsOpen(true)}>
|
||||||
<Button variant="link" onClick={openLightbox}>
|
<img className="performer" src={imagePreview} alt="Performer" />
|
||||||
<img className="performer" src={imagePreview} alt="Performer" />
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
</div>
|
<div className="col-6">
|
||||||
<div className="performer-head">
|
<div className="performer-head">
|
||||||
<h1 className="bp3-heading">
|
<h2>
|
||||||
{performer.name}
|
{performer.name}
|
||||||
{renderIcons()}
|
{renderIcons()}
|
||||||
</h1>
|
</h2>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
{maybeRenderAge()}
|
{maybeRenderAge()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="performer-body">
|
<div className="performer-body">
|
||||||
<div className="details-detail-container">{renderTabs()}</div>
|
<div className="performer-tabs">{renderTabs()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Lightbox
|
<Lightbox
|
||||||
images={photos}
|
images={photos}
|
||||||
onClose={closeLightbox}
|
onClose={() => setLightboxIsOpen(false)}
|
||||||
currentImage={0}
|
currentImage={0}
|
||||||
isOpen={lightboxIsOpen}
|
isOpen={lightboxIsOpen}
|
||||||
onClickImage={() => window.open(imagePreview, "_blank")}
|
onClickImage={() => window.open(imagePreview, "_blank")}
|
||||||
width={9999}
|
width={9999}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
Popover,
|
Popover,
|
||||||
OverlayTrigger,
|
OverlayTrigger,
|
||||||
Spinner,
|
|
||||||
Table
|
Table
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { Icon, Modal, ScrapePerformerSuggest } from "src/components/Shared";
|
import { Icon, Modal, ImageInput, ScrapePerformerSuggest, LoadingIndicator } from "src/components/Shared";
|
||||||
import { ImageUtils, TableUtils } from "src/utils";
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers]);
|
}, [Scrapers]);
|
||||||
|
|
||||||
if (isLoading) return <Spinner animation="border" variant="light" />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
function getPerformerInput() {
|
function getPerformerInput() {
|
||||||
const performerInput: Partial<
|
const performerInput: Partial<
|
||||||
@@ -237,7 +236,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<OverlayTrigger trigger="click" placement="bottom" overlay={popover}>
|
<OverlayTrigger trigger="click" placement="bottom" overlay={popover}>
|
||||||
<Button>Scrape with...</Button>
|
<Button variant="secondary">Scrape with...</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -277,7 +276,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button id="scrape-url-button" onClick={() => onScrapePerformerURL()}>
|
<Button className="minimal scrape-url-button" onClick={() => onScrapePerformerURL()}>
|
||||||
<Icon icon="file-upload" />
|
<Icon icon="file-upload" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
@@ -346,24 +345,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImageInput() {
|
|
||||||
if (!isEditing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td>Image</td>
|
|
||||||
<td>
|
|
||||||
<Form.Control
|
|
||||||
type="file"
|
|
||||||
onChange={onImageChangeHandler}
|
|
||||||
accept=".jpg,.jpeg"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function maybeRenderName() {
|
function maybeRenderName() {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return TableUtils.renderInputGroup({
|
return TableUtils.renderInputGroup({
|
||||||
@@ -465,7 +446,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setInstagram
|
onChange: setInstagram
|
||||||
})}
|
})}
|
||||||
{renderImageInput()}
|
<ImageInput isEditing={!!isEditing} onImageChange={onImageChangeHandler} />
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
|||||||
89
ui/v2.5/src/components/performers/styles.scss
Normal file
89
ui/v2.5/src/components/performers/styles.scss
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
.performer.image {
|
||||||
|
background-position: center !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
height: 50vh;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#performer-details {
|
||||||
|
td {
|
||||||
|
padding: 2px 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
td:first-child {
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#url-field {
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrape-url-button {
|
||||||
|
color: $text-color;
|
||||||
|
float: right;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-tabpane-scenes {
|
||||||
|
.grid {
|
||||||
|
margin-right: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#performer-page {
|
||||||
|
flex-direction: row;
|
||||||
|
margin: 10px auto;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
max-height: 960px;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performer-head {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
vertical-align: top;
|
||||||
|
|
||||||
|
.name-icons {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
.not-favorite {
|
||||||
|
color: rgba(191, 204, 214, .5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
color: #ff7373 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: rgb(191, 204, 214);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instagram {
|
||||||
|
color: pink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-view {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
padding: 1rem 1rem 1rem 2rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
if (props.scene.performers.length <= 0) return;
|
if (props.scene.performers.length <= 0) return;
|
||||||
|
|
||||||
const popoverContent = props.scene.performers.map(performer => (
|
const popoverContent = props.scene.performers.map(performer => (
|
||||||
<div className="performer-tag-container">
|
<div className="performer-tag-container" key="performer">
|
||||||
<Link
|
<Link
|
||||||
to={`/performers/${performer.id}`}
|
to={`/performers/${performer.id}`}
|
||||||
className="performer-tag previewable image"
|
className="performer-tag previewable image"
|
||||||
@@ -182,7 +182,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={`zoom-${props.zoomIndex}`}
|
className={`zoom-${props.zoomIndex} scene-card`}
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
onMouseLeave={onMouseLeave}
|
onMouseLeave={onMouseLeave}
|
||||||
>
|
>
|
||||||
@@ -197,6 +197,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{maybeRenderSceneStudioOverlay()}
|
||||||
<Link
|
<Link
|
||||||
to={`/scenes/${props.scene.id}`}
|
to={`/scenes/${props.scene.id}`}
|
||||||
className={cx("image", "previewable", { portrait: isPortrait() })}
|
className={cx("image", "previewable", { portrait: isPortrait() })}
|
||||||
@@ -204,7 +205,6 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
<div className="video-container">
|
<div className="video-container">
|
||||||
{maybeRenderRatingBanner()}
|
{maybeRenderRatingBanner()}
|
||||||
{maybeRenderSceneSpecsOverlay()}
|
{maybeRenderSceneSpecsOverlay()}
|
||||||
{maybeRenderSceneStudioOverlay()}
|
|
||||||
<video
|
<video
|
||||||
loop
|
loop
|
||||||
className={cx("preview", { portrait: isPortrait() })}
|
className={cx("preview", { portrait: isPortrait() })}
|
||||||
|
|||||||
70
ui/v2.5/src/components/scenes/SceneDetails/PrimaryTags.tsx
Normal file
70
ui/v2.5/src/components/scenes/SceneDetails/PrimaryTags.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { Button, Badge, Card } from 'react-bootstrap';
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
|
|
||||||
|
interface IPrimaryTags {
|
||||||
|
sceneMarkers: GQL.SceneMarkerDataFragment[];
|
||||||
|
onClickMarker: (marker:GQL.SceneMarkerDataFragment) => void;
|
||||||
|
onEdit: (marker:GQL.SceneMarkerDataFragment) =>void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PrimaryTags: React.FC<IPrimaryTags> = ({ sceneMarkers, onClickMarker, onEdit }) => {
|
||||||
|
if (!sceneMarkers?.length) return <div />;
|
||||||
|
|
||||||
|
const primaries:Record<string, GQL.Tag> = {};
|
||||||
|
const primaryTags:Record<string, GQL.SceneMarkerDataFragment[]> = {};
|
||||||
|
sceneMarkers.forEach(m => {
|
||||||
|
if(primaryTags[m.primary_tag.id])
|
||||||
|
primaryTags[m.primary_tag.id].push(m);
|
||||||
|
else {
|
||||||
|
primaryTags[m.primary_tag.id] = [m];
|
||||||
|
primaries[m.primary_tag.id] = m.primary_tag;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryCards = Object.keys(primaryTags).map(id => {
|
||||||
|
const markers = primaryTags[id].map(marker => {
|
||||||
|
const tags = marker.tags.map(tag => (
|
||||||
|
<Badge key={tag.id} variant="secondary" className="tag-item">
|
||||||
|
{tag.name}
|
||||||
|
</Badge>
|
||||||
|
));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={marker.id}>
|
||||||
|
<hr />
|
||||||
|
<div>
|
||||||
|
<Button variant="link" onClick={() => onClickMarker(marker)}>
|
||||||
|
{marker.title}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
style={{ float: "right" }}
|
||||||
|
onClick={() => onEdit(marker)}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
|
||||||
|
<div className="card-section centered">{tags}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="primary-card col-3" key={id}>
|
||||||
|
<h3>{primaries[id].name}</h3>
|
||||||
|
<Card.Body className="primary-card-body">
|
||||||
|
{ markers }
|
||||||
|
</Card.Body>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="primary-tag row">
|
||||||
|
{ primaryCards }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
import { Card, Spinner, Tab, Tabs } from "react-bootstrap";
|
import { Tab, Tabs } from "react-bootstrap";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useLocation, useHistory } from "react-router-dom";
|
import { useParams, useLocation, useHistory } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
import { GalleryViewer } from "src/components/Galleries/GalleryViewer";
|
||||||
import { ScenePlayer } from "../ScenePlayer/ScenePlayer";
|
import { LoadingIndicator } from 'src/components/Shared';
|
||||||
import { SceneDetailPanel } from "./SceneDetailPanel";
|
import { ScenePlayer } from "src/components/scenes/ScenePlayer/ScenePlayer";
|
||||||
import { SceneEditPanel } from "./SceneEditPanel";
|
|
||||||
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
|
||||||
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
|
||||||
import { ScenePerformerPanel } from "./ScenePerformerPanel";
|
import { ScenePerformerPanel } from "./ScenePerformerPanel";
|
||||||
|
import { SceneMarkersPanel } from "./SceneMarkersPanel";
|
||||||
|
import { SceneFileInfoPanel } from "./SceneFileInfoPanel";
|
||||||
|
import { SceneEditPanel } from "./SceneEditPanel";
|
||||||
|
import { SceneDetailPanel } from "./SceneDetailPanel";
|
||||||
|
|
||||||
export const Scene: React.FC = () => {
|
export const Scene: React.FC = () => {
|
||||||
const { id = "new" } = useParams();
|
const { id = "new" } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||||
const [scene, setScene] = useState<Partial<GQL.SceneDataFragment>>({});
|
const [scene, setScene] = useState<GQL.SceneDataFragment | undefined>();
|
||||||
const { data, error, loading } = StashService.useFindScene(id);
|
const { data, error, loading } = StashService.useFindScene(id);
|
||||||
|
|
||||||
const queryParams = queryString.parse(location.search);
|
const queryParams = queryString.parse(location.search);
|
||||||
const autoplay = queryParams?.autoplay === "true";
|
const autoplay = queryParams?.autoplay === "true";
|
||||||
|
|
||||||
useEffect(() => setScene(data?.findScene ?? {}), [data]);
|
useEffect(() => {
|
||||||
|
if(data?.findScene)
|
||||||
|
setScene(data.findScene)
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
function getInitialTimestamp() {
|
function getInitialTimestamp() {
|
||||||
const params = queryString.parse(location.search);
|
const params = queryString.parse(location.search);
|
||||||
@@ -38,61 +42,56 @@ export const Scene: React.FC = () => {
|
|||||||
setTimestamp(marker.seconds);
|
setTimestamp(marker.seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data?.findScene || loading || Object.keys(scene).length === 0) {
|
if (loading || !scene || !data?.findScene) {
|
||||||
return <Spinner animation="border" />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
|
|
||||||
const modifiedScene = {
|
|
||||||
scene_marker_tags: data.sceneMarkerTags,
|
|
||||||
...scene
|
|
||||||
} as GQL.SceneDataFragment; // TODO Hack from angular
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScenePlayer
|
<ScenePlayer
|
||||||
scene={modifiedScene}
|
scene={scene}
|
||||||
timestamp={timestamp}
|
timestamp={timestamp}
|
||||||
autoplay={autoplay}
|
autoplay={autoplay}
|
||||||
/>
|
/>
|
||||||
<Card id="details-container">
|
<div id="details-container">
|
||||||
<Tabs id="scene-tabs" mountOnEnter>
|
<Tabs id="scene-tabs" mountOnEnter>
|
||||||
<Tab eventKey="scene-details-panel" title="Details">
|
<Tab eventKey="scene-details-panel" title="Details">
|
||||||
<SceneDetailPanel scene={modifiedScene} />
|
<SceneDetailPanel scene={scene} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="scene-markers-panel" title="Markers">
|
<Tab eventKey="scene-markers-panel" title="Markers">
|
||||||
<SceneMarkersPanel
|
<SceneMarkersPanel
|
||||||
scene={modifiedScene}
|
scene={scene}
|
||||||
onClickMarker={onClickMarker}
|
onClickMarker={onClickMarker}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
{modifiedScene.performers.length > 0 ? (
|
{scene.performers.length > 0 ? (
|
||||||
<Tab eventKey="scene-performer-panel" title="Performers">
|
<Tab eventKey="scene-performer-panel" title="Performers">
|
||||||
<ScenePerformerPanel scene={modifiedScene} />
|
<ScenePerformerPanel scene={scene} />
|
||||||
</Tab>
|
</Tab>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{modifiedScene.gallery ? (
|
{scene.gallery ? (
|
||||||
<Tab eventKey="scene-gallery-panel" title="Gallery">
|
<Tab eventKey="scene-gallery-panel" title="Gallery">
|
||||||
<GalleryViewer gallery={modifiedScene.gallery} />
|
<GalleryViewer gallery={scene.gallery} />
|
||||||
</Tab>
|
</Tab>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
<Tab eventKey="scene-file-info-panel" title="File Info">
|
<Tab className="file-info-panel" eventKey="scene-file-info-panel" title="File Info">
|
||||||
<SceneFileInfoPanel scene={modifiedScene} />
|
<SceneFileInfoPanel scene={scene} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="scene-edit-panel" title="Edit">
|
<Tab eventKey="scene-edit-panel" title="Edit">
|
||||||
<SceneEditPanel
|
<SceneEditPanel
|
||||||
scene={modifiedScene}
|
scene={scene}
|
||||||
onUpdate={newScene => setScene(newScene)}
|
onUpdate={newScene => setScene(newScene)}
|
||||||
onDelete={() => history.push("/scenes")}
|
onDelete={() => history.push("/scenes")}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared";
|
||||||
import { SceneHelpers } from "../helpers";
|
|
||||||
|
|
||||||
interface ISceneDetailProps {
|
interface ISceneDetailProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -10,9 +10,7 @@ interface ISceneDetailProps {
|
|||||||
|
|
||||||
export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
|
export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
|
||||||
function renderDetails() {
|
function renderDetails() {
|
||||||
if (!props.scene.details || props.scene.details === "") {
|
if (!props.scene.details || props.scene.details === "")return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h6>Details</h6>
|
<h6>Details</h6>
|
||||||
@@ -22,9 +20,7 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderTags() {
|
function renderTags() {
|
||||||
if (props.scene.tags.length === 0) {
|
if (props.scene.tags.length === 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tags = props.scene.tags.map(tag => (
|
const tags = props.scene.tags.map(tag => (
|
||||||
<TagLink key={tag.id} tag={tag} />
|
<TagLink key={tag.id} tag={tag} />
|
||||||
));
|
));
|
||||||
@@ -37,22 +33,26 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="row">
|
||||||
{SceneHelpers.maybeRenderStudio(props.scene, 70)}
|
<h1 className="col scene-header">
|
||||||
<h1>
|
{props.scene.title ?? TextUtils.fileNameFromPath(props.scene.path)}
|
||||||
{props.scene.title
|
|
||||||
? props.scene.title
|
|
||||||
: TextUtils.fileNameFromPath(props.scene.path)}
|
|
||||||
</h1>
|
</h1>
|
||||||
{props.scene.date ? <h4>{props.scene.date}</h4> : ""}
|
<div className="col-6 scene-details">
|
||||||
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
|
<h4>{props.scene.date ?? ''}</h4>
|
||||||
{props.scene.file.height ? (
|
{props.scene.rating ? <h6>Rating: {props.scene.rating}</h6> : ""}
|
||||||
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
|
{props.scene.file.height && (
|
||||||
) : (
|
<h6>Resolution: {TextUtils.resolution(props.scene.file.height)}</h6>
|
||||||
""
|
)}
|
||||||
|
{renderDetails()}
|
||||||
|
{renderTags()}
|
||||||
|
</div>
|
||||||
|
<div className="col-4 offset-2">
|
||||||
|
{ props.scene.studio && (
|
||||||
|
<Link className="studio-logo" to={`/studios/${props.scene.studio.id}`}>
|
||||||
|
<img src={props.scene.studio.image_path ?? ''} alt={`${props.scene.studio.name} logo`} />
|
||||||
|
</Link>
|
||||||
)}
|
)}
|
||||||
{renderDetails()}
|
</div>
|
||||||
{renderTags()}
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,24 +2,26 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Collapse,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownButton,
|
DropdownButton,
|
||||||
Form,
|
Form,
|
||||||
Button,
|
Table
|
||||||
Spinner
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
FilterSelect,
|
PerformerSelect,
|
||||||
|
TagSelect,
|
||||||
StudioSelect,
|
StudioSelect,
|
||||||
SceneGallerySelect,
|
SceneGallerySelect,
|
||||||
Modal,
|
Modal,
|
||||||
Icon
|
Icon,
|
||||||
|
LoadingIndicator,
|
||||||
|
ImageInput
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { ImageUtils } from "src/utils";
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
@@ -47,11 +49,10 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||||
|
|
||||||
const [isCoverImageOpen, setIsCoverImageOpen] = useState<boolean>(false);
|
|
||||||
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
const [updateScene] = StashService.useSceneUpdate(getSceneInput());
|
const [updateScene] = StashService.useSceneUpdate(getSceneInput());
|
||||||
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput());
|
const [deleteScene] = StashService.useSceneDestroy(getSceneDeleteInput());
|
||||||
@@ -65,9 +66,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
}, [Scrapers]);
|
}, [Scrapers]);
|
||||||
|
|
||||||
function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) {
|
function updateSceneEditState(state: Partial<GQL.SceneDataFragment>) {
|
||||||
const perfIds = state.performers
|
const perfIds = state.performers?.map(performer => performer.id);
|
||||||
? state.performers.map(performer => performer.id)
|
|
||||||
: undefined;
|
|
||||||
const tIds = state.tags ? state.tags.map(tag => tag.id) : undefined;
|
const tIds = state.tags ? state.tags.map(tag => tag.id) : undefined;
|
||||||
|
|
||||||
setTitle(state.title ?? undefined);
|
setTitle(state.title ?? undefined);
|
||||||
@@ -84,6 +83,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateSceneEditState(props.scene);
|
updateSceneEditState(props.scene);
|
||||||
setCoverImagePreview(props.scene?.paths?.screenshot ?? undefined);
|
setCoverImagePreview(props.scene?.paths?.screenshot ?? undefined);
|
||||||
|
setIsLoading(false);
|
||||||
}, [props.scene]);
|
}, [props.scene]);
|
||||||
|
|
||||||
ImageUtils.usePasteImage(onImageLoad);
|
ImageUtils.usePasteImage(onImageLoad);
|
||||||
@@ -136,34 +136,9 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
props.onDelete();
|
props.onDelete();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMultiSelect(
|
|
||||||
type: "performers" | "tags",
|
|
||||||
initialIds: string[] = []
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<FilterSelect
|
|
||||||
type={type}
|
|
||||||
isMulti
|
|
||||||
onSelect={items => {
|
|
||||||
const ids = items.map(i => i.id);
|
|
||||||
switch (type) {
|
|
||||||
case "performers":
|
|
||||||
setPerformerIds(ids);
|
|
||||||
break;
|
|
||||||
case "tags":
|
|
||||||
setTagIds(ids);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
initialIds={initialIds}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -278,11 +253,7 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!tagIds?.length && scene?.tags?.length) {
|
||||||
(!tagIds || tagIds.length === 0) &&
|
|
||||||
scene.tags &&
|
|
||||||
scene.tags.length > 0
|
|
||||||
) {
|
|
||||||
const idTags = scene.tags.filter(p => {
|
const idTags = scene.tags.filter(p => {
|
||||||
return p.id !== undefined && p.id !== null;
|
return p.id !== undefined && p.id !== null;
|
||||||
});
|
});
|
||||||
@@ -323,126 +294,123 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if(isLoading)
|
||||||
<>
|
return <LoadingIndicator />;
|
||||||
{renderDeleteAlert()}
|
|
||||||
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
|
|
||||||
<div className="form-container " style={{ width: "50%" }}>
|
|
||||||
<Form.Group controlId="title">
|
|
||||||
<Form.Label>Title</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
onChange={(newValue: any) => setTitle(newValue.target.value)}
|
|
||||||
value={title}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="form-container row">
|
||||||
|
<div className="col-6">
|
||||||
|
<Table id="scene-details">
|
||||||
|
<tbody>
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Title",
|
||||||
|
value: title,
|
||||||
|
onChange: setTitle,
|
||||||
|
isEditing: true
|
||||||
|
})}
|
||||||
|
<tr>
|
||||||
|
<td>URL</td>
|
||||||
|
<td>
|
||||||
|
<Form.Control
|
||||||
|
onChange={(newValue: any) => setUrl(newValue.target.value)}
|
||||||
|
value={url}
|
||||||
|
placeholder="URL"
|
||||||
|
/>
|
||||||
|
{maybeRenderScrapeButton()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Date (YYYY-MM-DD)",
|
||||||
|
value: date,
|
||||||
|
isEditing: true,
|
||||||
|
onChange: setDate
|
||||||
|
})}
|
||||||
|
{TableUtils.renderHtmlSelect({
|
||||||
|
title: "Rating",
|
||||||
|
value: rating,
|
||||||
|
isEditing: true,
|
||||||
|
onChange: (value: string) => setRating(Number.parseInt(value, 10)),
|
||||||
|
selectOptions: ['', 1, 2, 3, 4, 5]
|
||||||
|
})}
|
||||||
|
<tr>
|
||||||
|
<td>Gallery</td>
|
||||||
|
<td>
|
||||||
|
<SceneGallerySelect
|
||||||
|
sceneId={props.scene.id}
|
||||||
|
initialId={galleryId}
|
||||||
|
onSelect={item => setGalleryId(item ? item.id : undefined)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Studio</td>
|
||||||
|
<td>
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={items => items.length && setStudioId(items[0]?.id)}
|
||||||
|
ids={studioId ? [studioId] : []}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Performers</td>
|
||||||
|
<td>
|
||||||
|
<PerformerSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={items => setPerformerIds(items.map(item => item.id))}
|
||||||
|
ids={performerIds}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Tags</td>
|
||||||
|
<td>
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={items => setTagIds(items.map(item => item.id))}
|
||||||
|
ids={tagIds}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<div className="col-5 offset-1">
|
||||||
<Form.Group controlId="details">
|
<Form.Group controlId="details">
|
||||||
<Form.Label>Details</Form.Label>
|
<Form.Label>Details</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
|
className="scene-description"
|
||||||
onChange={(newValue: any) => setDetails(newValue.target.value)}
|
onChange={(newValue: any) => setDetails(newValue.target.value)}
|
||||||
value={details}
|
value={details}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="url">
|
|
||||||
<Form.Label>URL</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
onChange={(newValue: any) => setUrl(newValue.target.value)}
|
|
||||||
value={url}
|
|
||||||
/>
|
|
||||||
{maybeRenderScrapeButton()}
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="date">
|
|
||||||
<Form.Label>Date</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
onChange={(newValue: any) => setDate(newValue.target.value)}
|
|
||||||
value={date}
|
|
||||||
/>
|
|
||||||
<div>YYYY-MM-DD</div>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="rating">
|
|
||||||
<Form.Label>Rating</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="select"
|
|
||||||
onChange={(event: any) =>
|
|
||||||
setRating(parseInt(event.target.value, 10))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{["", 1, 2, 3, 4, 5].map(opt => (
|
|
||||||
<option selected={opt === rating} value={opt}>
|
|
||||||
{opt}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="gallery">
|
|
||||||
<Form.Label>Gallery</Form.Label>
|
|
||||||
<SceneGallerySelect
|
|
||||||
sceneId={props.scene.id}
|
|
||||||
initialId={galleryId}
|
|
||||||
onSelect={item => setGalleryId(item ? item.id : undefined)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="studio">
|
|
||||||
<Form.Label>Studio</Form.Label>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={items => items.length && setStudioId(items[0]?.id)}
|
|
||||||
initialIds={studioId ? [studioId] : []}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="performers">
|
|
||||||
<Form.Label>Performers</Form.Label>
|
|
||||||
{renderMultiSelect("performers", performerIds)}
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="tags">
|
|
||||||
<Form.Label>Tags</Form.Label>
|
|
||||||
{renderMultiSelect("tags", tagIds)}
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Form.Group className="test" controlId="cover">
|
||||||
variant="link"
|
<Form.Label>Cover Image</Form.Label>
|
||||||
onClick={() => setIsCoverImageOpen(!isCoverImageOpen)}
|
<img
|
||||||
>
|
className="scene-cover"
|
||||||
<Icon icon={isCoverImageOpen ? "chevron-down" : "chevron-right"} />
|
src={coverImagePreview}
|
||||||
<span>Cover Image</span>
|
alt="Scene cover"
|
||||||
</Button>
|
/>
|
||||||
<Collapse in={isCoverImageOpen}>
|
<ImageInput isEditing onImageChange={onCoverImageChange} />
|
||||||
<div>
|
</Form.Group>
|
||||||
<img
|
|
||||||
className="scene-cover"
|
|
||||||
src={coverImagePreview}
|
|
||||||
alt="Scene cover"
|
|
||||||
/>
|
|
||||||
<Form.Group className="test" controlId="cover">
|
|
||||||
<Form.Control
|
|
||||||
type="file"
|
|
||||||
onChange={onCoverImageChange}
|
|
||||||
accept=".jpg,.jpeg,.png"
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
<div className="col edit-buttons">
|
||||||
Save
|
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||||
</Button>
|
Save
|
||||||
<Button
|
</Button>
|
||||||
className="edit-button"
|
<Button
|
||||||
variant="danger"
|
className="edit-button"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
variant="danger"
|
||||||
>
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
Delete
|
>
|
||||||
</Button>
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{renderScraperMenu()}
|
{renderScraperMenu()}
|
||||||
</>
|
{renderDeleteAlert()}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
177
ui/v2.5/src/components/scenes/SceneDetails/SceneMarkerForm.tsx
Normal file
177
ui/v2.5/src/components/scenes/SceneDetails/SceneMarkerForm.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { StashService } from "src/core/StashService";
|
||||||
|
import {
|
||||||
|
DurationInput,
|
||||||
|
TagSelect,
|
||||||
|
MarkerTitleSuggest
|
||||||
|
} from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
|
interface IFormFields {
|
||||||
|
title: string;
|
||||||
|
seconds: string;
|
||||||
|
primaryTagId: string;
|
||||||
|
tagIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ISceneMarkerForm {
|
||||||
|
sceneID: string;
|
||||||
|
editingMarker?: GQL.SceneMarkerDataFragment;
|
||||||
|
playerPosition?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({ sceneID, editingMarker, playerPosition, onClose }) => {
|
||||||
|
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate();
|
||||||
|
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate();
|
||||||
|
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const onSubmit = (values: IFormFields) => {
|
||||||
|
const variables:
|
||||||
|
| GQL.SceneMarkerUpdateInput
|
||||||
|
| GQL.SceneMarkerCreateInput = {
|
||||||
|
title: values.title,
|
||||||
|
seconds: parseFloat(values.seconds),
|
||||||
|
scene_id: sceneID,
|
||||||
|
primary_tag_id: values.primaryTagId,
|
||||||
|
tag_ids: values.tagIds
|
||||||
|
};
|
||||||
|
if (!editingMarker) {
|
||||||
|
sceneMarkerCreate({ variables })
|
||||||
|
.then(onClose)
|
||||||
|
.catch(err => Toast.error(err));
|
||||||
|
} else {
|
||||||
|
const updateVariables = variables as GQL.SceneMarkerUpdateInput;
|
||||||
|
updateVariables.id = editingMarker!.id;
|
||||||
|
sceneMarkerUpdate({ variables: updateVariables })
|
||||||
|
.then(onClose)
|
||||||
|
.catch(err => Toast.error(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
if (!editingMarker) return;
|
||||||
|
|
||||||
|
sceneMarkerDestroy({ variables: { id: editingMarker.id } })
|
||||||
|
.then(onClose)
|
||||||
|
.catch(err => Toast.error(err));
|
||||||
|
}
|
||||||
|
const renderTitleField = (fieldProps: FieldProps<string>) => (
|
||||||
|
<div className="col-6">
|
||||||
|
<MarkerTitleSuggest
|
||||||
|
initialMarkerTitle={fieldProps.field.value}
|
||||||
|
onChange={(query: string) =>
|
||||||
|
fieldProps.form.setFieldValue("title", query)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSecondsField = (fieldProps: FieldProps<string>) => (
|
||||||
|
<DurationInput
|
||||||
|
onValueChange={s => fieldProps.form.setFieldValue("seconds", s)}
|
||||||
|
onReset={() =>
|
||||||
|
fieldProps.form.setFieldValue(
|
||||||
|
"seconds",
|
||||||
|
Math.round(playerPosition ?? 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
numericValue={Number.parseInt(fieldProps.field.value ?? '0', 10)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPrimaryTagField = (fieldProps: FieldProps<string>) => (
|
||||||
|
<TagSelect
|
||||||
|
onSelect={tags =>
|
||||||
|
fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)
|
||||||
|
}
|
||||||
|
ids={fieldProps.field.value ? [fieldProps.field.value] : []}
|
||||||
|
noSelectionString="Select or create tag..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTagsField = (fieldProps: FieldProps<string[]>) => (
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={tags =>
|
||||||
|
fieldProps.form.setFieldValue(
|
||||||
|
"tagIds",
|
||||||
|
tags.map(tag => tag.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={fieldProps.field.value}
|
||||||
|
noSelectionString="Select or create tags..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const values:IFormFields = {
|
||||||
|
title: editingMarker?.title ?? '',
|
||||||
|
seconds: (editingMarker?.seconds ?? Math.round(playerPosition ?? 0)).toString(),
|
||||||
|
primaryTagId: editingMarker?.primary_tag.id ?? '',
|
||||||
|
tagIds: editingMarker?.tags.map(tag => tag.id) ?? []
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Formik
|
||||||
|
initialValues={values}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
>
|
||||||
|
<FormikForm>
|
||||||
|
<div>
|
||||||
|
<Form.Group className="row">
|
||||||
|
<Form.Label htmlFor="title" className="col-2">
|
||||||
|
Scene Marker Title
|
||||||
|
</Form.Label>
|
||||||
|
<Field name="title" className="col-6">
|
||||||
|
{renderTitleField}
|
||||||
|
</Field>
|
||||||
|
<Form.Label htmlFor="seconds" className="col-1">Time</Form.Label>
|
||||||
|
<Field name="seconds" className="col-2">
|
||||||
|
{renderSecondsField}
|
||||||
|
</Field>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group className="row">
|
||||||
|
<Form.Label htmlFor="primaryTagId" className="col-2">
|
||||||
|
Primary Tag
|
||||||
|
</Form.Label>
|
||||||
|
<div className="col-4">
|
||||||
|
<Field name="primaryTagId">
|
||||||
|
{renderPrimaryTagField}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
<Form.Label htmlFor="tagIds" className="col-2">Tags</Form.Label>
|
||||||
|
<div className="col-4">
|
||||||
|
<Field name="tagIds">
|
||||||
|
{renderTagsField}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
</div>
|
||||||
|
<div className="buttons-container">
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{ editingMarker && (
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
style={{ float: "right", marginRight: "10px" }}
|
||||||
|
onClick={() => onDelete()}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormikForm>
|
||||||
|
</Formik>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,312 +1,62 @@
|
|||||||
import React, { CSSProperties, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Badge,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Collapse,
|
|
||||||
Form as BootstrapForm
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { Field, FieldProps, Form, Formik } from "formik";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
|
||||||
import { TextUtils } from "src/utils";
|
|
||||||
import { useToast } from "src/hooks";
|
|
||||||
import {
|
|
||||||
DurationInput,
|
|
||||||
TagSelect,
|
|
||||||
MarkerTitleSuggest
|
|
||||||
} from "src/components/Shared";
|
|
||||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||||
import { SceneHelpers } from "../helpers";
|
import { JWUtils } from "src/utils";
|
||||||
|
import { PrimaryTags } from './PrimaryTags';
|
||||||
|
import { SceneMarkerForm } from './SceneMarkerForm';
|
||||||
|
|
||||||
interface ISceneMarkersPanelProps {
|
interface ISceneMarkersPanelProps {
|
||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
onClickMarker: (marker: GQL.SceneMarkerDataFragment) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFormFields {
|
|
||||||
title: string;
|
|
||||||
seconds: string;
|
|
||||||
primaryTagId: string;
|
|
||||||
tagIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||||
props: ISceneMarkersPanelProps
|
props: ISceneMarkersPanelProps
|
||||||
) => {
|
) => {
|
||||||
const Toast = useToast();
|
|
||||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||||
const [
|
const [
|
||||||
editingMarker,
|
editingMarker,
|
||||||
setEditingMarker
|
setEditingMarker
|
||||||
] = useState<GQL.SceneMarkerDataFragment | null>(null);
|
] = useState<GQL.SceneMarkerDataFragment>();
|
||||||
|
|
||||||
const [sceneMarkerCreate] = StashService.useSceneMarkerCreate();
|
const jwplayer = JWUtils.getPlayer();
|
||||||
const [sceneMarkerUpdate] = StashService.useSceneMarkerUpdate();
|
|
||||||
const [sceneMarkerDestroy] = StashService.useSceneMarkerDestroy();
|
|
||||||
|
|
||||||
const jwplayer = SceneHelpers.getPlayer();
|
|
||||||
|
|
||||||
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
function onOpenEditor(marker?: GQL.SceneMarkerDataFragment) {
|
||||||
setIsEditorOpen(true);
|
setIsEditorOpen(true);
|
||||||
setEditingMarker(marker ?? null);
|
setEditingMarker(marker ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
function onClickMarker(marker: GQL.SceneMarkerDataFragment) {
|
||||||
props.onClickMarker(marker);
|
props.onClickMarker(marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTags() {
|
const closeEditor = () => {
|
||||||
function renderMarkers(primaryTag: GQL.SceneMarkerTag) {
|
setEditingMarker(undefined);
|
||||||
const markers = primaryTag.scene_markers.map(marker => {
|
setIsEditorOpen(false);
|
||||||
const markerTags = marker.tags.map(tag => (
|
};
|
||||||
<Badge key={tag.id} variant="secondary" className="tag-item">
|
|
||||||
{tag.name}
|
|
||||||
</Badge>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
if(isEditorOpen)
|
||||||
<div key={marker.id}>
|
|
||||||
<hr />
|
|
||||||
<div>
|
|
||||||
<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)}</div>
|
|
||||||
<div className="card-section centered">{markerTags}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return markers;
|
|
||||||
}
|
|
||||||
|
|
||||||
const style: CSSProperties = {
|
|
||||||
height: "300px",
|
|
||||||
overflowY: "auto",
|
|
||||||
overflowX: "hidden",
|
|
||||||
display: "inline-block",
|
|
||||||
margin: "5px",
|
|
||||||
width: "300px",
|
|
||||||
flex: "0 0 auto"
|
|
||||||
};
|
|
||||||
const tags = (props.scene as any).scene_marker_tags.map(
|
|
||||||
(primaryTag: GQL.SceneMarkerTag) => {
|
|
||||||
return (
|
|
||||||
<div key={primaryTag.tag.id} style={{ padding: "1px" }}>
|
|
||||||
<Card style={style}>
|
|
||||||
<div className="content" style={{ whiteSpace: "normal" }}>
|
|
||||||
<h3>{primaryTag.tag.name}</h3>
|
|
||||||
{renderMarkers(primaryTag)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderForm() {
|
|
||||||
function onSubmit(values: IFormFields) {
|
|
||||||
const isEditing = !!editingMarker;
|
|
||||||
const variables:
|
|
||||||
| GQL.SceneMarkerUpdateInput
|
|
||||||
| GQL.SceneMarkerCreateInput = {
|
|
||||||
title: values.title,
|
|
||||||
seconds: parseFloat(values.seconds),
|
|
||||||
scene_id: props.scene.id,
|
|
||||||
primary_tag_id: values.primaryTagId,
|
|
||||||
tag_ids: values.tagIds
|
|
||||||
};
|
|
||||||
if (!isEditing) {
|
|
||||||
sceneMarkerCreate({ variables })
|
|
||||||
.then(() => {
|
|
||||||
setIsEditorOpen(false);
|
|
||||||
setEditingMarker(null);
|
|
||||||
})
|
|
||||||
.catch(err => Toast.error(err));
|
|
||||||
} else {
|
|
||||||
const updateVariables = variables as GQL.SceneMarkerUpdateInput;
|
|
||||||
updateVariables.id = editingMarker!.id;
|
|
||||||
sceneMarkerUpdate({ variables: updateVariables })
|
|
||||||
.then(() => {
|
|
||||||
setIsEditorOpen(false);
|
|
||||||
setEditingMarker(null);
|
|
||||||
})
|
|
||||||
.catch(err => Toast.error(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function onDelete() {
|
|
||||||
if (!editingMarker) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sceneMarkerDestroy({ variables: { id: editingMarker.id } })
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
.catch(err => console.error(err));
|
|
||||||
setIsEditorOpen(false);
|
|
||||||
setEditingMarker(null);
|
|
||||||
}
|
|
||||||
function renderTitleField(fieldProps: FieldProps<IFormFields>) {
|
|
||||||
return (
|
|
||||||
<MarkerTitleSuggest
|
|
||||||
initialMarkerTitle={editingMarker?.title}
|
|
||||||
onChange={(query: string) =>
|
|
||||||
fieldProps.form.setFieldValue("title", query)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function renderSecondsField(fieldProps: FieldProps<IFormFields>) {
|
|
||||||
return (
|
|
||||||
<DurationInput
|
|
||||||
onValueChange={s => fieldProps.form.setFieldValue("seconds", s)}
|
|
||||||
onReset={() =>
|
|
||||||
fieldProps.form.setFieldValue(
|
|
||||||
"seconds",
|
|
||||||
Math.round(jwplayer.getPosition())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
numericValue={Number.parseInt(fieldProps.field.value.seconds, 10)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function renderPrimaryTagField(fieldProps: FieldProps<IFormFields>) {
|
|
||||||
return (
|
|
||||||
<TagSelect
|
|
||||||
onSelect={tags =>
|
|
||||||
fieldProps.form.setFieldValue("primaryTagId", tags[0]?.id)
|
|
||||||
}
|
|
||||||
initialIds={editingMarker ? [editingMarker.primary_tag.id] : []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function renderTagsField(fieldProps: FieldProps<IFormFields>) {
|
|
||||||
return (
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={tags =>
|
|
||||||
fieldProps.form.setFieldValue(
|
|
||||||
"tagIds",
|
|
||||||
tags.map(tag => tag.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
initialIds={editingMarker ? fieldProps.form.values.tagIds : []}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
function renderFormFields() {
|
|
||||||
let deleteButton: JSX.Element | undefined;
|
|
||||||
if (editingMarker) {
|
|
||||||
deleteButton = (
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
style={{ float: "right", marginRight: "10px" }}
|
|
||||||
onClick={() => onDelete()}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Form style={{ marginTop: "10px" }}>
|
|
||||||
<div className="columns is-multiline is-gapless">
|
|
||||||
<BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Label htmlFor="title">
|
|
||||||
Scene Marker Title
|
|
||||||
</BootstrapForm.Label>
|
|
||||||
<Field name="title" render={renderTitleField} />
|
|
||||||
</BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Label htmlFor="seconds">Time</BootstrapForm.Label>
|
|
||||||
<Field name="seconds" render={renderSecondsField} />
|
|
||||||
</BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Label htmlFor="primaryTagId">
|
|
||||||
Primary Tag
|
|
||||||
</BootstrapForm.Label>
|
|
||||||
<Field name="primaryTagId" render={renderPrimaryTagField} />
|
|
||||||
</BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Group>
|
|
||||||
<BootstrapForm.Label htmlFor="tagIds">Tags</BootstrapForm.Label>
|
|
||||||
<Field name="tagIds" render={renderTagsField} />
|
|
||||||
</BootstrapForm.Group>
|
|
||||||
</div>
|
|
||||||
<div className="buttons-container">
|
|
||||||
<Button variant="primary" type="submit">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
<Button type="button" onClick={() => setIsEditorOpen(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
{deleteButton}
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let initialValues: any;
|
|
||||||
if (editingMarker) {
|
|
||||||
initialValues = {
|
|
||||||
title: editingMarker.title,
|
|
||||||
seconds: editingMarker.seconds,
|
|
||||||
primaryTagId: editingMarker.primary_tag.id,
|
|
||||||
tagIds: editingMarker.tags.map(tag => tag.id)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
initialValues = {
|
|
||||||
title: "",
|
|
||||||
seconds: Math.round(jwplayer.getPosition()),
|
|
||||||
primaryTagId: "",
|
|
||||||
tagIds: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Collapse in={isEditorOpen}>
|
<SceneMarkerForm
|
||||||
<div className="">
|
sceneID={props.scene.id}
|
||||||
<Formik
|
editingMarker={editingMarker}
|
||||||
initialValues={initialValues}
|
playerPosition={jwplayer.getPlayer?.().playerPosition}
|
||||||
onSubmit={onSubmit}
|
onClose={closeEditor}
|
||||||
render={renderFormFields}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
return (
|
||||||
const newMarkerForm = (
|
<>
|
||||||
<div style={{ margin: "5px" }}>
|
<Button onClick={() => onOpenEditor()}>Create Marker</Button>
|
||||||
<Button onClick={() => onOpenEditor()}>Create</Button>
|
<PrimaryTags
|
||||||
{renderForm()}
|
sceneMarkers={props.scene.scene_markers ?? []}
|
||||||
</div>
|
onClickMarker={onClickMarker}
|
||||||
);
|
onEdit={onOpenEditor}
|
||||||
if (props.scene.scene_markers.length === 0) {
|
/>
|
||||||
return newMarkerForm;
|
<div className="row">
|
||||||
}
|
|
||||||
|
|
||||||
const containerStyle: CSSProperties = {
|
|
||||||
overflowY: "hidden",
|
|
||||||
overflowX: "scroll",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "nowrap",
|
|
||||||
marginBottom: "20px"
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{newMarkerForm}
|
|
||||||
<div style={containerStyle}>{renderTags()}</div>
|
|
||||||
<WallPanel
|
<WallPanel
|
||||||
sceneMarkers={props.scene.scene_markers}
|
sceneMarkers={props.scene.scene_markers}
|
||||||
clickHandler={marker => {
|
clickHandler={marker => {
|
||||||
@@ -314,9 +64,7 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
|||||||
onClickMarker(marker as any);
|
onClickMarker(marker as any);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
</>
|
||||||
}
|
);
|
||||||
|
|
||||||
return render();
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownButton,
|
DropdownButton,
|
||||||
Form,
|
Form,
|
||||||
Table,
|
Table
|
||||||
Spinner
|
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FilterSelect, Icon, StudioSelect } from "src/components/Shared";
|
import { FilterSelect, Icon, StudioSelect, LoadingIndicator } from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Pagination } from "../list/Pagination";
|
import { Pagination } from "../list/Pagination";
|
||||||
@@ -1065,7 +1064,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
<h4>Scene Filename Parser</h4>
|
<h4>Scene Filename Parser</h4>
|
||||||
<ParserInput input={parserInput} onFind={input => onFindClicked(input)} />
|
<ParserInput input={parserInput} onFind={input => onFindClicked(input)} />
|
||||||
|
|
||||||
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
|
{isLoading && <LoadingIndicator />}
|
||||||
{renderTable()}
|
{renderTable()}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
|
|
||||||
function renderTags(tags: GQL.Tag[]) {
|
function renderTags(tags: GQL.Tag[]) {
|
||||||
return tags.map(tag => (
|
return tags.map(tag => (
|
||||||
<Link to={NavUtils.makeTagScenesUrl(tag)}>
|
<Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}>
|
||||||
<h6>{tag.name}</h6>
|
<h6>{tag.name}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
@@ -47,7 +47,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
|
|
||||||
function renderPerformers(performers: Partial<GQL.Performer>[]) {
|
function renderPerformers(performers: Partial<GQL.Performer>[]) {
|
||||||
return performers.map(performer => (
|
return performers.map(performer => (
|
||||||
<Link to={NavUtils.makePerformerScenesUrl(performer)}>
|
<Link key={performer.id} to={NavUtils.makePerformerScenesUrl(performer)}>
|
||||||
<h6>{performer.name}</h6>
|
<h6>{performer.name}</h6>
|
||||||
</Link>
|
</Link>
|
||||||
));
|
));
|
||||||
@@ -65,7 +65,7 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
|
|
||||||
function renderSceneRow(scene: GQL.SlimSceneDataFragment) {
|
function renderSceneRow(scene: GQL.SlimSceneDataFragment) {
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr key={scene.id}>
|
||||||
<td>{renderSceneImage(scene)}</td>
|
<td>{renderSceneImage(scene)}</td>
|
||||||
<td style={{ textAlign: "left" }}>
|
<td style={{ textAlign: "left" }}>
|
||||||
<Link to={`/scenes/${scene.id}`}>
|
<Link to={`/scenes/${scene.id}`}>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ReactJWPlayer from "react-jw-player";
|
|||||||
import { HotKeys } from "react-hotkeys";
|
import { HotKeys } from "react-hotkeys";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import { SceneHelpers } from "../helpers";
|
import { JWUtils } from 'src/utils';
|
||||||
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||||
|
|
||||||
interface IScenePlayerProps {
|
interface IScenePlayerProps {
|
||||||
@@ -84,7 +84,7 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
private onReady() {
|
private onReady() {
|
||||||
this.player = SceneHelpers.getPlayer();
|
this.player = JWUtils.getPlayer();
|
||||||
if (this.props.timestamp > 0) {
|
if (this.props.timestamp > 0) {
|
||||||
this.player.seek(this.props.timestamp);
|
this.player.seek(this.props.timestamp);
|
||||||
}
|
}
|
||||||
@@ -193,8 +193,8 @@ export class ScenePlayerImpl extends React.Component<
|
|||||||
const config = this.makeJWPlayerConfig(this.props.scene);
|
const config = this.makeJWPlayerConfig(this.props.scene);
|
||||||
return (
|
return (
|
||||||
<ReactJWPlayer
|
<ReactJWPlayer
|
||||||
playerId={SceneHelpers.getJWPlayerId()}
|
playerId={JWUtils.playerID}
|
||||||
playerScript="/jwplayer/jwplayer.js"
|
playerScript="http://192.168.1.65:9999/jwplayer/jwplayer.js"
|
||||||
customProps={config}
|
customProps={config}
|
||||||
onReady={this.onReady}
|
onReady={this.onReady}
|
||||||
onSeeked={this.onSeeked}
|
onSeeked={this.onSeeked}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.scrubber-wrapper {
|
.scrubber-wrapper {
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scrubber-back {
|
#scrubber-back {
|
||||||
@@ -13,116 +13,119 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-button {
|
.scrubber-button {
|
||||||
width: 1.5%;
|
border: 1px solid #555;
|
||||||
|
color: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
line-height: 120px;
|
line-height: 120px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border: 1px solid #555;
|
width: 1.5%;
|
||||||
font-weight: 800;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #FFF;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-content {
|
.scrubber-content {
|
||||||
-webkit-user-select: none;
|
cursor: grab;
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
cursor: -webkit-grab;
|
|
||||||
height: 120px;
|
|
||||||
width: 96%;
|
|
||||||
margin: 0 0.5%;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
height: 120px;
|
||||||
|
margin: 0 .5%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
position: relative;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
width: 96%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-content.dragging {
|
.scrubber-content.dragging {
|
||||||
cursor: -webkit-grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-tags-background {
|
.scrubber-tags-background {
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scrubber-position-indicator {
|
#scrubber-position-indicator {
|
||||||
background-color: #CCC;
|
background-color: #ccc;
|
||||||
width: 100%;
|
|
||||||
left: -100%;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
z-index: 0;
|
left: -100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#scrubber-current-position {
|
#scrubber-current-position {
|
||||||
background-color: #FFF;
|
background-color: #fff;
|
||||||
width: 2px;
|
|
||||||
height: 30px;
|
height: 30px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
width: 2px;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-viewport {
|
.scrubber-viewport {
|
||||||
position: static;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-slider {
|
.scrubber-slider {
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
position: absolute;
|
||||||
transition: 333ms ease-out;
|
transition: 333ms ease-out;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-tags {
|
.scrubber-tags {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
position: relative;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-tag {
|
.scrubber-tag {
|
||||||
position: absolute;
|
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
font-size: 10px;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 0 10px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
font-size: 10px;
|
||||||
.scrubber-tag:hover {
|
padding: 0 10px;
|
||||||
z-index: 1;
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
.scrubber-tag:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: -5px;
|
white-space: nowrap;
|
||||||
left: 50%;
|
}
|
||||||
margin-left: -5px;
|
|
||||||
border-top: solid 5px #000;
|
.scrubber-tag:hover {
|
||||||
|
background-color: #444;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrubber-tag::after {
|
||||||
border-left: solid 5px transparent;
|
border-left: solid 5px transparent;
|
||||||
border-right: solid 5px transparent;
|
border-right: solid 5px transparent;
|
||||||
|
border-top: solid 5px #000;
|
||||||
|
bottom: -5px;
|
||||||
|
content: "";
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -5px;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-item {
|
.scrubber-item {
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
margin-right: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: white;
|
color: white;
|
||||||
text-shadow: 1px 1px black;
|
cursor: pointer;
|
||||||
text-align: center;
|
display: flex;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 1px 1px black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrubber-item span {
|
.scrubber-item span {
|
||||||
display: inline-block;
|
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, ButtonGroup, Form, Spinner } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FilterSelect, StudioSelect } from "src/components/Shared";
|
import { FilterSelect, StudioSelect, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
interface IListOperationProps {
|
interface IListOperationProps {
|
||||||
@@ -245,7 +245,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
|||||||
const itemIDs = items.map(i => i.id);
|
const itemIDs = items.map(i => i.id);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "performers":
|
case "performers":
|
||||||
setPerformerIds(itemIDS);
|
setPerformerIds(itemIDs);
|
||||||
break;
|
break;
|
||||||
case "tags":
|
case "tags":
|
||||||
setTagIds(itemIDs);
|
setTagIds(itemIDs);
|
||||||
@@ -258,7 +258,7 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(isLoading)
|
if(isLoading)
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <LoadingIndicator />;
|
||||||
|
|
||||||
function render() {
|
function render() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
|
||||||
|
|
||||||
export class SceneHelpers {
|
|
||||||
public static maybeRenderStudio(
|
|
||||||
scene: GQL.SceneDataFragment | GQL.SlimSceneDataFragment,
|
|
||||||
height: number
|
|
||||||
) {
|
|
||||||
if (!scene.studio) return;
|
|
||||||
const style: React.CSSProperties = {
|
|
||||||
backgroundImage: `url('${scene.studio.image_path}')`,
|
|
||||||
width: "100%",
|
|
||||||
height: `${height}px`,
|
|
||||||
lineHeight: 5,
|
|
||||||
backgroundSize: "contain",
|
|
||||||
display: "inline-block",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
backgroundRepeat: "no-repeat"
|
|
||||||
};
|
|
||||||
return <Link to={`/studios/${scene.studio.id}`} style={style} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getJWPlayerId(): string {
|
|
||||||
return "main-jwplayer";
|
|
||||||
}
|
|
||||||
public static getPlayer(): any {
|
|
||||||
return (window as any).jwplayer("main-jwplayer");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 3px;
|
padding-bottom: 3px;
|
||||||
|
padding-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@@ -13,9 +13,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
.scene-card {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.performer-tag-container {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.performer-tag.image {
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: cover;
|
||||||
|
height: 150px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.operation-container {
|
.operation-container {
|
||||||
.operation-item {
|
.operation-item {
|
||||||
min-width: 200px;
|
min-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-operation {
|
.rating-operation {
|
||||||
@@ -26,3 +46,62 @@
|
|||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.marker-container {
|
||||||
|
display: "flex";
|
||||||
|
flex-wrap: "nowrap";
|
||||||
|
margin-bottom: "20px";
|
||||||
|
overflow-x: "scroll";
|
||||||
|
overflow-y: "hidden";
|
||||||
|
white-space: "nowrap";
|
||||||
|
}
|
||||||
|
|
||||||
|
.studio-logo {
|
||||||
|
img {
|
||||||
|
margin-top: 1rem;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-header {
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#details-container {
|
||||||
|
.tab-content {
|
||||||
|
min-height: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-description {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info-panel {
|
||||||
|
td {
|
||||||
|
padding: .4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#scene-details {
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#details {
|
||||||
|
min-height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-buttons {
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-card {
|
||||||
|
margin: 1rem 0;
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
max-height: 15rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Spinner } from "react-bootstrap";
|
|
||||||
import { ApolloError } from "apollo-client";
|
import { ApolloError } from "apollo-client";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
@@ -16,12 +15,13 @@ import {
|
|||||||
FindStudiosQueryResult,
|
FindStudiosQueryResult,
|
||||||
FindPerformersQueryResult
|
FindPerformersQueryResult
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { ListFilter } from "../components/list/ListFilter";
|
import { LoadingIndicator } from 'src/components/Shared';
|
||||||
import { Pagination } from "../components/list/Pagination";
|
import { ListFilter } from "src/components/list/ListFilter";
|
||||||
import { StashService } from "../core/StashService";
|
import { Pagination } from "src/components/list/Pagination";
|
||||||
import { Criterion } from "../models/list-filter/criteria/criterion";
|
import { StashService } from "src/core/StashService";
|
||||||
import { ListFilterModel } from "../models/list-filter/filter";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { DisplayMode, FilterMode } from "../models/list-filter/types";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { DisplayMode, FilterMode } from "src/models/list-filter/types";
|
||||||
|
|
||||||
interface IListHookData {
|
interface IListHookData {
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
@@ -294,12 +294,8 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
{options.renderSelectedOptions && selectedIds.size > 0
|
{options.renderSelectedOptions && selectedIds.size > 0
|
||||||
? options.renderSelectedOptions(result, selectedIds)
|
? options.renderSelectedOptions(result, selectedIds)
|
||||||
: undefined}
|
: undefined}
|
||||||
{result.loading ? (
|
{result.loading && <LoadingIndicator />}
|
||||||
<Spinner animation="border" variant="light" />
|
{result.error && <h1>{result.error.message}</h1>}
|
||||||
) : (
|
|
||||||
undefined
|
|
||||||
)}
|
|
||||||
{result.error ? <h1>{result.error.message}</h1> : undefined}
|
|
||||||
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
{options.renderContent(result, filter, selectedIds, zoomIndex)}
|
||||||
<Pagination
|
<Pagination
|
||||||
itemsPerPage={filter.itemsPerPage}
|
itemsPerPage={filter.itemsPerPage}
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
@import "styles/theme";
|
@import "styles/theme";
|
||||||
@import "styles/form/grid";
|
|
||||||
|
|
||||||
@import "styles/shared/details";
|
|
||||||
|
|
||||||
@import "styles/range";
|
@import "styles/range";
|
||||||
@import "styles/scrollbars";
|
@import "styles/scrollbars";
|
||||||
@import "styles/variables";
|
@import "styles/variables";
|
||||||
|
|
||||||
@import "./components/**/*.scss";
|
@import "./components/**/*.scss";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
font-family:
|
||||||
padding: $pt-navbar-height 0 0 0;
|
-apple-system,
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
BlinkMacSystemFont,
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
"Fira Sans",
|
||||||
|
"Droid Sans",
|
||||||
|
"Helvetica Neue",
|
||||||
sans-serif;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
height: 100vh;
|
margin: 0;
|
||||||
|
padding: $pt-navbar-height 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
@@ -30,24 +32,24 @@ code {
|
|||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: $pt-grid-size $pt-grid-size 0 0;
|
margin: $pt-grid-size $pt-grid-size 0 0;
|
||||||
padding: 0 100px;
|
padding: 0;
|
||||||
|
|
||||||
&.wall {
|
&.wall {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .performer-list-thumbnail {
|
& .performer-list-thumbnail {
|
||||||
min-width: 50px;
|
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
min-width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-list-thumbnail {
|
& .scene-list-thumbnail {
|
||||||
width: 150px;
|
|
||||||
min-height: 50px;
|
min-height: 50px;
|
||||||
|
width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
& table td {
|
table td {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@@ -55,55 +57,60 @@ code {
|
|||||||
.card {
|
.card {
|
||||||
margin: 0 0 10px 10px;
|
margin: 0 0 10px 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 20rem;
|
||||||
|
|
||||||
&.zoom-0 {
|
&.zoom-0 {
|
||||||
width: 15rem;
|
width: 15rem;
|
||||||
|
|
||||||
& .previewable {
|
.previewable {
|
||||||
max-height: 11.25rem;
|
max-height: 11.25rem;
|
||||||
}
|
}
|
||||||
& .previewable.portrait {
|
|
||||||
|
.previewable.portrait {
|
||||||
max-height: 11.25rem;
|
max-height: 11.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.zoom-1 {
|
&.zoom-1 {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
|
|
||||||
& .previewable {
|
.previewable {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
}
|
}
|
||||||
& .previewable.portrait {
|
|
||||||
|
.previewable.portrait {
|
||||||
height: 15rem;
|
height: 15rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.zoom-2 {
|
&.zoom-2 {
|
||||||
width: 30rem;
|
width: 30rem;
|
||||||
|
|
||||||
& .previewable {
|
.previewable {
|
||||||
max-height: 22.5rem;
|
max-height: 22.5rem;
|
||||||
}
|
}
|
||||||
& .previewable.portrait {
|
|
||||||
|
.previewable.portrait {
|
||||||
height: 22.5rem;
|
height: 22.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.zoom-3 {
|
&.zoom-3 {
|
||||||
width: 40rem;
|
width: 40rem;
|
||||||
|
|
||||||
& .previewable {
|
.previewable {
|
||||||
max-height: 30rem;
|
max-height: 30rem;
|
||||||
}
|
}
|
||||||
& .previewable.portrait {
|
|
||||||
|
.previewable.portrait {
|
||||||
height: 30rem;
|
height: 30rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-select {
|
.card-select {
|
||||||
position: absolute;
|
|
||||||
padding-left: 15px;
|
|
||||||
margin-top: -12px;
|
margin-top: -12px;
|
||||||
z-index: 1;
|
opacity: .5;
|
||||||
opacity: 0.5;
|
padding-left: 15px;
|
||||||
|
position: absolute;
|
||||||
width: 1.2rem;
|
width: 1.2rem;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,11 +118,11 @@ code {
|
|||||||
.previewable {
|
.previewable {
|
||||||
display: block;
|
display: block;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
overflow: hidden;
|
|
||||||
width: calc(100% + 40px);
|
|
||||||
margin: -20px 0 0 -20px;
|
margin: -20px 0 0 -20px;
|
||||||
position: relative;
|
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% + 40px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.previewable.portrait {
|
.previewable.portrait {
|
||||||
@@ -123,18 +130,15 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.preview {
|
video.preview {
|
||||||
// height: 225px; // slows down the page
|
|
||||||
width: 100%;
|
|
||||||
// width: calc(100% + 40px);
|
|
||||||
// margin: -20px 0 0 -20px;
|
|
||||||
object-fit: cover;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
video.preview.portrait {
|
video.preview.portrait {
|
||||||
@@ -142,7 +146,8 @@ video.preview.portrait {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-item, .operation-item {
|
.filter-item,
|
||||||
|
.operation-item {
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,19 +156,48 @@ video.preview.portrait {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tag-item {
|
.tag-item {
|
||||||
|
background-color: #bfccd6;
|
||||||
|
color: #182026;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 16px;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
bottom: 2px;
|
||||||
|
color: #182026;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
opacity: .5;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: unset;
|
color: unset;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
|
||||||
color: unset;
|
color: unset;
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container, .operation-container {
|
.filter-container,
|
||||||
|
.operation-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
@@ -171,93 +205,111 @@ video.preview.portrait {
|
|||||||
|
|
||||||
.card-section {
|
.card-section {
|
||||||
padding: 10px 0 0 0;
|
padding: 10px 0 0 0;
|
||||||
|
|
||||||
&.centered {
|
&.centered {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
|
||||||
flex-flow: wrap;
|
flex-flow: wrap;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-5 { background: #FF2F39; }
|
.rating-5 {
|
||||||
.rating-4 { background: $red1; }
|
background: #FF2F39;
|
||||||
.rating-3 { background: $orange1; }
|
}
|
||||||
.rating-2 { background: $sepia1; }
|
|
||||||
.rating-1 { background: $dark-gray5; }
|
.rating-4 {
|
||||||
|
background: $red1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-3 {
|
||||||
|
background: $orange1;
|
||||||
|
}
|
||||||
|
.rating-2 {
|
||||||
|
background: $sepia1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rating-1 {
|
||||||
|
background: $dark-gray5;
|
||||||
|
}
|
||||||
|
|
||||||
.rating-banner {
|
.rating-banner {
|
||||||
transform: rotate(-36deg);
|
|
||||||
display: block;
|
|
||||||
padding: 6px 45px;
|
|
||||||
font-weight: 400;
|
|
||||||
top: 14px;
|
|
||||||
position: absolute;
|
|
||||||
left: -46px;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
display: block;
|
||||||
|
font-size: .86rem;
|
||||||
|
font-weight: 400;
|
||||||
|
left: -46px;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-size-adjust: none;
|
line-height: 1.6rem;
|
||||||
font-size: .85714em;
|
padding: 6px 45px;
|
||||||
line-height: 1.6em;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-size-adjust: none;
|
||||||
|
top: 14px;
|
||||||
|
transform: rotate(-36deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-specs-overlay {
|
.scene-specs-overlay {
|
||||||
display: block;
|
bottom: 1rem;
|
||||||
position: absolute;
|
|
||||||
bottom: 1em;
|
|
||||||
right: .7em;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #f5f8fa;
|
color: #f5f8fa;
|
||||||
letter-spacing: -.03em;
|
display: block;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -.03rem;
|
||||||
|
position: absolute;
|
||||||
|
right: .7rem;
|
||||||
text-shadow: 0 0 3px #000;
|
text-shadow: 0 0 3px #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-studio-overlay {
|
.scene-studio-overlay {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
|
||||||
top: .7em;
|
|
||||||
right: .7em;
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
width: 40%;
|
|
||||||
height: 20%;
|
height: 20%;
|
||||||
opacity: 0.75;
|
opacity: .75;
|
||||||
|
position: absolute;
|
||||||
|
right: .7rem;
|
||||||
|
top: .7rem;
|
||||||
|
width: 40%;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
|
||||||
|
|
||||||
.scene-studio-overlay a {
|
a {
|
||||||
width: 100%;
|
background-position: right top;
|
||||||
height: 100%;
|
background-repeat: no-repeat;
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
display: inline-block;
|
color: #f5f8fa;
|
||||||
background-position: right top;
|
display: inline-block;
|
||||||
background-repeat: no-repeat;
|
height: 100%;
|
||||||
letter-spacing: -.03em;
|
letter-spacing: -.03rem;
|
||||||
text-shadow: 0 0 3px #000;
|
text-align: right;
|
||||||
text-align: right;
|
text-decoration: none;
|
||||||
text-decoration: none;
|
text-shadow: 0 0 3px #000;
|
||||||
color: #f5f8fa;
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay-resolution {
|
.overlay-resolution {
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
margin-right: .3rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-right:.3em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-card {
|
.scene-card {
|
||||||
& .scene-specs-overlay, .rating-banner, .scene-studio-overlay {
|
.scene-specs-overlay,
|
||||||
transition: opacity 0.5s;
|
.rating-banner,
|
||||||
|
.scene-studio-overlay {
|
||||||
|
transition: opacity .5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-card:hover {
|
.scene-card:hover {
|
||||||
& .scene-specs-overlay, .rating-banner, .scene-studio-overlay {
|
.scene-specs-overlay,
|
||||||
|
.rating-banner,
|
||||||
|
.scene-studio-overlay {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.5s;
|
transition: opacity .5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-studio-overlay:hover {
|
.scene-studio-overlay:hover {
|
||||||
opacity: 0.75;
|
opacity: .75;
|
||||||
transition: opacity 0.5s;
|
transition: opacity .5s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,8 +319,8 @@ video.preview.portrait {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-js {
|
.video-js {
|
||||||
width: 100%;
|
|
||||||
height: 90vh;
|
height: 90vh;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#details-container {
|
#details-container {
|
||||||
@@ -281,14 +333,14 @@ video.preview.portrait {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.logs {
|
.logs {
|
||||||
white-space: pre-wrap;
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
word-break: break-all;
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
padding-right: 10px;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
white-space: pre-wrap;
|
||||||
width: 120ch;
|
width: 120ch;
|
||||||
|
word-break: break-all;
|
||||||
|
|
||||||
.debug {
|
.debug {
|
||||||
color: lightgreen;
|
color: lightgreen;
|
||||||
@@ -311,192 +363,70 @@ video.preview.portrait {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span.block {
|
.studio {
|
||||||
display: block;
|
.image {
|
||||||
}
|
background-position: center !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
.performer.image {
|
background-size: contain !important;
|
||||||
height: 50vh;
|
height: 100px;
|
||||||
min-height: 400px;
|
}
|
||||||
background-size: cover !important;
|
|
||||||
background-position: center !important;
|
|
||||||
background-repeat: no-repeat !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performer-tag-container {
|
|
||||||
margin: 5px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.performer-tag.image {
|
|
||||||
height: 150px;
|
|
||||||
width: 100%;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio.image {
|
|
||||||
height: 100px;
|
|
||||||
background-size: contain !important;
|
|
||||||
background-position: center !important;
|
|
||||||
background-repeat: no-repeat !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-spacing {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-photo-gallery--gallery {
|
.react-photo-gallery--gallery {
|
||||||
& img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#tag-list-container {
|
|
||||||
width: 50vw;
|
|
||||||
margin: 0 auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
& .tag-list-row {
|
|
||||||
margin: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
& .bp3-button {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .tag-list-row:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#parser-container {
|
#parser-container {
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
width: 75%;
|
width: 75%;
|
||||||
|
|
||||||
& .inputs label {
|
.inputs label {
|
||||||
width: 12em;
|
width: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .inputs .bp3-input-group {
|
.scene-parser-results {
|
||||||
width: 80ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .scene-parser-results {
|
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .bp3-checkbox {
|
.scene-parser-row .parser-field-title input {
|
||||||
margin: 0px -20px 0px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-title input {
|
|
||||||
width: 50ch;
|
width: 50ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-date input {
|
.scene-parser-row .parser-field-date input {
|
||||||
width: 13ch;
|
width: 13ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-performers input {
|
.scene-parser-row .parser-field-performers input {
|
||||||
width: 20ch;
|
width: 20ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-tags input {
|
.scene-parser-row .parser-field-tags input {
|
||||||
width: 20ch;
|
width: 20ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .parser-field-studio input {
|
.scene-parser-row .parser-field-studio input {
|
||||||
width: 15ch;
|
width: 15ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row input {
|
.scene-parser-row input {
|
||||||
min-width: 10ch;
|
min-width: 10ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .scene-parser-row .bp3-form-group {
|
.scene-parser-row div:first-child > input {
|
||||||
margin-bottom: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .scene-parser-row div:first-child > input {
|
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#performer-details {
|
.aliases-field > label {
|
||||||
& td {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
& td:first-child {
|
|
||||||
width: 30ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
& #url-field {
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& #scrape-url-button {
|
|
||||||
float: right;
|
|
||||||
height: 30px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#performer-page {
|
|
||||||
margin: 10px auto;
|
|
||||||
width: 75%;
|
|
||||||
|
|
||||||
& .details-image-container {
|
|
||||||
max-height: 400px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .performer-head {
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
font-size: 1.2em;
|
|
||||||
|
|
||||||
& .name-icons {
|
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
& .not-favorite .bp3-icon {
|
|
||||||
color: rgba(191, 204, 214, 0.5) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .favorite .bp3-icon {
|
|
||||||
color: #ff7373 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .alias {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.zoom-slider {
|
|
||||||
margin: auto 5px;
|
|
||||||
width: 100px;
|
|
||||||
|
|
||||||
& .bp3-slider {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.aliases-field > label{
|
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-cover {
|
.scene-cover {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,58 +435,58 @@ span.block {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.label-icon {
|
.label-icon {
|
||||||
margin-right: 0.3em;
|
margin-right: .3rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
& +span {
|
+ span {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
color: #f5f8fa;
|
color: #f5f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
color: #f5f8fa;
|
color: #f5f8fa;
|
||||||
width: inherit;
|
width: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
.table td {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-striped tbody tr:nth-child(odd) td {
|
.table-striped tbody tr:nth-child(odd) td {
|
||||||
background:rgba(92, 112, 128, 0.15);
|
background: rgba(92, 112, 128, .15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-pane {
|
.tab-pane {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #30404d;
|
background-color: #30404d;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 0 0 1px rgba(16,22,26,.4), 0 0 0 rgba(16,22,26,0), 0 0 0 rgba(16,22,26,0);
|
box-shadow: 0 0 0 1px rgba(16, 22, 26, .4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0);
|
||||||
padding: 20px 20px 0px 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-container {
|
.toast-container {
|
||||||
z-index: 1031;
|
left: 45%;
|
||||||
position: fixed;
|
max-width: 350px;
|
||||||
top: 2rem;
|
position: fixed;
|
||||||
left: 45%;
|
top: 2rem;
|
||||||
max-width: 350px;
|
z-index: 1031;
|
||||||
|
|
||||||
.toast {
|
.toast {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-link {
|
.button-link {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #48aff0;
|
|
||||||
border-width: 0;
|
border-width: 0;
|
||||||
|
color: #48aff0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline;
|
display: inline;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -578,4 +508,59 @@ span.block {
|
|||||||
/* BOOTSTRAP OVERRIDES */
|
/* BOOTSTRAP OVERRIDES */
|
||||||
.form-control {
|
.form-control {
|
||||||
width: inherit;
|
width: inherit;
|
||||||
|
|
||||||
|
&-plaintext {
|
||||||
|
color: $text-color;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
&-body {
|
||||||
|
.btn {
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
.modal-body,
|
||||||
|
.modal-footer {
|
||||||
|
background-color: rgb(235, 241, 245);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[type=file] {
|
||||||
|
cursor: inherit;
|
||||||
|
display: block;
|
||||||
|
filter: alpha(opacity=0);
|
||||||
|
font-size: 999px;
|
||||||
|
min-height: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
text-align: right;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
padding-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable consistent-return */
|
/* eslint-disable consistent-return */
|
||||||
|
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
import { DurationUtils } from "src/utils";
|
import DurationUtils from "src/utils/duration";
|
||||||
import { ILabeledId, ILabeledValue } from "../types";
|
import { ILabeledId, ILabeledValue } from "../types";
|
||||||
|
|
||||||
export type CriterionType =
|
export type CriterionType =
|
||||||
|
|||||||
@@ -1,94 +1,106 @@
|
|||||||
input[type=range] {
|
input[type=range] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
background-color: transparent;
|
||||||
|
border-color: transparent;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
-webkit-appearance: none;
|
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
background-color: transparent;
|
|
||||||
border-color: transparent;
|
&:focus {
|
||||||
}
|
background-color: transparent;
|
||||||
input[type=range]:focus {
|
border: inherit;
|
||||||
border: inherit;
|
border-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
background-color: transparent;
|
}
|
||||||
border-color: transparent;
|
|
||||||
}
|
&::-webkit-slider-runnable-track {
|
||||||
input[type=range]::-webkit-slider-runnable-track {
|
animate: .2s;
|
||||||
width: 100%;
|
background: #137cbd;
|
||||||
height: 6px;
|
border: 0 solid #000101;
|
||||||
cursor: pointer;
|
border-radius: 25px;
|
||||||
animate: 0.2s;
|
box-shadow: 0 0 0 #000;
|
||||||
box-shadow: 0px 0px 0px #000000;
|
cursor: pointer;
|
||||||
background: #137cbd;
|
height: 6px;
|
||||||
border-radius: 25px;
|
width: 100%;
|
||||||
border: 0px solid #000101;
|
}
|
||||||
}
|
|
||||||
input[type=range]::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
box-shadow: 0px 0px 0px #000000;
|
-webkit-appearance: none;
|
||||||
border: 0px solid #000000;
|
background: #394b59;
|
||||||
height: 16px;
|
border: 0 solid #000;
|
||||||
width: 16px;
|
border-radius: 5px;
|
||||||
border-radius: 5px;
|
box-shadow: 0 0 0 #000;
|
||||||
background: #394B59;
|
cursor: pointer;
|
||||||
cursor: pointer;
|
height: 16px;
|
||||||
-webkit-appearance: none;
|
margin-top: -5px;
|
||||||
margin-top: -5px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
|
||||||
background: #137cbd;
|
&:focus::-webkit-slider-runnable-track {
|
||||||
}
|
background: #137cbd;
|
||||||
input[type=range]::-moz-range-track {
|
}
|
||||||
width: 100%;
|
|
||||||
height: 6px;
|
&::-moz-range-track {
|
||||||
cursor: pointer;
|
animate: .2s;
|
||||||
animate: 0.2s;
|
background: #137cbd;
|
||||||
box-shadow: 0px 0px 0px #000000;
|
border: 0 solid #000101;
|
||||||
background: #137cbd;
|
border-radius: 25px;
|
||||||
border-radius: 25px;
|
box-shadow: 0 0 0 #000;
|
||||||
border: 0px solid #000101;
|
cursor: pointer;
|
||||||
}
|
height: 6px;
|
||||||
input[type=range]::-moz-range-thumb {
|
width: 100%;
|
||||||
box-shadow: 0px 0px 0px #000000;
|
}
|
||||||
border: 0px solid #000000;
|
|
||||||
height: 16px;
|
&::-moz-range-thumb {
|
||||||
width: 16px;
|
background: #394b59;
|
||||||
border-radius: 5px;
|
border: 0 solid #000;
|
||||||
background: #394B59;
|
border-radius: 5px;
|
||||||
cursor: pointer;
|
box-shadow: 0 0 0 #000;
|
||||||
}
|
cursor: pointer;
|
||||||
input[type=range]::-ms-track {
|
height: 16px;
|
||||||
width: 100%;
|
width: 16px;
|
||||||
height: 6px;
|
}
|
||||||
cursor: pointer;
|
|
||||||
animate: 0.2s;
|
&::-ms-track {
|
||||||
background: transparent;
|
animate: .2s;
|
||||||
border-color: transparent;
|
background: transparent;
|
||||||
color: transparent;
|
border-color: transparent;
|
||||||
}
|
color: transparent;
|
||||||
input[type=range]::-ms-fill-lower {
|
cursor: pointer;
|
||||||
background: #137cbd;
|
height: 6px;
|
||||||
border: 0px solid #000101;
|
width: 100%;
|
||||||
border-radius: 50px;
|
}
|
||||||
box-shadow: 0px 0px 0px #000000;
|
|
||||||
}
|
&::-ms-fill-lower {
|
||||||
input[type=range]::-ms-fill-upper {
|
background: #137cbd;
|
||||||
background: #137cbd;
|
border: 0 solid #000101;
|
||||||
border: 0px solid #000101;
|
border-radius: 50px;
|
||||||
border-radius: 50px;
|
box-shadow: 0 0 0 #000;
|
||||||
box-shadow: 0px 0px 0px #000000;
|
}
|
||||||
}
|
|
||||||
input[type=range]::-ms-thumb {
|
&::-ms-fill-upper {
|
||||||
margin-top: 1px;
|
background: #137cbd;
|
||||||
box-shadow: 0px 0px 0px #000000;
|
border: 0 solid #000101;
|
||||||
border: 0px solid #000000;
|
border-radius: 50px;
|
||||||
height: 16px;
|
box-shadow: 0 0 0 #000;
|
||||||
width: 16px;
|
}
|
||||||
border-radius: 5px;
|
|
||||||
background: #394B59;
|
&::-ms-thumb {
|
||||||
cursor: pointer;
|
background: #394b59;
|
||||||
}
|
border: 0 solid #000;
|
||||||
input[type=range]:focus::-ms-fill-lower {
|
border-radius: 5px;
|
||||||
background: #137cbd;
|
box-shadow: 0 0 0 #000;
|
||||||
}
|
cursor: pointer;
|
||||||
input[type=range]:focus::-ms-fill-upper {
|
height: 16px;
|
||||||
background: #137cbd;
|
margin-top: 1px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::-ms-fill-lower {
|
||||||
|
background: #137cbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::-ms-fill-upper {
|
||||||
|
background: #137cbd;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,66 +2,43 @@
|
|||||||
|
|
||||||
/* Site */
|
/* Site */
|
||||||
|
|
||||||
::-webkit-selection {
|
|
||||||
background-color: #CCE2FF;
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-moz-selection {
|
|
||||||
background-color: #CCE2FF;
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: #CCE2FF;
|
background-color: #cce2ff;
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, .87);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form */
|
/* Form */
|
||||||
|
|
||||||
textarea::-webkit-selection,
|
|
||||||
input::-webkit-selection {
|
|
||||||
background-color: rgba(100, 100, 100, 0.4);
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea::-moz-selection,
|
|
||||||
input::-moz-selection {
|
|
||||||
background-color: rgba(100, 100, 100, 0.4);
|
|
||||||
color: rgba(0, 0, 0, 0.87);
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea::selection,
|
textarea::selection,
|
||||||
input::selection {
|
input::selection {
|
||||||
background-color: rgba(100, 100, 100, 0.4);
|
background-color: rgba(100, 100, 100, .4);
|
||||||
color: rgba(0, 0, 0, 0.87);
|
color: rgba(0, 0, 0, .87);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Force Simple Scrollbars */
|
/* Force Simple Scrollbars */
|
||||||
|
|
||||||
body ::-webkit-scrollbar {
|
body ::-webkit-scrollbar {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-scrollbar-track {
|
body ::-webkit-scrollbar-track {
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, .1);
|
||||||
border-radius: 0px;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-scrollbar-thumb {
|
body ::-webkit-scrollbar-thumb {
|
||||||
cursor: pointer;
|
background: rgba(0, 0, 0, .25);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: rgba(0, 0, 0, 0.25);
|
cursor: pointer;
|
||||||
-webkit-transition: color 0.2s ease;
|
transition: color .2s ease;
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-scrollbar-thumb:window-inactive {
|
body ::-webkit-scrollbar-thumb:window-inactive {
|
||||||
background: rgba(0, 0, 0, 0.15);
|
background: rgba(0, 0, 0, .15);
|
||||||
}
|
}
|
||||||
|
|
||||||
body ::-webkit-scrollbar-thumb:hover {
|
body ::-webkit-scrollbar-thumb:hover {
|
||||||
background: rgba(128, 135, 139, 0.8);
|
background: rgba(128, 135, 139, .8);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,13 @@ $link-hover-color: #48aff0;
|
|||||||
$text-color: #f5f8fa;
|
$text-color: #f5f8fa;
|
||||||
$pre-color: $text-color;
|
$pre-color: $text-color;
|
||||||
$navbar-dark-color: rgb(245, 248, 250);
|
$navbar-dark-color: rgb(245, 248, 250);
|
||||||
$input-bg: $secondary;
|
|
||||||
$input-color: #f5f8fa;
|
|
||||||
$popover-bg: $secondary;
|
$popover-bg: $secondary;
|
||||||
|
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
.btn.active:not(.disabled),
|
.btn.active:not(.disabled),
|
||||||
.btn.active.minimal:not(.disabled) {
|
.btn.active.minimal:not(.disabled) {
|
||||||
background-color: rgba(138,155,168,.3);
|
background-color: rgba(138, 155, 168, .3);
|
||||||
color: #f5f8fa;
|
color: #f5f8fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,23 +37,17 @@ button.minimal {
|
|||||||
transition: none;
|
transition: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: rgba(138,155,168,.15);
|
background: rgba(138, 155, 168, .15);
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background: rgba(138,155,168,.3);
|
background: rgba(138, 155, 168, .3);
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input.form-control {
|
.dropdown-toggle::after {
|
||||||
background-color: rgba(16, 22, 26, 0.3);
|
|
||||||
}
|
|
||||||
.form-control {
|
|
||||||
border-color: rgba(16,22,26,.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-toggle:after {
|
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,18 +55,40 @@ nav .svg-inline--fa {
|
|||||||
margin-right: 7px;
|
margin-right: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
border-bottom-color: gray;
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
border-color: gray;
|
||||||
|
border-bottom-color: transparent;
|
||||||
|
color: $text-color;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
th {
|
border: none;
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
th {
|
th {
|
||||||
border-bottom-width: 1px;
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
a {
|
||||||
|
color: $text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
max-width: inherit;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
.bp3-form-group .bp3-popover-target {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bp3-html-table, .form-container {
|
|
||||||
& .bp3-popover-target, & textarea {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
& textarea {
|
|
||||||
min-height: 250px;
|
|
||||||
resize: vertical;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form .columns.is-gapless > .column {
|
|
||||||
margin: 5px 0;
|
|
||||||
padding: 0 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .columns {
|
|
||||||
margin-bottom: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
form .buttons-container button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
.details-image-container {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh - 50px); // 50px for navbar
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
& img.performer {
|
|
||||||
height: 98%;
|
|
||||||
max-width: 98%;
|
|
||||||
object-fit: cover;
|
|
||||||
box-shadow: 0px 0px 10px black;
|
|
||||||
}
|
|
||||||
|
|
||||||
& img.studio {
|
|
||||||
height: 50%;
|
|
||||||
max-width: 50%;
|
|
||||||
object-fit: contain;
|
|
||||||
box-shadow: 0px 0px 10px black;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.details-detail-container {
|
|
||||||
padding: 10px !important;
|
|
||||||
|
|
||||||
& .bp3-navbar {
|
|
||||||
margin: 0 0 10px 0;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
& .bp3-button {
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .bp3-file-input {
|
|
||||||
width: 190px;
|
|
||||||
|
|
||||||
& input {
|
|
||||||
min-width: unset;
|
|
||||||
max-width: 190px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& .bp3-button.favorite .bp3-icon {
|
|
||||||
color: #ff7373 !important
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-content {
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
& .bp3-popover-target {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,3 +3,4 @@ export { default as NavUtils } from "./navigation";
|
|||||||
export { default as TableUtils } from "./table";
|
export { default as TableUtils } from "./table";
|
||||||
export { default as TextUtils } from "./text";
|
export { default as TextUtils } from "./text";
|
||||||
export { default as DurationUtils } from "./duration";
|
export { default as DurationUtils } from "./duration";
|
||||||
|
export { default as JWUtils } from './jwplayer';
|
||||||
|
|||||||
9
ui/v2.5/src/utils/jwplayer.ts
Normal file
9
ui/v2.5/src/utils/jwplayer.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const playerID = "main-jwplayer";
|
||||||
|
const getPlayer = () => (
|
||||||
|
(window as any).jwplayer(playerID)
|
||||||
|
)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
playerID,
|
||||||
|
getPlayer
|
||||||
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as GQL from "../core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { PerformersCriterion } from "../models/list-filter/criteria/performers";
|
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||||
import { StudiosCriterion } from "../models/list-filter/criteria/studios";
|
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||||
import { TagsCriterion } from "../models/list-filter/criteria/tags";
|
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||||
import { ListFilterModel } from "../models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { FilterMode } from "../models/list-filter/types";
|
import { FilterMode } from "src/models/list-filter/types";
|
||||||
|
|
||||||
const makePerformerScenesUrl = (
|
const makePerformerScenesUrl = (
|
||||||
performer: Partial<GQL.PerformerDataFragment>
|
performer: Partial<GQL.PerformerDataFragment>
|
||||||
@@ -54,11 +54,10 @@ const makeSceneMarkerUrl = (
|
|||||||
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
|
return `/scenes/${sceneMarker.scene.id}?t=${sceneMarker.seconds}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Nav = {
|
export default {
|
||||||
makePerformerScenesUrl,
|
makePerformerScenesUrl,
|
||||||
makeStudioScenesUrl,
|
makeStudioScenesUrl,
|
||||||
makeTagSceneMarkersUrl,
|
makeTagSceneMarkersUrl,
|
||||||
makeTagScenesUrl,
|
makeTagScenesUrl,
|
||||||
makeSceneMarkerUrl
|
makeSceneMarkerUrl
|
||||||
};
|
};
|
||||||
export default Nav;
|
|
||||||
|
|||||||
@@ -89,10 +89,13 @@ const renderHtmlSelect = (options: {
|
|||||||
as="select"
|
as="select"
|
||||||
readOnly={!options.isEditing}
|
readOnly={!options.isEditing}
|
||||||
plaintext={!options.isEditing}
|
plaintext={!options.isEditing}
|
||||||
|
value={options.value?.toString()}
|
||||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||||
options.onChange(event.currentTarget.value)
|
options.onChange(event.currentTarget.value)
|
||||||
}
|
}
|
||||||
/>
|
>
|
||||||
|
{ options.selectOptions.map(opt => <option value={opt} key={opt}>{opt}</option>)}
|
||||||
|
</Form.Control>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
"esnext"
|
"esnext"
|
||||||
],
|
],
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
@@ -15,7 +14,6 @@
|
|||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react",
|
"jsx": "react",
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user