mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Details page redesign (#3946)
* mobile improvements to performer page * updated remaining details pages * fixes tag page on mobile * implemented show hide for performer details * fixes card width cutoff on mobile(not related to redesign) * added background image option plus more improvements * add tooltip for age field * translate encoding message string
This commit is contained in:
@@ -466,7 +466,7 @@ export function makeItemList<T extends QueryResult, E extends IDataItem>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="item-list-container">
|
||||||
<ButtonToolbar className="justify-content-center">
|
<ButtonToolbar className="justify-content-center">
|
||||||
<ListFilter
|
<ListFilter
|
||||||
onFilterUpdate={updateFilter}
|
onFilterUpdate={updateFilter}
|
||||||
|
|||||||
@@ -359,3 +359,11 @@ input[type="range"].zoom-slider {
|
|||||||
.tilted {
|
.tilted {
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-list-container {
|
||||||
|
padding-top: 15px;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,9 +17,24 @@ import { useLightbox } from "src/hooks/Lightbox/hooks";
|
|||||||
import { ModalComponent } from "src/components/Shared/Modal";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||||
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
import {
|
||||||
|
CompressedMovieDetailsPanel,
|
||||||
|
MovieDetailsPanel,
|
||||||
|
} from "./MovieDetailsPanel";
|
||||||
import { MovieEditPanel } from "./MovieEditPanel";
|
import { MovieEditPanel } from "./MovieEditPanel";
|
||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
|
faLink,
|
||||||
|
faTrashAlt,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { IUIConfig } from "src/core/config";
|
||||||
|
import ImageUtils from "src/utils/image";
|
||||||
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
movie: GQL.MovieDataFragment;
|
movie: GQL.MovieDataFragment;
|
||||||
@@ -30,6 +45,16 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
|
// Configuration settings
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const uiConfig = configuration?.ui as IUIConfig | undefined;
|
||||||
|
const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false;
|
||||||
|
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||||
|
const showAllDetails = uiConfig?.showAllDetails ?? true;
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||||
|
const [loadStickyHeader, setLoadStickyHeader] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
@@ -87,6 +112,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
});
|
});
|
||||||
|
Mousetrap.bind(",", () => setCollapsed(!collapsed));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("e");
|
Mousetrap.unbind("e");
|
||||||
@@ -94,6 +120,27 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useRatingKeybinds(
|
||||||
|
true,
|
||||||
|
configuration?.ui?.ratingSystemOptions?.type,
|
||||||
|
setRating
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = () => {
|
||||||
|
if (document.documentElement.scrollTop <= 50) {
|
||||||
|
setLoadStickyHeader(false);
|
||||||
|
} else {
|
||||||
|
setLoadStickyHeader(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", f);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", f);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.MovieCreateInput) {
|
async function onSave(input: GQL.MovieCreateInput) {
|
||||||
await updateMovie({
|
await updateMovie({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -159,6 +206,25 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCollapseButtonIcon() {
|
||||||
|
return collapsed ? faChevronDown : faChevronUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderShowCollapseButton() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<span className="detail-expand-collapse">
|
||||||
|
<Button
|
||||||
|
className="minimal expand-collapse"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderFrontImage() {
|
function renderFrontImage() {
|
||||||
let image = movie.front_image_path;
|
let image = movie.front_image_path;
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -174,7 +240,11 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
if (image && defaultImage) {
|
if (image && defaultImage) {
|
||||||
return (
|
return (
|
||||||
<div className="movie-image-container">
|
<div className="movie-image-container">
|
||||||
<img alt="Front Cover" src={image} />
|
<img
|
||||||
|
alt="Front Cover"
|
||||||
|
src={image}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (image) {
|
} else if (image) {
|
||||||
@@ -184,7 +254,11 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => showLightbox()}
|
onClick={() => showLightbox()}
|
||||||
>
|
>
|
||||||
<img alt="Front Cover" src={image} />
|
<img
|
||||||
|
alt="Front Cover"
|
||||||
|
src={image}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -207,62 +281,180 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
variant="link"
|
variant="link"
|
||||||
onClick={() => showLightbox(index - 1)}
|
onClick={() => showLightbox(index - 1)}
|
||||||
>
|
>
|
||||||
<img alt="Back Cover" src={image} />
|
<img
|
||||||
|
alt="Back Cover"
|
||||||
|
src={image}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderClickableIcons = () => (
|
||||||
|
<span className="name-icons">
|
||||||
|
{movie.url && (
|
||||||
|
<Button className="minimal icon-link" title={movie.url}>
|
||||||
|
<a
|
||||||
|
href={TextUtils.sanitiseURL(movie.url)}
|
||||||
|
className="link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Icon icon={faLink} />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
function maybeRenderAliases() {
|
||||||
|
if (movie?.aliases) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="alias-head">{movie?.aliases}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRating(v: number | null) {
|
||||||
|
if (movie.id) {
|
||||||
|
updateMovie({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: movie.id,
|
||||||
|
rating100: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabs = () => <MovieScenesPanel active={true} movie={movie} />;
|
||||||
|
|
||||||
|
function maybeRenderDetails() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<MovieDetailsPanel
|
||||||
|
movie={movie}
|
||||||
|
fullWidth={!collapsed && !compactExpandedDetails}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderEditPanel() {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<MovieEditPanel
|
||||||
|
movie={movie}
|
||||||
|
onSubmit={onSave}
|
||||||
|
onCancel={() => toggleEditing()}
|
||||||
|
onDelete={onDelete}
|
||||||
|
setFrontImage={setFrontImage}
|
||||||
|
setBackImage={setBackImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<DetailsEditNavbar
|
||||||
|
objectName={movie.name}
|
||||||
|
isNew={false}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onToggleEdit={() => toggleEditing()}
|
||||||
|
onSave={() => {}}
|
||||||
|
onImageChange={() => {}}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderCompressedDetails() {
|
||||||
|
if (!isEditing && loadStickyHeader) {
|
||||||
|
return <CompressedMovieDetailsPanel movie={movie} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderHeaderBackgroundImage() {
|
||||||
|
let image = movie.front_image_path;
|
||||||
|
if (enableBackgroundImage && !isEditing && image) {
|
||||||
|
return (
|
||||||
|
<div className="background-image-container">
|
||||||
|
<picture>
|
||||||
|
<source src={image} />
|
||||||
|
<img
|
||||||
|
className="background-image"
|
||||||
|
src={image}
|
||||||
|
alt={`${movie.name} background`}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderTab() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return renderTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updating || deleting) return <LoadingIndicator />;
|
if (updating || deleting) return <LoadingIndicator />;
|
||||||
|
|
||||||
// TODO: CSS class
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div id="movie-page" className="row">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{movie?.name}</title>
|
<title>{movie?.name}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="movie-details mb-3 col col-xl-4 col-lg-6">
|
<div
|
||||||
<div className="logo w-100">
|
className={`detail-header ${isEditing ? "edit" : ""} ${
|
||||||
{encodingImage ? (
|
collapsed ? "collapsed" : !compactExpandedDetails ? "full-width" : ""
|
||||||
<LoadingIndicator message="Encoding image..." />
|
}`}
|
||||||
) : (
|
>
|
||||||
<div className="movie-images">
|
{maybeRenderHeaderBackgroundImage()}
|
||||||
{renderFrontImage()}
|
<div className="detail-container">
|
||||||
{renderBackImage()}
|
<div className="detail-header-image">
|
||||||
|
<div className="logo w-100">
|
||||||
|
{encodingImage ? (
|
||||||
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="movie-images">
|
||||||
|
{renderFrontImage()}
|
||||||
|
{renderBackImage()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="movie-head col">
|
||||||
|
<h2>
|
||||||
|
<span className="movie-name">{movie.name}</span>
|
||||||
|
{maybeRenderShowCollapseButton()}
|
||||||
|
{renderClickableIcons()}
|
||||||
|
</h2>
|
||||||
|
{maybeRenderAliases()}
|
||||||
|
<RatingSystem
|
||||||
|
value={movie.rating100 ?? undefined}
|
||||||
|
onSetRating={(value) => setRating(value ?? null)}
|
||||||
|
/>
|
||||||
|
{maybeRenderDetails()}
|
||||||
|
{maybeRenderEditPanel()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isEditing ? (
|
|
||||||
<>
|
|
||||||
<MovieDetailsPanel movie={movie} />
|
|
||||||
{/* HACK - this is also rendered in the MovieEditPanel */}
|
|
||||||
<DetailsEditNavbar
|
|
||||||
objectName={movie.name}
|
|
||||||
isNew={false}
|
|
||||||
isEditing={isEditing}
|
|
||||||
onToggleEdit={() => toggleEditing()}
|
|
||||||
onSave={() => {}}
|
|
||||||
onImageChange={() => {}}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<MovieEditPanel
|
|
||||||
movie={movie}
|
|
||||||
onSubmit={onSave}
|
|
||||||
onCancel={() => toggleEditing()}
|
|
||||||
onDelete={onDelete}
|
|
||||||
setFrontImage={setFrontImage}
|
|
||||||
setBackImage={setBackImage}
|
|
||||||
setEncodingImage={setEncodingImage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{maybeRenderCompressedDetails()}
|
||||||
<div className="col-xl-8 col-lg-6">
|
<div className="detail-body">
|
||||||
<MovieScenesPanel active={true} movie={movie} />
|
<div className="movie-body">
|
||||||
|
<div className="movie-tabs">{maybeRenderTab()}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderDeleteAlert()}
|
{renderDeleteAlert()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,7 +66,9 @@ const MovieCreate: React.FC = () => {
|
|||||||
<div className="movie-details mb-3 col">
|
<div className="movie-details mb-3 col">
|
||||||
<div className="logo w-100">
|
<div className="logo w-100">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="movie-images">
|
<div className="movie-images">
|
||||||
{renderFrontImage()}
|
{renderFrontImage()}
|
||||||
|
|||||||
@@ -3,82 +3,73 @@ import { useIntl } from "react-intl";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
import { TextField, URLField } from "src/utils/field";
|
|
||||||
|
|
||||||
interface IMovieDetailsPanel {
|
interface IMovieDetailsPanel {
|
||||||
movie: GQL.MovieDataFragment;
|
movie: GQL.MovieDataFragment;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
|
||||||
|
movie,
|
||||||
|
fullWidth,
|
||||||
|
}) => {
|
||||||
// Network state
|
// Network state
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
function maybeRenderAliases() {
|
|
||||||
if (movie.aliases) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span className="alias-head">
|
|
||||||
{intl.formatMessage({ id: "also_known_as" })}{" "}
|
|
||||||
</span>
|
|
||||||
<span className="alias">{movie.aliases}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderRatingField() {
|
|
||||||
if (!movie.rating100) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
|
||||||
<dd>
|
|
||||||
<RatingSystem value={movie.rating100} disabled />
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: CSS class
|
|
||||||
return (
|
return (
|
||||||
<div className="movie-details">
|
<div className="detail-group">
|
||||||
<div>
|
<DetailItem
|
||||||
<h2>{movie.name}</h2>
|
id="duration"
|
||||||
</div>
|
value={
|
||||||
|
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
||||||
|
}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="date"
|
||||||
|
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="studio"
|
||||||
|
value={
|
||||||
|
movie.studio?.id ? (
|
||||||
|
<a href={`/studios/${movie.studio?.id}`} target="_self">
|
||||||
|
{movie.studio?.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
|
||||||
{maybeRenderAliases()}
|
<DetailItem id="director" value={movie.director} fullWidth={fullWidth} />
|
||||||
|
<DetailItem id="synopsis" value={movie.synopsis} fullWidth={fullWidth} />
|
||||||
<dl className="details-list">
|
</div>
|
||||||
<TextField
|
);
|
||||||
id="duration"
|
};
|
||||||
value={
|
|
||||||
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
export const CompressedMovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
|
||||||
}
|
movie,
|
||||||
/>
|
}) => {
|
||||||
<TextField
|
function scrollToTop() {
|
||||||
id="date"
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
|
}
|
||||||
/>
|
|
||||||
<URLField
|
return (
|
||||||
id="studio"
|
<div className="sticky detail-header">
|
||||||
value={movie.studio?.name}
|
<div className="sticky detail-header-group">
|
||||||
url={`/studios/${movie.studio?.id}`}
|
<a className="movie-name" onClick={() => scrollToTop()}>
|
||||||
/>
|
{movie.name}
|
||||||
<TextField id="director" value={movie.director} />
|
</a>
|
||||||
|
{movie?.studio?.name ? (
|
||||||
{renderRatingField()}
|
<span className="movie-studio">{movie?.studio?.name}</span>
|
||||||
|
) : (
|
||||||
<URLField
|
""
|
||||||
id="url"
|
)}
|
||||||
value={movie.url}
|
</div>
|
||||||
url={TextUtils.sanitiseURL(movie.url ?? "")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField id="synopsis" value={movie.synopsis} />
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ import { URLField } from "src/components/Shared/URLField";
|
|||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
import { Modal as BSModal, Form, Button, Col, Row } from "react-bootstrap";
|
||||||
import DurationUtils from "src/utils/duration";
|
import DurationUtils from "src/utils/duration";
|
||||||
import FormUtils from "src/utils/form";
|
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { DateInput } from "src/components/Shared/DateInput";
|
import { DateInput } from "src/components/Shared/DateInput";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
@@ -48,7 +44,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
|
||||||
|
|
||||||
const isNew = movie.id === undefined;
|
const isNew = movie.id === undefined;
|
||||||
|
|
||||||
@@ -60,6 +55,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListMovieScrapers();
|
||||||
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||||
|
|
||||||
|
const labelXS = 3;
|
||||||
|
const labelXL = 2;
|
||||||
|
const fieldXS = 9;
|
||||||
|
const fieldXL = 7;
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
aliases: yup.string().ensure(),
|
aliases: yup.string().ensure(),
|
||||||
@@ -79,7 +79,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}),
|
}),
|
||||||
studio_id: yup.string().required().nullable(),
|
studio_id: yup.string().required().nullable(),
|
||||||
director: yup.string().ensure(),
|
director: yup.string().ensure(),
|
||||||
rating100: yup.number().nullable().defined(),
|
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
synopsis: yup.string().ensure(),
|
synopsis: yup.string().ensure(),
|
||||||
front_image: yup.string().nullable().optional(),
|
front_image: yup.string().nullable().optional(),
|
||||||
@@ -93,7 +92,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
date: movie?.date ?? "",
|
date: movie?.date ?? "",
|
||||||
studio_id: movie?.studio?.id ?? null,
|
studio_id: movie?.studio?.id ?? null,
|
||||||
director: movie?.director ?? "",
|
director: movie?.director ?? "",
|
||||||
rating100: movie?.rating100 ?? null,
|
|
||||||
url: movie?.url ?? "",
|
url: movie?.url ?? "",
|
||||||
synopsis: movie?.synopsis ?? "",
|
synopsis: movie?.synopsis ?? "",
|
||||||
};
|
};
|
||||||
@@ -107,16 +105,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
|
||||||
formik.setFieldValue("rating100", v);
|
|
||||||
}
|
|
||||||
|
|
||||||
useRatingKeybinds(
|
|
||||||
true,
|
|
||||||
stashConfig?.ui?.ratingSystemOptions?.type,
|
|
||||||
setRating
|
|
||||||
);
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Mousetrap.bind("u", (e) => {
|
// Mousetrap.bind("u", (e) => {
|
||||||
@@ -347,10 +335,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={field} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title,
|
<FormattedMessage id={title} />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder={placeholder ?? title}
|
placeholder={placeholder ?? title}
|
||||||
@@ -390,10 +378,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="movie-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
<Form.Group controlId="name" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "name" }),
|
<FormattedMessage id="name" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder={intl.formatMessage({ id: "name" })}
|
placeholder={intl.formatMessage({ id: "name" })}
|
||||||
@@ -409,10 +397,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
{renderTextField("aliases", intl.formatMessage({ id: "aliases" }))}
|
{renderTextField("aliases", intl.formatMessage({ id: "aliases" }))}
|
||||||
|
|
||||||
<Form.Group controlId="duration" as={Row}>
|
<Form.Group controlId="duration" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "duration" }),
|
<FormattedMessage id="duration" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<DurationInput
|
<DurationInput
|
||||||
numericValue={formik.values.duration ?? undefined}
|
numericValue={formik.values.duration ?? undefined}
|
||||||
onValueChange={(valueAsNumber) => {
|
onValueChange={(valueAsNumber) => {
|
||||||
@@ -423,10 +411,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="date" as={Row}>
|
<Form.Group controlId="date" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "date" }),
|
<FormattedMessage id="date" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<DateInput
|
<DateInput
|
||||||
value={formik.values.date}
|
value={formik.values.date}
|
||||||
onValueChange={(value) => formik.setFieldValue("date", value)}
|
onValueChange={(value) => formik.setFieldValue("date", value)}
|
||||||
@@ -436,10 +424,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "studio" }),
|
<FormattedMessage id="studio" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -454,24 +442,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
{renderTextField("director", intl.formatMessage({ id: "director" }))}
|
{renderTextField("director", intl.formatMessage({ id: "director" }))}
|
||||||
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<RatingSystem
|
|
||||||
value={formik.values.rating100 ?? undefined}
|
|
||||||
onSetRating={(value) =>
|
|
||||||
formik.setFieldValue("rating100", value ?? null)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "url" }),
|
<FormattedMessage id="url" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<URLField
|
<URLField
|
||||||
{...formik.getFieldProps("url")}
|
{...formik.getFieldProps("url")}
|
||||||
onScrapeClick={onScrapeMovieURL}
|
onScrapeClick={onScrapeMovieURL}
|
||||||
@@ -481,10 +456,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="synopsis" as={Row}>
|
<Form.Group controlId="synopsis" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "synopsis" }),
|
<FormattedMessage id="synopsis" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -498,6 +473,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
|
classNames="col-xl-9 mt-3"
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
|
|||||||
@@ -28,12 +28,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
margin: 1rem;
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
.movie-image-container {
|
.movie-image-container {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
margin: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { Button, Tabs, Tab, Col, Row } from "react-bootstrap";
|
import { Button, Tabs, Tab, Col, Row } from "react-bootstrap";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Counter } from "src/components/Shared/Counter";
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
@@ -23,7 +22,10 @@ import { useToast } from "src/hooks/Toast";
|
|||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
import {
|
||||||
|
CompressedPerformerDetailsPanel,
|
||||||
|
PerformerDetailsPanel,
|
||||||
|
} from "./PerformerDetailsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
|
||||||
import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
|
import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
|
||||||
@@ -31,16 +33,16 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
|||||||
import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel";
|
import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel";
|
||||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||||
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
||||||
import GenderIcon from "../GenderIcon";
|
|
||||||
import {
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
faHeart,
|
faHeart,
|
||||||
faLink,
|
faLink,
|
||||||
faChevronRight,
|
|
||||||
faChevronLeft,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { IUIConfig } from "src/core/config";
|
import { IUIConfig } from "src/core/config";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
|
import ImageUtils from "src/utils/image";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
@@ -55,16 +57,20 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { tab = "details" } = useParams<IPerformerParams>();
|
const { tab = "details" } = useParams<IPerformerParams>();
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
// Configuration settings
|
// Configuration settings
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const abbreviateCounter =
|
const uiConfig = configuration?.ui as IUIConfig | undefined;
|
||||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||||
|
const enableBackgroundImage =
|
||||||
|
uiConfig?.enablePerformerBackgroundImage ?? false;
|
||||||
|
const showAllDetails = uiConfig?.showAllDetails ?? false;
|
||||||
|
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
const [loadStickyHeader, setLoadStickyHeader] = useState<boolean>(false);
|
||||||
|
|
||||||
const activeImage = useMemo(() => {
|
const activeImage = useMemo(() => {
|
||||||
const performerImage = performer.image_path;
|
const performerImage = performer.image_path;
|
||||||
@@ -99,10 +105,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
tab === "movies" ||
|
tab === "movies" ||
|
||||||
tab == "appearswith"
|
tab == "appearswith"
|
||||||
? tab
|
? tab
|
||||||
: "details";
|
: "scenes";
|
||||||
const setActiveTabKey = (newTab: string | null) => {
|
const setActiveTabKey = (newTab: string | null) => {
|
||||||
if (tab !== newTab) {
|
if (tab !== newTab) {
|
||||||
const tabParam = newTab === "details" ? "" : `/${newTab}`;
|
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||||
history.replace(`/performers/${performer.id}${tabParam}`);
|
history.replace(`/performers/${performer.id}${tabParam}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -126,7 +132,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("a", () => setActiveTabKey("details"));
|
|
||||||
Mousetrap.bind("e", () => toggleEditing());
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
||||||
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
|
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
|
||||||
@@ -186,44 +191,24 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
if (activeImage) {
|
if (activeImage) {
|
||||||
return (
|
return (
|
||||||
<Button variant="link" onClick={() => showLightbox()}>
|
<Button variant="link" onClick={() => showLightbox()}>
|
||||||
<img className="performer" src={activeImage} alt={performer.name} />
|
<img
|
||||||
|
className="performer"
|
||||||
|
src={activeImage}
|
||||||
|
alt={performer.name}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const renderTabs = () => (
|
const renderTabs = () => (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Col>
|
|
||||||
<Row xs={8}>
|
|
||||||
<DetailsEditNavbar
|
|
||||||
objectName={
|
|
||||||
performer?.name ?? intl.formatMessage({ id: "performer" })
|
|
||||||
}
|
|
||||||
onToggleEdit={() => toggleEditing()}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onAutoTag={onAutoTag}
|
|
||||||
isNew={false}
|
|
||||||
isEditing={false}
|
|
||||||
onSave={() => {}}
|
|
||||||
onImageChange={() => {}}
|
|
||||||
classNames="mb-2"
|
|
||||||
customButtons={
|
|
||||||
<div>
|
|
||||||
<PerformerSubmitButton performer={performer} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
></DetailsEditNavbar>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={activeTabKey}
|
activeKey={activeTabKey}
|
||||||
onSelect={setActiveTabKey}
|
onSelect={setActiveTabKey}
|
||||||
id="performer-details"
|
id="performer-details"
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
>
|
>
|
||||||
<Tab eventKey="details" title={intl.formatMessage({ id: "details" })}>
|
|
||||||
<PerformerDetailsPanel performer={performer} />
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
<Tab
|
||||||
eventKey="scenes"
|
eventKey="scenes"
|
||||||
title={
|
title={
|
||||||
@@ -318,7 +303,24 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderTabsOrEditPanel() {
|
function maybeRenderHeaderBackgroundImage() {
|
||||||
|
if (enableBackgroundImage && !isEditing && activeImage) {
|
||||||
|
return (
|
||||||
|
<div className="background-image-container">
|
||||||
|
<picture>
|
||||||
|
<source src={activeImage} />
|
||||||
|
<img
|
||||||
|
className="background-image"
|
||||||
|
src={activeImage}
|
||||||
|
alt={`${performer.name} background`}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderEditPanel() {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
@@ -330,37 +332,83 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
setEncodingImage={setEncodingImage}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
return renderTabs();
|
{
|
||||||
|
return (
|
||||||
|
<Col>
|
||||||
|
<Row xs={8}>
|
||||||
|
<DetailsEditNavbar
|
||||||
|
objectName={
|
||||||
|
performer?.name ?? intl.formatMessage({ id: "performer" })
|
||||||
|
}
|
||||||
|
onToggleEdit={() => toggleEditing()}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onAutoTag={onAutoTag}
|
||||||
|
isNew={false}
|
||||||
|
isEditing={false}
|
||||||
|
onSave={() => {}}
|
||||||
|
onImageChange={() => {}}
|
||||||
|
classNames="mb-2"
|
||||||
|
customButtons={
|
||||||
|
<div>
|
||||||
|
<PerformerSubmitButton performer={performer} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
></DetailsEditNavbar>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderAge() {
|
function getCollapseButtonIcon() {
|
||||||
if (performer?.birthdate) {
|
return collapsed ? faChevronDown : faChevronUp;
|
||||||
// calculate the age from birthdate. In future, this should probably be
|
}
|
||||||
// provided by the server
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = () => {
|
||||||
|
if (document.documentElement.scrollTop <= 50) {
|
||||||
|
setLoadStickyHeader(false);
|
||||||
|
} else {
|
||||||
|
setLoadStickyHeader(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", f);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", f);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function maybeRenderDetails() {
|
||||||
|
if (!isEditing) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<PerformerDetailsPanel
|
||||||
<span className="age">
|
performer={performer}
|
||||||
{TextUtils.age(performer.birthdate, performer.death_date)}
|
collapsed={collapsed}
|
||||||
</span>
|
fullWidth={!collapsed && !compactExpandedDetails}
|
||||||
<span className="age-tail">
|
/>
|
||||||
{" "}
|
|
||||||
<FormattedMessage id="years_old" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderCompressedDetails() {
|
||||||
|
if (!isEditing && loadStickyHeader) {
|
||||||
|
return <CompressedPerformerDetailsPanel performer={performer} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderTab() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return renderTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function maybeRenderAliases() {
|
function maybeRenderAliases() {
|
||||||
if (performer?.alias_list?.length) {
|
if (performer?.alias_list?.length) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="alias-head">
|
<span className="alias-head">{performer.alias_list?.join(", ")}</span>
|
||||||
<FormattedMessage id="also_known_as" />{" "}
|
|
||||||
</span>
|
|
||||||
<span className="alias">{performer.alias_list?.join(", ")}</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -392,61 +440,95 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderClickableIcons = () => (
|
function maybeRenderShowCollapseButton() {
|
||||||
<span className="name-icons">
|
if (!isEditing) {
|
||||||
<Button
|
return (
|
||||||
className={cx(
|
<span className="detail-expand-collapse">
|
||||||
"minimal",
|
<Button
|
||||||
performer.favorite ? "favorite" : "not-favorite"
|
className="minimal expand-collapse"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderClickableIcons() {
|
||||||
|
/* Collect urls adding into details */
|
||||||
|
/* This code can be removed once multple urls are supported for performers */
|
||||||
|
const detailURLsRegex = /\[((?:http|www\.)[^\n\]]+)\]/gm;
|
||||||
|
let urls = performer?.details?.match(detailURLsRegex);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="name-icons">
|
||||||
|
<Button
|
||||||
|
className={cx(
|
||||||
|
"minimal",
|
||||||
|
performer.favorite ? "favorite" : "not-favorite"
|
||||||
|
)}
|
||||||
|
onClick={() => setFavorite(!performer.favorite)}
|
||||||
|
>
|
||||||
|
<Icon icon={faHeart} />
|
||||||
|
</Button>
|
||||||
|
{performer.url && (
|
||||||
|
<Button className="minimal icon-link" title={performer.url}>
|
||||||
|
<a
|
||||||
|
href={TextUtils.sanitiseURL(performer.url)}
|
||||||
|
className="link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Icon icon={faLink} />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
onClick={() => setFavorite(!performer.favorite)}
|
{(urls ?? []).map((url, index) => (
|
||||||
>
|
<Button key={index} className="minimal icon-link" title={url}>
|
||||||
<Icon icon={faHeart} />
|
<a
|
||||||
</Button>
|
href={TextUtils.sanitiseURL(url)}
|
||||||
{performer.url && (
|
className={`detail-link ${index}`}
|
||||||
<Button className="minimal icon-link">
|
target="_blank"
|
||||||
<a
|
rel="noopener noreferrer"
|
||||||
href={TextUtils.sanitiseURL(performer.url)}
|
>
|
||||||
className="link"
|
<Icon icon={faLink} />
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer"
|
</Button>
|
||||||
>
|
))}
|
||||||
<Icon icon={faLink} />
|
{performer.twitter && (
|
||||||
</a>
|
<Button className="minimal icon-link" title={performer.twitter}>
|
||||||
</Button>
|
<a
|
||||||
)}
|
href={TextUtils.sanitiseURL(
|
||||||
{performer.twitter && (
|
performer.twitter,
|
||||||
<Button className="minimal icon-link">
|
TextUtils.twitterURL
|
||||||
<a
|
)}
|
||||||
href={TextUtils.sanitiseURL(
|
className="twitter"
|
||||||
performer.twitter,
|
target="_blank"
|
||||||
TextUtils.twitterURL
|
rel="noopener noreferrer"
|
||||||
)}
|
>
|
||||||
className="twitter"
|
<Icon icon={faTwitter} />
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer"
|
</Button>
|
||||||
>
|
)}
|
||||||
<Icon icon={faTwitter} />
|
{performer.instagram && (
|
||||||
</a>
|
<Button className="minimal icon-link" title={performer.instagram}>
|
||||||
</Button>
|
<a
|
||||||
)}
|
href={TextUtils.sanitiseURL(
|
||||||
{performer.instagram && (
|
performer.instagram,
|
||||||
<Button className="minimal icon-link">
|
TextUtils.instagramURL
|
||||||
<a
|
)}
|
||||||
href={TextUtils.sanitiseURL(
|
className="instagram"
|
||||||
performer.instagram,
|
target="_blank"
|
||||||
TextUtils.instagramURL
|
rel="noopener noreferrer"
|
||||||
)}
|
>
|
||||||
className="instagram"
|
<Icon icon={faInstagram} />
|
||||||
target="_blank"
|
</a>
|
||||||
rel="noopener noreferrer"
|
</Button>
|
||||||
>
|
)}
|
||||||
<Icon icon={faInstagram} />
|
</span>
|
||||||
</a>
|
);
|
||||||
</Button>
|
}
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDestroying)
|
if (isDestroying)
|
||||||
return (
|
return (
|
||||||
@@ -455,10 +537,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
function getCollapseButtonIcon() {
|
|
||||||
return collapsed ? faChevronRight : faChevronLeft;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="performer-page" className="row">
|
<div id="performer-page" className="row">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@@ -466,48 +544,48 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`performer-image-container details-tab text-center text-center ${
|
className={`detail-header ${isEditing ? "edit" : ""} ${
|
||||||
collapsed ? "collapsed" : ""
|
collapsed ? "collapsed" : !compactExpandedDetails ? "full-width" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{encodingImage ? (
|
{maybeRenderHeaderBackgroundImage()}
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<div className="detail-container">
|
||||||
) : (
|
<div className="detail-header-image">
|
||||||
renderImage()
|
{encodingImage ? (
|
||||||
)}
|
<LoadingIndicator
|
||||||
</div>
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
<div className="details-divider d-none d-xl-block">
|
|
||||||
<Button onClick={() => setCollapsed(!collapsed)}>
|
|
||||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={`content-container ${collapsed ? "expanded" : ""}`}>
|
|
||||||
<div className="row">
|
|
||||||
<div className="performer-head col">
|
|
||||||
<h2>
|
|
||||||
<GenderIcon
|
|
||||||
gender={performer.gender}
|
|
||||||
className="gender-icon mr-2 fi"
|
|
||||||
/>
|
/>
|
||||||
<CountryFlag country={performer.country} className="mr-2" />
|
) : (
|
||||||
<span className="performer-name">{performer.name}</span>
|
renderImage()
|
||||||
{performer.disambiguation && (
|
)}
|
||||||
<span className="performer-disambiguation">
|
</div>
|
||||||
{` (${performer.disambiguation})`}
|
<div className="row">
|
||||||
</span>
|
<div className="performer-head col">
|
||||||
)}
|
<h2>
|
||||||
{renderClickableIcons()}
|
<span className="performer-name">{performer.name}</span>
|
||||||
</h2>
|
{performer.disambiguation && (
|
||||||
<RatingSystem
|
<span className="performer-disambiguation">
|
||||||
value={performer.rating100 ?? undefined}
|
{` (${performer.disambiguation})`}
|
||||||
onSetRating={(value) => setRating(value ?? null)}
|
</span>
|
||||||
/>
|
)}
|
||||||
{maybeRenderAliases()}
|
{maybeRenderShowCollapseButton()}
|
||||||
{maybeRenderAge()}
|
{renderClickableIcons()}
|
||||||
|
</h2>
|
||||||
|
{maybeRenderAliases()}
|
||||||
|
<RatingSystem
|
||||||
|
value={performer.rating100 ?? undefined}
|
||||||
|
onSetRating={(value) => setRating(value ?? null)}
|
||||||
|
/>
|
||||||
|
{maybeRenderDetails()}
|
||||||
|
{maybeRenderEditPanel()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{maybeRenderCompressedDetails()}
|
||||||
|
<div className="detail-body">
|
||||||
<div className="performer-body">
|
<div className="performer-body">
|
||||||
<div className="performer-tabs">{renderTabsOrEditPanel()}</div>
|
<div className="performer-tabs">{maybeRenderTab()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ const PerformerCreate: React.FC = () => {
|
|||||||
|
|
||||||
function renderPerformerImage() {
|
function renderPerformerImage() {
|
||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
return <LoadingIndicator message="Encoding image..." />;
|
return (
|
||||||
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (image) {
|
if (image) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { TagLink } from "src/components/Shared/TagLink";
|
import { TagLink } from "src/components/Shared/TagLink";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
import { getCountryByISO } from "src/utils/country";
|
|
||||||
import { TextField, URLField } from "src/utils/field";
|
|
||||||
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
|
import { cmToImperial, cmToInches, kgToLbs } from "src/utils/units";
|
||||||
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
|
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||||
|
|
||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
|
collapsed?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
performer,
|
performer,
|
||||||
|
collapsed,
|
||||||
|
fullWidth,
|
||||||
}) => {
|
}) => {
|
||||||
// Network state
|
// Network state
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -22,20 +26,12 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
if (!performer.tags.length) {
|
if (!performer.tags.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className="pl-0">
|
||||||
<dt>
|
{(performer.tags ?? []).map((tag) => (
|
||||||
<FormattedMessage id="tags" />
|
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||||
</dt>
|
))}
|
||||||
<dd>
|
</ul>
|
||||||
<ul className="pl-0">
|
|
||||||
{(performer.tags ?? []).map((tag) => (
|
|
||||||
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,32 +41,27 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className="pl-0">
|
||||||
<dt>StashIDs</dt>
|
{performer.stash_ids.map((stashID) => {
|
||||||
<dd>
|
const base = getStashboxBase(stashID.endpoint);
|
||||||
<ul className="pl-0">
|
const link = base ? (
|
||||||
{performer.stash_ids.map((stashID) => {
|
<a
|
||||||
const base = getStashboxBase(stashID.endpoint);
|
href={`${base}performers/${stashID.stash_id}`}
|
||||||
const link = base ? (
|
target="_blank"
|
||||||
<a
|
rel="noopener noreferrer"
|
||||||
href={`${base}performers/${stashID.stash_id}`}
|
>
|
||||||
target="_blank"
|
{stashID.stash_id}
|
||||||
rel="noopener noreferrer"
|
</a>
|
||||||
>
|
) : (
|
||||||
{stashID.stash_id}
|
stashID.stash_id
|
||||||
</a>
|
);
|
||||||
) : (
|
return (
|
||||||
stashID.stash_id
|
<li key={stashID.stash_id} className="row no-gutters">
|
||||||
);
|
{link}
|
||||||
return (
|
</li>
|
||||||
<li key={stashID.stash_id} className="row no-gutters">
|
);
|
||||||
{link}
|
})}
|
||||||
</li>
|
</ul>
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,92 +167,169 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function maybeRenderExtraDetails() {
|
||||||
|
if (!collapsed) {
|
||||||
|
/* Remove extra urls provided in details since they will be present by perfomr name */
|
||||||
|
/* This code can be removed once multple urls are supported for performers */
|
||||||
|
let details = performer?.details
|
||||||
|
?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "")
|
||||||
|
.trim();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DetailItem
|
||||||
|
id="tattoos"
|
||||||
|
value={performer?.tattoos}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="piercings"
|
||||||
|
value={performer?.piercings}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem id="details" value={details} fullWidth={fullWidth} />
|
||||||
|
<DetailItem
|
||||||
|
id="tags"
|
||||||
|
value={renderTagsField()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="StashIDs"
|
||||||
|
value={renderStashIDs()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="details-list">
|
<div className="detail-group">
|
||||||
<TextField
|
{performer.gender ? (
|
||||||
id="gender"
|
<DetailItem
|
||||||
value={
|
id="gender"
|
||||||
performer.gender
|
value={intl.formatMessage({ id: "gender_types." + performer.gender })}
|
||||||
? intl.formatMessage({ id: "gender_types." + performer.gender })
|
fullWidth={fullWidth}
|
||||||
: undefined
|
/>
|
||||||
}
|
) : (
|
||||||
/>
|
""
|
||||||
<TextField
|
|
||||||
id="birthdate"
|
|
||||||
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="death_date"
|
|
||||||
value={TextUtils.formatDate(intl, performer.death_date ?? undefined)}
|
|
||||||
/>
|
|
||||||
<TextField id="ethnicity" value={performer.ethnicity} />
|
|
||||||
<TextField id="hair_color" value={performer.hair_color} />
|
|
||||||
<TextField id="eye_color" value={performer.eye_color} />
|
|
||||||
<TextField
|
|
||||||
id="country"
|
|
||||||
value={
|
|
||||||
getCountryByISO(performer.country, intl.locale) ?? performer.country
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!!performer.height_cm && (
|
|
||||||
<>
|
|
||||||
<dt>
|
|
||||||
<FormattedMessage id="height" />
|
|
||||||
</dt>
|
|
||||||
<dd>{formatHeight(performer.height_cm)}</dd>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
<DetailItem
|
||||||
{!!performer.weight && (
|
id="age"
|
||||||
<>
|
value={TextUtils.age(performer.birthdate, performer.death_date)}
|
||||||
<dt>
|
title={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
||||||
<FormattedMessage id="weight" />
|
fullWidth={fullWidth}
|
||||||
</dt>
|
/>
|
||||||
<dd>{formatWeight(performer.weight)}</dd>
|
<DetailItem id="death_date" value={performer.death_date} />
|
||||||
</>
|
{performer.country ? (
|
||||||
|
<DetailItem
|
||||||
|
id="country"
|
||||||
|
value={
|
||||||
|
<CountryFlag
|
||||||
|
country={performer.country}
|
||||||
|
className="mr-2"
|
||||||
|
includeName={true}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
)}
|
)}
|
||||||
|
<DetailItem
|
||||||
{(performer.penis_length || performer.circumcised) && (
|
id="ethnicity"
|
||||||
<>
|
value={performer?.ethnicity}
|
||||||
<dt>
|
fullWidth={fullWidth}
|
||||||
<FormattedMessage id="penis" />:
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
{formatPenisLength(performer.penis_length)}
|
|
||||||
{formatCircumcised(performer.circumcised)}
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<TextField id="measurements" value={performer.measurements} />
|
|
||||||
<TextField id="fake_tits" value={performer.fake_tits} />
|
|
||||||
<TextField id="career_length" value={performer.career_length} />
|
|
||||||
<TextField id="tattoos" value={performer.tattoos} />
|
|
||||||
<TextField id="piercings" value={performer.piercings} />
|
|
||||||
<TextField id="details" value={performer.details} />
|
|
||||||
<URLField
|
|
||||||
id="url"
|
|
||||||
value={performer.url}
|
|
||||||
url={TextUtils.sanitiseURL(performer.url ?? "")}
|
|
||||||
/>
|
/>
|
||||||
<URLField
|
<DetailItem
|
||||||
id="twitter"
|
id="hair_color"
|
||||||
value={performer.twitter}
|
value={performer?.hair_color}
|
||||||
url={TextUtils.sanitiseURL(
|
fullWidth={fullWidth}
|
||||||
performer.twitter ?? "",
|
|
||||||
TextUtils.twitterURL
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<URLField
|
<DetailItem
|
||||||
id="instagram"
|
id="eye_color"
|
||||||
value={performer.instagram}
|
value={performer?.eye_color}
|
||||||
url={TextUtils.sanitiseURL(
|
fullWidth={fullWidth}
|
||||||
performer.instagram ?? "",
|
|
||||||
TextUtils.instagramURL
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
{renderTagsField()}
|
<DetailItem
|
||||||
{renderStashIDs()}
|
id="height"
|
||||||
</dl>
|
value={formatHeight(performer.height_cm)}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="weight"
|
||||||
|
value={formatWeight(performer.weight)}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="penis_length"
|
||||||
|
value={formatPenisLength(performer.penis_length)}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="circumcised"
|
||||||
|
value={formatCircumcised(performer.circumcised)}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="measurements"
|
||||||
|
value={performer?.measurements}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="fake_tits"
|
||||||
|
value={performer?.fake_tits}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
{maybeRenderExtraDetails()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CompressedPerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||||
|
performer,
|
||||||
|
}) => {
|
||||||
|
// Network state
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky detail-header">
|
||||||
|
<div className="sticky detail-header-group">
|
||||||
|
<a className="performer-name" onClick={() => scrollToTop()}>
|
||||||
|
{performer.name}
|
||||||
|
</a>
|
||||||
|
{performer.gender ? (
|
||||||
|
<span className="performer-gender">
|
||||||
|
{intl.formatMessage({ id: "gender_types." + performer.gender })}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
{performer.birthdate ? (
|
||||||
|
<span
|
||||||
|
className="performer-age"
|
||||||
|
title={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
||||||
|
>
|
||||||
|
{TextUtils.age(performer.birthdate, performer.death_date)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
{performer.country ? (
|
||||||
|
<span className="performer-country">
|
||||||
|
<CountryFlag
|
||||||
|
country={performer.country}
|
||||||
|
className="mr-2"
|
||||||
|
includeName={true}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,12 +16,9 @@
|
|||||||
|
|
||||||
.performer-head {
|
.performer-head {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 2rem;
|
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
.name-icons {
|
.name-icons {
|
||||||
margin-left: 10px;
|
|
||||||
|
|
||||||
.not-favorite {
|
.not-favorite {
|
||||||
color: rgba(191, 204, 214, 0.5);
|
color: rgba(191, 204, 214, 0.5);
|
||||||
}
|
}
|
||||||
@@ -213,4 +210,5 @@
|
|||||||
/* stylelint-disable */
|
/* stylelint-disable */
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
/* stylelint-enable */
|
/* stylelint-enable */
|
||||||
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -683,7 +683,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
return <LoadingIndicator message="Encoding image..." />;
|
return (
|
||||||
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImagePreview) {
|
if (coverImagePreview) {
|
||||||
|
|||||||
@@ -493,6 +493,64 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
|
<SettingSection headingID="config.ui.detail.heading">
|
||||||
|
<div className="setting-group">
|
||||||
|
<div className="setting">
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.ui.detail.enable_background_image.heading",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
<div className="sub-heading">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.ui.detail.enable_background_image.description",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<BooleanSetting
|
||||||
|
id="enableMovieBackgroundImage"
|
||||||
|
headingID="movie"
|
||||||
|
checked={ui.enableMovieBackgroundImage ?? undefined}
|
||||||
|
onChange={(v) => saveUI({ enableMovieBackgroundImage: v })}
|
||||||
|
/>
|
||||||
|
<BooleanSetting
|
||||||
|
id="enablePerformerBackgroundImage"
|
||||||
|
headingID="performer"
|
||||||
|
checked={ui.enablePerformerBackgroundImage ?? undefined}
|
||||||
|
onChange={(v) => saveUI({ enablePerformerBackgroundImage: v })}
|
||||||
|
/>
|
||||||
|
<BooleanSetting
|
||||||
|
id="enableStudioBackgroundImage"
|
||||||
|
headingID="studio"
|
||||||
|
checked={ui.enableStudioBackgroundImage ?? undefined}
|
||||||
|
onChange={(v) => saveUI({ enableStudioBackgroundImage: v })}
|
||||||
|
/>
|
||||||
|
<BooleanSetting
|
||||||
|
id="enableTagBackgroundImage"
|
||||||
|
headingID="tag"
|
||||||
|
checked={ui.enableTagBackgroundImage ?? undefined}
|
||||||
|
onChange={(v) => saveUI({ enableTagBackgroundImage: v })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BooleanSetting
|
||||||
|
id="show_all_details"
|
||||||
|
headingID="config.ui.detail.show_all_details.heading"
|
||||||
|
subHeadingID="config.ui.detail.show_all_details.description"
|
||||||
|
checked={ui.showAllDetails ?? true}
|
||||||
|
onChange={(v) => saveUI({ showAllDetails: v })}
|
||||||
|
/>
|
||||||
|
<BooleanSetting
|
||||||
|
id="compact_expanded_details"
|
||||||
|
headingID="config.ui.detail.compact_expanded_details.heading"
|
||||||
|
subHeadingID="config.ui.detail.compact_expanded_details.description"
|
||||||
|
checked={ui.compactExpandedDetails ?? undefined}
|
||||||
|
onChange={(v) => saveUI({ compactExpandedDetails: v })}
|
||||||
|
/>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection headingID="config.ui.editing.heading">
|
<SettingSection headingID="config.ui.editing.heading">
|
||||||
<div className="setting-group">
|
<div className="setting-group">
|
||||||
<div className="setting">
|
<div className="setting">
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import { getCountryByISO } from "src/utils/country";
|
|||||||
interface ICountryFlag {
|
interface ICountryFlag {
|
||||||
country?: string | null;
|
country?: string | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
includeName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CountryFlag: React.FC<ICountryFlag> = ({
|
export const CountryFlag: React.FC<ICountryFlag> = ({
|
||||||
className,
|
className,
|
||||||
country: isoCountry,
|
country: isoCountry,
|
||||||
|
includeName,
|
||||||
}) => {
|
}) => {
|
||||||
const { locale } = useIntl();
|
const { locale } = useIntl();
|
||||||
|
|
||||||
@@ -18,9 +20,12 @@ export const CountryFlag: React.FC<ICountryFlag> = ({
|
|||||||
if (!isoCountry || !country) return <></>;
|
if (!isoCountry || !country) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<>
|
||||||
className={`${className ?? ""} fi fi-${isoCountry.toLowerCase()}`}
|
{includeName ? country : ""}
|
||||||
title={country}
|
<span
|
||||||
/>
|
className={`${className ?? ""} fi fi-${isoCountry.toLowerCase()}`}
|
||||||
|
title={country}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
39
ui/v2.5/src/components/Shared/DetailItem.tsx
Normal file
39
ui/v2.5/src/components/Shared/DetailItem.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
interface IDetailItem {
|
||||||
|
id?: string | null;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
value?: any;
|
||||||
|
title?: string;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailItem: React.FC<IDetailItem> = ({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
title,
|
||||||
|
fullWidth,
|
||||||
|
}) => {
|
||||||
|
if (!id || !value || value === "Na") {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = <FormattedMessage id={id} />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
// according to linter rule CSS classes shouldn't use underscores
|
||||||
|
<div className={`detail-item ${id}`}>
|
||||||
|
<span className={`detail-item-title ${id.replace("_", "-")}`}>
|
||||||
|
{message}
|
||||||
|
{fullWidth ? ":" : ""}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`detail-item-value ${id.replace("_", "-")}`}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@ export const GridCard: React.FC<ICardProps> = (props: ICardProps) => {
|
|||||||
props.onSelectedChanged(!props.selected, shiftKey);
|
props.onSelectedChanged(!props.selected, shiftKey);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDrag(event: React.DragEvent<HTMLElement>) {
|
function handleDrag(event: React.DragEvent<HTMLElement>) {
|
||||||
|
|||||||
@@ -46,11 +46,11 @@
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
div:nth-last-child(2) {
|
.detail-header.edit .details-edit div:nth-last-child(2) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-suggest {
|
.select-suggest {
|
||||||
|
|||||||
@@ -26,14 +26,22 @@ import { StudioImagesPanel } from "./StudioImagesPanel";
|
|||||||
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
||||||
import { StudioPerformersPanel } from "./StudioPerformersPanel";
|
import { StudioPerformersPanel } from "./StudioPerformersPanel";
|
||||||
import { StudioEditPanel } from "./StudioEditPanel";
|
import { StudioEditPanel } from "./StudioEditPanel";
|
||||||
import { StudioDetailsPanel } from "./StudioDetailsPanel";
|
import {
|
||||||
|
CompressedStudioDetailsPanel,
|
||||||
|
StudioDetailsPanel,
|
||||||
|
} from "./StudioDetailsPanel";
|
||||||
import { StudioMoviesPanel } from "./StudioMoviesPanel";
|
import { StudioMoviesPanel } from "./StudioMoviesPanel";
|
||||||
import {
|
import {
|
||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faChevronRight,
|
faLink,
|
||||||
faChevronLeft,
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { IUIConfig } from "src/core/config";
|
import { IUIConfig } from "src/core/config";
|
||||||
|
import TextUtils from "src/utils/text";
|
||||||
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
|
import ImageUtils from "src/utils/image";
|
||||||
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
@@ -49,12 +57,16 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { tab = "details" } = useParams<IStudioParams>();
|
const { tab = "details" } = useParams<IStudioParams>();
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
// Configuration settings
|
// Configuration settings
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const abbreviateCounter =
|
const uiConfig = configuration?.ui as IUIConfig | undefined;
|
||||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||||
|
const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false;
|
||||||
|
const showAllDetails = uiConfig?.showAllDetails ?? false;
|
||||||
|
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||||
|
const [loadStickyHeader, setLoadStickyHeader] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
@@ -95,6 +107,27 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useRatingKeybinds(
|
||||||
|
true,
|
||||||
|
configuration?.ui?.ratingSystemOptions?.type,
|
||||||
|
setRating
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = () => {
|
||||||
|
if (document.documentElement.scrollTop <= 50) {
|
||||||
|
setLoadStickyHeader(false);
|
||||||
|
} else {
|
||||||
|
setLoadStickyHeader(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", f);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", f);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.StudioCreateInput) {
|
async function onSave(input: GQL.StudioCreateInput) {
|
||||||
await updateStudio({
|
await updateStudio({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -162,6 +195,20 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderAliases() {
|
||||||
|
if (studio?.aliases?.length) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="alias-head">{studio?.aliases?.join(", ")}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCollapseButtonIcon() {
|
||||||
|
return collapsed ? faChevronDown : faChevronUp;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleEditing(value?: boolean) {
|
function toggleEditing(value?: boolean) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
setIsEditing(value);
|
setIsEditing(value);
|
||||||
@@ -184,7 +231,14 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (studioImage) {
|
if (studioImage) {
|
||||||
return <img className="logo" alt={studio.name} src={studioImage} />;
|
return (
|
||||||
|
<img
|
||||||
|
className="logo"
|
||||||
|
alt={studio.name}
|
||||||
|
src={studioImage}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,175 +257,289 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCollapseButtonIcon() {
|
const renderClickableIcons = () => (
|
||||||
return collapsed ? faChevronRight : faChevronLeft;
|
<span className="name-icons">
|
||||||
|
{studio.url && (
|
||||||
|
<Button className="minimal icon-link" title={studio.url}>
|
||||||
|
<a
|
||||||
|
href={TextUtils.sanitiseURL(studio.url)}
|
||||||
|
className="link"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Icon icon={faLink} />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
function setRating(v: number | null) {
|
||||||
|
if (studio.id) {
|
||||||
|
updateStudio({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: studio.id,
|
||||||
|
rating100: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderDetails() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<StudioDetailsPanel
|
||||||
|
studio={studio}
|
||||||
|
collapsed={collapsed}
|
||||||
|
fullWidth={!collapsed && !compactExpandedDetails}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderShowCollapseButton() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<span className="detail-expand-collapse">
|
||||||
|
<Button
|
||||||
|
className="minimal expand-collapse"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderCompressedDetails() {
|
||||||
|
if (!isEditing && loadStickyHeader) {
|
||||||
|
return <CompressedStudioDetailsPanel studio={studio} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabs = () => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
id="studio-tabs"
|
||||||
|
mountOnEnter
|
||||||
|
unmountOnExit
|
||||||
|
activeKey={activeTabKey}
|
||||||
|
onSelect={setActiveTabKey}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
eventKey="scenes"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "scenes" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={sceneCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioScenesPanel
|
||||||
|
active={activeTabKey == "scenes"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="galleries"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "galleries" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={galleryCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioGalleriesPanel
|
||||||
|
active={activeTabKey == "galleries"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="images"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "images" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={imageCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioImagesPanel
|
||||||
|
active={activeTabKey == "images"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="performers"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "performers" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={performerCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioPerformersPanel
|
||||||
|
active={activeTabKey == "performers"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="movies"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "movies" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={movieCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioMoviesPanel
|
||||||
|
active={activeTabKey == "movies"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="childstudios"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "subsidiary_studios" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={false}
|
||||||
|
count={studio.child_studios.length}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StudioChildrenPanel
|
||||||
|
active={activeTabKey == "childstudios"}
|
||||||
|
studio={studio}
|
||||||
|
/>
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
function maybeRenderHeaderBackgroundImage() {
|
||||||
|
let studioImage = studio.image_path;
|
||||||
|
if (enableBackgroundImage && !isEditing && studioImage) {
|
||||||
|
return (
|
||||||
|
<div className="background-image-container">
|
||||||
|
<picture>
|
||||||
|
<source src={studioImage} />
|
||||||
|
<img
|
||||||
|
className="background-image"
|
||||||
|
src={studioImage}
|
||||||
|
alt={`${studio.name} background`}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderTab() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return renderTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderEditPanel() {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<StudioEditPanel
|
||||||
|
studio={studio}
|
||||||
|
onSubmit={onSave}
|
||||||
|
onCancel={() => toggleEditing()}
|
||||||
|
onDelete={onDelete}
|
||||||
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<DetailsEditNavbar
|
||||||
|
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||||
|
isNew={false}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onToggleEdit={() => toggleEditing()}
|
||||||
|
onSave={() => {}}
|
||||||
|
onImageChange={() => {}}
|
||||||
|
onClearImage={() => {}}
|
||||||
|
onAutoTag={onAutoTag}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div id="studio-page" className="row">
|
||||||
|
<Helmet>
|
||||||
|
<title>{studio.name ?? intl.formatMessage({ id: "studio" })}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={`studio-details details-tab ${collapsed ? "collapsed" : ""}`}
|
className={`detail-header ${isEditing ? "edit" : ""} ${
|
||||||
|
collapsed ? "collapsed" : !compactExpandedDetails ? "full-width" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="text-center">
|
{maybeRenderHeaderBackgroundImage()}
|
||||||
{encodingImage ? (
|
<div className="detail-container">
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<div className="detail-header-image">
|
||||||
) : (
|
{encodingImage ? (
|
||||||
renderImage()
|
<LoadingIndicator
|
||||||
)}
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
renderImage()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
<div className="studio-head col">
|
||||||
|
<h2>
|
||||||
|
<span className="studio-name">{studio.name}</span>
|
||||||
|
{maybeRenderShowCollapseButton()}
|
||||||
|
{renderClickableIcons()}
|
||||||
|
</h2>
|
||||||
|
{maybeRenderAliases()}
|
||||||
|
<RatingSystem
|
||||||
|
value={studio.rating100 ?? undefined}
|
||||||
|
onSetRating={(value) => setRating(value ?? null)}
|
||||||
|
/>
|
||||||
|
{maybeRenderDetails()}
|
||||||
|
{maybeRenderEditPanel()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isEditing ? (
|
|
||||||
<>
|
|
||||||
<Helmet>
|
|
||||||
<title>
|
|
||||||
{studio.name ?? intl.formatMessage({ id: "studio" })}
|
|
||||||
</title>
|
|
||||||
</Helmet>
|
|
||||||
<StudioDetailsPanel studio={studio} />
|
|
||||||
<DetailsEditNavbar
|
|
||||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
|
||||||
isNew={false}
|
|
||||||
isEditing={isEditing}
|
|
||||||
onToggleEdit={() => toggleEditing()}
|
|
||||||
onSave={() => {}}
|
|
||||||
onImageChange={() => {}}
|
|
||||||
onClearImage={() => {}}
|
|
||||||
onAutoTag={onAutoTag}
|
|
||||||
onDelete={onDelete}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<StudioEditPanel
|
|
||||||
studio={studio}
|
|
||||||
onSubmit={onSave}
|
|
||||||
onCancel={() => toggleEditing()}
|
|
||||||
onDelete={onDelete}
|
|
||||||
setImage={setImage}
|
|
||||||
setEncodingImage={setEncodingImage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="details-divider d-none d-xl-block">
|
{maybeRenderCompressedDetails()}
|
||||||
<Button onClick={() => setCollapsed(!collapsed)}>
|
<div className="detail-body">
|
||||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
<div className="studio-body">
|
||||||
</Button>
|
<div className="studio-tabs">{maybeRenderTab()}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`col content-container ${collapsed ? "expanded" : ""}`}>
|
|
||||||
<Tabs
|
|
||||||
id="studio-tabs"
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit
|
|
||||||
activeKey={activeTabKey}
|
|
||||||
onSelect={setActiveTabKey}
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
eventKey="scenes"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "scenes" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={sceneCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioScenesPanel
|
|
||||||
active={activeTabKey == "scenes"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="galleries"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "galleries" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={galleryCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioGalleriesPanel
|
|
||||||
active={activeTabKey == "galleries"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="images"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "images" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={imageCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioImagesPanel
|
|
||||||
active={activeTabKey == "images"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="performers"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "performers" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={performerCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioPerformersPanel
|
|
||||||
active={activeTabKey == "performers"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="movies"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "movies" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={movieCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioMoviesPanel
|
|
||||||
active={activeTabKey == "movies"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="childstudios"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "subsidiary_studios" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={false}
|
|
||||||
count={studio.child_studios.length}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<StudioChildrenPanel
|
|
||||||
active={activeTabKey == "childstudios"}
|
|
||||||
studio={studio}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
{renderDeleteAlert()}
|
{renderDeleteAlert()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ const StudioCreate: React.FC = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,119 +1,100 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Badge } from "react-bootstrap";
|
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import TextUtils from "src/utils/text";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { TextField, URLField } from "src/utils/field";
|
|
||||||
|
|
||||||
interface IStudioDetailsPanel {
|
interface IStudioDetailsPanel {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
|
collapsed?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||||
studio,
|
studio,
|
||||||
|
collapsed,
|
||||||
|
fullWidth,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
|
||||||
function renderRatingField() {
|
|
||||||
if (!studio.rating100) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
|
||||||
<dd>
|
|
||||||
<RatingSystem value={studio.rating100} disabled />
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderTagsList() {
|
|
||||||
if (!studio.aliases?.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<dt>
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</dt>
|
|
||||||
<dd>
|
|
||||||
{studio.aliases.map((a) => (
|
|
||||||
<Badge className="tag-item" variant="secondary" key={a}>
|
|
||||||
{a}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderStashIDs() {
|
function renderStashIDs() {
|
||||||
if (!studio.stash_ids?.length) {
|
if (!studio.stash_ids?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ul className="pl-0">
|
||||||
<dt>
|
{studio.stash_ids.map((stashID) => {
|
||||||
<FormattedMessage id="stash_ids" />
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
</dt>
|
const link = base ? (
|
||||||
<dd>
|
<a
|
||||||
<ul className="pl-0">
|
href={`${base}studios/${stashID.stash_id}`}
|
||||||
{studio.stash_ids.map((stashID) => {
|
target="_blank"
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
rel="noopener noreferrer"
|
||||||
const link = base ? (
|
>
|
||||||
<a
|
{stashID.stash_id}
|
||||||
href={`${base}studios/${stashID.stash_id}`}
|
</a>
|
||||||
target="_blank"
|
) : (
|
||||||
rel="noopener noreferrer"
|
stashID.stash_id
|
||||||
>
|
);
|
||||||
{stashID.stash_id}
|
return (
|
||||||
</a>
|
<li key={stashID.stash_id} className="row no-gutters">
|
||||||
) : (
|
{link}
|
||||||
stashID.stash_id
|
</li>
|
||||||
);
|
);
|
||||||
return (
|
})}
|
||||||
<li key={stashID.stash_id} className="row no-gutters">
|
</ul>
|
||||||
{link}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function maybeRenderExtraDetails() {
|
||||||
|
if (!collapsed) {
|
||||||
|
return (
|
||||||
|
<DetailItem
|
||||||
|
id="StashIDs"
|
||||||
|
value={renderStashIDs()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="studio-details">
|
<div className="detail-group">
|
||||||
<div>
|
<DetailItem id="details" value={studio.details} fullWidth={fullWidth} />
|
||||||
<h2>{studio.name}</h2>
|
<DetailItem
|
||||||
</div>
|
id="parent_studios"
|
||||||
|
value={
|
||||||
<dl className="details-list">
|
studio.parent_studio?.name ? (
|
||||||
<URLField
|
<a href={`/studios/${studio.parent_studio?.id}`} target="_self">
|
||||||
id="url"
|
{studio.parent_studio.name}
|
||||||
value={studio.url}
|
</a>
|
||||||
url={TextUtils.sanitiseURL(studio.url ?? "")}
|
) : (
|
||||||
/>
|
""
|
||||||
|
)
|
||||||
<TextField id="details" value={studio.details} />
|
}
|
||||||
|
fullWidth={fullWidth}
|
||||||
<URLField
|
/>
|
||||||
id="parent_studios"
|
{maybeRenderExtraDetails()}
|
||||||
value={studio.parent_studio?.name}
|
</div>
|
||||||
url={`/studios/${studio.parent_studio?.id}`}
|
);
|
||||||
trusted
|
};
|
||||||
target="_self"
|
|
||||||
/>
|
export const CompressedStudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
||||||
|
studio,
|
||||||
{renderRatingField()}
|
}) => {
|
||||||
{renderTagsList()}
|
function scrollToTop() {
|
||||||
{renderStashIDs()}
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
</dl>
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky detail-header">
|
||||||
|
<div className="sticky detail-header-group">
|
||||||
|
<a className="studio-name" onClick={() => scrollToTop()}>
|
||||||
|
{studio.name}
|
||||||
|
</a>
|
||||||
|
{studio?.parent_studio?.name ? (
|
||||||
|
<span className="studio-parent">{studio?.parent_studio?.name}</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,16 +8,12 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
|||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||||
import FormUtils from "src/utils/form";
|
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { getStashIDs } from "src/utils/stashIds";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { StringListInput } from "../../Shared/StringListInput";
|
import { StringListInput } from "../../Shared/StringListInput";
|
||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
@@ -43,7 +39,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
const isNew = studio.id === undefined;
|
const isNew = studio.id === undefined;
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
|
||||||
|
const labelXS = 3;
|
||||||
|
const labelXL = 2;
|
||||||
|
const fieldXS = 9;
|
||||||
|
const fieldXL = 7;
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -53,7 +53,6 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
details: yup.string().ensure(),
|
details: yup.string().ensure(),
|
||||||
parent_id: yup.string().required().nullable(),
|
parent_id: yup.string().required().nullable(),
|
||||||
rating100: yup.number().nullable().defined(),
|
|
||||||
aliases: yup
|
aliases: yup
|
||||||
.array(yup.string().required())
|
.array(yup.string().required())
|
||||||
.defined()
|
.defined()
|
||||||
@@ -85,7 +84,6 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
url: studio.url ?? "",
|
url: studio.url ?? "",
|
||||||
details: studio.details ?? "",
|
details: studio.details ?? "",
|
||||||
parent_id: studio.parent_studio?.id ?? null,
|
parent_id: studio.parent_studio?.id ?? null,
|
||||||
rating100: studio.rating100 ?? null,
|
|
||||||
aliases: studio.aliases ?? [],
|
aliases: studio.aliases ?? [],
|
||||||
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
||||||
stash_ids: getStashIDs(studio.stash_ids),
|
stash_ids: getStashIDs(studio.stash_ids),
|
||||||
@@ -112,16 +110,6 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
setEncodingImage(encodingImage);
|
setEncodingImage(encodingImage);
|
||||||
}, [setEncodingImage, encodingImage]);
|
}, [setEncodingImage, encodingImage]);
|
||||||
|
|
||||||
function setRating(v: number) {
|
|
||||||
formik.setFieldValue("rating100", v);
|
|
||||||
}
|
|
||||||
|
|
||||||
useRatingKeybinds(
|
|
||||||
true,
|
|
||||||
configuration?.ui?.ratingSystemOptions?.type,
|
|
||||||
setRating
|
|
||||||
);
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
@@ -171,8 +159,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<Form.Label column>StashIDs</Form.Label>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
<Col xs={9}>
|
StashIDs
|
||||||
|
</Form.Label>
|
||||||
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<ul className="pl-0">
|
<ul className="pl-0">
|
||||||
{formik.values.stash_ids.map((stashID) => {
|
{formik.values.stash_ids.map((stashID) => {
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
@@ -235,10 +225,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
|
|
||||||
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="studio-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
<Form.Group controlId="name" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "name" }),
|
<FormattedMessage id="name" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
{...formik.getFieldProps("name")}
|
{...formik.getFieldProps("name")}
|
||||||
@@ -250,11 +240,25 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="aliases" as={Row}>
|
||||||
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
|
<FormattedMessage id="aliases" />
|
||||||
|
</Form.Label>
|
||||||
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
|
<StringListInput
|
||||||
|
value={formik.values.aliases ?? []}
|
||||||
|
setValue={(value) => formik.setFieldValue("aliases", value)}
|
||||||
|
errors={aliasErrorMsg}
|
||||||
|
errorIdx={aliasErrorIdx}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "url" }),
|
<FormattedMessage id="url" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
{...formik.getFieldProps("url")}
|
{...formik.getFieldProps("url")}
|
||||||
@@ -267,10 +271,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="details" as={Row}>
|
<Form.Group controlId="details" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "details" }),
|
<FormattedMessage id="details" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -284,10 +288,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="parent_studio" as={Row}>
|
<Form.Group controlId="parent_studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "parent_studios" }),
|
<FormattedMessage id="parent_studios" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
@@ -301,44 +305,16 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: intl.formatMessage({ id: "rating" }),
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<RatingSystem
|
|
||||||
value={formik.values.rating100 ?? undefined}
|
|
||||||
onSetRating={(value) =>
|
|
||||||
formik.setFieldValue("rating100", value ?? null)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
{renderStashIDs()}
|
{renderStashIDs()}
|
||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
|
||||||
<Form.Label column xs={3}>
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</Form.Label>
|
|
||||||
<Col xs={9}>
|
|
||||||
<StringListInput
|
|
||||||
value={formik.values.aliases ?? []}
|
|
||||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
|
||||||
errors={aliasErrorMsg}
|
|
||||||
errorIdx={aliasErrorIdx}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group controlId="ignore-auto-tag" as={Row}>
|
<Form.Group controlId="ignore-auto-tag" as={Row}>
|
||||||
<Form.Label column xs={3}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
<FormattedMessage id="ignore_auto_tag" />
|
<FormattedMessage id="ignore_auto_tag" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
{...formik.getFieldProps({
|
{...formik.getFieldProps({
|
||||||
name: "ignore_auto_tag",
|
name: "ignore_auto_tag",
|
||||||
@@ -350,6 +326,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
|
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
||||||
|
classNames="col-xl-9 mt-3"
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing
|
isEditing
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button, Tabs, Tab, Dropdown } from "react-bootstrap";
|
import { Tabs, Tab, Dropdown, Button } 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 { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
@@ -26,17 +26,18 @@ import { TagMarkersPanel } from "./TagMarkersPanel";
|
|||||||
import { TagImagesPanel } from "./TagImagesPanel";
|
import { TagImagesPanel } from "./TagImagesPanel";
|
||||||
import { TagPerformersPanel } from "./TagPerformersPanel";
|
import { TagPerformersPanel } from "./TagPerformersPanel";
|
||||||
import { TagGalleriesPanel } from "./TagGalleriesPanel";
|
import { TagGalleriesPanel } from "./TagGalleriesPanel";
|
||||||
import { TagDetailsPanel } from "./TagDetailsPanel";
|
import { CompressedTagDetailsPanel, TagDetailsPanel } from "./TagDetailsPanel";
|
||||||
import { TagEditPanel } from "./TagEditPanel";
|
import { TagEditPanel } from "./TagEditPanel";
|
||||||
import { TagMergeModal } from "./TagMergeDialog";
|
import { TagMergeModal } from "./TagMergeDialog";
|
||||||
import {
|
import {
|
||||||
|
faChevronDown,
|
||||||
|
faChevronUp,
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faChevronRight,
|
|
||||||
faChevronLeft,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { IUIConfig } from "src/core/config";
|
import { IUIConfig } from "src/core/config";
|
||||||
|
import ImageUtils from "src/utils/image";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment;
|
||||||
@@ -51,12 +52,16 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
|
||||||
|
|
||||||
// Configuration settings
|
// Configuration settings
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const abbreviateCounter =
|
const uiConfig = configuration?.ui as IUIConfig | undefined;
|
||||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||||
|
const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false;
|
||||||
|
const showAllDetails = uiConfig?.showAllDetails ?? false;
|
||||||
|
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||||
|
const [loadStickyHeader, setLoadStickyHeader] = useState<boolean>(false);
|
||||||
|
|
||||||
const { tab = "scenes" } = useParams<ITabParams>();
|
const { tab = "scenes" } = useParams<ITabParams>();
|
||||||
|
|
||||||
@@ -117,6 +122,21 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const f = () => {
|
||||||
|
if (document.documentElement.scrollTop <= 50) {
|
||||||
|
setLoadStickyHeader(false);
|
||||||
|
} else {
|
||||||
|
setLoadStickyHeader(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("scroll", f);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", f);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.TagCreateInput) {
|
async function onSave(input: GQL.TagCreateInput) {
|
||||||
const oldRelations = {
|
const oldRelations = {
|
||||||
parents: tag.parents ?? [],
|
parents: tag.parents ?? [],
|
||||||
@@ -203,6 +223,35 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCollapseButtonIcon() {
|
||||||
|
return collapsed ? faChevronDown : faChevronUp;
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderShowCollapseButton() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<span className="detail-expand-collapse">
|
||||||
|
<Button
|
||||||
|
className="minimal expand-collapse"
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
>
|
||||||
|
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderAliases() {
|
||||||
|
if (tag?.aliases?.length) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="alias-head">{tag?.aliases?.join(", ")}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleEditing(value?: boolean) {
|
function toggleEditing(value?: boolean) {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
setIsEditing(value);
|
setIsEditing(value);
|
||||||
@@ -225,7 +274,14 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tagImage) {
|
if (tagImage) {
|
||||||
return <img className="logo" alt={tag.name} src={tagImage} />;
|
return (
|
||||||
|
<img
|
||||||
|
className="logo"
|
||||||
|
alt={tag.name}
|
||||||
|
src={tagImage}
|
||||||
|
onLoad={ImageUtils.verifyImageSize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,157 +326,211 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCollapseButtonIcon() {
|
function maybeRenderDetails() {
|
||||||
return collapsed ? faChevronRight : faChevronLeft;
|
if (!isEditing) {
|
||||||
|
return (
|
||||||
|
<TagDetailsPanel
|
||||||
|
tag={tag}
|
||||||
|
fullWidth={!collapsed && !compactExpandedDetails}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderEditPanel() {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<TagEditPanel
|
||||||
|
tag={tag}
|
||||||
|
onSubmit={onSave}
|
||||||
|
onCancel={() => toggleEditing()}
|
||||||
|
onDelete={onDelete}
|
||||||
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
<DetailsEditNavbar
|
||||||
|
objectName={tag.name}
|
||||||
|
isNew={false}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onToggleEdit={() => toggleEditing()}
|
||||||
|
onSave={() => {}}
|
||||||
|
onImageChange={() => {}}
|
||||||
|
onClearImage={() => {}}
|
||||||
|
onAutoTag={onAutoTag}
|
||||||
|
onDelete={onDelete}
|
||||||
|
classNames="mb-2"
|
||||||
|
customButtons={renderMergeButton()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabs = () => (
|
||||||
|
<React.Fragment>
|
||||||
|
<Tabs
|
||||||
|
id="tag-tabs"
|
||||||
|
mountOnEnter
|
||||||
|
unmountOnExit
|
||||||
|
activeKey={activeTabKey}
|
||||||
|
onSelect={setActiveTabKey}
|
||||||
|
>
|
||||||
|
<Tab
|
||||||
|
eventKey="scenes"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "scenes" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={sceneCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TagScenesPanel active={activeTabKey == "scenes"} tag={tag} />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="images"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "images" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={imageCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TagImagesPanel active={activeTabKey == "images"} tag={tag} />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="galleries"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "galleries" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={galleryCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TagGalleriesPanel active={activeTabKey == "galleries"} tag={tag} />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="markers"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "markers" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={sceneMarkerCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TagMarkersPanel active={activeTabKey == "markers"} tag={tag} />
|
||||||
|
</Tab>
|
||||||
|
<Tab
|
||||||
|
eventKey="performers"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
{intl.formatMessage({ id: "performers" })}
|
||||||
|
<Counter
|
||||||
|
abbreviateCounter={abbreviateCounter}
|
||||||
|
count={performerCount}
|
||||||
|
hideZero
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TagPerformersPanel active={activeTabKey == "performers"} tag={tag} />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
function maybeRenderHeaderBackgroundImage() {
|
||||||
|
let tagImage = tag.image_path;
|
||||||
|
if (enableBackgroundImage && !isEditing && tagImage) {
|
||||||
|
return (
|
||||||
|
<div className="background-image-container">
|
||||||
|
<picture>
|
||||||
|
<source src={tagImage} />
|
||||||
|
<img
|
||||||
|
className="background-image"
|
||||||
|
src={tagImage}
|
||||||
|
alt={`${tag.name} background`}
|
||||||
|
/>
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderTab() {
|
||||||
|
if (!isEditing) {
|
||||||
|
return renderTabs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderCompressedDetails() {
|
||||||
|
if (!isEditing && loadStickyHeader) {
|
||||||
|
return <CompressedTagDetailsPanel tag={tag} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div id="tag-page" className="row">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{tag.name}</title>
|
<title>{tag.name}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="row">
|
|
||||||
<div
|
<div
|
||||||
className={`tag-details details-tab ${collapsed ? "collapsed" : ""}`}
|
className={`detail-header ${isEditing ? "edit" : ""} ${
|
||||||
>
|
collapsed ? "collapsed" : !compactExpandedDetails ? "full-width" : ""
|
||||||
<div className="text-center logo-container">
|
}`}
|
||||||
|
>
|
||||||
|
{maybeRenderHeaderBackgroundImage()}
|
||||||
|
<div className="detail-container">
|
||||||
|
<div className="detail-header-image">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
)}
|
)}
|
||||||
<h2>{tag.name}</h2>
|
|
||||||
<p>{tag.description}</p>
|
|
||||||
</div>
|
</div>
|
||||||
{!isEditing ? (
|
<div className="row">
|
||||||
<>
|
<div className="studio-head col">
|
||||||
<TagDetailsPanel tag={tag} />
|
<h2>
|
||||||
{/* HACK - this is also rendered in the TagEditPanel */}
|
<span className="tag-name">{tag.name}</span>
|
||||||
<DetailsEditNavbar
|
{maybeRenderShowCollapseButton()}
|
||||||
objectName={tag.name}
|
</h2>
|
||||||
isNew={false}
|
{maybeRenderAliases()}
|
||||||
isEditing={isEditing}
|
{maybeRenderDetails()}
|
||||||
onToggleEdit={() => toggleEditing()}
|
{maybeRenderEditPanel()}
|
||||||
onSave={() => {}}
|
</div>
|
||||||
onImageChange={() => {}}
|
</div>
|
||||||
onClearImage={() => {}}
|
|
||||||
onAutoTag={onAutoTag}
|
|
||||||
onDelete={onDelete}
|
|
||||||
classNames="mb-2"
|
|
||||||
customButtons={renderMergeButton()}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<TagEditPanel
|
|
||||||
tag={tag}
|
|
||||||
onSubmit={onSave}
|
|
||||||
onCancel={() => toggleEditing()}
|
|
||||||
onDelete={onDelete}
|
|
||||||
setImage={setImage}
|
|
||||||
setEncodingImage={setEncodingImage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="details-divider d-none d-xl-block">
|
|
||||||
<Button onClick={() => setCollapsed(!collapsed)}>
|
|
||||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className={`col content-container ${collapsed ? "expanded" : ""}`}>
|
|
||||||
<Tabs
|
|
||||||
id="tag-tabs"
|
|
||||||
mountOnEnter
|
|
||||||
unmountOnExit
|
|
||||||
activeKey={activeTabKey}
|
|
||||||
onSelect={setActiveTabKey}
|
|
||||||
>
|
|
||||||
<Tab
|
|
||||||
eventKey="scenes"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "scenes" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={sceneCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TagScenesPanel active={activeTabKey == "scenes"} tag={tag} />
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="images"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "images" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={imageCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TagImagesPanel active={activeTabKey == "images"} tag={tag} />
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="galleries"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "galleries" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={galleryCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TagGalleriesPanel
|
|
||||||
active={activeTabKey == "galleries"}
|
|
||||||
tag={tag}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="markers"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "markers" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={sceneMarkerCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TagMarkersPanel active={activeTabKey == "markers"} tag={tag} />
|
|
||||||
</Tab>
|
|
||||||
<Tab
|
|
||||||
eventKey="performers"
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
{intl.formatMessage({ id: "performers" })}
|
|
||||||
<Counter
|
|
||||||
abbreviateCounter={abbreviateCounter}
|
|
||||||
count={performerCount}
|
|
||||||
hideZero
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TagPerformersPanel
|
|
||||||
active={activeTabKey == "performers"}
|
|
||||||
tag={tag}
|
|
||||||
/>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
{renderDeleteAlert()}
|
|
||||||
{renderMergeDialog()}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
{maybeRenderCompressedDetails()}
|
||||||
|
<div className="detail-body">
|
||||||
|
<div className="tag-body">
|
||||||
|
<div className="tag-tabs">{maybeRenderTab()}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{renderDeleteAlert()}
|
||||||
|
{renderMergeDialog()}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,9 @@ const TagCreate: React.FC = () => {
|
|||||||
<div className="tag-details col-md-8">
|
<div className="tag-details col-md-8">
|
||||||
<div className="text-center logo-container">
|
<div className="text-center logo-container">
|
||||||
{encodingImage ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator
|
||||||
|
message={`${intl.formatMessage({ id: "encoding_image" })}...`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,53 +1,28 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Badge } from "react-bootstrap";
|
import { Badge } from "react-bootstrap";
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface ITagDetails {
|
interface ITagDetails {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment;
|
||||||
|
fullWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
||||||
function renderAliasesField() {
|
|
||||||
if (!tag.aliases.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dl className="row">
|
|
||||||
<dt className="col-3 col-xl-2">
|
|
||||||
<FormattedMessage id="aliases" />
|
|
||||||
</dt>
|
|
||||||
<dd className="col-9 col-xl-10">
|
|
||||||
{tag.aliases.map((a) => (
|
|
||||||
<Badge className="tag-item" variant="secondary" key={a}>
|
|
||||||
{a}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderParentsField() {
|
function renderParentsField() {
|
||||||
if (!tag.parents?.length) {
|
if (!tag.parents?.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row">
|
<>
|
||||||
<dt className="col-3 col-xl-2">
|
{tag.parents.map((p) => (
|
||||||
<FormattedMessage id="parent_tags" />
|
<Badge key={p.id} className="tag-item" variant="secondary">
|
||||||
</dt>
|
<Link to={`/tags/${p.id}`}>{p.name}</Link>
|
||||||
<dd className="col-9 col-xl-10">
|
</Badge>
|
||||||
{tag.parents.map((p) => (
|
))}
|
||||||
<Badge key={p.id} className="tag-item" variant="secondary">
|
</>
|
||||||
<Link to={`/tags/${p.id}`}>{p.name}</Link>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,26 +32,54 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row">
|
<>
|
||||||
<dt className="col-3 col-xl-2">
|
{tag.children.map((c) => (
|
||||||
<FormattedMessage id="sub_tags" />
|
<Badge key={c.id} className="tag-item" variant="secondary">
|
||||||
</dt>
|
<Link to={`/tags/${c.id}`}>{c.name}</Link>
|
||||||
<dd className="col-9 col-xl-10">
|
</Badge>
|
||||||
{tag.children.map((c) => (
|
))}
|
||||||
<Badge key={c.id} className="tag-item" variant="secondary">
|
</>
|
||||||
<Link to={`/tags/${c.id}`}>{c.name}</Link>
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="detail-group">
|
||||||
{renderAliasesField()}
|
<DetailItem
|
||||||
{renderParentsField()}
|
id="description"
|
||||||
{renderChildrenField()}
|
value={tag.description}
|
||||||
</>
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="parent_tags"
|
||||||
|
value={renderParentsField()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
<DetailItem
|
||||||
|
id="sub_tags"
|
||||||
|
value={renderChildrenField()}
|
||||||
|
fullWidth={fullWidth}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CompressedTagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
||||||
|
function scrollToTop() {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky detail-header">
|
||||||
|
<div className="sticky detail-header-group">
|
||||||
|
<a className="tag-name" onClick={() => scrollToTop()}>
|
||||||
|
{tag.name}
|
||||||
|
</a>
|
||||||
|
{tag.description ? (
|
||||||
|
<span className="tag-desc">{tag.description}</span>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import * as yup from "yup";
|
|||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
import FormUtils from "src/utils/form";
|
|
||||||
import ImageUtils from "src/utils/image";
|
import ImageUtils from "src/utils/image";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
@@ -42,9 +41,9 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const labelXS = 3;
|
const labelXS = 3;
|
||||||
const labelXL = 3;
|
const labelXL = 2;
|
||||||
const fieldXS = 9;
|
const fieldXS = 9;
|
||||||
const fieldXL = 9;
|
const fieldXL = 7;
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
@@ -204,10 +203,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="description" as={Row}>
|
<Form.Group controlId="description" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "description" }),
|
<FormattedMessage id="description" />
|
||||||
})}
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -218,15 +217,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="parent_tags" as={Row}>
|
<Form.Group controlId="parent_tags" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "parent_tags" }),
|
<FormattedMessage id="parent_tags" />
|
||||||
labelProps: {
|
</Form.Label>
|
||||||
column: true,
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
<TagSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
@@ -247,15 +241,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="sub_tags" as={Row}>
|
<Form.Group controlId="sub_tags" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
title: intl.formatMessage({ id: "sub_tags" }),
|
<FormattedMessage id="sub_tags" />
|
||||||
labelProps: {
|
</Form.Label>
|
||||||
column: true,
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
<TagSelect
|
||||||
isMulti
|
isMulti
|
||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
@@ -294,6 +283,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
|
|
||||||
<DetailsEditNavbar
|
<DetailsEditNavbar
|
||||||
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
||||||
|
classNames="col-xl-9 mt-3"
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
|
|||||||
@@ -43,6 +43,18 @@ export interface IUIConfig {
|
|||||||
|
|
||||||
ratingSystemOptions?: RatingSystemOptions;
|
ratingSystemOptions?: RatingSystemOptions;
|
||||||
|
|
||||||
|
// if true a background image will be display on header
|
||||||
|
enableMovieBackgroundImage?: boolean;
|
||||||
|
// if true a background image will be display on header
|
||||||
|
enablePerformerBackgroundImage?: boolean;
|
||||||
|
// if true a background image will be display on header
|
||||||
|
enableStudioBackgroundImage?: boolean;
|
||||||
|
// if true a background image will be display on header
|
||||||
|
enableTagBackgroundImage?: boolean;
|
||||||
|
// if true view expanded details compact
|
||||||
|
compactExpandedDetails?: boolean;
|
||||||
|
// if true show all content details by default
|
||||||
|
showAllDetails?: boolean;
|
||||||
// if true the chromecast option will enabled
|
// if true the chromecast option will enabled
|
||||||
enableChromecast?: boolean;
|
enableChromecast?: boolean;
|
||||||
// if true continue scene will always play from the beginning
|
// if true continue scene will always play from the beginning
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
| `r 0` | [Edit mode] Unset rating (stars) |
|
| `r 0` | [Edit mode] Unset rating (stars) |
|
||||||
| `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) |
|
| `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) |
|
||||||
| ``r ` `` | [Edit mode] Unset rating (decimal) |
|
| ``r ` `` | [Edit mode] Unset rating (decimal) |
|
||||||
|
| `,` | Expand/Collapse Details |
|
||||||
| `Ctrl + v` | Paste Movie image |
|
| `Ctrl + v` | Paste Movie image |
|
||||||
|
|
||||||
[//]: # "Commented until implementation is dealt with"
|
[//]: # "Commented until implementation is dealt with"
|
||||||
@@ -144,11 +145,11 @@
|
|||||||
|
|
||||||
| Keyboard sequence | Action |
|
| Keyboard sequence | Action |
|
||||||
|-------------------|--------|
|
|-------------------|--------|
|
||||||
| `a` | Details tab |
|
|
||||||
| `c` | Scenes tab |
|
| `c` | Scenes tab |
|
||||||
| `e` | Edit tab |
|
| `e` | Edit tab |
|
||||||
| `o` | Operations tab |
|
| `o` | Operations tab |
|
||||||
| `f` | Toggle favourite |
|
| `f` | Toggle favourite |
|
||||||
|
| `,` | Expand/Collapse Details |
|
||||||
|
|
||||||
### Edit Performer tab shortcuts
|
### Edit Performer tab shortcuts
|
||||||
|
|
||||||
@@ -171,6 +172,7 @@
|
|||||||
| `e` | Edit Studio |
|
| `e` | Edit Studio |
|
||||||
| `s s` | Save Studio |
|
| `s s` | Save Studio |
|
||||||
| `d d` | Delete Studio |
|
| `d d` | Delete Studio |
|
||||||
|
| `,` | Expand/Collapse Details |
|
||||||
| `Ctrl + v` | Paste Studio image |
|
| `Ctrl + v` | Paste Studio image |
|
||||||
|
|
||||||
## Tags Page shortcuts
|
## Tags Page shortcuts
|
||||||
@@ -186,4 +188,5 @@
|
|||||||
| `e` | Edit Tag |
|
| `e` | Edit Tag |
|
||||||
| `s s` | Save Tag |
|
| `s s` | Save Tag |
|
||||||
| `d d` | Delete Tag |
|
| `d d` | Delete Tag |
|
||||||
|
| `,` | Expand/Collapse Details |
|
||||||
| `Ctrl + v` | Paste Tag image |
|
| `Ctrl + v` | Paste Tag image |
|
||||||
@@ -39,10 +39,11 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 4rem 0 0 0;
|
overflow-x: hidden;
|
||||||
|
padding: 3.5rem 0 0 0;
|
||||||
|
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(xs) {
|
||||||
@media (orientation: portrait) {
|
@media (orientation: portrait) {
|
||||||
@@ -61,6 +62,303 @@ dd {
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky.detail-header-group {
|
||||||
|
padding: 1rem 2.5rem;
|
||||||
|
|
||||||
|
a.movie-name,
|
||||||
|
a.performer-name,
|
||||||
|
a.studio-name,
|
||||||
|
a.tag-name {
|
||||||
|
color: #f5f8fa;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
span {
|
||||||
|
color: #d7d9db;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky.detail-header {
|
||||||
|
display: block;
|
||||||
|
min-height: 50px;
|
||||||
|
padding: unset;
|
||||||
|
position: fixed;
|
||||||
|
top: 3.3rem;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-name,
|
||||||
|
.performer-name,
|
||||||
|
.studio-name,
|
||||||
|
.tag-name {
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-expand-collapse {
|
||||||
|
.btn-primary:focus,
|
||||||
|
.btn-primary.focus,
|
||||||
|
.btn-primary:not(:disabled):not(.disabled):active,
|
||||||
|
.btn-primary:not(:disabled):not(.disabled).active,
|
||||||
|
.show > .btn-primary.dropdown-toggle,
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: rgba(138, 155, 168, 0.15);
|
||||||
|
background-color: rgba(138, 155, 168, 0.15);
|
||||||
|
border-color: rgba(138, 155, 168, 0.15);
|
||||||
|
box-shadow: unset;
|
||||||
|
color: #f5f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header {
|
||||||
|
background-color: #192127;
|
||||||
|
min-height: 15rem;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem;
|
||||||
|
position: relative;
|
||||||
|
transition: 0.3s;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 11;
|
||||||
|
|
||||||
|
.detail-group,
|
||||||
|
.col {
|
||||||
|
transition: 0.2s;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-image-container {
|
||||||
|
bottom: -0.2rem;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0.2;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: -0.2rem;
|
||||||
|
z-index: auto;
|
||||||
|
|
||||||
|
.background-image {
|
||||||
|
filter: blur(16px);
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: 50% 30%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-container {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
.detail-item-value.age {
|
||||||
|
border-bottom: 1px dotted #f5f8fa;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country,
|
||||||
|
.performer-country {
|
||||||
|
.mr-2.fi {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alias-head {
|
||||||
|
color: #868791;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-expand-collapse,
|
||||||
|
.name-icons {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header.edit {
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-edit {
|
||||||
|
padding-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-image {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header.collapsed {
|
||||||
|
.detail-header-image img {
|
||||||
|
max-width: 11rem;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-body {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
nav {
|
||||||
|
align-content: center;
|
||||||
|
border-bottom: solid 2px #192127;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsed .detail-item-value {
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
.detail-header-image {
|
||||||
|
height: auto;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
flex-direction: unset;
|
||||||
|
padding-right: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.detail-item-title {
|
||||||
|
display: table-cell;
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-value {
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-title.tags,
|
||||||
|
.detail-item-title.parent-tags,
|
||||||
|
.detail-item-title.sub-tags {
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-image {
|
||||||
|
display: flex;
|
||||||
|
float: left;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 1rem;
|
||||||
|
|
||||||
|
.movie-images {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
float: unset;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.movie-images {
|
||||||
|
.img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 14rem;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-images img {
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#movie-page .detail-header-image .movie-images img {
|
||||||
|
max-width: 13rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#movie-page .detail-header-image img,
|
||||||
|
#performer-page .detail-header-image img,
|
||||||
|
#tag-page .detail-header-image img {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tag-page .full-width .detail-header-image img {
|
||||||
|
max-width: 22rem;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#tag-page .detail-header-image img {
|
||||||
|
max-width: 18rem;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item.tags .pl-0 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
align-items: left;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-right: 4rem;
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-title {
|
||||||
|
color: #868791;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item-value {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.input-control,
|
.input-control,
|
||||||
.text-input {
|
.text-input {
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -130,6 +428,12 @@ textarea.text-input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.row.justify-content-center {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (min-width: 576px) {
|
@media (min-width: 576px) {
|
||||||
.zoom-0 {
|
.zoom-0 {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"download_backup": "Download Backup",
|
"download_backup": "Download Backup",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"edit_entity": "Edit {entityType}",
|
"edit_entity": "Edit {entityType}",
|
||||||
|
"encoding_image": "Encoding image",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"export_all": "Export all…",
|
"export_all": "Export all…",
|
||||||
"find": "Find",
|
"find": "Find",
|
||||||
@@ -580,6 +581,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"detail": {
|
||||||
|
"enable_background_image": {
|
||||||
|
"description": "Display background image on detail page.",
|
||||||
|
"heading": "Enable background image"
|
||||||
|
},
|
||||||
|
"heading": "Detail Page",
|
||||||
|
"compact_expanded_details": {
|
||||||
|
"description": "When enabled, this option will present expanded details while maintaining a compact presentation",
|
||||||
|
"heading": "Compact expanded details"
|
||||||
|
},
|
||||||
|
"show_all_details": {
|
||||||
|
"description": "When enabled, all content details will be shown by default and each detail item will fit under a single column",
|
||||||
|
"heading": "Show all details"
|
||||||
|
}
|
||||||
|
},
|
||||||
"funscript_offset": {
|
"funscript_offset": {
|
||||||
"description": "Time offset in milliseconds for interactive scripts playback.",
|
"description": "Time offset in milliseconds for interactive scripts playback.",
|
||||||
"heading": "Funscript Offset (ms)"
|
"heading": "Funscript Offset (ms)"
|
||||||
|
|||||||
@@ -70,6 +70,17 @@ const ImageUtils = {
|
|||||||
onImageChange,
|
onImageChange,
|
||||||
usePasteImage,
|
usePasteImage,
|
||||||
imageToDataURL,
|
imageToDataURL,
|
||||||
|
verifyImageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function verifyImageSize(e: React.UIEvent<HTMLImageElement>) {
|
||||||
|
const img = e.target as HTMLImageElement;
|
||||||
|
// set width = 200px if zero-sized image (SVG w/o intrinsic size)
|
||||||
|
if (img.width === 0 && img.height === 0) {
|
||||||
|
img.setAttribute("width", "200");
|
||||||
|
} else {
|
||||||
|
img.removeAttribute("width");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default ImageUtils;
|
export default ImageUtils;
|
||||||
|
|||||||
Reference in New Issue
Block a user