mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Incorporate i18n into UI elements (#1471)
* Update zh-tw string table (till 975343d2)
* Prepare localization table
* Implement i18n for Performers & Tags
* Add "add" action strings
* Use Lodash merge for deep merging language JSONs
The original implementation does not properly merge language files, causing unexpected localization string fallback behavior.
* Localize pagination strings
* Use Field name value as null id fallback
...otherwise FormattedMessage is gonna throw when the ID is null
* Use localized "Path" string for all instances
* Localize the "Interface" tab under settings
* Localize scene & performer cards
* Rename locale folder for better compatibility with i18n-ally
* Localize majority of the categories and features
This commit is contained in:
9
ui/v2.5/.vscode/settings.json
vendored
9
ui/v2.5/.vscode/settings.json
vendored
@@ -6,5 +6,12 @@
|
|||||||
"javascript.preferences.importModuleSpecifier": "relative",
|
"javascript.preferences.importModuleSpecifier": "relative",
|
||||||
"typescript.preferences.importModuleSpecifier": "relative",
|
"typescript.preferences.importModuleSpecifier": "relative",
|
||||||
"editor.wordWrapColumn": 120,
|
"editor.wordWrapColumn": 120,
|
||||||
"editor.rulers": [120]
|
"editor.rulers": [
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"src/locales"
|
||||||
|
],
|
||||||
|
"i18n-ally.keystyle": "nested",
|
||||||
|
"i18n-ally.sourceLanguage": "en-GB"
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { Route, Switch, useRouteMatch } from "react-router-dom";
|
import { Route, Switch, useRouteMatch } from "react-router-dom";
|
||||||
import { IntlProvider } from "react-intl";
|
import { IntlProvider } from "react-intl";
|
||||||
|
import { merge } from "lodash";
|
||||||
import { ToastProvider } from "src/hooks/Toast";
|
import { ToastProvider } from "src/hooks/Toast";
|
||||||
import LightboxProvider from "src/hooks/Lightbox/context";
|
import LightboxProvider from "src/hooks/Lightbox/context";
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { initPolyfills } from "src/polyfills";
|
import { initPolyfills } from "src/polyfills";
|
||||||
|
|
||||||
import locales from "src/locale";
|
import locales from "src/locales";
|
||||||
import { useConfiguration, useSystemStatus } from "src/core/StashService";
|
import { useConfiguration, useSystemStatus } from "src/core/StashService";
|
||||||
import { flattenMessages } from "src/utils";
|
import { flattenMessages } from "src/utils";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
@@ -58,12 +59,12 @@ export const App: React.FC = () => {
|
|||||||
const messageLanguage = languageMessageString(language);
|
const messageLanguage = languageMessageString(language);
|
||||||
|
|
||||||
// use en-GB as default messages if any messages aren't found in the chosen language
|
// use en-GB as default messages if any messages aren't found in the chosen language
|
||||||
const mergedMessages = {
|
const mergedMessages = merge(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
...(locales as any)[defaultMessageLanguage],
|
(locales as any)[defaultMessageLanguage],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
...(locales as any)[messageLanguage],
|
(locales as any)[messageLanguage]
|
||||||
};
|
);
|
||||||
const messages = flattenMessages(mergedMessages);
|
const messages = flattenMessages(mergedMessages);
|
||||||
|
|
||||||
const setupMatch = useRouteMatch(["/setup", "/migrate"]);
|
const setupMatch = useRouteMatch(["/setup", "/migrate"]);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
|
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Added internationalisation for all UI pages and added zh-TW language option. ([#1471](https://github.com/stashapp/stash/pull/1471))
|
||||||
* Add option to disable audio for generated previews. ([#1454](https://github.com/stashapp/stash/pull/1454))
|
* Add option to disable audio for generated previews. ([#1454](https://github.com/stashapp/stash/pull/1454))
|
||||||
* Prompt when leaving scene edit page with unsaved changes. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
* Prompt when leaving scene edit page with unsaved changes. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
||||||
* Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435))
|
* Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IDeleteGalleryDialogProps {
|
interface IDeleteGalleryDialogProps {
|
||||||
selected: Pick<GQL.Gallery, "id">[];
|
selected: Pick<GQL.Gallery, "id">[];
|
||||||
@@ -14,20 +14,22 @@ interface IDeleteGalleryDialogProps {
|
|||||||
export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
||||||
props: IDeleteGalleryDialogProps
|
props: IDeleteGalleryDialogProps
|
||||||
) => {
|
) => {
|
||||||
const plural = props.selected.length > 1;
|
const intl = useIntl();
|
||||||
|
const singularEntity = intl.formatMessage({ id: "gallery" });
|
||||||
|
const pluralEntity = intl.formatMessage({ id: "galleries" });
|
||||||
|
|
||||||
const singleMessageId = "deleteGalleryText";
|
const header = intl.formatMessage(
|
||||||
const pluralMessageId = "deleteGallerysText";
|
{ id: "dialogs.delete_entity_title" },
|
||||||
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const singleMessage =
|
);
|
||||||
"Are you sure you want to delete this gallery? Galleries for zip files will be re-added during the next scan unless the zip file is also deleted.";
|
const toastMessage = intl.formatMessage(
|
||||||
const pluralMessage =
|
{ id: "toast.delete_entity" },
|
||||||
"Are you sure you want to delete these galleries? Galleries for zip files will be re-added during the next scan unless the zip files are also deleted.";
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
|
);
|
||||||
const header = plural ? "Delete Galleries" : "Delete Gallery";
|
const message = intl.formatMessage(
|
||||||
const toastMessage = plural ? "Deleted galleries" : "Deleted gallery";
|
{ id: "dialogs.delete_entity_desc" },
|
||||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const message = plural ? pluralMessage : singleMessage;
|
);
|
||||||
|
|
||||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||||
@@ -63,17 +65,19 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
|||||||
show
|
show
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
header={header}
|
header={header}
|
||||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
accept={{
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isDeleting}
|
isRunning={isDeleting}
|
||||||
>
|
>
|
||||||
<p>
|
<p>{message}</p>
|
||||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
|
||||||
</p>
|
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-file"
|
id="delete-file"
|
||||||
@@ -84,7 +88,9 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-generated"
|
id="delete-generated"
|
||||||
checked={deleteGenerated}
|
checked={deleteGenerated}
|
||||||
label="Delete generated supporting files"
|
label={intl.formatMessage({
|
||||||
|
id: "actions.delete_generated_supporting_files",
|
||||||
|
})}
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useBulkGalleryUpdate } from "src/core/StashService";
|
import { useBulkGalleryUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
|||||||
export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
@@ -138,7 +140,14 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
input: getGalleryInput(),
|
input: getGalleryInput(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Updated galleries" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl.formatMessage({ id: "galleries" }).toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
props.onClose(true);
|
props.onClose(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -347,11 +356,21 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Edit Galleries"
|
header={intl.formatMessage(
|
||||||
accept={{ onClick: onSave, text: "Apply" }}
|
{ id: "dialogs.edit_entity_title" },
|
||||||
|
{
|
||||||
|
count: props?.selected?.length ?? 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "gallery" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "galleries" }),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
accept={{
|
||||||
|
onClick: onSave,
|
||||||
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isUpdating}
|
isRunning={isUpdating}
|
||||||
@@ -359,7 +378,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -372,7 +391,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -386,19 +405,23 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="performers">
|
<Form.Group controlId="performers">
|
||||||
<Form.Label>Performers</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="performers" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("performers", performerIds)}
|
{renderMultiSelect("performers", performerIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="tags">
|
<Form.Group controlId="tags">
|
||||||
<Form.Label>Tags</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="tags" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="organized">
|
<Form.Group controlId="organized">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
label="Organized"
|
label={intl.formatMessage({ id: "organized" })}
|
||||||
checked={organized}
|
checked={organized}
|
||||||
ref={checkboxRef}
|
ref={checkboxRef}
|
||||||
onChange={() => cycleOrganized()}
|
onChange={() => cycleOrganized()}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useHistory, Link } from "react-router-dom";
|
import { useParams, useHistory, Link } from "react-router-dom";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
mutateMetadataScan,
|
mutateMetadataScan,
|
||||||
useFindGallery,
|
useFindGallery,
|
||||||
@@ -28,6 +29,7 @@ export const Gallery: React.FC = () => {
|
|||||||
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
|
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
const { data, error, loading } = useFindGallery(id);
|
const { data, error, loading } = useFindGallery(id);
|
||||||
@@ -73,7 +75,15 @@ export const Gallery: React.FC = () => {
|
|||||||
paths: [gallery.path],
|
paths: [gallery.path],
|
||||||
});
|
});
|
||||||
|
|
||||||
Toast.success({ content: "Rescanning image" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.rescanning_entity" },
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "gallery" }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
@@ -103,7 +113,7 @@ export const Gallery: React.FC = () => {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
id="operation-menu"
|
id="operation-menu"
|
||||||
className="minimal"
|
className="minimal"
|
||||||
title="Operations"
|
title={intl.formatMessage({ id: "operations" })}
|
||||||
>
|
>
|
||||||
<Icon icon="ellipsis-v" />
|
<Icon icon="ellipsis-v" />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
@@ -114,7 +124,7 @@ export const Gallery: React.FC = () => {
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onRescan()}
|
onClick={() => onRescan()}
|
||||||
>
|
>
|
||||||
Rescan
|
<FormattedMessage id="actions.rescan" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
@@ -122,7 +132,10 @@ export const Gallery: React.FC = () => {
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Delete Gallery
|
<FormattedMessage
|
||||||
|
id="actions.delete_entity"
|
||||||
|
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||||
|
/>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -142,22 +155,28 @@ export const Gallery: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Nav variant="tabs" className="mr-auto">
|
<Nav variant="tabs" className="mr-auto">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="gallery-details-panel">Details</Nav.Link>
|
<Nav.Link eventKey="gallery-details-panel">
|
||||||
|
<FormattedMessage id="details" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
{gallery.scenes.length > 0 && (
|
{gallery.scenes.length > 0 && (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="gallery-scenes-panel">Scenes</Nav.Link>
|
<Nav.Link eventKey="gallery-scenes-panel">
|
||||||
|
<FormattedMessage id="scenes" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
)}
|
)}
|
||||||
{gallery.path ? (
|
{gallery.path ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="gallery-file-info-panel">
|
<Nav.Link eventKey="gallery-file-info-panel">
|
||||||
File Info
|
<FormattedMessage id="file_info" />
|
||||||
</Nav.Link>
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="gallery-edit-panel">Edit</Nav.Link>
|
<Nav.Link eventKey="gallery-edit-panel">
|
||||||
|
<FormattedMessage id="actions.edit" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item className="ml-auto">
|
<Nav.Item className="ml-auto">
|
||||||
<OrganizedButton
|
<OrganizedButton
|
||||||
@@ -212,10 +231,14 @@ export const Gallery: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Nav variant="tabs" className="mr-auto">
|
<Nav variant="tabs" className="mr-auto">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="images">Images</Nav.Link>
|
<Nav.Link eventKey="images">
|
||||||
|
<FormattedMessage id="images" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="add">Add</Nav.Link>
|
<Nav.Link eventKey="add">
|
||||||
|
<FormattedMessage id="actions.add" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
</Nav>
|
</Nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -255,7 +278,12 @@ export const Gallery: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="row new-view">
|
<div className="row new-view">
|
||||||
<div className="col-6">
|
<div className="col-6">
|
||||||
<h2>Create Gallery</h2>
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id="actions.create_entity"
|
||||||
|
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
<GalleryEditPanel
|
<GalleryEditPanel
|
||||||
isNew
|
isNew
|
||||||
gallery={undefined}
|
gallery={undefined}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { showWhenSelected } from "src/hooks/ListHook";
|
|||||||
import { mutateAddGalleryImages } from "src/core/StashService";
|
import { mutateAddGalleryImages } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IGalleryAddProps {
|
interface IGalleryAddProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -14,6 +15,7 @@ interface IGalleryAddProps {
|
|||||||
|
|
||||||
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
function filterHook(filter: ListFilterModel) {
|
function filterHook(filter: ListFilterModel) {
|
||||||
const galleryValue = {
|
const galleryValue = {
|
||||||
@@ -60,8 +62,16 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
|||||||
gallery_id: gallery.id!,
|
gallery_id: gallery.id!,
|
||||||
image_ids: Array.from(selectedIds.values()),
|
image_ids: Array.from(selectedIds.values()),
|
||||||
});
|
});
|
||||||
|
const imageCount = selectedIds.size;
|
||||||
Toast.success({
|
Toast.success({
|
||||||
content: "Added images",
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.added_entity" },
|
||||||
|
{
|
||||||
|
count: imageCount,
|
||||||
|
singularEntity: intl.formatMessage({ id: "image" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "images" }),
|
||||||
|
}
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -70,7 +80,10 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
|||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "Add to Gallery",
|
text: intl.formatMessage(
|
||||||
|
{ id: "actions.add_to_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "gallery" }) }
|
||||||
|
),
|
||||||
onClick: addImages,
|
onClick: addImages,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
postRefetch: true,
|
postRefetch: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -49,6 +50,7 @@ interface IExistingProps {
|
|||||||
export const GalleryEditPanel: React.FC<
|
export const GalleryEditPanel: React.FC<
|
||||||
IProps & (INewProps | IExistingProps)
|
IProps & (INewProps | IExistingProps)
|
||||||
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [title, setTitle] = useState<string>(gallery?.title ?? "");
|
const [title, setTitle] = useState<string>(gallery?.title ?? "");
|
||||||
@@ -173,7 +175,16 @@ export const GalleryEditPanel: React.FC<
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.galleryUpdate) {
|
if (result.data?.galleryUpdate) {
|
||||||
Toast.success({ content: "Updated gallery" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl
|
||||||
|
.formatMessage({ id: "gallery" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -249,7 +260,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
<DropdownButton
|
<DropdownButton
|
||||||
className="d-inline-block"
|
className="d-inline-block"
|
||||||
id="gallery-scrape"
|
id="gallery-scrape"
|
||||||
title="Scrape with..."
|
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||||
>
|
>
|
||||||
{queryableScrapers.map((s) => (
|
{queryableScrapers.map((s) => (
|
||||||
<Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}>
|
<Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}>
|
||||||
@@ -260,7 +271,9 @@ export const GalleryEditPanel: React.FC<
|
|||||||
<span className="fa-icon">
|
<span className="fa-icon">
|
||||||
<Icon icon="sync-alt" />
|
<Icon icon="sync-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span>Reload scrapers</span>
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_scrapers" />
|
||||||
|
</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
@@ -359,14 +372,14 @@ export const GalleryEditPanel: React.FC<
|
|||||||
<div className="form-container row px-3 pt-3">
|
<div className="form-container row px-3 pt-3">
|
||||||
<div className="col edit-buttons mb-3 pl-0">
|
<div className="col edit-buttons mb-3 pl-0">
|
||||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Col xs={6} className="text-right">
|
<Col xs={6} className="text-right">
|
||||||
@@ -376,21 +389,23 @@ export const GalleryEditPanel: React.FC<
|
|||||||
<div className="form-container row px-3">
|
<div className="form-container row px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{FormUtils.renderInputGroup({
|
{FormUtils.renderInputGroup({
|
||||||
title: "Title",
|
title: intl.formatMessage({ id: "title" }),
|
||||||
value: title,
|
value: title,
|
||||||
onChange: setTitle,
|
onChange: setTitle,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
})}
|
})}
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
<Col xs={3} className="pr-0 url-label">
|
<Col xs={3} className="pr-0 url-label">
|
||||||
<Form.Label className="col-form-label">URL</Form.Label>
|
<Form.Label className="col-form-label">
|
||||||
|
{intl.formatMessage({ id: "url" })}
|
||||||
|
</Form.Label>
|
||||||
<div className="float-right scrape-button-container">
|
<div className="float-right scrape-button-container">
|
||||||
{maybeRenderScrapeButton()}
|
{maybeRenderScrapeButton()}
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
{EditableTextUtils.renderInputGroup({
|
{EditableTextUtils.renderInputGroup({
|
||||||
title: "URL",
|
title: intl.formatMessage({ id: "url" }),
|
||||||
value: url,
|
value: url,
|
||||||
onChange: setUrl,
|
onChange: setUrl,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
@@ -398,7 +413,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
{FormUtils.renderInputGroup({
|
{FormUtils.renderInputGroup({
|
||||||
title: "Date",
|
title: intl.formatMessage({ id: "date" }),
|
||||||
value: date,
|
value: date,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
onChange: setDate,
|
onChange: setDate,
|
||||||
@@ -406,7 +421,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
})}
|
})}
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -418,7 +433,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -432,7 +447,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
<Form.Group controlId="performers" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Performers",
|
title: intl.formatMessage({ id: "performers" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -452,7 +467,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
<Form.Group controlId="tags" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Tags",
|
title: intl.formatMessage({ id: "tags" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -470,7 +485,7 @@ export const GalleryEditPanel: React.FC<
|
|||||||
|
|
||||||
<Form.Group controlId="scenes" as={Row}>
|
<Form.Group controlId="scenes" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Scenes",
|
title: intl.formatMessage({ id: "scenes" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -487,7 +502,9 @@ export const GalleryEditPanel: React.FC<
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
<Form.Group controlId="details">
|
<Form.Group controlId="details">
|
||||||
<Form.Label>Details</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="details" />
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="gallery-description text-input"
|
className="gallery-description text-input"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
interface IGalleryFileInfoPanelProps {
|
interface IGalleryFileInfoPanelProps {
|
||||||
gallery: GQL.GalleryDataFragment;
|
gallery: GQL.GalleryDataFragment;
|
||||||
@@ -12,7 +13,9 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
|||||||
function renderChecksum() {
|
function renderChecksum() {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Checksum</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.checksum" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.gallery.checksum} />
|
<TruncatedText className="col-8" text={props.gallery.checksum} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -23,7 +26,9 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Path</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="path" />
|
||||||
|
</span>
|
||||||
<a href={filePath} className="col-8">
|
<a href={filePath} className="col-8">
|
||||||
<TruncatedText text={filePath} />
|
<TruncatedText text={filePath} />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { mutateRemoveGalleryImages } from "src/core/StashService";
|
|||||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IGalleryDetailsProps {
|
interface IGalleryDetailsProps {
|
||||||
gallery: GQL.GalleryDataFragment;
|
gallery: GQL.GalleryDataFragment;
|
||||||
@@ -15,6 +16,7 @@ interface IGalleryDetailsProps {
|
|||||||
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
||||||
gallery,
|
gallery,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
function filterHook(filter: ListFilterModel) {
|
function filterHook(filter: ListFilterModel) {
|
||||||
@@ -63,7 +65,10 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
|||||||
image_ids: Array.from(selectedIds.values()),
|
image_ids: Array.from(selectedIds.values()),
|
||||||
});
|
});
|
||||||
Toast.success({
|
Toast.success({
|
||||||
content: "Added images",
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.added_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "images" }) }
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { StudioSelect, PerformerSelect } from "src/components/Shared";
|
import { StudioSelect, PerformerSelect } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TagSelect } from "src/components/Shared/Select";
|
import { TagSelect } from "src/components/Shared/Select";
|
||||||
@@ -41,6 +42,7 @@ function renderScrapedStudio(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedStudioRow(
|
function renderScrapedStudioRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
onChange: (value: ScrapeResult<string>) => void,
|
onChange: (value: ScrapeResult<string>) => void,
|
||||||
newStudio?: GQL.ScrapedSceneStudio,
|
newStudio?: GQL.ScrapedSceneStudio,
|
||||||
@@ -48,7 +50,7 @@ function renderScrapedStudioRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Studio"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedStudio(result)}
|
renderOriginalField={() => renderScrapedStudio(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -87,6 +89,7 @@ function renderScrapedPerformers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedPerformersRow(
|
function renderScrapedPerformersRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newPerformers: GQL.ScrapedScenePerformer[],
|
newPerformers: GQL.ScrapedScenePerformer[],
|
||||||
@@ -94,7 +97,7 @@ function renderScrapedPerformersRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Performers"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -133,6 +136,7 @@ function renderScrapedTags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedTagsRow(
|
function renderScrapedTagsRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newTags: GQL.ScrapedSceneTag[],
|
newTags: GQL.ScrapedSceneTag[],
|
||||||
@@ -140,7 +144,7 @@ function renderScrapedTagsRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Tags"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedTags(result)}
|
renderOriginalField={() => renderScrapedTags(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -169,6 +173,7 @@ interface IHasStoredID {
|
|||||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||||
props: IGalleryScrapeDialogProps
|
props: IGalleryScrapeDialogProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [title, setTitle] = useState<ScrapeResult<string>>(
|
const [title, setTitle] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
|
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
|
||||||
);
|
);
|
||||||
@@ -288,7 +293,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
Toast.success({
|
Toast.success({
|
||||||
content: (
|
content: (
|
||||||
<span>
|
<span>
|
||||||
Created studio: <b>{toCreate.name}</b>
|
<FormattedMessage
|
||||||
|
id="actions.created_entity"
|
||||||
|
values={{
|
||||||
|
entity_type: intl.formatMessage({ id: "studio" }),
|
||||||
|
entity_name: <b>{toCreate.name}</b>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -323,7 +334,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
Toast.success({
|
Toast.success({
|
||||||
content: (
|
content: (
|
||||||
<span>
|
<span>
|
||||||
Created performer: <b>{toCreate.name}</b>
|
<FormattedMessage
|
||||||
|
id="actions.created_entity"
|
||||||
|
values={{
|
||||||
|
entity_type: intl.formatMessage({ id: "performer" }),
|
||||||
|
entity_name: <b>{toCreate.name}</b>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -359,7 +376,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
Toast.success({
|
Toast.success({
|
||||||
content: (
|
content: (
|
||||||
<span>
|
<span>
|
||||||
Created tag: <b>{toCreate.name}</b>
|
<FormattedMessage
|
||||||
|
id="actions.created_entity"
|
||||||
|
values={{
|
||||||
|
entity_type: intl.formatMessage({ id: "tag" }),
|
||||||
|
entity_name: <b>{toCreate.name}</b>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -401,41 +424,44 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Title"
|
title={intl.formatMessage({ id: "title" })}
|
||||||
result={title}
|
result={title}
|
||||||
onChange={(value) => setTitle(value)}
|
onChange={(value) => setTitle(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="URL"
|
title={intl.formatMessage({ id: "url" })}
|
||||||
result={url}
|
result={url}
|
||||||
onChange={(value) => setURL(value)}
|
onChange={(value) => setURL(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Date"
|
title={intl.formatMessage({ id: "date" })}
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
result={date}
|
result={date}
|
||||||
onChange={(value) => setDate(value)}
|
onChange={(value) => setDate(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedStudioRow(
|
{renderScrapedStudioRow(
|
||||||
|
intl.formatMessage({ id: "studios" }),
|
||||||
studio,
|
studio,
|
||||||
(value) => setStudio(value),
|
(value) => setStudio(value),
|
||||||
newStudio,
|
newStudio,
|
||||||
createNewStudio
|
createNewStudio
|
||||||
)}
|
)}
|
||||||
{renderScrapedPerformersRow(
|
{renderScrapedPerformersRow(
|
||||||
|
intl.formatMessage({ id: "performers" }),
|
||||||
performers,
|
performers,
|
||||||
(value) => setPerformers(value),
|
(value) => setPerformers(value),
|
||||||
newPerformers,
|
newPerformers,
|
||||||
createNewPerformer
|
createNewPerformer
|
||||||
)}
|
)}
|
||||||
{renderScrapedTagsRow(
|
{renderScrapedTagsRow(
|
||||||
|
intl.formatMessage({ id: "tags" }),
|
||||||
tags,
|
tags,
|
||||||
(value) => setTags(value),
|
(value) => setTags(value),
|
||||||
newTags,
|
newTags,
|
||||||
createNewTag
|
createNewTag
|
||||||
)}
|
)}
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Details"
|
title={intl.formatMessage({ id: "details" })}
|
||||||
result={details}
|
result={details}
|
||||||
onChange={(value) => setDetails(value)}
|
onChange={(value) => setDetails(value)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { Table } from "react-bootstrap";
|
import { Table } from "react-bootstrap";
|
||||||
import { Link, useHistory } from "react-router-dom";
|
import { Link, useHistory } from "react-router-dom";
|
||||||
@@ -28,22 +29,23 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||||||
filterHook,
|
filterHook,
|
||||||
persistState,
|
persistState,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "View Random",
|
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||||
onClick: viewRandom,
|
onClick: viewRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -183,8 +185,10 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
|||||||
<Table className="col col-sm-6 mx-auto">
|
<Table className="col col-sm-6 mx-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Preview</th>
|
<th>{intl.formatMessage({ id: "actions.preview" })}</th>
|
||||||
<th className="d-none d-sm-none">Title</th>
|
<th className="d-none d-sm-none">
|
||||||
|
{intl.formatMessage({ id: "title" })}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IDeleteImageDialogProps {
|
interface IDeleteImageDialogProps {
|
||||||
selected: GQL.SlimImageDataFragment[];
|
selected: GQL.SlimImageDataFragment[];
|
||||||
@@ -14,20 +14,22 @@ interface IDeleteImageDialogProps {
|
|||||||
export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
||||||
props: IDeleteImageDialogProps
|
props: IDeleteImageDialogProps
|
||||||
) => {
|
) => {
|
||||||
const plural = props.selected.length > 1;
|
const intl = useIntl();
|
||||||
|
const singularEntity = intl.formatMessage({ id: "image" });
|
||||||
|
const pluralEntity = intl.formatMessage({ id: "images" });
|
||||||
|
|
||||||
const singleMessageId = "deleteImageText";
|
const header = intl.formatMessage(
|
||||||
const pluralMessageId = "deleteImagesText";
|
{ id: "dialogs.delete_entity_title" },
|
||||||
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const singleMessage =
|
);
|
||||||
"Are you sure you want to delete this image? Unless the file is also deleted, this image will be re-added when scan is performed.";
|
const toastMessage = intl.formatMessage(
|
||||||
const pluralMessage =
|
{ id: "toast.delete_entity" },
|
||||||
"Are you sure you want to delete these images? Unless the files are also deleted, these images will be re-added when scan is performed.";
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
|
);
|
||||||
const header = plural ? "Delete Images" : "Delete Image";
|
const message = intl.formatMessage(
|
||||||
const toastMessage = plural ? "Deleted images" : "Deleted image";
|
{ id: "dialogs.delete_entity_desc" },
|
||||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const message = plural ? pluralMessage : singleMessage;
|
);
|
||||||
|
|
||||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||||
@@ -63,28 +65,32 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
|||||||
show
|
show
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
header={header}
|
header={header}
|
||||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
accept={{
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isDeleting}
|
isRunning={isDeleting}
|
||||||
>
|
>
|
||||||
<p>
|
<p>{message}</p>
|
||||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
|
||||||
</p>
|
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-image"
|
id="delete-image"
|
||||||
checked={deleteFile}
|
checked={deleteFile}
|
||||||
label="Delete file"
|
label={intl.formatMessage({ id: "actions.delete_file" })}
|
||||||
onChange={() => setDeleteFile(!deleteFile)}
|
onChange={() => setDeleteFile(!deleteFile)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-image-generated"
|
id="delete-image-generated"
|
||||||
checked={deleteGenerated}
|
checked={deleteGenerated}
|
||||||
label="Delete generated supporting files"
|
label={intl.formatMessage({
|
||||||
|
id: "actions.delete_generated_supporting_files",
|
||||||
|
})}
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useBulkImageUpdate } from "src/core/StashService";
|
import { useBulkImageUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
|||||||
export const EditImagesDialog: React.FC<IListOperationProps> = (
|
export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
@@ -138,7 +140,12 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
input: getImageInput(),
|
input: getImageInput(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Updated images" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "images" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
props.onClose(true);
|
props.onClose(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -344,11 +351,21 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Edit Images"
|
header={intl.formatMessage(
|
||||||
accept={{ onClick: onSave, text: "Apply" }}
|
{ id: "dialogs.edit_entity_title" },
|
||||||
|
{
|
||||||
|
count: props?.selected?.length ?? 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "image" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "images" }),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
accept={{
|
||||||
|
onClick: onSave,
|
||||||
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isUpdating}
|
isRunning={isUpdating}
|
||||||
@@ -356,7 +373,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -369,7 +386,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -383,19 +400,23 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="performers">
|
<Form.Group controlId="performers">
|
||||||
<Form.Label>Performers</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="performers" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("performers", performerIds)}
|
{renderMultiSelect("performers", performerIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="tags">
|
<Form.Group controlId="tags">
|
||||||
<Form.Label>Tags</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="tags" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="organized">
|
<Form.Group controlId="organized">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
label="Organized"
|
label={intl.formatMessage({ id: "organized" })}
|
||||||
checked={organized}
|
checked={organized}
|
||||||
ref={checkboxRef}
|
ref={checkboxRef}
|
||||||
onChange={() => cycleOrganized()}
|
onChange={() => cycleOrganized()}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useParams, useHistory, Link } from "react-router-dom";
|
import { useParams, useHistory, Link } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
useFindImage,
|
useFindImage,
|
||||||
@@ -28,6 +29,7 @@ export const Image: React.FC = () => {
|
|||||||
const { id = "new" } = useParams<IImageParams>();
|
const { id = "new" } = useParams<IImageParams>();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const { data, error, loading } = useFindImage(id);
|
const { data, error, loading } = useFindImage(id);
|
||||||
const image = data?.findImage;
|
const image = data?.findImage;
|
||||||
@@ -53,7 +55,15 @@ export const Image: React.FC = () => {
|
|||||||
paths: [image.path],
|
paths: [image.path],
|
||||||
});
|
});
|
||||||
|
|
||||||
Toast.success({ content: "Rescanning image" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.rescanning_entity" },
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "image" }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const onOrganizedClick = async () => {
|
const onOrganizedClick = async () => {
|
||||||
@@ -139,14 +149,17 @@ export const Image: React.FC = () => {
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onRescan()}
|
onClick={() => onRescan()}
|
||||||
>
|
>
|
||||||
Rescan
|
<FormattedMessage id="actions.rescan" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="delete-image"
|
key="delete-image"
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Delete Image
|
<FormattedMessage
|
||||||
|
id="actions.delete_entity"
|
||||||
|
values={{ entityType: intl.formatMessage({ id: "image" }) }}
|
||||||
|
/>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -166,13 +179,19 @@ export const Image: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Nav variant="tabs" className="mr-auto">
|
<Nav variant="tabs" className="mr-auto">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="image-details-panel">Details</Nav.Link>
|
<Nav.Link eventKey="image-details-panel">
|
||||||
|
<FormattedMessage id="details" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="image-file-info-panel">File Info</Nav.Link>
|
<Nav.Link eventKey="image-file-info-panel">
|
||||||
|
<FormattedMessage id="file_info" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="image-edit-panel">Edit</Nav.Link>
|
<Nav.Link eventKey="image-edit-panel">
|
||||||
|
<FormattedMessage id="actions.edit" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item className="ml-auto">
|
<Nav.Item className="ml-auto">
|
||||||
<OCounterButton
|
<OCounterButton
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useImageUpdate } from "src/core/StashService";
|
import { useImageUpdate } from "src/core/StashService";
|
||||||
@@ -24,6 +25,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
isVisible,
|
isVisible,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [title, setTitle] = useState<string>(image?.title ?? "");
|
const [title, setTitle] = useState<string>(image?.title ?? "");
|
||||||
const [rating, setRating] = useState<number>(image.rating ?? NaN);
|
const [rating, setRating] = useState<number>(image.rating ?? NaN);
|
||||||
@@ -102,7 +104,12 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.imageUpdate) {
|
if (result.data?.imageUpdate) {
|
||||||
Toast.success({ content: "Updated image" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -117,28 +124,28 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
<div className="form-container row px-3 pt-3">
|
<div className="form-container row px-3 pt-3">
|
||||||
<div className="col edit-buttons mb-3 pl-0">
|
<div className="col edit-buttons mb-3 pl-0">
|
||||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="form-container row px-3">
|
<div className="form-container row px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{FormUtils.renderInputGroup({
|
{FormUtils.renderInputGroup({
|
||||||
title: "Title",
|
title: intl.formatMessage({ id: "title" }),
|
||||||
value: title,
|
value: title,
|
||||||
onChange: setTitle,
|
onChange: setTitle,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
})}
|
})}
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -150,7 +157,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -164,7 +171,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
<Form.Group controlId="performers" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Performers",
|
title: intl.formatMessage({ id: "performers" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -184,7 +191,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
<Form.Group controlId="tags" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Tags",
|
title: intl.formatMessage({ id: "tags" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared";
|
||||||
@@ -14,7 +14,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
|||||||
function renderChecksum() {
|
function renderChecksum() {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Checksum</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.checksum" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.image.checksum} />
|
<TruncatedText className="col-8" text={props.image.checksum} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +28,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
|||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Path</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="path" />
|
||||||
|
</span>
|
||||||
<a href={`file://${path}`} className="col-8">
|
<a href={`file://${path}`} className="col-8">
|
||||||
<TruncatedText text={`file://${props.image.path}`} />
|
<TruncatedText text={`file://${props.image.path}`} />
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
@@ -118,22 +119,23 @@ export const ImageList: React.FC<IImageList> = ({
|
|||||||
persistanceKey,
|
persistanceKey,
|
||||||
extraOperations,
|
extraOperations,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
|
|
||||||
const otherOperations = (extraOperations ?? []).concat([
|
const otherOperations = (extraOperations ?? []).concat([
|
||||||
{
|
{
|
||||||
text: "View Random",
|
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||||
onClick: viewRandom,
|
onClick: viewRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { defineMessages, useIntl } from "react-intl";
|
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
criterionIsHierarchicalLabelValue,
|
criterionIsHierarchicalLabelValue,
|
||||||
CriterionType,
|
CriterionType,
|
||||||
@@ -339,7 +339,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId="filter">
|
<Form.Group controlId="filter">
|
||||||
<Form.Label>Filter</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="search_filter.name" />
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
onChange={onChangedCriteriaType}
|
onChange={onChangedCriteriaType}
|
||||||
@@ -356,12 +358,18 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = !props.editingCriterion ? "Add Filter" : "Update Filter";
|
const title = !props.editingCriterion
|
||||||
|
? intl.formatMessage({ id: "search_filter.add_filter" })
|
||||||
|
: intl.formatMessage({ id: "search_filter.update_filter" });
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="top"
|
placement="top"
|
||||||
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
|
overlay={
|
||||||
|
<Tooltip id="filter-tooltip">
|
||||||
|
<FormattedMessage id="search_filter.name" />
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" onClick={() => onToggle()} active={isOpen}>
|
<Button variant="secondary" onClick={() => onToggle()} active={isOpen}>
|
||||||
<Icon icon="filter" />
|
<Icon icon="filter" />
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
|||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { useFocus } from "src/utils";
|
import { useFocus } from "src/utils";
|
||||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||||
import { useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
Criterion,
|
Criterion,
|
||||||
CriterionValue,
|
CriterionValue,
|
||||||
@@ -280,16 +280,22 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getLabel(option: DisplayMode) {
|
function getLabel(option: DisplayMode) {
|
||||||
|
let displayModeId = "unknown";
|
||||||
switch (option) {
|
switch (option) {
|
||||||
case DisplayMode.Grid:
|
case DisplayMode.Grid:
|
||||||
return "Grid";
|
displayModeId = "grid";
|
||||||
|
break;
|
||||||
case DisplayMode.List:
|
case DisplayMode.List:
|
||||||
return "List";
|
displayModeId = "list";
|
||||||
|
break;
|
||||||
case DisplayMode.Wall:
|
case DisplayMode.Wall:
|
||||||
return "Wall";
|
displayModeId = "wall";
|
||||||
|
break;
|
||||||
case DisplayMode.Tagger:
|
case DisplayMode.Tagger:
|
||||||
return "Tagger";
|
displayModeId = "tagger";
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.filterOptions.displayModeOptions.map((option) => (
|
return props.filterOptions.displayModeOptions.map((option) => (
|
||||||
@@ -361,7 +367,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onSelectAll()}
|
onClick={() => onSelectAll()}
|
||||||
>
|
>
|
||||||
Select All
|
<FormattedMessage id="actions.select_all" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -375,7 +381,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onSelectNone()}
|
onClick={() => onSelectNone()}
|
||||||
>
|
>
|
||||||
Select None
|
<FormattedMessage id="actions.select_none" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -450,7 +456,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
return (
|
return (
|
||||||
<ButtonGroup className="ml-2">
|
<ButtonGroup className="ml-2">
|
||||||
{props.onEdit && (
|
{props.onEdit && (
|
||||||
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="edit">
|
||||||
|
{intl.formatMessage({ id: "actions.edit" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button variant="secondary" onClick={onEdit}>
|
<Button variant="secondary" onClick={onEdit}>
|
||||||
<Icon icon="pencil-alt" />
|
<Icon icon="pencil-alt" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -458,7 +470,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{props.onDelete && (
|
{props.onDelete && (
|
||||||
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="delete">
|
||||||
|
{intl.formatMessage({ id: "actions.delete" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button variant="danger" onClick={onDelete}>
|
<Button variant="danger" onClick={onDelete}>
|
||||||
<Icon icon="trash" />
|
<Icon icon="trash" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -481,7 +499,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
<InputGroup className="mr-2 flex-grow-1">
|
<InputGroup className="mr-2 flex-grow-1">
|
||||||
<FormControl
|
<FormControl
|
||||||
ref={queryRef}
|
ref={queryRef}
|
||||||
placeholder="Search..."
|
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
|
||||||
defaultValue={props.filter.searchTerm}
|
defaultValue={props.filter.searchTerm}
|
||||||
onInput={onChangeQuery}
|
onInput={onChangeQuery}
|
||||||
className="bg-secondary text-white border-secondary w-50"
|
className="bg-secondary text-white border-secondary w-50"
|
||||||
@@ -510,8 +528,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
overlay={
|
overlay={
|
||||||
<Tooltip id="sort-direction-tooltip">
|
<Tooltip id="sort-direction-tooltip">
|
||||||
{props.filter.sortDirection === SortDirectionEnum.Asc
|
{props.filter.sortDirection === SortDirectionEnum.Asc
|
||||||
? "Ascending"
|
? intl.formatMessage({ id: "ascending" })
|
||||||
: "Descending"}
|
: intl.formatMessage({ id: "descending" })}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -528,7 +546,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||||||
{props.filter.sortBy === "random" && (
|
{props.filter.sortBy === "random" && (
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
overlay={
|
overlay={
|
||||||
<Tooltip id="sort-reshuffle-tooltip">Reshuffle</Tooltip>
|
<Tooltip id="sort-reshuffle-tooltip">
|
||||||
|
{intl.formatMessage({ id: "actions.reshuffle" })}
|
||||||
|
</Tooltip>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import { FormattedNumber, useIntl } from "react-intl";
|
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IPaginationProps {
|
interface IPaginationProps {
|
||||||
itemsPerPage: number;
|
itemsPerPage: number;
|
||||||
@@ -75,7 +75,9 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => onChangePage(1)}
|
onClick={() => onChangePage(1)}
|
||||||
>
|
>
|
||||||
<span className="d-none d-sm-inline">First</span>
|
<span className="d-none d-sm-inline">
|
||||||
|
<FormattedMessage id="pagination.first" />
|
||||||
|
</span>
|
||||||
<span className="d-inline d-sm-none">《</span>
|
<span className="d-inline d-sm-none">《</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -84,7 +86,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
onClick={() => onChangePage(currentPage - 1)}
|
onClick={() => onChangePage(currentPage - 1)}
|
||||||
>
|
>
|
||||||
Previous
|
<FormattedMessage id="pagination.previous" />
|
||||||
</Button>
|
</Button>
|
||||||
{pageButtons}
|
{pageButtons}
|
||||||
<Button
|
<Button
|
||||||
@@ -93,14 +95,16 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={() => onChangePage(currentPage + 1)}
|
onClick={() => onChangePage(currentPage + 1)}
|
||||||
>
|
>
|
||||||
Next
|
<FormattedMessage id="pagination.next" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
onClick={() => onChangePage(totalPages)}
|
onClick={() => onChangePage(totalPages)}
|
||||||
>
|
>
|
||||||
<span className="d-none d-sm-inline">Last</span>
|
<span className="d-none d-sm-inline">
|
||||||
|
<FormattedMessage id="pagination.last" />
|
||||||
|
</span>
|
||||||
<span className="d-inline d-sm-none">》</span>
|
<span className="d-inline d-sm-none">》</span>
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -24,6 +25,7 @@ interface IMovieParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Movie: React.FC = () => {
|
export const Movie: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const { id = "new" } = useParams<IMovieParams>();
|
const { id = "new" } = useParams<IMovieParams>();
|
||||||
@@ -141,10 +143,23 @@ export const Movie: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
<p>Are you sure you want to delete {movie?.name ?? "movie"}?</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{
|
||||||
|
entityName:
|
||||||
|
movie?.name ??
|
||||||
|
intl.formatMessage({ id: "movie" }).toLocaleLowerCase(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||||||
if (movie.aliases) {
|
if (movie.aliases) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="alias-head">Also known as </span>
|
<span className="alias-head">
|
||||||
|
{intl.formatMessage({ id: "also_known_as" })}{" "}
|
||||||
|
</span>
|
||||||
<span className="alias">{movie.aliases}</span>
|
<span className="alias">{movie.aliases}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -31,7 +33,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row">
|
<dl className="row">
|
||||||
<dt className="col-3 col-xl-2">Rating</dt>
|
<dt className="col-3 col-xl-2">
|
||||||
|
{intl.formatMessage({ id: "rating" })}
|
||||||
|
</dt>
|
||||||
<dd className="col-9 col-xl-10">
|
<dd className="col-9 col-xl-10">
|
||||||
<RatingStars value={movie.rating} disabled />
|
<RatingStars value={movie.rating} disabled />
|
||||||
</dd>
|
</dd>
|
||||||
@@ -50,31 +54,31 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
name="Duration"
|
id="duration"
|
||||||
value={
|
value={
|
||||||
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
name="Date"
|
id="date"
|
||||||
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
|
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
|
||||||
/>
|
/>
|
||||||
<URLField
|
<URLField
|
||||||
name="Studio"
|
id="studio"
|
||||||
value={movie.studio?.name}
|
value={movie.studio?.name}
|
||||||
url={`/studios/${movie.studio?.id}`}
|
url={`/studios/${movie.studio?.id}`}
|
||||||
/>
|
/>
|
||||||
<TextField name="Director" value={movie.director} />
|
<TextField id="director" value={movie.director} />
|
||||||
|
|
||||||
{renderRatingField()}
|
{renderRatingField()}
|
||||||
|
|
||||||
<URLField
|
<URLField
|
||||||
name="URL"
|
id="url"
|
||||||
value={movie.url}
|
value={movie.url}
|
||||||
url={TextUtils.sanitiseURL(movie.url ?? "")}
|
url={TextUtils.sanitiseURL(movie.url ?? "")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField name="Synopsis" value={movie.synopsis} />
|
<TextField id="synopsis" value={movie.synopsis} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
@@ -49,6 +50,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
setBackImage,
|
setBackImage,
|
||||||
onImageEncoding,
|
onImageEncoding,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
const isNew = movie === undefined;
|
const isNew = movie === undefined;
|
||||||
@@ -332,7 +334,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setIsImageAlertOpen(false)}
|
onClick={() => setIsImageAlertOpen(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="actions.cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -378,22 +380,29 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isNew && <h2>Add Movie</h2>}
|
{isNew && (
|
||||||
|
<h2>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "actions.add_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "movie" }) }
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
<Prompt
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
message="Unsaved changes. Are you sure you want to leave?"
|
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Name
|
{intl.formatMessage({ id: "name" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder="Name"
|
placeholder={intl.formatMessage({ id: "name" })}
|
||||||
{...formik.getFieldProps("name")}
|
{...formik.getFieldProps("name")}
|
||||||
isInvalid={!!formik.errors.name}
|
isInvalid={!!formik.errors.name}
|
||||||
/>
|
/>
|
||||||
@@ -403,11 +412,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("aliases", "Aliases")}
|
{renderTextField("aliases", intl.formatMessage({ id: "aliases" }))}
|
||||||
|
|
||||||
<Form.Group controlId="duration" as={Row}>
|
<Form.Group controlId="duration" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Duration
|
{intl.formatMessage({ id: "duration" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<DurationInput
|
<DurationInput
|
||||||
@@ -419,11 +428,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("date", "Date (YYYY-MM-DD)")}
|
{renderTextField("date", intl.formatMessage({ id: "date" }))}
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Studio
|
{intl.formatMessage({ id: "studio" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -438,11 +447,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("director", "Director")}
|
{renderTextField("director", intl.formatMessage({ id: "director" }))}
|
||||||
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Rating
|
{intl.formatMessage({ id: "rating" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -456,13 +465,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
URL
|
{intl.formatMessage({ id: "url" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder="URL"
|
placeholder={intl.formatMessage({ id: "url" })}
|
||||||
{...formik.getFieldProps("url")}
|
{...formik.getFieldProps("url")}
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>{maybeRenderScrapeButton()}</InputGroup.Append>
|
<InputGroup.Append>{maybeRenderScrapeButton()}</InputGroup.Append>
|
||||||
@@ -472,13 +481,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="synopsis" as={Row}>
|
<Form.Group controlId="synopsis" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Synopsis
|
{intl.formatMessage({ id: "synopsis" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder="Synopsis"
|
placeholder={intl.formatMessage({ id: "synopsis" })}
|
||||||
{...formik.getFieldProps("synopsis")}
|
{...formik.getFieldProps("synopsis")}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
@@ -18,22 +19,23 @@ import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
|||||||
import { MovieCard } from "./MovieCard";
|
import { MovieCard } from "./MovieCard";
|
||||||
|
|
||||||
export const MovieList: React.FC = () => {
|
export const MovieList: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "View Random",
|
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||||
onClick: viewRandom,
|
onClick: viewRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -58,8 +60,8 @@ export const MovieList: React.FC = () => {
|
|||||||
<DeleteEntityDialog
|
<DeleteEntityDialog
|
||||||
selected={selectedMovies}
|
selected={selectedMovies}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
singularEntity="movie"
|
singularEntity={intl.formatMessage({ id: "movie" })}
|
||||||
pluralEntity="movies"
|
pluralEntity={intl.formatMessage({ id: "movies" })}
|
||||||
destroyMutation={useMoviesDestroy}
|
destroyMutation={useMoviesDestroy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
|||||||
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating, setRating] = useState<number>();
|
||||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||||
@@ -92,7 +94,16 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
await updatePerformers();
|
await updatePerformers();
|
||||||
Toast.success({ content: "Updated performers" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl
|
||||||
|
.formatMessage({ id: "performers" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
props.onClose(true);
|
props.onClose(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -232,17 +243,20 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
show
|
show
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Edit Performers"
|
header="Edit Performers"
|
||||||
accept={{ onClick: onSave, text: "Apply" }}
|
accept={{
|
||||||
|
onClick: onSave,
|
||||||
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isUpdating}
|
isRunning={isUpdating}
|
||||||
>
|
>
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -254,7 +268,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="tags">
|
<Form.Group controlId="tags">
|
||||||
<Form.Label>Tags</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="tags" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import {
|
import {
|
||||||
@@ -39,11 +40,22 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
onSelectedChanged,
|
onSelectedChanged,
|
||||||
extraCriteria,
|
extraCriteria,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const age = TextUtils.age(
|
const age = TextUtils.age(
|
||||||
performer.birthdate,
|
performer.birthdate,
|
||||||
ageFromDate ?? performer.death_date
|
ageFromDate ?? performer.death_date
|
||||||
);
|
);
|
||||||
const ageString = `${age} years old${ageFromDate ? " in this scene." : "."}`;
|
const ageL10nId = ageFromDate
|
||||||
|
? "media_info.performer_card.age_context"
|
||||||
|
: "media_info.performer_card.age";
|
||||||
|
const ageL10String = intl.formatMessage({
|
||||||
|
id: "years_old",
|
||||||
|
defaultMessage: "years old",
|
||||||
|
});
|
||||||
|
const ageString = intl.formatMessage(
|
||||||
|
{ id: ageL10nId },
|
||||||
|
{ age, years_old: ageL10String }
|
||||||
|
);
|
||||||
|
|
||||||
function maybeRenderFavoriteIcon() {
|
function maybeRenderFavoriteIcon() {
|
||||||
if (performer.favorite === false) {
|
if (performer.favorite === false) {
|
||||||
@@ -143,7 +155,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||||||
performer.rating ? `rating-${performer.rating}` : ""
|
performer.rating ? `rating-${performer.rating}` : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
RATING: {performer.rating}
|
<FormattedMessage id="rating" />: {performer.rating}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { Button, Tabs, Tab } from "react-bootstrap";
|
import { Button, Tabs, Tab } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
@@ -32,6 +33,7 @@ interface IPerformerParams {
|
|||||||
export const Performer: React.FC = () => {
|
export const Performer: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const intl = useIntl();
|
||||||
const { tab = "details", id = "new" } = useParams<IPerformerParams>();
|
const { tab = "details", id = "new" } = useParams<IPerformerParams>();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
@@ -126,19 +128,19 @@ export const Performer: React.FC = () => {
|
|||||||
id="performer-details"
|
id="performer-details"
|
||||||
unmountOnExit
|
unmountOnExit
|
||||||
>
|
>
|
||||||
<Tab eventKey="details" title="Details">
|
<Tab eventKey="details" title={intl.formatMessage({ id: "details" })}>
|
||||||
<PerformerDetailsPanel performer={performer} />
|
<PerformerDetailsPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||||
<PerformerScenesPanel performer={performer} />
|
<PerformerScenesPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="galleries" title="Galleries">
|
<Tab eventKey="galleries" title={intl.formatMessage({ id: "galleries" })}>
|
||||||
<PerformerGalleriesPanel performer={performer} />
|
<PerformerGalleriesPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="images" title="Images">
|
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||||
<PerformerImagesPanel performer={performer} />
|
<PerformerImagesPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="edit" title="Edit">
|
<Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}>
|
||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible={activeTabKey === "edit"}
|
isVisible={activeTabKey === "edit"}
|
||||||
@@ -148,7 +150,10 @@ export const Performer: React.FC = () => {
|
|||||||
onImageEncoding={onImageEncoding}
|
onImageEncoding={onImageEncoding}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="operations" title="Operations">
|
<Tab
|
||||||
|
eventKey="operations"
|
||||||
|
title={intl.formatMessage({ id: "operations" })}
|
||||||
|
>
|
||||||
<PerformerOperationsPanel performer={performer} />
|
<PerformerOperationsPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -163,7 +168,10 @@ export const Performer: React.FC = () => {
|
|||||||
<span className="age">
|
<span className="age">
|
||||||
{TextUtils.age(performer.birthdate, performer.death_date)}
|
{TextUtils.age(performer.birthdate, performer.death_date)}
|
||||||
</span>
|
</span>
|
||||||
<span className="age-tail"> years old</span>
|
<span className="age-tail">
|
||||||
|
{" "}
|
||||||
|
<FormattedMessage id="years_old" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -173,7 +181,9 @@ export const Performer: React.FC = () => {
|
|||||||
if (performer?.aliases) {
|
if (performer?.aliases) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<span className="alias-head">Also known as </span>
|
<span className="alias-head">
|
||||||
|
<FormattedMessage id="also_known_as" />{" "}
|
||||||
|
</span>
|
||||||
<span className="alias">{performer.aliases}</span>
|
<span className="alias">{performer.aliases}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { TagLink } from "src/components/Shared";
|
import { TagLink } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { genderToString } from "src/core/StashService";
|
import { genderToString } from "src/core/StashService";
|
||||||
@@ -24,7 +24,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row">
|
<dl className="row">
|
||||||
<dt className="col-3 col-xl-2">Tags</dt>
|
<dt className="col-3 col-xl-2">
|
||||||
|
<FormattedMessage id="tags" />
|
||||||
|
</dt>
|
||||||
<dd className="col-9 col-xl-10">
|
<dd className="col-9 col-xl-10">
|
||||||
<ul className="pl-0">
|
<ul className="pl-0">
|
||||||
{(performer.tags ?? []).map((tag) => (
|
{(performer.tags ?? []).map((tag) => (
|
||||||
@@ -43,7 +45,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row mb-0">
|
<dl className="row mb-0">
|
||||||
<dt className="col-3 col-xl-2">Rating:</dt>
|
<dt className="col-3 col-xl-2">
|
||||||
|
<FormattedMessage id="rating" />:
|
||||||
|
</dt>
|
||||||
<dd className="col-9 col-xl-10">
|
<dd className="col-9 col-xl-10">
|
||||||
<RatingStars value={performer.rating} />
|
<RatingStars value={performer.rating} />
|
||||||
</dd>
|
</dd>
|
||||||
@@ -111,36 +115,36 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TextField
|
<TextField
|
||||||
name="Gender"
|
id="gender"
|
||||||
value={genderToString(performer.gender ?? undefined)}
|
value={genderToString(performer.gender ?? undefined)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
name="Birthdate"
|
id="birthdate"
|
||||||
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
name="Death Date"
|
id="death_date"
|
||||||
value={TextUtils.formatDate(intl, performer.death_date ?? undefined)}
|
value={TextUtils.formatDate(intl, performer.death_date ?? undefined)}
|
||||||
/>
|
/>
|
||||||
<TextField name="Ethnicity" value={performer.ethnicity} />
|
<TextField id="ethnicity" value={performer.ethnicity} />
|
||||||
<TextField name="Hair Color" value={performer.hair_color} />
|
<TextField id="hair_color" value={performer.hair_color} />
|
||||||
<TextField name="Eye Color" value={performer.eye_color} />
|
<TextField id="eye_color" value={performer.eye_color} />
|
||||||
<TextField name="Country" value={performer.country} />
|
<TextField id="country" value={performer.country} />
|
||||||
<TextField name="Height" value={formatHeight(performer.height)} />
|
<TextField id="height" value={formatHeight(performer.height)} />
|
||||||
<TextField name="Weight" value={formatWeight(performer.weight)} />
|
<TextField id="weight" value={formatWeight(performer.weight)} />
|
||||||
<TextField name="Measurements" value={performer.measurements} />
|
<TextField id="measurements" value={performer.measurements} />
|
||||||
<TextField name="Fake Tits" value={performer.fake_tits} />
|
<TextField id="fake_tits" value={performer.fake_tits} />
|
||||||
<TextField name="Career Length" value={performer.career_length} />
|
<TextField id="career_length" value={performer.career_length} />
|
||||||
<TextField name="Tattoos" value={performer.tattoos} />
|
<TextField id="tattoos" value={performer.tattoos} />
|
||||||
<TextField name="Piercings" value={performer.piercings} />
|
<TextField id="piercings" value={performer.piercings} />
|
||||||
<TextField name="Details" value={performer.details} />
|
<TextField id="details" value={performer.details} />
|
||||||
<URLField
|
<URLField
|
||||||
name="URL"
|
id="url"
|
||||||
value={performer.url}
|
value={performer.url}
|
||||||
url={TextUtils.sanitiseURL(performer.url ?? "")}
|
url={TextUtils.sanitiseURL(performer.url ?? "")}
|
||||||
/>
|
/>
|
||||||
<URLField
|
<URLField
|
||||||
name="Twitter"
|
id="twitter"
|
||||||
value={performer.twitter}
|
value={performer.twitter}
|
||||||
url={TextUtils.sanitiseURL(
|
url={TextUtils.sanitiseURL(
|
||||||
performer.twitter ?? "",
|
performer.twitter ?? "",
|
||||||
@@ -148,7 +152,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<URLField
|
<URLField
|
||||||
name="Instagram"
|
id="instagram"
|
||||||
value={performer.instagram}
|
value={performer.instagram}
|
||||||
url={TextUtils.sanitiseURL(
|
url={TextUtils.sanitiseURL(
|
||||||
performer.instagram ?? "",
|
performer.instagram ?? "",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Badge,
|
Badge,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -64,6 +65,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
onImageChange,
|
onImageChange,
|
||||||
onImageEncoding,
|
onImageEncoding,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -610,7 +612,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<span className="fa-icon">
|
<span className="fa-icon">
|
||||||
<Icon icon="sync-alt" />
|
<Icon icon="sync-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span>Reload scrapers</span>
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_scrapers" />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -626,7 +630,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
rootClose
|
rootClose
|
||||||
>
|
>
|
||||||
<Button variant="secondary" className="mr-2">
|
<Button variant="secondary" className="mr-2">
|
||||||
Scrape with...
|
<FormattedMessage id="actions.scrape_with" />
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
);
|
);
|
||||||
@@ -694,7 +698,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
disabled={!formik.dirty}
|
disabled={!formik.dirty}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
{!isNew ? (
|
{!isNew ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -702,7 +706,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
@@ -718,7 +722,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => formik.setFieldValue("image", null)}
|
onClick={() => formik.setFieldValue("image", null)}
|
||||||
>
|
>
|
||||||
Clear image
|
<FormattedMessage id="actions.clear_image" />
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@@ -747,10 +751,19 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
<p>Are you sure you want to delete {performer.name}?</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{ entityName: performer.name }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -759,7 +772,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return (
|
return (
|
||||||
<Form.Group controlId="tags" as={Row}>
|
<Form.Group controlId="tags" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Tags
|
<FormattedMessage id="tags" defaultMessage="Tags" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<TagSelect
|
<TagSelect
|
||||||
@@ -838,7 +851,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return (
|
return (
|
||||||
<Form.Group controlId={field} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
{title}
|
<FormattedMessage id={field} defaultMessage={title} />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -866,7 +879,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<Form noValidate onSubmit={formik.handleSubmit} id="performer-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="performer-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
<Form.Group controlId="name" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Name
|
<FormattedMessage id="name" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -883,7 +896,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
<Form.Group controlId="aliases" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Alias
|
<FormattedMessage id="aliases" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -897,7 +910,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
<Form.Group as={Row}>
|
<Form.Group as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Gender
|
<FormattedMessage id="gender" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs="auto">
|
<Col xs="auto">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -927,7 +940,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="tattoos" as={Row}>
|
<Form.Group controlId="tattoos" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Tattoos
|
<FormattedMessage id="tattoos" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -941,7 +954,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="piercings" as={Row}>
|
<Form.Group controlId="piercings" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Piercings
|
<FormattedMessage id="piercings" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -955,9 +968,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
{renderTextField("career_length", "Career Length")}
|
{renderTextField("career_length", "Career Length")}
|
||||||
|
|
||||||
<Form.Group controlId="name" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
URL
|
<FormattedMessage id="url" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
@@ -975,7 +988,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{renderTextField("instagram", "Instagram")}
|
{renderTextField("instagram", "Instagram")}
|
||||||
<Form.Group controlId="details" as={Row}>
|
<Form.Group controlId="details" as={Row}>
|
||||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||||
Details
|
<FormattedMessage id="details" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -990,7 +1003,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Rating
|
<FormattedMessage id="rating" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { mutateMetadataAutoTag } from "src/core/StashService";
|
import { mutateMetadataAutoTag } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
@@ -25,5 +26,9 @@ export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Button onClick={onAutoTag}>Auto Tag</Button>;
|
return (
|
||||||
|
<Button onClick={onAutoTag}>
|
||||||
|
<FormattedMessage id="actions.auto_tag" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
ScrapeDialog,
|
ScrapeDialog,
|
||||||
@@ -49,12 +50,13 @@ function renderScrapedGender(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedGenderRow(
|
function renderScrapedGenderRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
onChange: (value: ScrapeResult<string>) => void
|
onChange: (value: ScrapeResult<string>) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Gender"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedGender(result)}
|
renderOriginalField={() => renderScrapedGender(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -91,6 +93,7 @@ function renderScrapedTags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedTagsRow(
|
function renderScrapedTagsRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newTags: GQL.ScrapedSceneTag[],
|
newTags: GQL.ScrapedSceneTag[],
|
||||||
@@ -98,7 +101,7 @@ function renderScrapedTagsRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Tags"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedTags(result)}
|
renderOriginalField={() => renderScrapedTags(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -123,6 +126,8 @@ interface IPerformerScrapeDialogProps {
|
|||||||
export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||||
props: IPerformerScrapeDialogProps
|
props: IPerformerScrapeDialogProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
function translateScrapedGender(scrapedGender?: string | null) {
|
function translateScrapedGender(scrapedGender?: string | null) {
|
||||||
if (!scrapedGender) {
|
if (!scrapedGender) {
|
||||||
return;
|
return;
|
||||||
@@ -385,109 +390,114 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Name"
|
title={intl.formatMessage({ id: "name" })}
|
||||||
result={name}
|
result={name}
|
||||||
onChange={(value) => setName(value)}
|
onChange={(value) => setName(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Aliases"
|
title={intl.formatMessage({ id: "aliases" })}
|
||||||
result={aliases}
|
result={aliases}
|
||||||
onChange={(value) => setAliases(value)}
|
onChange={(value) => setAliases(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedGenderRow(gender, (value) => setGender(value))}
|
{renderScrapedGenderRow(
|
||||||
|
intl.formatMessage({ id: "gender" }),
|
||||||
|
gender,
|
||||||
|
(value) => setGender(value)
|
||||||
|
)}
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Birthdate"
|
title={intl.formatMessage({ id: "birthdate" })}
|
||||||
result={birthdate}
|
result={birthdate}
|
||||||
onChange={(value) => setBirthdate(value)}
|
onChange={(value) => setBirthdate(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Death Date"
|
title={intl.formatMessage({ id: "death_date" })}
|
||||||
result={deathDate}
|
result={deathDate}
|
||||||
onChange={(value) => setDeathDate(value)}
|
onChange={(value) => setDeathDate(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Ethnicity"
|
title={intl.formatMessage({ id: "ethnicity" })}
|
||||||
result={ethnicity}
|
result={ethnicity}
|
||||||
onChange={(value) => setEthnicity(value)}
|
onChange={(value) => setEthnicity(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Country"
|
title={intl.formatMessage({ id: "country" })}
|
||||||
result={country}
|
result={country}
|
||||||
onChange={(value) => setCountry(value)}
|
onChange={(value) => setCountry(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Hair Color"
|
title={intl.formatMessage({ id: "hair_color" })}
|
||||||
result={hairColor}
|
result={hairColor}
|
||||||
onChange={(value) => setHairColor(value)}
|
onChange={(value) => setHairColor(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Eye Color"
|
title={intl.formatMessage({ id: "eye_color" })}
|
||||||
result={eyeColor}
|
result={eyeColor}
|
||||||
onChange={(value) => setEyeColor(value)}
|
onChange={(value) => setEyeColor(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Weight"
|
title={intl.formatMessage({ id: "weight" })}
|
||||||
result={weight}
|
result={weight}
|
||||||
onChange={(value) => setWeight(value)}
|
onChange={(value) => setWeight(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Height"
|
title={intl.formatMessage({ id: "height" })}
|
||||||
result={height}
|
result={height}
|
||||||
onChange={(value) => setHeight(value)}
|
onChange={(value) => setHeight(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Measurements"
|
title={intl.formatMessage({ id: "measurements" })}
|
||||||
result={measurements}
|
result={measurements}
|
||||||
onChange={(value) => setMeasurements(value)}
|
onChange={(value) => setMeasurements(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Fake Tits"
|
title={intl.formatMessage({ id: "fake_tits" })}
|
||||||
result={fakeTits}
|
result={fakeTits}
|
||||||
onChange={(value) => setFakeTits(value)}
|
onChange={(value) => setFakeTits(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Career Length"
|
title={intl.formatMessage({ id: "career_length" })}
|
||||||
result={careerLength}
|
result={careerLength}
|
||||||
onChange={(value) => setCareerLength(value)}
|
onChange={(value) => setCareerLength(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Tattoos"
|
title={intl.formatMessage({ id: "tattoos" })}
|
||||||
result={tattoos}
|
result={tattoos}
|
||||||
onChange={(value) => setTattoos(value)}
|
onChange={(value) => setTattoos(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Piercings"
|
title={intl.formatMessage({ id: "piercings" })}
|
||||||
result={piercings}
|
result={piercings}
|
||||||
onChange={(value) => setPiercings(value)}
|
onChange={(value) => setPiercings(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="URL"
|
title={intl.formatMessage({ id: "url" })}
|
||||||
result={url}
|
result={url}
|
||||||
onChange={(value) => setURL(value)}
|
onChange={(value) => setURL(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Twitter"
|
title={intl.formatMessage({ id: "twitter" })}
|
||||||
result={twitter}
|
result={twitter}
|
||||||
onChange={(value) => setTwitter(value)}
|
onChange={(value) => setTwitter(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Instagram"
|
title={intl.formatMessage({ id: "instagram" })}
|
||||||
result={instagram}
|
result={instagram}
|
||||||
onChange={(value) => setInstagram(value)}
|
onChange={(value) => setInstagram(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Details"
|
title={intl.formatMessage({ id: "details" })}
|
||||||
result={details}
|
result={details}
|
||||||
onChange={(value) => setDetails(value)}
|
onChange={(value) => setDetails(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedTagsRow(
|
{renderScrapedTagsRow(
|
||||||
|
intl.formatMessage({ id: "tags" }),
|
||||||
tags,
|
tags,
|
||||||
(value) => setTags(value),
|
(value) => setTags(value),
|
||||||
newTags,
|
newTags,
|
||||||
createNewTag
|
createNewTag
|
||||||
)}
|
)}
|
||||||
<ScrapedImageRow
|
<ScrapedImageRow
|
||||||
title="Performer Image"
|
title={intl.formatMessage({ id: "performer_image" })}
|
||||||
className="performer-image"
|
className="performer-image"
|
||||||
result={image}
|
result={image}
|
||||||
onChange={(value) => setImage(value)}
|
onChange={(value) => setImage(value)}
|
||||||
@@ -498,7 +508,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapeDialog
|
<ScrapeDialog
|
||||||
title="Performer Scrape Results"
|
title={intl.formatMessage({ id: "dialogs.scrape_entity_title" })}
|
||||||
renderScrapeRows={renderScrapeRows}
|
renderScrapeRows={renderScrapeRows}
|
||||||
onClose={(apply) => {
|
onClose={(apply) => {
|
||||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal, LoadingIndicator } from "src/components/Shared";
|
import { Modal, LoadingIndicator } from "src/components/Shared";
|
||||||
@@ -24,6 +25,7 @@ const PerformerScrapeModal: React.FC<IProps> = ({
|
|||||||
onHide,
|
onHide,
|
||||||
onSelectPerformer,
|
onSelectPerformer,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [query, setQuery] = useState<string>(name ?? "");
|
const [query, setQuery] = useState<string>(name ?? "");
|
||||||
const { data, loading } = useScrapePerformerList(scraper.id, query);
|
const { data, loading } = useScrapePerformerList(scraper.id, query);
|
||||||
@@ -41,7 +43,11 @@ const PerformerScrapeModal: React.FC<IProps> = ({
|
|||||||
show
|
show
|
||||||
onHide={onHide}
|
onHide={onHide}
|
||||||
header={`Scrape performer from ${scraper.name}`}
|
header={`Scrape performer from ${scraper.name}`}
|
||||||
accept={{ text: "Cancel", onClick: onHide, variant: "secondary" }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
onClick: onHide,
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={CLASSNAME}>
|
<div className={CLASSNAME}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal, LoadingIndicator } from "src/components/Shared";
|
import { Modal, LoadingIndicator } from "src/components/Shared";
|
||||||
@@ -24,6 +25,7 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
|||||||
onHide,
|
onHide,
|
||||||
onSelectPerformer,
|
onSelectPerformer,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [query, setQuery] = useState<string>(name ?? "");
|
const [query, setQuery] = useState<string>(name ?? "");
|
||||||
const { data, loading } = GQL.useQueryStashBoxPerformerQuery({
|
const { data, loading } = GQL.useQueryStashBoxPerformerQuery({
|
||||||
@@ -49,7 +51,11 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
|||||||
show
|
show
|
||||||
onHide={onHide}
|
onHide={onHide}
|
||||||
header={`Scrape performer from ${instance.name ?? "Stash-Box"}`}
|
header={`Scrape performer from ${instance.name ?? "Stash-Box"}`}
|
||||||
accept={{ text: "Cancel", onClick: onHide, variant: "secondary" }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
onClick: onHide,
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={CLASSNAME}>
|
<div className={CLASSNAME}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,7 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
|||||||
persistState,
|
persistState,
|
||||||
extraCriteria,
|
extraCriteria,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
@@ -41,12 +43,12 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
|||||||
onClick: getRandom,
|
onClick: getRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -112,8 +114,8 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
|||||||
<DeleteEntityDialog
|
<DeleteEntityDialog
|
||||||
selected={selectedPerformers}
|
selected={selectedPerformers}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
singularEntity="performer"
|
singularEntity={intl.formatMessage({ id: "performer" })}
|
||||||
pluralEntity="performers"
|
pluralEntity={intl.formatMessage({ id: "performers" })}
|
||||||
destroyMutation={usePerformersDestroy}
|
destroyMutation={usePerformersDestroy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { Link, useHistory } from "react-router-dom";
|
import { Link, useHistory } from "react-router-dom";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
import querystring from "query-string";
|
import querystring from "query-string";
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -32,6 +32,7 @@ import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
|||||||
const CLASSNAME = "duplicate-checker";
|
const CLASSNAME = "duplicate-checker";
|
||||||
|
|
||||||
export const SceneDuplicateChecker: React.FC = () => {
|
export const SceneDuplicateChecker: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { page, size, distance } = querystring.parse(history.location.search);
|
const { page, size, distance } = querystring.parse(history.location.search);
|
||||||
const currentPage = Number.parseInt(
|
const currentPage = Number.parseInt(
|
||||||
@@ -321,10 +322,14 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{maybeRenderEdit()}
|
{maybeRenderEdit()}
|
||||||
<h4>Duplicate Scenes</h4>
|
<h4>
|
||||||
|
<FormattedMessage id="dupe_check.title" />
|
||||||
|
</h4>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Row noGutters>
|
<Row noGutters>
|
||||||
<Form.Label>Search Accuracy</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="dupe_check.search_accuracy_label" />
|
||||||
|
</Form.Label>
|
||||||
<Col xs={2}>
|
<Col xs={2}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
@@ -340,31 +345,53 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
defaultValue={distance ?? 0}
|
defaultValue={distance ?? 0}
|
||||||
className="input-control ml-4"
|
className="input-control ml-4"
|
||||||
>
|
>
|
||||||
<option value={0}>Exact</option>
|
<option value={0}>
|
||||||
<option value={4}>High</option>
|
{intl.formatMessage({ id: "dupe_check.options.exact" })}
|
||||||
<option value={8}>Medium</option>
|
</option>
|
||||||
<option value={10}>Low</option>
|
<option value={4}>
|
||||||
|
{intl.formatMessage({ id: "dupe_check.options.high" })}
|
||||||
|
</option>
|
||||||
|
<option value={8}>
|
||||||
|
{intl.formatMessage({ id: "dupe_check.options.medium" })}
|
||||||
|
</option>
|
||||||
|
<option value={10}>
|
||||||
|
{intl.formatMessage({ id: "dupe_check.options.low" })}
|
||||||
|
</option>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Form.Text>
|
<Form.Text>
|
||||||
Levels below “Exact” can take longer to calculate. False
|
<FormattedMessage id="dupe_check.description" />
|
||||||
positives might also be returned on lower accuracy levels.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
{maybeRenderMissingPhashWarning()}
|
{maybeRenderMissingPhashWarning()}
|
||||||
<div className="d-flex mb-2">
|
<div className="d-flex mb-2">
|
||||||
<h6 className="mr-auto align-self-center">
|
<h6 className="mr-auto align-self-center">
|
||||||
{scenes.length} sets of duplicates found.
|
<FormattedMessage
|
||||||
|
id="dupe_check.found_sets"
|
||||||
|
values={{ setCount: scenes.length }}
|
||||||
|
/>
|
||||||
</h6>
|
</h6>
|
||||||
{checkCount > 0 && (
|
{checkCount > 0 && (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="edit">
|
||||||
|
{intl.formatMessage({ id: "actions.edit" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button variant="secondary" onClick={onEdit}>
|
<Button variant="secondary" onClick={onEdit}>
|
||||||
<Icon icon="pencil-alt" />
|
<Icon icon="pencil-alt" />
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
<OverlayTrigger
|
||||||
|
overlay={
|
||||||
|
<Tooltip id="delete">
|
||||||
|
{intl.formatMessage({ id: "actions.delete" })}
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button variant="danger" onClick={handleDeleteChecked}>
|
<Button variant="danger" onClick={handleDeleteChecked}>
|
||||||
<Icon icon="trash" />
|
<Icon icon="trash" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -416,14 +443,14 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
<th>Details</th>
|
<th>{intl.formatMessage({ id: "details" })}</th>
|
||||||
<th> </th>
|
<th> </th>
|
||||||
<th>Duration</th>
|
<th>{intl.formatMessage({ id: "duration" })}</th>
|
||||||
<th>Filesize</th>
|
<th>{intl.formatMessage({ id: "filesize" })}</th>
|
||||||
<th>Resolution</th>
|
<th>{intl.formatMessage({ id: "resolution" })}</th>
|
||||||
<th>Bitrate</th>
|
<th>{intl.formatMessage({ id: "bitrate" })}</th>
|
||||||
<th>Codec</th>
|
<th>{intl.formatMessage({ id: "media_info.video_codec" })}</th>
|
||||||
<th>Delete</th>
|
<th>{intl.formatMessage({ id: "actions.delete" })}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -494,7 +521,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => handleDeleteScene(scene)}
|
onClick={() => handleDeleteScene(scene)}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { ParserField } from "./ParserField";
|
import { ParserField } from "./ParserField";
|
||||||
import { ShowFields } from "./ShowFields";
|
import { ShowFields } from "./ShowFields";
|
||||||
|
|
||||||
@@ -83,6 +84,7 @@ interface IParserInputProps {
|
|||||||
export const ParserInput: React.FC<IParserInputProps> = (
|
export const ParserInput: React.FC<IParserInputProps> = (
|
||||||
props: IParserInputProps
|
props: IParserInputProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
||||||
const [ignoreWords, setIgnoreWords] = useState<string>(
|
const [ignoreWords, setIgnoreWords] = useState<string>(
|
||||||
props.input.ignoreWords.join(" ")
|
props.input.ignoreWords.join(" ")
|
||||||
@@ -127,7 +129,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Group className="row">
|
<Form.Group className="row">
|
||||||
<Form.Label htmlFor="filename-pattern" className="col-2">
|
<Form.Label htmlFor="filename-pattern" className="col-2">
|
||||||
Filename Pattern
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.filename_pattern",
|
||||||
|
})}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<InputGroup className="col-8">
|
<InputGroup className="col-8">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -139,7 +143,12 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
value={pattern}
|
value={pattern}
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<DropdownButton id="parser-field-select" title="Add Field">
|
<DropdownButton
|
||||||
|
id="parser-field-select"
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.add_field",
|
||||||
|
})}
|
||||||
|
>
|
||||||
{validFields.map((item) => (
|
{validFields.map((item) => (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key={item.field}
|
key={item.field}
|
||||||
@@ -153,12 +162,18 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Form.Text className="text-muted row col-10 offset-2">
|
<Form.Text className="text-muted row col-10 offset-2">
|
||||||
Use '\' to escape literal {} characters
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.escape_chars",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className="row" controlId="ignored-words">
|
<Form.Group className="row" controlId="ignored-words">
|
||||||
<Form.Label className="col-2">Ignored words</Form.Label>
|
<Form.Label className="col-2">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.ignored_words",
|
||||||
|
})}
|
||||||
|
</Form.Label>
|
||||||
<InputGroup className="col-8">
|
<InputGroup className="col-8">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -169,14 +184,18 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Form.Text className="text-muted col-10 offset-2">
|
<Form.Text className="text-muted col-10 offset-2">
|
||||||
Matches with {"{i}"}
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.matches_with",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<h5>Title</h5>
|
<h5>{intl.formatMessage({ id: "title" })}</h5>
|
||||||
<Form.Group className="row">
|
<Form.Group className="row">
|
||||||
<Form.Label htmlFor="whitespace-characters" className="col-2">
|
<Form.Label htmlFor="whitespace-characters" className="col-2">
|
||||||
Whitespace characters:
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.whitespace_chars",
|
||||||
|
})}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<InputGroup className="col-8">
|
<InputGroup className="col-8">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -188,7 +207,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
/>
|
/>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Form.Text className="text-muted col-10 offset-2">
|
<Form.Text className="text-muted col-10 offset-2">
|
||||||
These characters will be replaced with whitespace in the title
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.whitespace_chars_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
@@ -199,7 +220,11 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
checked={capitalizeTitle}
|
checked={capitalizeTitle}
|
||||||
onChange={() => setCapitalizeTitle(!capitalizeTitle)}
|
onChange={() => setCapitalizeTitle(!capitalizeTitle)}
|
||||||
/>
|
/>
|
||||||
<Form.Label htmlFor="capitalize-title">Capitalize title</Form.Label>
|
<Form.Label htmlFor="capitalize-title">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.capitalize_title",
|
||||||
|
})}
|
||||||
|
</Form.Label>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{/* TODO - mapping stuff will go here */}
|
{/* TODO - mapping stuff will go here */}
|
||||||
@@ -208,7 +233,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
<DropdownButton
|
<DropdownButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
id="recipe-select"
|
id="recipe-select"
|
||||||
title="Select Parser Recipe"
|
title={intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.select_parser_recipe",
|
||||||
|
})}
|
||||||
drop="up"
|
drop="up"
|
||||||
>
|
>
|
||||||
{builtInRecipes.map((item) => (
|
{builtInRecipes.map((item) => (
|
||||||
@@ -232,7 +259,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
|||||||
|
|
||||||
<Form.Group className="row">
|
<Form.Group className="row">
|
||||||
<Button variant="secondary" className="ml-3 col-1" onClick={onFind}>
|
<Button variant="secondary" className="ml-3 col-1" onClick={onFind}>
|
||||||
Find
|
{intl.formatMessage({ id: "actions.find" })}
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { Button, Card, Form, Table } from "react-bootstrap";
|
import { Button, Card, Form, Table } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import {
|
import {
|
||||||
queryParseSceneFilenames,
|
queryParseSceneFilenames,
|
||||||
@@ -35,6 +36,7 @@ const initialShowFieldsState = new Map<string, boolean>([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const SceneFilenameParser: React.FC = () => {
|
export const SceneFilenameParser: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
||||||
const [parserInput, setParserInput] = useState<IParserInput>(
|
const [parserInput, setParserInput] = useState<IParserInput>(
|
||||||
@@ -184,7 +186,12 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await updateScenes();
|
await updateScenes();
|
||||||
Toast.success({ content: "Updated scenes" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "scenes" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -333,17 +340,41 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="scene-parser-row">
|
<tr className="scene-parser-row">
|
||||||
<th className="parser-field-filename">Filename</th>
|
<th className="parser-field-filename">
|
||||||
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
|
{intl.formatMessage({
|
||||||
{renderHeader("Date", allDateSet, onSelectAllDateSet)}
|
id: "config.tools.scene_filename_parser.filename",
|
||||||
{renderHeader("Rating", allRatingSet, onSelectAllRatingSet)}
|
})}
|
||||||
|
</th>
|
||||||
{renderHeader(
|
{renderHeader(
|
||||||
"Performers",
|
intl.formatMessage({ id: "title" }),
|
||||||
|
allTitleSet,
|
||||||
|
onSelectAllTitleSet
|
||||||
|
)}
|
||||||
|
{renderHeader(
|
||||||
|
intl.formatMessage({ id: "date" }),
|
||||||
|
allDateSet,
|
||||||
|
onSelectAllDateSet
|
||||||
|
)}
|
||||||
|
{renderHeader(
|
||||||
|
intl.formatMessage({ id: "rating" }),
|
||||||
|
allRatingSet,
|
||||||
|
onSelectAllRatingSet
|
||||||
|
)}
|
||||||
|
{renderHeader(
|
||||||
|
intl.formatMessage({ id: "performers" }),
|
||||||
allPerformerSet,
|
allPerformerSet,
|
||||||
onSelectAllPerformerSet
|
onSelectAllPerformerSet
|
||||||
)}
|
)}
|
||||||
{renderHeader("Tags", allTagSet, onSelectAllTagSet)}
|
{renderHeader(
|
||||||
{renderHeader("Studio", allStudioSet, onSelectAllStudioSet)}
|
intl.formatMessage({ id: "tags" }),
|
||||||
|
allTagSet,
|
||||||
|
onSelectAllTagSet
|
||||||
|
)}
|
||||||
|
{renderHeader(
|
||||||
|
intl.formatMessage({ id: "studio" }),
|
||||||
|
allStudioSet,
|
||||||
|
onSelectAllStudioSet
|
||||||
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -365,7 +396,7 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
onChangePage={(page) => onPageChanged(page)}
|
onChangePage={(page) => onPageChanged(page)}
|
||||||
/>
|
/>
|
||||||
<Button variant="primary" onClick={onApply}>
|
<Button variant="primary" onClick={onApply}>
|
||||||
Apply
|
<FormattedMessage id="actions.apply" />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -373,7 +404,9 @@ export const SceneFilenameParser: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card id="parser-container" className="col col-sm-9 mx-auto">
|
<Card id="parser-container" className="col col-sm-9 mx-auto">
|
||||||
<h4>Scene Filename Parser</h4>
|
<h4>
|
||||||
|
{intl.formatMessage({ id: "config.tools.scene_filename_parser.title" })}
|
||||||
|
</h4>
|
||||||
<ParserInput
|
<ParserInput
|
||||||
input={parserInput}
|
input={parserInput}
|
||||||
onFind={(input) => onFindClicked(input)}
|
onFind={(input) => onFindClicked(input)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Collapse } from "react-bootstrap";
|
import { Button, Collapse } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
|
|
||||||
interface IShowFieldsProps {
|
interface IShowFieldsProps {
|
||||||
@@ -8,6 +9,7 @@ interface IShowFieldsProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShowFields = (props: IShowFieldsProps) => {
|
export const ShowFields = (props: IShowFieldsProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
function handleClick(label: string) {
|
function handleClick(label: string) {
|
||||||
@@ -33,7 +35,11 @@ export const ShowFields = (props: IShowFieldsProps) => {
|
|||||||
<div>
|
<div>
|
||||||
<Button onClick={() => setOpen(!open)} className="minimal">
|
<Button onClick={() => setOpen(!open)} className="minimal">
|
||||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||||
<span>Display fields</span>
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.tools.scene_filename_parser.display_fields",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Collapse in={open}>
|
<Collapse in={open}>
|
||||||
<div>{fieldRows}</div>
|
<div>{fieldRows}</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IDeleteSceneDialogProps {
|
interface IDeleteSceneDialogProps {
|
||||||
selected: GQL.SlimSceneDataFragment[];
|
selected: GQL.SlimSceneDataFragment[];
|
||||||
@@ -14,20 +14,22 @@ interface IDeleteSceneDialogProps {
|
|||||||
export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
||||||
props: IDeleteSceneDialogProps
|
props: IDeleteSceneDialogProps
|
||||||
) => {
|
) => {
|
||||||
const plural = props.selected.length > 1;
|
const intl = useIntl();
|
||||||
|
const singularEntity = intl.formatMessage({ id: "scene" });
|
||||||
|
const pluralEntity = intl.formatMessage({ id: "scenes" });
|
||||||
|
|
||||||
const singleMessageId = "deleteSceneText";
|
const header = intl.formatMessage(
|
||||||
const pluralMessageId = "deleteScenesText";
|
{ id: "dialogs.delete_entity_title" },
|
||||||
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const singleMessage =
|
);
|
||||||
"Are you sure you want to delete this scene? Unless the file is also deleted, this scene will be re-added when scan is performed.";
|
const toastMessage = intl.formatMessage(
|
||||||
const pluralMessage =
|
{ id: "toast.delete_entity" },
|
||||||
"Are you sure you want to delete these scenes? Unless the files are also deleted, these scenes will be re-added when scan is performed.";
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
|
);
|
||||||
const header = plural ? "Delete Scenes" : "Delete Scene";
|
const message = intl.formatMessage(
|
||||||
const toastMessage = plural ? "Deleted scenes" : "Deleted scene";
|
{ id: "dialogs.delete_entity_desc" },
|
||||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||||
const message = plural ? pluralMessage : singleMessage;
|
);
|
||||||
|
|
||||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||||
@@ -63,28 +65,32 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
|||||||
show
|
show
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
header={header}
|
header={header}
|
||||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
accept={{
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isDeleting}
|
isRunning={isDeleting}
|
||||||
>
|
>
|
||||||
<p>
|
<p>{message}</p>
|
||||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
|
||||||
</p>
|
|
||||||
<Form>
|
<Form>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-file"
|
id="delete-file"
|
||||||
checked={deleteFile}
|
checked={deleteFile}
|
||||||
label="Delete file"
|
label={intl.formatMessage({ id: "actions.delete_file" })}
|
||||||
onChange={() => setDeleteFile(!deleteFile)}
|
onChange={() => setDeleteFile(!deleteFile)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="delete-generated"
|
id="delete-generated"
|
||||||
checked={deleteGenerated}
|
checked={deleteGenerated}
|
||||||
label="Delete generated supporting files"
|
label={intl.formatMessage({
|
||||||
|
id: "actions.delete_generated_supporting_files",
|
||||||
|
})}
|
||||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Form, Col, Row } from "react-bootstrap";
|
import { Form, Col, Row } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useBulkSceneUpdate } from "src/core/StashService";
|
import { useBulkSceneUpdate } from "src/core/StashService";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
|||||||
export const EditScenesDialog: React.FC<IListOperationProps> = (
|
export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||||
props: IListOperationProps
|
props: IListOperationProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [rating, setRating] = useState<number>();
|
const [rating, setRating] = useState<number>();
|
||||||
const [studioId, setStudioId] = useState<string>();
|
const [studioId, setStudioId] = useState<string>();
|
||||||
@@ -134,7 +136,12 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
setIsUpdating(true);
|
setIsUpdating(true);
|
||||||
try {
|
try {
|
||||||
await updateScenes();
|
await updateScenes();
|
||||||
Toast.success({ content: "Updated scenes" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "scenes" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
props.onClose(true);
|
props.onClose(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -340,11 +347,21 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Edit Scenes"
|
header={intl.formatMessage(
|
||||||
accept={{ onClick: onSave, text: "Apply" }}
|
{ id: "dialogs.edit_entity_title" },
|
||||||
|
{
|
||||||
|
count: props?.selected?.length ?? 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "scene" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "scenes" }),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
accept={{
|
||||||
|
onClick: onSave,
|
||||||
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(false),
|
onClick: () => props.onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isUpdating}
|
isRunning={isUpdating}
|
||||||
@@ -352,7 +369,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form>
|
<Form>
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -365,7 +382,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studio" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -379,19 +396,23 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="performers">
|
<Form.Group controlId="performers">
|
||||||
<Form.Label>Performers</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="performers" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("performers", performerIds)}
|
{renderMultiSelect("performers", performerIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="tags">
|
<Form.Group controlId="tags">
|
||||||
<Form.Label>Tags</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="performers" />
|
||||||
|
</Form.Label>
|
||||||
{renderMultiSelect("tags", tagIds)}
|
{renderMultiSelect("tags", tagIds)}
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="organized">
|
<Form.Group controlId="organized">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
label="Organized"
|
label={intl.formatMessage({ id: "organized" })}
|
||||||
checked={organized}
|
checked={organized}
|
||||||
ref={checkboxRef}
|
ref={checkboxRef}
|
||||||
onChange={() => cycleOrganized()}
|
onChange={() => cycleOrganized()}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DropdownButton,
|
DropdownButton,
|
||||||
Spinner,
|
Spinner,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { Icon, SweatDrops } from "src/components/Shared";
|
import { Icon, SweatDrops } from "src/components/Shared";
|
||||||
|
|
||||||
export interface IOCounterButtonProps {
|
export interface IOCounterButtonProps {
|
||||||
@@ -21,6 +22,7 @@ export interface IOCounterButtonProps {
|
|||||||
export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||||
props: IOCounterButtonProps
|
props: IOCounterButtonProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
if (props.loading) return <Spinner animation="border" role="status" />;
|
if (props.loading) return <Spinner animation="border" role="status" />;
|
||||||
|
|
||||||
const renderButton = () => (
|
const renderButton = () => (
|
||||||
@@ -28,7 +30,7 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||||||
className="minimal pr-1"
|
className="minimal pr-1"
|
||||||
onClick={props.onIncrement}
|
onClick={props.onIncrement}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
title="O-Counter"
|
title={intl.formatMessage({ id: "o_counter" })}
|
||||||
>
|
>
|
||||||
<SweatDrops />
|
<SweatDrops />
|
||||||
<span className="ml-2">{props.value}</span>
|
<span className="ml-2">{props.value}</span>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Button, Badge, Card } from "react-bootstrap";
|
import { Button, Badge, Card } from "react-bootstrap";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
@@ -46,7 +47,7 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
|||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
onClick={() => onEdit(marker)}
|
onClick={() => onEdit(marker)}
|
||||||
>
|
>
|
||||||
Edit
|
<FormattedMessage id="actions.edit" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
|
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
|
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
@@ -45,6 +46,7 @@ export const Scene: React.FC = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const [updateScene] = useSceneUpdate();
|
const [updateScene] = useSceneUpdate();
|
||||||
const [generateScreenshot] = useSceneGenerateScreenshot();
|
const [generateScreenshot] = useSceneGenerateScreenshot();
|
||||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||||
@@ -197,7 +199,17 @@ export const Scene: React.FC = () => {
|
|||||||
paths: [scene.path],
|
paths: [scene.path],
|
||||||
});
|
});
|
||||||
|
|
||||||
Toast.success({ content: "Rescanning scene" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.rescanning_entity" },
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
singularEntity: intl
|
||||||
|
.formatMessage({ id: "scene" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onGenerateScreenshot(at?: number) {
|
async function onGenerateScreenshot(at?: number) {
|
||||||
@@ -211,7 +223,9 @@ export const Scene: React.FC = () => {
|
|||||||
at,
|
at,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Generating screenshot" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage({ id: "toast.generating_screenshot" }),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onQueueLessScenes() {
|
async function onQueueLessScenes() {
|
||||||
@@ -343,14 +357,14 @@ export const Scene: React.FC = () => {
|
|||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onRescan()}
|
onClick={() => onRescan()}
|
||||||
>
|
>
|
||||||
Rescan
|
<FormattedMessage id="actions.rescan" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="generate"
|
key="generate"
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => setIsGenerateDialogOpen(true)}
|
onClick={() => setIsGenerateDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Generate...
|
<FormattedMessage id="actions.generate" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="generate-screenshot"
|
key="generate-screenshot"
|
||||||
@@ -359,21 +373,24 @@ export const Scene: React.FC = () => {
|
|||||||
onGenerateScreenshot(JWUtils.getPlayer().getPosition())
|
onGenerateScreenshot(JWUtils.getPlayer().getPosition())
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Generate thumbnail from current
|
<FormattedMessage id="actions.generate_thumb_from_current" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="generate-default"
|
key="generate-default"
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => onGenerateScreenshot()}
|
onClick={() => onGenerateScreenshot()}
|
||||||
>
|
>
|
||||||
Generate default thumbnail
|
<FormattedMessage id="actions.generate_thumb_default" />
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
key="delete-scene"
|
key="delete-scene"
|
||||||
className="bg-secondary text-white"
|
className="bg-secondary text-white"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Delete Scene
|
<FormattedMessage
|
||||||
|
id="actions.delete_entity"
|
||||||
|
values={{ entityType: intl.formatMessage({ id: "scene" }) }}
|
||||||
|
/>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@@ -393,43 +410,60 @@ export const Scene: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Nav variant="tabs" className="mr-auto">
|
<Nav variant="tabs" className="mr-auto">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-details-panel">Details</Nav.Link>
|
<Nav.Link eventKey="scene-details-panel">
|
||||||
|
<FormattedMessage id="scenes" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
{(queueScenes ?? []).length > 0 ? (
|
{(queueScenes ?? []).length > 0 ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-queue-panel">Queue</Nav.Link>
|
<Nav.Link eventKey="scene-queue-panel">
|
||||||
|
<FormattedMessage id="queue" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-markers-panel">Markers</Nav.Link>
|
<Nav.Link eventKey="scene-markers-panel">
|
||||||
|
<FormattedMessage id="markers" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
{scene.movies.length > 0 ? (
|
{scene.movies.length > 0 ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-movie-panel">Movies</Nav.Link>
|
<Nav.Link eventKey="scene-movie-panel">
|
||||||
|
<FormattedMessage
|
||||||
|
id="countables.movies"
|
||||||
|
values={{ count: scene.movies.length }}
|
||||||
|
/>
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{scene.galleries.length === 1 ? (
|
{scene.galleries.length >= 1 ? (
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-gallery-panel">Gallery</Nav.Link>
|
<Nav.Link eventKey="scene-galleries-panel">
|
||||||
</Nav.Item>
|
<FormattedMessage
|
||||||
) : undefined}
|
id="countables.gallery"
|
||||||
{scene.galleries.length > 1 ? (
|
values={{ count: scene.galleries.length }}
|
||||||
<Nav.Item>
|
/>
|
||||||
<Nav.Link eventKey="scene-galleries-panel">Galleries</Nav.Link>
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-video-filter-panel">Filters</Nav.Link>
|
<Nav.Link eventKey="scene-video-filter-panel">
|
||||||
|
<FormattedMessage id="effect_filters.name" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-file-info-panel">File Info</Nav.Link>
|
<Nav.Link eventKey="scene-file-info-panel">
|
||||||
|
<FormattedMessage id="file_info" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scene-edit-panel">Edit</Nav.Link>
|
<Nav.Link eventKey="scene-edit-panel">
|
||||||
|
<FormattedMessage id="actions.edit" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<ButtonGroup className="ml-auto">
|
<ButtonGroup className="ml-auto">
|
||||||
<Nav.Item className="ml-auto">
|
<Nav.Item className="ml-auto">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { FormattedDate } from "react-intl";
|
import { FormattedDate, FormattedMessage } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TagLink, TruncatedText } from "src/components/Shared";
|
import { TagLink, TruncatedText } from "src/components/Shared";
|
||||||
@@ -17,7 +17,9 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
if (!props.scene.details || props.scene.details === "") return;
|
if (!props.scene.details || props.scene.details === "") return;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h6>Details</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="details" />
|
||||||
|
</h6>
|
||||||
<p className="pre">{props.scene.details}</p>
|
<p className="pre">{props.scene.details}</p>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -30,7 +32,12 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
));
|
));
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h6>Tags</h6>
|
<h6>
|
||||||
|
<FormattedMessage
|
||||||
|
id="countables.tags"
|
||||||
|
values={{ count: props.scene.tags.length }}
|
||||||
|
/>
|
||||||
|
</h6>
|
||||||
{tags}
|
{tags}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -49,7 +56,12 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h6>Performers</h6>
|
<h6>
|
||||||
|
<FormattedMessage
|
||||||
|
id="countables.performers"
|
||||||
|
values={{ count: props.scene.performers.length }}
|
||||||
|
/>
|
||||||
|
</h6>
|
||||||
<div className="row justify-content-center scene-performers">
|
<div className="row justify-content-center scene-performers">
|
||||||
{cards}
|
{cards}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,14 +97,15 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
|||||||
) : undefined}
|
) : undefined}
|
||||||
{props.scene.rating ? (
|
{props.scene.rating ? (
|
||||||
<h6>
|
<h6>
|
||||||
Rating: <RatingStars value={props.scene.rating} />
|
<FormattedMessage id="rating" />:{" "}
|
||||||
|
<RatingStars value={props.scene.rating} />
|
||||||
</h6>
|
</h6>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)}
|
)}
|
||||||
{props.scene.file.width && props.scene.file.height && (
|
{props.scene.file.width && props.scene.file.height && (
|
||||||
<h6>
|
<h6>
|
||||||
Resolution:{" "}
|
<FormattedMessage id="resolution" />:{" "}
|
||||||
{TextUtils.resolution(
|
{TextUtils.resolution(
|
||||||
props.scene.file.width,
|
props.scene.file.width,
|
||||||
props.scene.file.height
|
props.scene.file.height
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
@@ -49,6 +50,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
isVisible,
|
isVisible,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||||
scene.galleries.map((g) => ({
|
scene.galleries.map((g) => ({
|
||||||
@@ -229,7 +231,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.sceneUpdate) {
|
if (result.data?.sceneUpdate) {
|
||||||
Toast.success({ content: "Updated scene" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
// clear the cover image so that it doesn't appear dirty
|
// clear the cover image so that it doesn't appear dirty
|
||||||
formik.resetForm({ values: formik.values });
|
formik.resetForm({ values: formik.values });
|
||||||
}
|
}
|
||||||
@@ -360,7 +367,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<DropdownButton
|
<DropdownButton
|
||||||
className="d-inline-block"
|
className="d-inline-block"
|
||||||
id="scene-scrape"
|
id="scene-scrape"
|
||||||
title="Scrape with..."
|
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||||
>
|
>
|
||||||
{stashBoxes.map((s, index) => (
|
{stashBoxes.map((s, index) => (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
@@ -379,7 +386,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<span className="fa-icon">
|
<span className="fa-icon">
|
||||||
<Icon icon="sync-alt" />
|
<Icon icon="sync-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span>Reload scrapers</span>
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_scrapers" />
|
||||||
|
</span>
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
);
|
);
|
||||||
@@ -549,7 +558,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<div id="scene-edit-details">
|
<div id="scene-edit-details">
|
||||||
<Prompt
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
message="Unsaved changes. Are you sure you want to leave?"
|
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
@@ -562,14 +571,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
disabled={!formik.dirty}
|
disabled={!formik.dirty}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Col xs={6} className="text-right">
|
<Col xs={6} className="text-right">
|
||||||
@@ -579,10 +588,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-container row px-3">
|
<div className="form-container row px-3">
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{renderTextField("title", "Title")}
|
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
<Col xs={3} className="pr-0 url-label">
|
<Col xs={3} className="pr-0 url-label">
|
||||||
<Form.Label className="col-form-label">URL</Form.Label>
|
<Form.Label className="col-form-label">
|
||||||
|
<FormattedMessage id="url" />
|
||||||
|
</Form.Label>
|
||||||
<div className="float-right scrape-button-container">
|
<div className="float-right scrape-button-container">
|
||||||
{maybeRenderScrapeButton()}
|
{maybeRenderScrapeButton()}
|
||||||
</div>
|
</div>
|
||||||
@@ -590,16 +601,20 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder="URL"
|
placeholder={intl.formatMessage({ id: "url" })}
|
||||||
{...formik.getFieldProps("url")}
|
{...formik.getFieldProps("url")}
|
||||||
isInvalid={!!formik.getFieldMeta("url").error}
|
isInvalid={!!formik.getFieldMeta("url").error}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
{renderTextField("date", "Date", "YYYY-MM-DD")}
|
{renderTextField(
|
||||||
|
"date",
|
||||||
|
intl.formatMessage({ id: "date" }),
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
)}
|
||||||
<Form.Group controlId="rating" as={Row}>
|
<Form.Group controlId="rating" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Rating",
|
title: intl.formatMessage({ id: "rating" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
@@ -612,7 +627,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group controlId="galleries" as={Row}>
|
<Form.Group controlId="galleries" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Galleries",
|
title: intl.formatMessage({ id: "galleries" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<GallerySelect
|
<GallerySelect
|
||||||
@@ -624,7 +639,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Studio",
|
title: intl.formatMessage({ id: "studios" }),
|
||||||
})}
|
})}
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
@@ -641,7 +656,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
<Form.Group controlId="performers" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Performers",
|
title: intl.formatMessage({ id: "performers" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -664,7 +679,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="moviesScenes" as={Row}>
|
<Form.Group controlId="moviesScenes" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Movies/Scenes",
|
title: `${intl.formatMessage({
|
||||||
|
id: "movies",
|
||||||
|
})}/${intl.formatMessage({ id: "scenes" })}`,
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -685,7 +702,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
<Form.Group controlId="tags" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title: "Tags",
|
title: intl.formatMessage({ id: "tags" }),
|
||||||
labelProps: {
|
labelProps: {
|
||||||
column: true,
|
column: true,
|
||||||
sm: 3,
|
sm: 3,
|
||||||
@@ -726,7 +743,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
className="mr-2 py-0"
|
className="mr-2 py-0"
|
||||||
title="Delete StashID"
|
title={intl.formatMessage(
|
||||||
|
{ id: "actions.delete_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||||
|
)}
|
||||||
onClick={() => removeStashID(stashID)}
|
onClick={() => removeStashID(stashID)}
|
||||||
>
|
>
|
||||||
<Icon icon="trash-alt" />
|
<Icon icon="trash-alt" />
|
||||||
@@ -740,7 +760,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
<Form.Group controlId="details">
|
<Form.Group controlId="details">
|
||||||
<Form.Label>Details</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="details" />
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="scene-description text-input"
|
className="scene-description text-input"
|
||||||
@@ -752,14 +774,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
<div>
|
<div>
|
||||||
<Form.Group controlId="cover">
|
<Form.Group controlId="cover">
|
||||||
<Form.Label>Cover Image</Form.Label>
|
<Form.Label>
|
||||||
|
<FormattedMessage id="cover_image" />
|
||||||
|
</Form.Label>
|
||||||
{imageEncoding ? (
|
{imageEncoding ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
className="scene-cover"
|
className="scene-cover"
|
||||||
src={coverImagePreview}
|
src={coverImagePreview}
|
||||||
alt="Scene cover"
|
alt={intl.formatMessage({ id: "cover_image" })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<ImageInput
|
<ImageInput
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared";
|
||||||
@@ -15,7 +15,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
if (props.scene.oshash) {
|
if (props.scene.oshash) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Hash</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.hash" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.scene.oshash} />
|
<TruncatedText className="col-8" text={props.scene.oshash} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -26,7 +28,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
if (props.scene.checksum) {
|
if (props.scene.checksum) {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Checksum</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.checksum" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.scene.checksum} />
|
<TruncatedText className="col-8" text={props.scene.checksum} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -39,7 +43,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Path</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="path" />
|
||||||
|
</span>
|
||||||
<a href={`file://${path}`} className="col-8">
|
<a href={`file://${path}`} className="col-8">
|
||||||
<TruncatedText text={`file://${props.scene.path}`} />
|
<TruncatedText text={`file://${props.scene.path}`} />
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -50,7 +56,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
function renderStream() {
|
function renderStream() {
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Stream</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.stream" />
|
||||||
|
</span>
|
||||||
<a href={props.scene.paths.stream ?? ""} className="col-8">
|
<a href={props.scene.paths.stream ?? ""} className="col-8">
|
||||||
<TruncatedText text={props.scene.paths.stream} />
|
<TruncatedText text={props.scene.paths.stream} />
|
||||||
</a>{" "}
|
</a>{" "}
|
||||||
@@ -69,7 +77,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">File Size</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="filesize" />
|
||||||
|
</span>
|
||||||
<span className="col-8 text-truncate">
|
<span className="col-8 text-truncate">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={size}
|
value={size}
|
||||||
@@ -90,7 +100,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Duration</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="duration" />
|
||||||
|
</span>
|
||||||
<TruncatedText
|
<TruncatedText
|
||||||
className="col-8"
|
className="col-8"
|
||||||
text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
|
text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
|
||||||
@@ -105,7 +117,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Dimensions</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="dimensions" />
|
||||||
|
</span>
|
||||||
<TruncatedText
|
<TruncatedText
|
||||||
className="col-8"
|
className="col-8"
|
||||||
text={`${props.scene.file.width} x ${props.scene.file.height}`}
|
text={`${props.scene.file.width} x ${props.scene.file.height}`}
|
||||||
@@ -120,7 +134,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Frame Rate</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="framerate" />
|
||||||
|
</span>
|
||||||
<span className="col-8 text-truncate">
|
<span className="col-8 text-truncate">
|
||||||
<FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
|
<FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
|
||||||
second
|
second
|
||||||
@@ -136,7 +152,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Bit Rate</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="bitrate" />
|
||||||
|
</span>
|
||||||
<span className="col-8 text-truncate">
|
<span className="col-8 text-truncate">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={(props.scene.file.bitrate ?? 0) / 1000000}
|
value={(props.scene.file.bitrate ?? 0) / 1000000}
|
||||||
@@ -154,7 +172,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Video Codec</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.video_codec" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.scene.file.video_codec} />
|
<TruncatedText className="col-8" text={props.scene.file.video_codec} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -166,7 +186,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Audio Codec</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.audio_codec" />
|
||||||
|
</span>
|
||||||
<TruncatedText className="col-8" text={props.scene.file.audio_codec} />
|
<TruncatedText className="col-8" text={props.scene.file.audio_codec} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -178,7 +200,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Downloaded From</span>
|
<span className="col-4">
|
||||||
|
<FormattedMessage id="media_info.downloaded_from" />
|
||||||
|
</span>
|
||||||
<a href={TextUtils.sanitiseURL(props.scene.url)} className="col-8">
|
<a href={TextUtils.sanitiseURL(props.scene.url)} className="col-8">
|
||||||
<TruncatedText text={props.scene.url} />
|
<TruncatedText text={props.scene.url} />
|
||||||
</a>
|
</a>
|
||||||
@@ -224,7 +248,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<abbr className="col-4" title="Perceptual hash">
|
<abbr className="col-4" title="Perceptual hash">
|
||||||
PHash
|
<FormattedMessage id="media_info.phash" />
|
||||||
</abbr>
|
</abbr>
|
||||||
<TruncatedText className="col-8" text={props.scene.phash} />
|
<TruncatedText className="col-8" text={props.scene.phash} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
|
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
@@ -169,7 +170,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="actions.cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
{editingMarker && (
|
{editingMarker && (
|
||||||
<Button
|
<Button
|
||||||
@@ -177,7 +178,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||||
@@ -57,7 +58,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scene-markers-panel">
|
<div className="scene-markers-panel">
|
||||||
<Button onClick={() => onOpenEditor()}>Create Marker</Button>
|
<Button onClick={() => onOpenEditor()}>
|
||||||
|
<FormattedMessage id="actions.create_marker" />
|
||||||
|
</Button>
|
||||||
<div className="container">
|
<div className="container">
|
||||||
<PrimaryTags
|
<PrimaryTags
|
||||||
sceneMarkers={props.scene.scene_markers ?? []}
|
sceneMarkers={props.scene.scene_markers ?? []}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useAllMoviesForFilter } from "src/core/StashService";
|
import { useAllMoviesForFilter } from "src/core/StashService";
|
||||||
import { Form, Row, Col } from "react-bootstrap";
|
import { Form, Row, Col } from "react-bootstrap";
|
||||||
@@ -13,6 +14,7 @@ export interface IProps {
|
|||||||
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||||
props: IProps
|
props: IProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { data } = useAllMoviesForFilter();
|
const { data } = useAllMoviesForFilter();
|
||||||
|
|
||||||
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
||||||
@@ -72,10 +74,10 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
|||||||
<div className="movie-table">
|
<div className="movie-table">
|
||||||
<Row>
|
<Row>
|
||||||
<Form.Label column xs={9}>
|
<Form.Label column xs={9}>
|
||||||
Movie
|
{intl.formatMessage({ id: "movie" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Label column xs={3}>
|
<Form.Label column xs={3}>
|
||||||
Scene #
|
{intl.formatMessage({ id: "movie_scene_number" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
</Row>
|
</Row>
|
||||||
{renderTableData()}
|
{renderTableData()}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { DurationUtils } from "src/utils";
|
import { DurationUtils } from "src/utils";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
@@ -44,6 +45,7 @@ function renderScrapedStudio(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedStudioRow(
|
function renderScrapedStudioRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
onChange: (value: ScrapeResult<string>) => void,
|
onChange: (value: ScrapeResult<string>) => void,
|
||||||
newStudio?: GQL.ScrapedSceneStudio,
|
newStudio?: GQL.ScrapedSceneStudio,
|
||||||
@@ -51,7 +53,7 @@ function renderScrapedStudioRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Studio"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedStudio(result)}
|
renderOriginalField={() => renderScrapedStudio(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -90,6 +92,7 @@ function renderScrapedPerformers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedPerformersRow(
|
function renderScrapedPerformersRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newPerformers: GQL.ScrapedScenePerformer[],
|
newPerformers: GQL.ScrapedScenePerformer[],
|
||||||
@@ -97,7 +100,7 @@ function renderScrapedPerformersRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Performers"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -136,6 +139,7 @@ function renderScrapedMovies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedMoviesRow(
|
function renderScrapedMoviesRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newMovies: GQL.ScrapedSceneMovie[],
|
newMovies: GQL.ScrapedSceneMovie[],
|
||||||
@@ -143,7 +147,7 @@ function renderScrapedMoviesRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Movies"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedMovies(result)}
|
renderOriginalField={() => renderScrapedMovies(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -182,6 +186,7 @@ function renderScrapedTags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapedTagsRow(
|
function renderScrapedTagsRow(
|
||||||
|
title: string,
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void,
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
newTags: GQL.ScrapedSceneTag[],
|
newTags: GQL.ScrapedSceneTag[],
|
||||||
@@ -189,7 +194,7 @@ function renderScrapedTagsRow(
|
|||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
title="Tags"
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderOriginalField={() => renderScrapedTags(result)}
|
renderOriginalField={() => renderScrapedTags(result)}
|
||||||
renderNewField={() =>
|
renderNewField={() =>
|
||||||
@@ -321,6 +326,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
const [createMovie] = useMovieCreate();
|
const [createMovie] = useMovieCreate();
|
||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
// don't show the dialog if nothing was scraped
|
// don't show the dialog if nothing was scraped
|
||||||
@@ -520,52 +526,56 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Title"
|
title={intl.formatMessage({ id: "title" })}
|
||||||
result={title}
|
result={title}
|
||||||
onChange={(value) => setTitle(value)}
|
onChange={(value) => setTitle(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="URL"
|
title={intl.formatMessage({ id: "url" })}
|
||||||
result={url}
|
result={url}
|
||||||
onChange={(value) => setURL(value)}
|
onChange={(value) => setURL(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedInputGroupRow
|
<ScrapedInputGroupRow
|
||||||
title="Date"
|
title={intl.formatMessage({ id: "date" })}
|
||||||
placeholder="YYYY-MM-DD"
|
placeholder="YYYY-MM-DD"
|
||||||
result={date}
|
result={date}
|
||||||
onChange={(value) => setDate(value)}
|
onChange={(value) => setDate(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedStudioRow(
|
{renderScrapedStudioRow(
|
||||||
|
intl.formatMessage({ id: "studios" }),
|
||||||
studio,
|
studio,
|
||||||
(value) => setStudio(value),
|
(value) => setStudio(value),
|
||||||
newStudio,
|
newStudio,
|
||||||
createNewStudio
|
createNewStudio
|
||||||
)}
|
)}
|
||||||
{renderScrapedPerformersRow(
|
{renderScrapedPerformersRow(
|
||||||
|
intl.formatMessage({ id: "performers" }),
|
||||||
performers,
|
performers,
|
||||||
(value) => setPerformers(value),
|
(value) => setPerformers(value),
|
||||||
newPerformers,
|
newPerformers,
|
||||||
createNewPerformer
|
createNewPerformer
|
||||||
)}
|
)}
|
||||||
{renderScrapedMoviesRow(
|
{renderScrapedMoviesRow(
|
||||||
|
intl.formatMessage({ id: "movies" }),
|
||||||
movies,
|
movies,
|
||||||
(value) => setMovies(value),
|
(value) => setMovies(value),
|
||||||
newMovies,
|
newMovies,
|
||||||
createNewMovie
|
createNewMovie
|
||||||
)}
|
)}
|
||||||
{renderScrapedTagsRow(
|
{renderScrapedTagsRow(
|
||||||
|
intl.formatMessage({ id: "tags" }),
|
||||||
tags,
|
tags,
|
||||||
(value) => setTags(value),
|
(value) => setTags(value),
|
||||||
newTags,
|
newTags,
|
||||||
createNewTag
|
createNewTag
|
||||||
)}
|
)}
|
||||||
<ScrapedTextAreaRow
|
<ScrapedTextAreaRow
|
||||||
title="Details"
|
title={intl.formatMessage({ id: "details" })}
|
||||||
result={details}
|
result={details}
|
||||||
onChange={(value) => setDetails(value)}
|
onChange={(value) => setDetails(value)}
|
||||||
/>
|
/>
|
||||||
<ScrapedImageRow
|
<ScrapedImageRow
|
||||||
title="Cover Image"
|
title={intl.formatMessage({ id: "cover_image" })}
|
||||||
className="scene-cover"
|
className="scene-cover"
|
||||||
result={image}
|
result={image}
|
||||||
onChange={(value) => setImage(value)}
|
onChange={(value) => setImage(value)}
|
||||||
@@ -576,7 +586,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapeDialog
|
<ScrapeDialog
|
||||||
title="Scene Scrape Results"
|
title={intl.formatMessage({ id: "dialogs.scrape_entity_title" })}
|
||||||
renderScrapeRows={renderScrapeRows}
|
renderScrapeRows={renderScrapeRows}
|
||||||
onClose={(apply) => {
|
onClose={(apply) => {
|
||||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { TruncatedText } from "src/components/Shared";
|
import { TruncatedText } from "src/components/Shared";
|
||||||
import { JWUtils } from "src/utils";
|
import { JWUtils } from "src/utils";
|
||||||
@@ -85,6 +86,8 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
divider: 100,
|
divider: 100,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const [contrastValue, setContrastValue] = useState(contrastRange.default);
|
const [contrastValue, setContrastValue] = useState(contrastRange.default);
|
||||||
const [brightnessValue, setBrightnessValue] = useState(
|
const [brightnessValue, setBrightnessValue] = useState(
|
||||||
brightnessRange.default
|
brightnessRange.default
|
||||||
@@ -342,7 +345,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderBlur() {
|
function renderBlur() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Blur",
|
title: intl.formatMessage({ id: "effect_filters.blur" }),
|
||||||
range: blurRange,
|
range: blurRange,
|
||||||
value: blurValue,
|
value: blurValue,
|
||||||
setValue: setBlurValue,
|
setValue: setBlurValue,
|
||||||
@@ -352,7 +355,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderContrast() {
|
function renderContrast() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Contrast",
|
title: intl.formatMessage({ id: "effect_filters.contrast" }),
|
||||||
className: "contrast-slider",
|
className: "contrast-slider",
|
||||||
range: contrastRange,
|
range: contrastRange,
|
||||||
value: contrastValue,
|
value: contrastValue,
|
||||||
@@ -363,7 +366,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderBrightness() {
|
function renderBrightness() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Brightness",
|
title: intl.formatMessage({ id: "effect_filters.brightness" }),
|
||||||
className: "brightness-slider",
|
className: "brightness-slider",
|
||||||
range: brightnessRange,
|
range: brightnessRange,
|
||||||
value: brightnessValue,
|
value: brightnessValue,
|
||||||
@@ -374,7 +377,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderGammaSlider() {
|
function renderGammaSlider() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Gamma",
|
title: intl.formatMessage({ id: "effect_filters.gamma" }),
|
||||||
className: "gamma-slider",
|
className: "gamma-slider",
|
||||||
range: gammaRange,
|
range: gammaRange,
|
||||||
value: gammaValue,
|
value: gammaValue,
|
||||||
@@ -385,7 +388,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderSaturate() {
|
function renderSaturate() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Saturation",
|
title: intl.formatMessage({ id: "effect_filters.saturation" }),
|
||||||
className: "saturation-slider",
|
className: "saturation-slider",
|
||||||
range: saturateRange,
|
range: saturateRange,
|
||||||
value: saturateValue,
|
value: saturateValue,
|
||||||
@@ -396,7 +399,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderHueRotateSlider() {
|
function renderHueRotateSlider() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Hue",
|
title: intl.formatMessage({ id: "effect_filters.hue" }),
|
||||||
className: "hue-rotate-slider",
|
className: "hue-rotate-slider",
|
||||||
range: hueRotateRange,
|
range: hueRotateRange,
|
||||||
value: hueRotateValue,
|
value: hueRotateValue,
|
||||||
@@ -407,7 +410,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderWhiteBalance() {
|
function renderWhiteBalance() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Warmth",
|
title: intl.formatMessage({ id: "effect_filters.warmth" }),
|
||||||
className: "white-balance-slider",
|
className: "white-balance-slider",
|
||||||
range: whiteBalanceRange,
|
range: whiteBalanceRange,
|
||||||
value: whiteBalanceValue,
|
value: whiteBalanceValue,
|
||||||
@@ -421,7 +424,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderRedSlider() {
|
function renderRedSlider() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Red",
|
title: intl.formatMessage({ id: "effect_filters.red" }),
|
||||||
className: "red-slider",
|
className: "red-slider",
|
||||||
range: colourRange,
|
range: colourRange,
|
||||||
value: redValue,
|
value: redValue,
|
||||||
@@ -434,7 +437,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderGreenSlider() {
|
function renderGreenSlider() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Green",
|
title: intl.formatMessage({ id: "effect_filters.green" }),
|
||||||
className: "green-slider",
|
className: "green-slider",
|
||||||
range: colourRange,
|
range: colourRange,
|
||||||
value: greenValue,
|
value: greenValue,
|
||||||
@@ -447,7 +450,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderBlueSlider() {
|
function renderBlueSlider() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Blue",
|
title: intl.formatMessage({ id: "effect_filters.blue" }),
|
||||||
className: "blue-slider",
|
className: "blue-slider",
|
||||||
range: colourRange,
|
range: colourRange,
|
||||||
value: blueValue,
|
value: blueValue,
|
||||||
@@ -460,7 +463,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderRotate() {
|
function renderRotate() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Rotate",
|
title: intl.formatMessage({ id: "effect_filters.rotate" }),
|
||||||
range: rotateRange,
|
range: rotateRange,
|
||||||
value: rotateValue,
|
value: rotateValue,
|
||||||
setValue: setRotateValue,
|
setValue: setRotateValue,
|
||||||
@@ -472,7 +475,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderScale() {
|
function renderScale() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Scale",
|
title: intl.formatMessage({ id: "effect_filters.scale" }),
|
||||||
range: scaleRange,
|
range: scaleRange,
|
||||||
value: scaleValue,
|
value: scaleValue,
|
||||||
setValue: setScaleValue,
|
setValue: setScaleValue,
|
||||||
@@ -482,7 +485,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
|
|
||||||
function renderAspectRatio() {
|
function renderAspectRatio() {
|
||||||
return renderSlider({
|
return renderSlider({
|
||||||
title: "Aspect",
|
title: intl.formatMessage({ id: "effect_filters.aspect" }),
|
||||||
range: aspectRatioRange,
|
range: aspectRatioRange,
|
||||||
value: aspectRatioValue,
|
value: aspectRatioValue,
|
||||||
setValue: setAspectRatioValue,
|
setValue: setAspectRatioValue,
|
||||||
@@ -557,7 +560,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRotateAndScale(0)}
|
onClick={() => onRotateAndScale(0)}
|
||||||
>
|
>
|
||||||
Rotate Left & Scale
|
<FormattedMessage id="effect_filters.rotate_left_and_scale" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
<span className="col-6">
|
<span className="col-6">
|
||||||
@@ -567,7 +570,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRotateAndScale(1)}
|
onClick={() => onRotateAndScale(1)}
|
||||||
>
|
>
|
||||||
Rotate Right & Scale
|
<FormattedMessage id="effect_filters.rotate_right_and_scale" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -603,7 +606,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onResetFilters()}
|
onClick={() => onResetFilters()}
|
||||||
>
|
>
|
||||||
Reset Filters
|
<FormattedMessage id="effect_filters.reset_filters" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
<span className="col-6">
|
<span className="col-6">
|
||||||
@@ -613,7 +616,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => onResetTransforms()}
|
onClick={() => onResetTransforms()}
|
||||||
>
|
>
|
||||||
Reset Transforms
|
<FormattedMessage id="effect_filters.reset_transforms" />
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -632,7 +635,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
<div className="container scene-video-filter">
|
<div className="container scene-video-filter">
|
||||||
<div className="row form-group">
|
<div className="row form-group">
|
||||||
<span className="col-12">
|
<span className="col-12">
|
||||||
<h5>Filters</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="effect_filters.name" />
|
||||||
|
</h5>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{renderBrightness()}
|
{renderBrightness()}
|
||||||
@@ -647,7 +652,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
{renderBlur()}
|
{renderBlur()}
|
||||||
<div className="row form-group">
|
<div className="row form-group">
|
||||||
<span className="col-12">
|
<span className="col-12">
|
||||||
<h5>Transforms</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="effect_filters.name_transforms" />
|
||||||
|
</h5>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{renderRotate()}
|
{renderRotate()}
|
||||||
@@ -655,7 +662,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
|||||||
{renderAspectRatio()}
|
{renderAspectRatio()}
|
||||||
<div className="row form-group">
|
<div className="row form-group">
|
||||||
<span className="col-12">
|
<span className="col-12">
|
||||||
<h5>Actions</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="actions_name" />
|
||||||
|
</h5>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{renderRotateAndScale()}
|
{renderRotateAndScale()}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { Modal, Icon } from "src/components/Shared";
|
import { Modal, Icon } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface ISceneGenerateDialogProps {
|
interface ISceneGenerateDialogProps {
|
||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
@@ -42,6 +43,7 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
|
|
||||||
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,11 +99,14 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="cogs"
|
icon="cogs"
|
||||||
header="Generate"
|
header={intl.formatMessage({ id: "actions.generate" })}
|
||||||
accept={{ onClick: onGenerate, text: "Generate" }}
|
accept={{
|
||||||
|
onClick: onGenerate,
|
||||||
|
text: intl.formatMessage({ id: "actions.generate" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -110,7 +115,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="preview-task"
|
id="preview-task"
|
||||||
checked={previews}
|
checked={previews}
|
||||||
label="Previews (video previews which play when hovering over a scene)"
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.video_previews",
|
||||||
|
})}
|
||||||
onChange={() => setPreviews(!previews)}
|
onChange={() => setPreviews(!previews)}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
@@ -119,7 +126,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
id="image-preview-task"
|
id="image-preview-task"
|
||||||
checked={imagePreviews}
|
checked={imagePreviews}
|
||||||
disabled={!previews}
|
disabled={!previews}
|
||||||
label="Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)"
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.image_previews",
|
||||||
|
})}
|
||||||
onChange={() => setImagePreviews(!imagePreviews)}
|
onChange={() => setImagePreviews(!imagePreviews)}
|
||||||
className="ml-2 flex-grow"
|
className="ml-2 flex-grow"
|
||||||
/>
|
/>
|
||||||
@@ -132,12 +141,20 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
<Icon
|
<Icon
|
||||||
icon={previewOptionsOpen ? "chevron-down" : "chevron-right"}
|
icon={previewOptionsOpen ? "chevron-down" : "chevron-right"}
|
||||||
/>
|
/>
|
||||||
<span>Preview Options</span>
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_options",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Collapse in={previewOptionsOpen}>
|
<Collapse in={previewOptionsOpen}>
|
||||||
<div>
|
<div>
|
||||||
<Form.Group id="transcode-size">
|
<Form.Group id="transcode-size">
|
||||||
<h6>Preview encoding preset</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_preset_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="w-auto input-control"
|
className="w-auto input-control"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -153,14 +170,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
The preset regulates size, quality and encoding time of
|
{intl.formatMessage({
|
||||||
preview generation. Presets beyond “slow” have diminishing
|
id: "dialogs.scene_gen.preview_preset_desc",
|
||||||
returns and are not recommended.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-segments">
|
<Form.Group id="preview-segments">
|
||||||
<h6>Number of segments in preview</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -172,12 +193,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Number of segments in preview files.
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-segment-duration">
|
<Form.Group id="preview-segment-duration">
|
||||||
<h6>Preview segment duration</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -189,12 +216,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Duration of each preview segment, in seconds.
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
<Form.Group id="preview-exclude-start">
|
||||||
<h6>Exclude start time</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_start_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={previewExcludeStart}
|
defaultValue={previewExcludeStart}
|
||||||
@@ -203,14 +236,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Exclude the first x seconds from scene previews. This can be
|
{intl.formatMessage({
|
||||||
a value in seconds, or a percentage (eg 2%) of the total
|
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||||
scene duration.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
<Form.Group id="preview-exclude-start">
|
||||||
<h6>Exclude end time</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_end_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={previewExcludeEnd}
|
defaultValue={previewExcludeEnd}
|
||||||
@@ -219,9 +256,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Exclude the last x seconds from scene previews. This can be
|
{intl.formatMessage({
|
||||||
a value in seconds, or a percentage (eg 2%) of the total
|
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||||
scene duration.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</div>
|
</div>
|
||||||
@@ -230,25 +267,25 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="sprite-task"
|
id="sprite-task"
|
||||||
checked={sprites}
|
checked={sprites}
|
||||||
label="Sprites (for the scene scrubber)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||||
onChange={() => setSprites(!sprites)}
|
onChange={() => setSprites(!sprites)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="marker-task"
|
id="marker-task"
|
||||||
checked={markers}
|
checked={markers}
|
||||||
label="Markers (20 second videos which begin at the given timecode)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||||
onChange={() => setMarkers(!markers)}
|
onChange={() => setMarkers(!markers)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="transcode-task"
|
id="transcode-task"
|
||||||
checked={transcodes}
|
checked={transcodes}
|
||||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||||
onChange={() => setTranscodes(!transcodes)}
|
onChange={() => setTranscodes(!transcodes)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="phash-task"
|
id="phash-task"
|
||||||
checked={phashes}
|
checked={phashes}
|
||||||
label="Perceptual hashes (for deduplication)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||||
onChange={() => setPhashes(!phashes)}
|
onChange={() => setPhashes(!phashes)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -256,7 +293,7 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="overwrite"
|
id="overwrite"
|
||||||
checked={overwrite}
|
checked={overwrite}
|
||||||
label="Overwrite existing generated files"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.overwrite" })}
|
||||||
onChange={() => setOverwrite(!overwrite)}
|
onChange={() => setOverwrite(!overwrite)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +33,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
defaultSort,
|
defaultSort,
|
||||||
persistState,
|
persistState,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
@@ -39,26 +41,26 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "Play selected",
|
text: intl.formatMessage({ id: "actions.play_selected" }),
|
||||||
onClick: playSelected,
|
onClick: playSelected,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Play Random",
|
text: intl.formatMessage({ id: "actions.play_random" }),
|
||||||
onClick: playRandom,
|
onClick: playRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Generate...",
|
text: intl.formatMessage({ id: "actions.generate" }),
|
||||||
onClick: generate,
|
onClick: generate,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
import { Icon, TruncatedText } from "src/components/Shared";
|
import { Icon, TruncatedText } from "src/components/Shared";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
interface ISceneListTableProps {
|
interface ISceneListTableProps {
|
||||||
scenes: GQL.SlimSceneDataFragment[];
|
scenes: GQL.SlimSceneDataFragment[];
|
||||||
@@ -97,14 +98,30 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th />
|
<th />
|
||||||
<th className="text-left">Title</th>
|
<th className="text-left">
|
||||||
<th>Rating</th>
|
<FormattedMessage id="title" />
|
||||||
<th>Duration</th>
|
</th>
|
||||||
<th>Tags</th>
|
<th>
|
||||||
<th>Performers</th>
|
<FormattedMessage id="rating" />
|
||||||
<th>Studio</th>
|
</th>
|
||||||
<th>Movies</th>
|
<th>
|
||||||
<th>Gallery</th>
|
<FormattedMessage id="duration" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="tags" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="performers" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="studio" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="movies" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage id="gallery" />
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{props.scenes.map(renderSceneRow)}</tbody>
|
<tbody>{props.scenes.map(renderSceneRow)}</tbody>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
|
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
|
||||||
import { queryFindSceneMarkers } from "src/core/StashService";
|
import { queryFindSceneMarkers } from "src/core/StashService";
|
||||||
@@ -16,10 +17,11 @@ interface ISceneMarkerList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
|
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "Play Random",
|
text: intl.formatMessage({ id: "actions.play_random" }),
|
||||||
onClick: playRandom,
|
onClick: playRandom,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
|
import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
||||||
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
||||||
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
|
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
|
||||||
@@ -30,31 +31,47 @@ export const Settings: React.FC = () => {
|
|||||||
<Col sm={3} md={2}>
|
<Col sm={3} md={2}>
|
||||||
<Nav variant="pills" className="flex-column">
|
<Nav variant="pills" className="flex-column">
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="configuration">Configuration</Nav.Link>
|
<Nav.Link eventKey="configuration">
|
||||||
|
<FormattedMessage id="configuration" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="interface">Interface</Nav.Link>
|
<Nav.Link eventKey="interface">
|
||||||
|
<FormattedMessage id="config.categories.interface" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="tasks">Tasks</Nav.Link>
|
<Nav.Link eventKey="tasks">
|
||||||
|
<FormattedMessage id="config.categories.tasks" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="dlna">DLNA</Nav.Link>
|
<Nav.Link eventKey="dlna">DLNA</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="tools">Tools</Nav.Link>
|
<Nav.Link eventKey="tools">
|
||||||
|
<FormattedMessage id="config.categories.tools" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="scrapers">Scrapers</Nav.Link>
|
<Nav.Link eventKey="scrapers">
|
||||||
|
<FormattedMessage id="config.categories.scrapers" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="plugins">Plugins</Nav.Link>
|
<Nav.Link eventKey="plugins">
|
||||||
|
<FormattedMessage id="config.categories.plugins" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="logs">Logs</Nav.Link>
|
<Nav.Link eventKey="logs">
|
||||||
|
<FormattedMessage id="config.categories.logs" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<Nav.Item>
|
<Nav.Item>
|
||||||
<Nav.Link eventKey="about">About</Nav.Link>
|
<Nav.Link eventKey="about">
|
||||||
|
<FormattedMessage id="config.categories.about" />
|
||||||
|
</Nav.Link>
|
||||||
</Nav.Item>
|
</Nav.Item>
|
||||||
<hr className="d-sm-none" />
|
<hr className="d-sm-none" />
|
||||||
</Nav>
|
</Nav>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Table } from "react-bootstrap";
|
import { Button, Table } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared";
|
||||||
import { useLatestVersion } from "src/core/StashService";
|
import { useLatestVersion } from "src/core/StashService";
|
||||||
|
|
||||||
@@ -8,6 +9,8 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
const stashVersion = process.env.REACT_APP_STASH_VERSION;
|
const stashVersion = process.env.REACT_APP_STASH_VERSION;
|
||||||
const buildTime = process.env.REACT_APP_DATE;
|
const buildTime = process.env.REACT_APP_DATE;
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: dataLatest,
|
data: dataLatest,
|
||||||
error: errorLatest,
|
error: errorLatest,
|
||||||
@@ -22,7 +25,7 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>Version:</td>
|
<td>{intl.formatMessage({ id: "config.about.version" })}:</td>
|
||||||
<td>{stashVersion}</td>
|
<td>{stashVersion}</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -39,8 +42,13 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
if (gitHash !== dataLatest.latestversion.shorthash) {
|
if (gitHash !== dataLatest.latestversion.shorthash) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<strong>{dataLatest.latestversion.shorthash} [NEW] </strong>
|
<strong>
|
||||||
<a href={dataLatest.latestversion.url}>Download</a>
|
{dataLatest.latestversion.shorthash}{" "}
|
||||||
|
{intl.formatMessage({ id: "config.about.new_version_notice" })}{" "}
|
||||||
|
</strong>
|
||||||
|
<a href={dataLatest.latestversion.url}>
|
||||||
|
{intl.formatMessage({ id: "actions.download" })}
|
||||||
|
</a>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -53,12 +61,20 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Latest Version Build Hash: </td>
|
<td>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.about.latest_version_build_hash",
|
||||||
|
})}{" "}
|
||||||
|
</td>
|
||||||
<td>{maybeRenderLatestVersion()} </td>
|
<td>{maybeRenderLatestVersion()} </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<Button onClick={() => refetch()}>Check for new version</Button>
|
<Button onClick={() => refetch()}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.about.check_for_new_version",
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -73,11 +89,11 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{maybeRenderTag()}
|
{maybeRenderTag()}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Build hash:</td>
|
<td>{intl.formatMessage({ id: "config.about.build_hash" })}</td>
|
||||||
<td>{gitHash}</td>
|
<td>{gitHash}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Build time:</td>
|
<td>{intl.formatMessage({ id: "config.about.build_time" })}</td>
|
||||||
<td>{buildTime}</td>
|
<td>{buildTime}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -87,57 +103,79 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>About</h4>
|
<h4>{intl.formatMessage({ id: "config.categories.about" })}</h4>
|
||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Stash home at{" "}
|
{intl.formatMessage(
|
||||||
<a
|
{ id: "config.about.stash_home" },
|
||||||
href="https://github.com/stashapp/stash"
|
{
|
||||||
rel="noopener noreferrer"
|
url: (
|
||||||
target="_blank"
|
<a
|
||||||
>
|
href="https://github.com/stashapp/stash"
|
||||||
Github
|
rel="noopener noreferrer"
|
||||||
</a>
|
target="_blank"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Stash{" "}
|
{intl.formatMessage(
|
||||||
<a
|
{ id: "config.about.stash_wiki" },
|
||||||
href="https://github.com/stashapp/stash/wiki"
|
{
|
||||||
rel="noopener noreferrer"
|
url: (
|
||||||
target="_blank"
|
<a
|
||||||
>
|
href="https://github.com/stashapp/stash/wiki"
|
||||||
Wiki
|
rel="noopener noreferrer"
|
||||||
</a>{" "}
|
target="_blank"
|
||||||
page
|
>
|
||||||
|
Wiki
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Join our{" "}
|
{intl.formatMessage(
|
||||||
<a
|
{ id: "config.about.stash_discord" },
|
||||||
href="https://discord.gg/2TsNFKt"
|
{
|
||||||
rel="noopener noreferrer"
|
url: (
|
||||||
target="_blank"
|
<a
|
||||||
>
|
href="https://discord.gg/2TsNFKt"
|
||||||
Discord
|
rel="noopener noreferrer"
|
||||||
</a>{" "}
|
target="_blank"
|
||||||
channel
|
>
|
||||||
|
Discord
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
Support us through{" "}
|
{intl.formatMessage(
|
||||||
<a
|
{ id: "config.about.stash_open_collective" },
|
||||||
href="https://opencollective.com/stashapp"
|
{
|
||||||
rel="noopener noreferrer"
|
url: (
|
||||||
target="_blank"
|
<a
|
||||||
>
|
href="https://opencollective.com/stashapp"
|
||||||
Open Collective
|
rel="noopener noreferrer"
|
||||||
</a>
|
target="_blank"
|
||||||
|
>
|
||||||
|
Open Collective
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +70,7 @@ const ExclusionPatterns: React.FC<IExclusionPatternsProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SettingsConfigurationPanel: React.FC = () => {
|
export const SettingsConfigurationPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
// Editing config state
|
// Editing config state
|
||||||
const [stashes, setStashes] = useState<GQL.StashConfig[]>([]);
|
const [stashes, setStashes] = useState<GQL.StashConfig[]>([]);
|
||||||
@@ -278,7 +280,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
const result = await updateGeneralConfig();
|
const result = await updateGeneralConfig();
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(result);
|
console.log(result);
|
||||||
Toast.success({ content: "Updated config" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl
|
||||||
|
.formatMessage({ id: "configuration" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -363,7 +374,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Library</h4>
|
<h4>
|
||||||
|
<FormattedMessage id="library" />
|
||||||
|
</h4>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Group id="stashes">
|
<Form.Group id="stashes">
|
||||||
<h6>Stashes</h6>
|
<h6>Stashes</h6>
|
||||||
@@ -372,12 +385,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
setStashes={(s) => setStashes(s)}
|
setStashes={(s) => setStashes(s)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Directory locations to your content
|
{intl.formatMessage({
|
||||||
|
id: "config.general.directory_locations_to_your_content",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="database-path">
|
<Form.Group id="database-path">
|
||||||
<h6>Database Path</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="config.general.db_path_head" />
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={databasePath}
|
defaultValue={databasePath}
|
||||||
@@ -386,12 +403,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
File location for the SQLite database (requires restart)
|
{intl.formatMessage({ id: "config.general.sqlite_location" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="generated-path">
|
<Form.Group id="generated-path">
|
||||||
<h6>Generated Path</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="config.general.generated_path_head" />
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={generatedPath}
|
defaultValue={generatedPath}
|
||||||
@@ -400,13 +419,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Directory location for the generated files (scene markers, scene
|
{intl.formatMessage({
|
||||||
previews, sprites, etc)
|
id: "config.general.generated_files_location",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="cache-path">
|
<Form.Group id="cache-path">
|
||||||
<h6>Cache Path</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="config.general.cache_path_head" />
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={cachePath}
|
defaultValue={cachePath}
|
||||||
@@ -415,12 +437,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Directory location of the cache
|
{intl.formatMessage({ id: "config.general.cache_location" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="video-extensions">
|
<Form.Group id="video-extensions">
|
||||||
<h6>Video Extensions</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="config.general.video_ext_head" />
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={videoExtensions}
|
defaultValue={videoExtensions}
|
||||||
@@ -429,13 +453,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Comma-delimited list of file extensions that will be identified as
|
{intl.formatMessage({ id: "config.general.video_ext_desc" })}
|
||||||
videos.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="image-extensions">
|
<Form.Group id="image-extensions">
|
||||||
<h6>Image Extensions</h6>
|
<h6>
|
||||||
|
<FormattedMessage id="config.general.image_ext_head" />
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={imageExtensions}
|
defaultValue={imageExtensions}
|
||||||
@@ -444,13 +469,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Comma-delimited list of file extensions that will be identified as
|
{intl.formatMessage({ id: "config.general.image_ext_desc" })}
|
||||||
images.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="gallery-extensions">
|
<Form.Group id="gallery-extensions">
|
||||||
<h6>Gallery zip Extensions</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.general.gallery_ext_head" })}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={galleryExtensions}
|
defaultValue={galleryExtensions}
|
||||||
@@ -459,16 +485,21 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Comma-delimited list of file extensions that will be identified as
|
{intl.formatMessage({ id: "config.general.gallery_ext_desc" })}
|
||||||
gallery zip files.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Excluded Video Patterns</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.excluded_video_patterns_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<ExclusionPatterns excludes={excludes} setExcludes={setExcludes} />
|
<ExclusionPatterns excludes={excludes} setExcludes={setExcludes} />
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Regexps of video files/paths to exclude from Scan and add to Clean
|
{intl.formatMessage({
|
||||||
|
id: "config.general.excluded_video_patterns_desc",
|
||||||
|
})}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -480,14 +511,19 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Excluded Image/Gallery Patterns</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.excluded_image_gallery_patterns_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<ExclusionPatterns
|
<ExclusionPatterns
|
||||||
excludes={imageExcludes}
|
excludes={imageExcludes}
|
||||||
setExcludes={setImageExcludes}
|
setExcludes={setImageExcludes}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Regexps of image and gallery files/paths to exclude from Scan and
|
{intl.formatMessage({
|
||||||
add to Clean
|
id: "config.general.excluded_image_gallery_patterns_desc",
|
||||||
|
})}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -502,13 +538,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="log-terminal"
|
id="log-terminal"
|
||||||
checked={createGalleriesFromFolders}
|
checked={createGalleriesFromFolders}
|
||||||
label="Create galleries from folders containing images"
|
label={intl.formatMessage({
|
||||||
|
id: "config.general.create_galleries_from_folders_label",
|
||||||
|
})}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
setCreateGalleriesFromFolders(!createGalleriesFromFolders)
|
setCreateGalleriesFromFolders(!createGalleriesFromFolders)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
If true, creates galleries from folders containing images.
|
{intl.formatMessage({
|
||||||
|
id: "config.general.create_galleries_from_folders_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -516,22 +556,28 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Hashing</h4>
|
<h4>{intl.formatMessage({ id: "config.general.hashing" })}</h4>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={calculateMD5}
|
checked={calculateMD5}
|
||||||
label="Calculate MD5 for videos"
|
label={intl.formatMessage({
|
||||||
|
id: "config.general.calculate_md5_and_ohash_label",
|
||||||
|
})}
|
||||||
onChange={() => setCalculateMD5(!calculateMD5)}
|
onChange={() => setCalculateMD5(!calculateMD5)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Calculate MD5 checksum in addition to oshash. Enabling will cause
|
{intl.formatMessage({
|
||||||
initial scans to be slower. File naming hash must be set to oshash
|
id: "config.general.calculate_md5_and_ohash_desc",
|
||||||
to disable MD5 calculation.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="transcode-size">
|
<Form.Group id="transcode-size">
|
||||||
<h6>Generated file naming hash</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.generated_file_naming_hash_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="w-auto input-control"
|
className="w-auto input-control"
|
||||||
@@ -551,10 +597,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Use MD5 or oshash for generated file naming. Changing this requires
|
{intl.formatMessage({
|
||||||
that all scenes have the applicable MD5/oshash value populated.
|
id: "config.general.generated_file_naming_hash_desc",
|
||||||
After changing this value, existing generated files will need to be
|
})}
|
||||||
migrated or regenerated. See Tasks page for migration.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -562,9 +607,13 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Video</h4>
|
<h4>{intl.formatMessage({ id: "config.general.video_head" })}</h4>
|
||||||
<Form.Group id="transcode-size">
|
<Form.Group id="transcode-size">
|
||||||
<h6>Maximum transcode size</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.maximum_transcode_size_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="w-auto input-control"
|
className="w-auto input-control"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -580,11 +629,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Maximum size for generated transcodes
|
{intl.formatMessage({
|
||||||
|
id: "config.general.maximum_transcode_size_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group id="streaming-transcode-size">
|
<Form.Group id="streaming-transcode-size">
|
||||||
<h6>Maximum streaming transcode size</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.maximum_streaming_transcode_size_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="w-auto input-control"
|
className="w-auto input-control"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -602,7 +657,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Maximum size for transcoded streams
|
{intl.formatMessage({
|
||||||
|
id: "config.general.maximum_streaming_transcode_size_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -610,10 +667,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Parallel Scan/Generation</h4>
|
<h4>
|
||||||
|
{intl.formatMessage({ id: "config.general.parallel_scan_head" })}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<Form.Group id="parallel-tasks">
|
<Form.Group id="parallel-tasks">
|
||||||
<h6>Number of parallel task for scan/generation</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id:
|
||||||
|
"config.general.number_of_parallel_task_for_scan_generation_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -625,9 +689,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Set to 0 for auto-detection. Warning running more tasks than is
|
{intl.formatMessage({
|
||||||
required to achieve 100% cpu utilisation will decrease performance
|
id:
|
||||||
and potentially cause other issues.
|
"config.general.number_of_parallel_task_for_scan_generation_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -635,10 +700,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Preview Generation</h4>
|
<h4>
|
||||||
|
{intl.formatMessage({ id: "config.general.preview_generation" })}
|
||||||
|
</h4>
|
||||||
|
|
||||||
<Form.Group id="transcode-size">
|
<Form.Group id="transcode-size">
|
||||||
<h6>Preview encoding preset</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_preset_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="w-auto input-control"
|
className="w-auto input-control"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -654,9 +725,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
The preset regulates size, quality and encoding time of preview
|
{intl.formatMessage({
|
||||||
generation. Presets beyond “slow” have diminishing returns and are
|
id: "dialogs.scene_gen.preview_preset_desc",
|
||||||
not recommended.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -673,7 +744,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-segments">
|
<Form.Group id="preview-segments">
|
||||||
<h6>Number of segments in preview</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -685,12 +760,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Number of segments in preview files.
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-segment-duration">
|
<Form.Group id="preview-segment-duration">
|
||||||
<h6>Preview segment duration</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -702,12 +783,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Duration of each preview segment, in seconds.
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
<Form.Group id="preview-exclude-start">
|
||||||
<h6>Exclude start time</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_start_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={previewExcludeStart}
|
defaultValue={previewExcludeStart}
|
||||||
@@ -716,13 +803,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Exclude the first x seconds from scene previews. This can be a value
|
{intl.formatMessage({
|
||||||
in seconds, or a percentage (eg 2%) of the total scene duration.
|
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
<Form.Group id="preview-exclude-start">
|
||||||
<h6>Exclude end time</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_end_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={previewExcludeEnd}
|
defaultValue={previewExcludeEnd}
|
||||||
@@ -731,16 +823,19 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Exclude the last x seconds from scene previews. This can be a value
|
{intl.formatMessage({
|
||||||
in seconds, or a percentage (eg 2%) of the total scene duration.
|
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Scraping</h4>
|
<h4>{intl.formatMessage({ id: "config.general.scraping" })}</h4>
|
||||||
<Form.Group id="scraperUserAgent">
|
<Form.Group id="scraperUserAgent">
|
||||||
<h6>Scraper User Agent</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.general.scraper_user_agent" })}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={scraperUserAgent}
|
defaultValue={scraperUserAgent}
|
||||||
@@ -749,12 +844,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
User-Agent string used during scrape http requests
|
{intl.formatMessage({
|
||||||
|
id: "config.general.scraper_user_agent_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="scraperCDPPath">
|
<Form.Group id="scraperCDPPath">
|
||||||
<h6>Chrome CDP path</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.general.chrome_cdp_path" })}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={scraperCDPPath}
|
defaultValue={scraperCDPPath}
|
||||||
@@ -763,9 +862,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
File path to the Chrome executable, or a remote address (starting
|
{intl.formatMessage({ id: "config.general.chrome_cdp_path_desc" })}
|
||||||
with http:// or https://, for example
|
|
||||||
http://localhost:9222/json/version) to a Chrome instance.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -773,13 +870,15 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="scaper-cert-check"
|
id="scaper-cert-check"
|
||||||
checked={scraperCertCheck}
|
checked={scraperCertCheck}
|
||||||
label="Check for insecure certificates"
|
label={intl.formatMessage({
|
||||||
|
id: "config.general.check_for_insecure_certificates",
|
||||||
|
})}
|
||||||
onChange={() => setScraperCertCheck(!scraperCertCheck)}
|
onChange={() => setScraperCertCheck(!scraperCertCheck)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Some sites use insecure ssl certificates. When unticked the scraper
|
{intl.formatMessage({
|
||||||
skips the insecure certificates check and allows scraping of those
|
id: "config.general.check_for_insecure_certificates_desc",
|
||||||
sites. If you get a certificate error when scraping untick this.
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -787,16 +886,22 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group id="stashbox">
|
<Form.Group id="stashbox">
|
||||||
<h4>Stash-box integration</h4>
|
<h4>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.auth.stash-box_integration",
|
||||||
|
})}
|
||||||
|
</h4>
|
||||||
<StashBoxConfiguration boxes={stashBoxes} saveBoxes={setStashBoxes} />
|
<StashBoxConfiguration boxes={stashBoxes} saveBoxes={setStashBoxes} />
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h4>Authentication</h4>
|
<h4>
|
||||||
|
{intl.formatMessage({ id: "config.general.auth.authentication" })}
|
||||||
|
</h4>
|
||||||
<Form.Group id="username">
|
<Form.Group id="username">
|
||||||
<h6>Username</h6>
|
<h6>{intl.formatMessage({ id: "config.general.auth.username" })}</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={username}
|
defaultValue={username}
|
||||||
@@ -805,11 +910,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Username to access Stash. Leave blank to disable user authentication
|
{intl.formatMessage({ id: "config.general.auth.username_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group id="password">
|
<Form.Group id="password">
|
||||||
<h6>Password</h6>
|
<h6>{intl.formatMessage({ id: "config.general.auth.password" })}</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="password"
|
type="password"
|
||||||
@@ -819,12 +924,12 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Password to access Stash. Leave blank to disable user authentication
|
{intl.formatMessage({ id: "config.general.auth.password_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="apikey">
|
<Form.Group id="apikey">
|
||||||
<h6>API Key</h6>
|
<h6>{intl.formatMessage({ id: "config.general.auth.api_key" })}</h6>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
@@ -834,7 +939,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button
|
<Button
|
||||||
className=""
|
className=""
|
||||||
title="Generate API key"
|
title={intl.formatMessage({
|
||||||
|
id: "config.general.auth.generate_api_key",
|
||||||
|
})}
|
||||||
onClick={() => onGenerateAPIKey()}
|
onClick={() => onGenerateAPIKey()}
|
||||||
>
|
>
|
||||||
<Icon icon="redo" />
|
<Icon icon="redo" />
|
||||||
@@ -842,7 +949,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
className=""
|
className=""
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title="Clear API key"
|
title={intl.formatMessage({
|
||||||
|
id: "config.general.auth.clear_api_key",
|
||||||
|
})}
|
||||||
onClick={() => onClearAPIKey()}
|
onClick={() => onClearAPIKey()}
|
||||||
>
|
>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
@@ -850,13 +959,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
API key for external systems. Only required when username/password
|
{intl.formatMessage({ id: "config.general.auth.api_key_desc" })}
|
||||||
is configured. Username must be saved before generating API key.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="maxSessionAge">
|
<Form.Group id="maxSessionAge">
|
||||||
<h6>Maximum Session Age</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.general.auth.maximum_session_age",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -868,16 +980,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Maximum idle time before a login session is expired, in seconds.
|
{intl.formatMessage({
|
||||||
|
id: "config.general.auth.maximum_session_age_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h4>Logging</h4>
|
<h4>{intl.formatMessage({ id: "config.general.logging" })}</h4>
|
||||||
<Form.Group id="log-file">
|
<Form.Group id="log-file">
|
||||||
<h6>Log file</h6>
|
<h6>{intl.formatMessage({ id: "config.general.auth.log_file" })}</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
defaultValue={logFile}
|
defaultValue={logFile}
|
||||||
@@ -886,8 +1000,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Path to the file to output logging to. Blank to disable file logging.
|
{intl.formatMessage({ id: "config.general.auth.log_file_desc" })}
|
||||||
Requires restart.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -895,17 +1008,20 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="log-terminal"
|
id="log-terminal"
|
||||||
checked={logOut}
|
checked={logOut}
|
||||||
label="Log to terminal"
|
label={intl.formatMessage({
|
||||||
|
id: "config.general.auth.log_to_terminal",
|
||||||
|
})}
|
||||||
onChange={() => setLogOut(!logOut)}
|
onChange={() => setLogOut(!logOut)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Logs to the terminal in addition to a file. Always true if file
|
{intl.formatMessage({
|
||||||
logging is disabled. Requires restart.
|
id: "config.general.auth.log_to_terminal_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="log-level">
|
<Form.Group id="log-level">
|
||||||
<h6>Log Level</h6>
|
<h6>{intl.formatMessage({ id: "config.logs.log_level" })}</h6>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 input-control"
|
className="col col-sm-6 input-control"
|
||||||
as="select"
|
as="select"
|
||||||
@@ -926,18 +1042,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="log-http"
|
id="log-http"
|
||||||
checked={logAccess}
|
checked={logAccess}
|
||||||
label="Log http access"
|
label={intl.formatMessage({ id: "config.general.auth.log_http" })}
|
||||||
onChange={() => setLogAccess(!logAccess)}
|
onChange={() => setLogAccess(!logAccess)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Logs http access to the terminal. Requires restart.
|
{intl.formatMessage({ id: "config.general.auth.log_http_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Button variant="primary" onClick={() => onSave()}>
|
<Button variant="primary" onClick={() => onSave()}>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
|||||||
import { Formik, useFormikContext } from "formik";
|
import { Formik, useFormikContext } from "formik";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { Prompt } from "react-router";
|
import { Prompt } from "react-router";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import {
|
import {
|
||||||
useConfiguration,
|
useConfiguration,
|
||||||
@@ -17,6 +18,7 @@ import { DurationInput, Icon, LoadingIndicator, Modal } from "../Shared";
|
|||||||
import { StringListInput } from "../Shared/StringListInput";
|
import { StringListInput } from "../Shared/StringListInput";
|
||||||
|
|
||||||
export const SettingsDLNAPanel: React.FC = () => {
|
export const SettingsDLNAPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
// undefined to hide dialog, true for enable, false for disable
|
// undefined to hide dialog, true for enable, false for disable
|
||||||
@@ -74,7 +76,16 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
configRefetch();
|
configRefetch();
|
||||||
Toast.success({ content: "Updated config" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl
|
||||||
|
.formatMessage({ id: "configuration" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -166,7 +177,9 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { dlnaStatus } = statusData;
|
const { dlnaStatus } = statusData;
|
||||||
const runningText = dlnaStatus.running ? "running" : "not running";
|
const runningText = intl.formatMessage({
|
||||||
|
id: dlnaStatus.running ? "actions.running" : "actions.not_running",
|
||||||
|
});
|
||||||
|
|
||||||
return `${runningText} ${renderDeadline(dlnaStatus.until)}`;
|
return `${runningText} ${renderDeadline(dlnaStatus.until)}`;
|
||||||
}
|
}
|
||||||
@@ -181,14 +194,14 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
if (data?.configuration.dlna.enabled) {
|
if (data?.configuration.dlna.enabled) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => setEnableDisable(false)} className="mr-1">
|
<Button onClick={() => setEnableDisable(false)} className="mr-1">
|
||||||
Disable temporarily...
|
<FormattedMessage id="actions.temp_disable" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={() => setEnableDisable(true)} className="mr-1">
|
<Button onClick={() => setEnableDisable(true)} className="mr-1">
|
||||||
Enable temporarily...
|
<FormattedMessage id="actions.temp_enable" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -267,7 +280,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={enableUntilRestart}
|
checked={enableUntilRestart}
|
||||||
label="until restart"
|
label={intl.formatMessage({ id: "config.dlna.until_restart" })}
|
||||||
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -290,10 +303,13 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show={tempIP !== undefined}
|
show={tempIP !== undefined}
|
||||||
header={`Allow ${tempIP}`}
|
header={intl.formatMessage(
|
||||||
|
{ id: "config.dlna.allow_temp_ip" },
|
||||||
|
{ tempIP }
|
||||||
|
)}
|
||||||
icon="clock"
|
icon="clock"
|
||||||
accept={{
|
accept={{
|
||||||
text: "Allow",
|
text: intl.formatMessage({ id: "actions.allow" }),
|
||||||
variant: "primary",
|
variant: "primary",
|
||||||
onClick: onAllowTempIP,
|
onClick: onAllowTempIP,
|
||||||
}}
|
}}
|
||||||
@@ -306,7 +322,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={enableUntilRestart}
|
checked={enableUntilRestart}
|
||||||
label="until restart"
|
label={intl.formatMessage({ id: "config.dlna.until_restart" })}
|
||||||
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -333,7 +349,9 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
const { allowedIPAddresses } = statusData.dlnaStatus;
|
const { allowedIPAddresses } = statusData.dlnaStatus;
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Allowed IP addresses</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.dlna.allowed_ip_addresses" })}
|
||||||
|
</h6>
|
||||||
|
|
||||||
<ul className="addresses">
|
<ul className="addresses">
|
||||||
{allowedIPAddresses.map((a) => (
|
{allowedIPAddresses.map((a) => (
|
||||||
@@ -347,7 +365,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Disallow"
|
title={intl.formatMessage({ id: "actions.disallow" })}
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onDisallowTempIP(a.ipAddress)}
|
onClick={() => onDisallowTempIP(a.ipAddress)}
|
||||||
>
|
>
|
||||||
@@ -377,7 +395,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Allow temporarily"
|
title={intl.formatMessage({ id: "actions.allow_temporarily" })}
|
||||||
onClick={() => setTempIP(a)}
|
onClick={() => setTempIP(a)}
|
||||||
>
|
>
|
||||||
<Icon icon="user-clock" />
|
<Icon icon="user-clock" />
|
||||||
@@ -398,7 +416,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Allow temporarily"
|
title={intl.formatMessage({ id: "actions.allow_temporarily" })}
|
||||||
onClick={() => setTempIP(ipEntry)}
|
onClick={() => setTempIP(ipEntry)}
|
||||||
disabled={!ipEntry}
|
disabled={!ipEntry}
|
||||||
>
|
>
|
||||||
@@ -422,13 +440,15 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<Form noValidate onSubmit={handleSubmit}>
|
<Form noValidate onSubmit={handleSubmit}>
|
||||||
<Prompt
|
<Prompt
|
||||||
when={dirty}
|
when={dirty}
|
||||||
message="Unsaved changes. Are you sure you want to leave?"
|
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Settings</h5>
|
<h5>{intl.formatMessage({ id: "settings" })}</h5>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Label>Server Display Name</Form.Label>
|
<Form.Label>
|
||||||
|
{intl.formatMessage({ id: "config.dlna.server_display_name" })}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input server-name"
|
className="text-input server-name"
|
||||||
value={values.serverName}
|
value={values.serverName}
|
||||||
@@ -437,20 +457,26 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Display name for the DLNA server. Defaults to <code>stash</code>{" "}
|
{intl.formatMessage(
|
||||||
if empty.
|
{ id: "config.dlna.server_display_name_desc" },
|
||||||
|
{ server_name: <code>stash</code> }
|
||||||
|
)}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={values.enabled}
|
checked={values.enabled}
|
||||||
label="Enabled by default"
|
label={intl.formatMessage({
|
||||||
|
id: "config.dlna.enabled_by_default",
|
||||||
|
})}
|
||||||
onChange={() => setFieldValue("enabled", !values.enabled)}
|
onChange={() => setFieldValue("enabled", !values.enabled)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Interfaces</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.dlna.network_interfaces" })}
|
||||||
|
</h6>
|
||||||
<StringListInput
|
<StringListInput
|
||||||
value={values.interfaces}
|
value={values.interfaces}
|
||||||
setValue={(value) => setFieldValue("interfaces", value)}
|
setValue={(value) => setFieldValue("interfaces", value)}
|
||||||
@@ -458,13 +484,16 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
className="interfaces-input"
|
className="interfaces-input"
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Interfaces to expose DLNA server on. An empty list results in
|
{intl.formatMessage({
|
||||||
running on all interfaces. Requires DLNA restart after changing.
|
id: "config.dlna.network_interfaces_desc",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Default IP Whitelist</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.dlna.default_ip_whitelist" })}
|
||||||
|
</h6>
|
||||||
<StringListInput
|
<StringListInput
|
||||||
value={values.whitelistedIPs}
|
value={values.whitelistedIPs}
|
||||||
setValue={(value) => setFieldValue("whitelistedIPs", value)}
|
setValue={(value) => setFieldValue("whitelistedIPs", value)}
|
||||||
@@ -472,8 +501,10 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
className="ip-whitelist-input"
|
className="ip-whitelist-input"
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Default IP addresses allow to access DLNA. Use <code>*</code> to
|
{intl.formatMessage(
|
||||||
allow all IP addresses.
|
{ id: "config.dlna.default_ip_whitelist_desc" },
|
||||||
|
{ wildcard: <code>*</code> }
|
||||||
|
)}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -481,7 +512,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Button variant="primary" type="submit" disabled={!dirty}>
|
<Button variant="primary" type="submit" disabled={!dirty}>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
@@ -495,11 +526,13 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
<h4>DLNA</h4>
|
<h4>DLNA</h4>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Status: {renderStatus()}</h5>
|
<h5>
|
||||||
|
{intl.formatMessage({ id: "status" }, { statusText: renderStatus() })}
|
||||||
|
</h5>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Actions</h5>
|
<h5>{intl.formatMessage({ id: "actions_name" })}</h5>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
{renderEnableButton()}
|
{renderEnableButton()}
|
||||||
@@ -509,10 +542,14 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
{renderAllowedIPs()}
|
{renderAllowedIPs()}
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Recent IP addresses</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.dlna.recent_ip_addresses" })}
|
||||||
|
</h6>
|
||||||
<Form.Group>{renderRecentIPs()}</Form.Group>
|
<Form.Group>{renderRecentIPs()}</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button onClick={() => statusRefetch()}>Refresh</Button>
|
<Button onClick={() => statusRefetch()}>
|
||||||
|
<FormattedMessage id="actions.refresh" />
|
||||||
|
</Button>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { DurationInput, LoadingIndicator } from "src/components/Shared";
|
import { DurationInput, LoadingIndicator } from "src/components/Shared";
|
||||||
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
|
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
@@ -19,6 +20,7 @@ const allMenuItems = [
|
|||||||
const SECONDS_TO_MS = 1000;
|
const SECONDS_TO_MS = 1000;
|
||||||
|
|
||||||
export const SettingsInterfacePanel: React.FC = () => {
|
export const SettingsInterfacePanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const { data: config, error, loading } = useConfiguration();
|
const { data: config, error, loading } = useConfiguration();
|
||||||
const [menuItemIds, setMenuItemIds] = useState<string[]>(
|
const [menuItemIds, setMenuItemIds] = useState<string[]>(
|
||||||
@@ -84,7 +86,16 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.success({ content: "Updated config" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.updated_entity" },
|
||||||
|
{
|
||||||
|
entity: intl
|
||||||
|
.formatMessage({ id: "configuration" })
|
||||||
|
.toLocaleLowerCase(),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -95,9 +106,9 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>User Interface</h4>
|
<h4>{intl.formatMessage({ id: "config.ui.title" })}</h4>
|
||||||
<Form.Group controlId="language">
|
<Form.Group controlId="language">
|
||||||
<h5>Language</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.language.heading" })}</h5>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
className="col-4 input-control"
|
className="col-4 input-control"
|
||||||
@@ -108,11 +119,11 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<option value="en-US">English (United States)</option>
|
<option value="en-US">English (United States)</option>
|
||||||
<option value="en-GB">English (United Kingdom)</option>
|
<option value="en-GB">English (United Kingdom)</option>
|
||||||
<option value="zh-TW">Chinese (Taiwan)</option>
|
<option value="zh-TW">繁體中文 (台灣)</option>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Menu items</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.menu_items.heading" })}</h5>
|
||||||
<CheckboxGroup
|
<CheckboxGroup
|
||||||
groupId="menu-items"
|
groupId="menu-items"
|
||||||
items={allMenuItems}
|
items={allMenuItems}
|
||||||
@@ -120,25 +131,31 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
onChange={setMenuItemIds}
|
onChange={setMenuItemIds}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Show or hide different types of content on the navigation bar
|
{intl.formatMessage({ id: "config.ui.menu_items.description" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Scene / Marker Wall</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.scene_wall.heading" })}</h5>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="wall-show-title"
|
id="wall-show-title"
|
||||||
checked={wallShowTitle}
|
checked={wallShowTitle}
|
||||||
label="Display title and tags"
|
label={intl.formatMessage({
|
||||||
|
id: "config.ui.scene_wall.options.display_title",
|
||||||
|
})}
|
||||||
onChange={() => setWallShowTitle(!wallShowTitle)}
|
onChange={() => setWallShowTitle(!wallShowTitle)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="wall-sound-enabled"
|
id="wall-sound-enabled"
|
||||||
checked={soundOnPreview}
|
checked={soundOnPreview}
|
||||||
label="Enable sound"
|
label={intl.formatMessage({
|
||||||
|
id: "config.ui.scene_wall.options.toggle_sound",
|
||||||
|
})}
|
||||||
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
||||||
/>
|
/>
|
||||||
<Form.Label htmlFor="wall-preview">
|
<Form.Label htmlFor="wall-preview">
|
||||||
<h6>Preview Type</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.ui.preview_type.heading" })}
|
||||||
|
</h6>
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
@@ -149,21 +166,33 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
setWallPlayback(e.currentTarget.value)
|
setWallPlayback(e.currentTarget.value)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="video">Video</option>
|
<option value="video">
|
||||||
<option value="animation">Animated Image</option>
|
{intl.formatMessage({ id: "config.ui.preview_type.options.video" })}
|
||||||
<option value="image">Static Image</option>
|
</option>
|
||||||
|
<option value="animation">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.ui.preview_type.options.animated",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
|
<option value="image">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "config.ui.preview_type.options.static",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Configuration for wall items
|
{intl.formatMessage({ id: "config.ui.preview_type.description" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Scene List</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.scene_list.heading" })}</h5>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="show-text-studios"
|
id="show-text-studios"
|
||||||
checked={showStudioAsText}
|
checked={showStudioAsText}
|
||||||
label="Show Studios as text"
|
label={intl.formatMessage({
|
||||||
|
id: "config.ui.scene_list.options.show_studio_as_text",
|
||||||
|
})}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setShowStudioAsText(!showStudioAsText);
|
setShowStudioAsText(!showStudioAsText);
|
||||||
}}
|
}}
|
||||||
@@ -171,11 +200,13 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Scene Player</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.scene_player.heading" })}</h5>
|
||||||
<Form.Group id="auto-start-video">
|
<Form.Group id="auto-start-video">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
checked={autostartVideo}
|
checked={autostartVideo}
|
||||||
label="Auto-start video"
|
label={intl.formatMessage({
|
||||||
|
id: "config.ui.scene_player.options.auto_start_video",
|
||||||
|
})}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setAutostartVideo(!autostartVideo);
|
setAutostartVideo(!autostartVideo);
|
||||||
}}
|
}}
|
||||||
@@ -183,21 +214,26 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="max-loop-duration">
|
<Form.Group id="max-loop-duration">
|
||||||
<h6>Maximum loop duration</h6>
|
<h6>
|
||||||
|
{intl.formatMessage({ id: "config.ui.max_loop_duration.heading" })}
|
||||||
|
</h6>
|
||||||
<DurationInput
|
<DurationInput
|
||||||
className="row col col-4"
|
className="row col col-4"
|
||||||
numericValue={maximumLoopDuration}
|
numericValue={maximumLoopDuration}
|
||||||
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Maximum scene duration where scene player will loop the video - 0 to
|
{intl.formatMessage({
|
||||||
disable
|
id: "config.ui.max_loop_duration.description",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group id="slideshow-delay">
|
<Form.Group id="slideshow-delay">
|
||||||
<h5>Slideshow Delay</h5>
|
<h5>
|
||||||
|
{intl.formatMessage({ id: "config.ui.slideshow_delay.heading" })}
|
||||||
|
</h5>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
type="number"
|
type="number"
|
||||||
@@ -209,16 +245,18 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Slideshow is available in galleries when in wall view mode
|
{intl.formatMessage({ id: "config.ui.slideshow_delay.description" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Custom CSS</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="custom-css"
|
id="custom-css"
|
||||||
checked={cssEnabled}
|
checked={cssEnabled}
|
||||||
label="Custom CSS enabled"
|
label={intl.formatMessage({
|
||||||
|
id: "config.ui.custom_css.option_label",
|
||||||
|
})}
|
||||||
onChange={() => {
|
onChange={() => {
|
||||||
setCSSEnabled(!cssEnabled);
|
setCSSEnabled(!cssEnabled);
|
||||||
}}
|
}}
|
||||||
@@ -234,12 +272,12 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
className="col col-sm-6 text-input code"
|
className="col col-sm-6 text-input code"
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Page must be reloaded for changes to take effect.
|
{intl.formatMessage({ id: "config.ui.custom_css.description" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>Handy Connection Key</h5>
|
<h5>{intl.formatMessage({ id: "config.ui.handy_connection_key" })}</h5>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col col-sm-6 text-input"
|
className="col col-sm-6 text-input"
|
||||||
value={handyKey}
|
value={handyKey}
|
||||||
@@ -248,13 +286,13 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Handy connection key to use for interactive scenes.
|
{intl.formatMessage({ id: "config.ui.handy_connection_key_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<Button variant="primary" onClick={() => onSave()}>
|
<Button variant="primary" onClick={() => onSave()}>
|
||||||
Save
|
{intl.formatMessage({ id: "actions.save" })}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useReducer, useState } from "react";
|
import React, { useEffect, useReducer, useState } from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useLogs, useLoggingSubscribe } from "src/core/StashService";
|
import { useLogs, useLoggingSubscribe } from "src/core/StashService";
|
||||||
|
|
||||||
@@ -74,6 +75,7 @@ const logReducer = (existingEntries: LogEntry[], newEntries: LogEntry[]) => [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const SettingsLogsPanel: React.FC = () => {
|
export const SettingsLogsPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const { data, error } = useLoggingSubscribe();
|
const { data, error } = useLoggingSubscribe();
|
||||||
const { data: existingData } = useLogs();
|
const { data: existingData } = useLogs();
|
||||||
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
|
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
|
||||||
@@ -106,9 +108,11 @@ export const SettingsLogsPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Logs</h4>
|
<h4>{intl.formatMessage({ id: "config.categories.logs" })}</h4>
|
||||||
<Form.Row id="log-level">
|
<Form.Row id="log-level">
|
||||||
<Form.Label className="col-6 col-sm-2">Log Level</Form.Label>
|
<Form.Label className="col-6 col-sm-2">
|
||||||
|
{intl.formatMessage({ id: "config.logs.log_level" })}
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="col-6 col-sm-2 input-control"
|
className="col-6 col-sm-2 input-control"
|
||||||
as="select"
|
as="select"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } 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 { mutateReloadPlugins, usePlugins } from "src/core/StashService";
|
import { mutateReloadPlugins, usePlugins } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
@@ -8,6 +9,8 @@ import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared";
|
|||||||
|
|
||||||
export const SettingsPluginsPanel: React.FC = () => {
|
export const SettingsPluginsPanel: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const { data, loading } = usePlugins();
|
const { data, loading } = usePlugins();
|
||||||
|
|
||||||
async function onReloadPlugins() {
|
async function onReloadPlugins() {
|
||||||
@@ -58,11 +61,15 @@ export const SettingsPluginsPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<h5>Hooks</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="config.plugins.hooks" />
|
||||||
|
</h5>
|
||||||
{hooks.map((h) => (
|
{hooks.map((h) => (
|
||||||
<div key={`${h.name}`} className="mb-3">
|
<div key={`${h.name}`} className="mb-3">
|
||||||
<h6>{h.name}</h6>
|
<h6>{h.name}</h6>
|
||||||
<CollapseButton text="Triggers on">
|
<CollapseButton
|
||||||
|
text={intl.formatMessage({ id: "config.plugins.triggers_on" })}
|
||||||
|
>
|
||||||
<ul>
|
<ul>
|
||||||
{h.hooks?.map((hh) => (
|
{h.hooks?.map((hh) => (
|
||||||
<li>
|
<li>
|
||||||
@@ -82,14 +89,18 @@ export const SettingsPluginsPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>Plugins</h3>
|
<h3>
|
||||||
|
<FormattedMessage id="config.categories.plugins" />
|
||||||
|
</h3>
|
||||||
<hr />
|
<hr />
|
||||||
{renderPlugins()}
|
{renderPlugins()}
|
||||||
<Button onClick={() => onReloadPlugins()}>
|
<Button onClick={() => onReloadPlugins()}>
|
||||||
<span className="fa-icon">
|
<span className="fa-icon">
|
||||||
<Icon icon="sync-alt" />
|
<Icon icon="sync-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span>Reload plugins</span>
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_plugins" />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
@@ -68,6 +69,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
|
|||||||
|
|
||||||
export const SettingsScrapersPanel: React.FC = () => {
|
export const SettingsScrapersPanel: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const {
|
const {
|
||||||
data: performerScrapers,
|
data: performerScrapers,
|
||||||
loading: loadingPerformers,
|
loading: loadingPerformers,
|
||||||
@@ -95,7 +97,7 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
.map((t) => {
|
.map((t) => {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case ScrapeType.Name:
|
case ScrapeType.Name:
|
||||||
return "Search by name";
|
return intl.formatMessage({ id: "config.scrapers.search_by_name" });
|
||||||
default:
|
default:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -114,7 +116,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
const typeStrings = types.map((t) => {
|
const typeStrings = types.map((t) => {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case ScrapeType.Fragment:
|
case ScrapeType.Fragment:
|
||||||
return "Scene Metadata";
|
return intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_metadata" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "scene" }) }
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -133,7 +138,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
const typeStrings = types.map((t) => {
|
const typeStrings = types.map((t) => {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case ScrapeType.Fragment:
|
case ScrapeType.Fragment:
|
||||||
return "Gallery Metadata";
|
return intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_metadata" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "gallery" }) }
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -152,7 +160,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
const typeStrings = types.map((t) => {
|
const typeStrings = types.map((t) => {
|
||||||
switch (t) {
|
switch (t) {
|
||||||
case ScrapeType.Fragment:
|
case ScrapeType.Fragment:
|
||||||
return "Movie Metadata";
|
return intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_metadata" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "movie" }) }
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
@@ -182,7 +193,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
));
|
));
|
||||||
|
|
||||||
return renderTable("Scene scrapers", elements);
|
return renderTable(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_scrapers" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "scene" }) }
|
||||||
|
),
|
||||||
|
elements
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGalleryScrapers() {
|
function renderGalleryScrapers() {
|
||||||
@@ -198,7 +215,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderTable("Gallery Scrapers", elements);
|
return renderTable(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_scrapers" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "gallery" }) }
|
||||||
|
),
|
||||||
|
elements
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPerformerScrapers() {
|
function renderPerformerScrapers() {
|
||||||
@@ -216,7 +239,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderTable("Performer scrapers", elements);
|
return renderTable(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_scrapers" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "performer" }) }
|
||||||
|
),
|
||||||
|
elements
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMovieScrapers() {
|
function renderMovieScrapers() {
|
||||||
@@ -230,7 +259,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
));
|
));
|
||||||
|
|
||||||
return renderTable("Movie scrapers", elements);
|
return renderTable(
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "config.scrapers.entity_scrapers" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "movie" }) }
|
||||||
|
),
|
||||||
|
elements
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTable(title: string, elements: JSX.Element[]) {
|
function renderTable(title: string, elements: JSX.Element[]) {
|
||||||
@@ -241,9 +276,15 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
<table className="scraper-table">
|
<table className="scraper-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{intl.formatMessage({ id: "name" })}</th>
|
||||||
<th>Supported types</th>
|
<th>
|
||||||
<th>URLs</th>
|
{intl.formatMessage({
|
||||||
|
id: "config.scrapers.supported_types",
|
||||||
|
})}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{intl.formatMessage({ id: "config.scrapers.supported_urls" })}
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{elements}</tbody>
|
<tbody>{elements}</tbody>
|
||||||
@@ -258,13 +299,15 @@ export const SettingsScrapersPanel: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Scrapers</h4>
|
<h4>{intl.formatMessage({ id: "config.categories.scrapers" })}</h4>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Button onClick={() => onReloadScrapers()}>
|
<Button onClick={() => onReloadScrapers()}>
|
||||||
<span className="fa-icon">
|
<span className="fa-icon">
|
||||||
<Icon icon="sync-alt" />
|
<Icon icon="sync-alt" />
|
||||||
</span>
|
</span>
|
||||||
<span>Reload scrapers</span>
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_scrapers" />
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Col, Form, Row } from "react-bootstrap";
|
import { Button, Col, Form, Row } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { useConfiguration } from "src/core/StashService";
|
import { useConfiguration } from "src/core/StashService";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { Icon, Modal } from "src/components/Shared";
|
||||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||||
@@ -11,6 +12,7 @@ interface IDirectorySelectionDialogProps {
|
|||||||
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = (
|
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = (
|
||||||
props: IDirectorySelectionDialogProps
|
props: IDirectorySelectionDialogProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const { data } = useConfiguration();
|
const { data } = useConfiguration();
|
||||||
|
|
||||||
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path);
|
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path);
|
||||||
@@ -42,7 +44,7 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
|
|||||||
}}
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -57,7 +59,7 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
|
|||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title="Delete"
|
title={intl.formatMessage({ id: "actions.delete" })}
|
||||||
onClick={() => removePath(p)}
|
onClick={() => removePath(p)}
|
||||||
>
|
>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
export const GenerateButton: React.FC = () => {
|
export const GenerateButton: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const [sprites, setSprites] = useState(true);
|
const [sprites, setSprites] = useState(true);
|
||||||
const [phashes, setPhashes] = useState(true);
|
const [phashes, setPhashes] = useState(true);
|
||||||
const [previews, setPreviews] = useState(true);
|
const [previews, setPreviews] = useState(true);
|
||||||
@@ -22,7 +24,11 @@ export const GenerateButton: React.FC = () => {
|
|||||||
markers,
|
markers,
|
||||||
transcodes,
|
transcodes,
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Added generation job to queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: "toast.added_generation_job_to_queue",
|
||||||
|
}),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -34,7 +40,7 @@ export const GenerateButton: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="preview-task"
|
id="preview-task"
|
||||||
checked={previews}
|
checked={previews}
|
||||||
label="Previews (video previews which play when hovering over a scene)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.video_previews" })}
|
||||||
onChange={() => setPreviews(!previews)}
|
onChange={() => setPreviews(!previews)}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
@@ -43,7 +49,9 @@ export const GenerateButton: React.FC = () => {
|
|||||||
id="image-preview-task"
|
id="image-preview-task"
|
||||||
checked={imagePreviews}
|
checked={imagePreviews}
|
||||||
disabled={!previews}
|
disabled={!previews}
|
||||||
label="Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)"
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.image_previews",
|
||||||
|
})}
|
||||||
onChange={() => setImagePreviews(!imagePreviews)}
|
onChange={() => setImagePreviews(!imagePreviews)}
|
||||||
className="ml-2 flex-grow"
|
className="ml-2 flex-grow"
|
||||||
/>
|
/>
|
||||||
@@ -51,25 +59,25 @@ export const GenerateButton: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="sprite-task"
|
id="sprite-task"
|
||||||
checked={sprites}
|
checked={sprites}
|
||||||
label="Sprites (for the scene scrubber)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||||
onChange={() => setSprites(!sprites)}
|
onChange={() => setSprites(!sprites)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="marker-task"
|
id="marker-task"
|
||||||
checked={markers}
|
checked={markers}
|
||||||
label="Markers (20 second videos which begin at the given timecode)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||||
onChange={() => setMarkers(!markers)}
|
onChange={() => setMarkers(!markers)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="transcode-task"
|
id="transcode-task"
|
||||||
checked={transcodes}
|
checked={transcodes}
|
||||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||||
onChange={() => setTranscodes(!transcodes)}
|
onChange={() => setTranscodes(!transcodes)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="phash-task"
|
id="phash-task"
|
||||||
checked={phashes}
|
checked={phashes}
|
||||||
label="Phashes (for deduplication and scene identification)"
|
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||||
onChange={() => setPhashes(!phashes)}
|
onChange={() => setPhashes(!phashes)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -80,10 +88,10 @@ export const GenerateButton: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onGenerate()}
|
onClick={() => onGenerate()}
|
||||||
>
|
>
|
||||||
Generate
|
<FormattedMessage id="actions.generate" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Generate supporting image, sprite, video, vtt and other files.
|
{intl.formatMessage({ id: "config.tasks.generate_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { mutateImportObjects } from "src/core/StashService";
|
|||||||
import { Modal } from "src/components/Shared";
|
import { Modal } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IImportDialogProps {
|
interface IImportDialogProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -25,6 +26,7 @@ export const ImportDialog: React.FC<IImportDialogProps> = (
|
|||||||
// Network state
|
// Network state
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
function duplicateHandlingToString(
|
function duplicateHandlingToString(
|
||||||
@@ -112,16 +114,16 @@ export const ImportDialog: React.FC<IImportDialogProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="pencil-alt"
|
icon="pencil-alt"
|
||||||
header="Import"
|
header={intl.formatMessage({ id: "actions.import" })}
|
||||||
accept={{
|
accept={{
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onImport();
|
onImport();
|
||||||
},
|
},
|
||||||
text: "Import",
|
text: intl.formatMessage({ id: "actions.import" }),
|
||||||
}}
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
disabled={!file}
|
disabled={!file}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
mutateMetadataImport,
|
mutateMetadataImport,
|
||||||
@@ -24,6 +25,7 @@ type Plugin = Pick<GQL.Plugin, "id">;
|
|||||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||||
|
|
||||||
export const SettingsTasksPanel: React.FC = () => {
|
export const SettingsTasksPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
||||||
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||||
@@ -60,7 +62,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
setIsImportAlertOpen(false);
|
setIsImportAlertOpen(false);
|
||||||
try {
|
try {
|
||||||
await mutateMetadataImport();
|
await mutateMetadataImport();
|
||||||
Toast.success({ content: "Added import task to queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.import" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -71,13 +78,14 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
show={isImportAlertOpen}
|
show={isImportAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Import", variant: "danger", onClick: onImport }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.import" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onImport,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsImportAlertOpen(false) }}
|
cancel={{ onClick: () => setIsImportAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
<p>
|
<p>{intl.formatMessage({ id: "actions.tasks.import_warning" })}</p>
|
||||||
Are you sure you want to import? This will delete the database and
|
|
||||||
re-import from your exported metadata.
|
|
||||||
</p>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -93,16 +101,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
let msg;
|
let msg;
|
||||||
if (cleanDryRun) {
|
if (cleanDryRun) {
|
||||||
msg = (
|
msg = (
|
||||||
<p>
|
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
||||||
Dry Mode selected. No actual deleting will take place, only logging.
|
|
||||||
</p>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
msg = (
|
msg = (
|
||||||
<p>
|
<p>
|
||||||
Are you sure you want to Clean? This will delete database information
|
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
||||||
and generated content for all scenes and galleries that are no longer
|
|
||||||
found in the filesystem.
|
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,11 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
show={isCleanAlertOpen}
|
show={isCleanAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Clean", variant: "danger", onClick: onClean }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.clean" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onClean,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
|
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
{msg}
|
{msg}
|
||||||
@@ -154,7 +162,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
scanGenerateSprites,
|
scanGenerateSprites,
|
||||||
scanGeneratePhashes,
|
scanGeneratePhashes,
|
||||||
});
|
});
|
||||||
Toast.success({ content: "Added scan to job queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.scan" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -189,7 +202,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
async function onAutoTag(paths?: string[]) {
|
async function onAutoTag(paths?: string[]) {
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag(getAutoTagInput(paths));
|
await mutateMetadataAutoTag(getAutoTagInput(paths));
|
||||||
Toast.success({ content: "Added Auto tagging job to queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.auto_tag" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -197,7 +215,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
||||||
await mutateRunPluginTask(plugin.id, operation.name);
|
await mutateRunPluginTask(plugin.id, operation.name);
|
||||||
Toast.success({ content: `Added ${operation.name} job to queue` });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: operation.name }
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPluginTasks(plugin: Plugin, pluginTasks: PluginTask[]) {
|
function renderPluginTasks(plugin: Plugin, pluginTasks: PluginTask[]) {
|
||||||
@@ -251,7 +274,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
<h5>Plugin Tasks</h5>
|
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
||||||
{plugins.data.plugins.map((o) => {
|
{plugins.data.plugins.map((o) => {
|
||||||
return (
|
return (
|
||||||
<div key={`${o.id}`} className="mb-3">
|
<div key={`${o.id}`} className="mb-3">
|
||||||
@@ -268,7 +291,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
async function onMigrateHashNaming() {
|
async function onMigrateHashNaming() {
|
||||||
try {
|
try {
|
||||||
await mutateMigrateHashNaming();
|
await mutateMigrateHashNaming();
|
||||||
Toast.success({ content: "Added hash migration task to queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{
|
||||||
|
operation_name: intl.formatMessage({
|
||||||
|
id: "actions.hash_migration",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error(err);
|
Toast.error(err);
|
||||||
}
|
}
|
||||||
@@ -277,14 +309,23 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
async function onExport() {
|
async function onExport() {
|
||||||
try {
|
try {
|
||||||
await mutateMetadataExport();
|
await mutateMetadataExport();
|
||||||
Toast.success({ content: "Added export task to queue" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.backup" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error(err);
|
Toast.error(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBackupRunning) {
|
if (isBackupRunning) {
|
||||||
return <LoadingIndicator message="Backup up database" />;
|
return (
|
||||||
|
<LoadingIndicator
|
||||||
|
message={intl.formatMessage({ id: "config.tasks.backing_up_database" })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -295,30 +336,36 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
{renderScanDialog()}
|
{renderScanDialog()}
|
||||||
{renderAutoTagDialog()}
|
{renderAutoTagDialog()}
|
||||||
|
|
||||||
<h4>Job Queue</h4>
|
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
||||||
|
|
||||||
<JobTable />
|
<JobTable />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Library</h5>
|
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="use-file-metadata"
|
id="use-file-metadata"
|
||||||
checked={useFileMetadata}
|
checked={useFileMetadata}
|
||||||
label="Set name, date, details from metadata (if present)"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.set_name_date_details_from_metadata_if_present",
|
||||||
|
})}
|
||||||
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="strip-file-extension"
|
id="strip-file-extension"
|
||||||
checked={stripFileExtension}
|
checked={stripFileExtension}
|
||||||
label="Don't include file extension as part of the title"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.dont_include_file_extension_as_part_of_the_title",
|
||||||
|
})}
|
||||||
onChange={() => setStripFileExtension(!stripFileExtension)}
|
onChange={() => setStripFileExtension(!stripFileExtension)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="scan-generate-previews"
|
id="scan-generate-previews"
|
||||||
checked={scanGeneratePreviews}
|
checked={scanGeneratePreviews}
|
||||||
label="Generate previews during scan (video previews which play when hovering over a scene)"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_video_previews_during_scan",
|
||||||
|
})}
|
||||||
onChange={() => setScanGeneratePreviews(!scanGeneratePreviews)}
|
onChange={() => setScanGeneratePreviews(!scanGeneratePreviews)}
|
||||||
/>
|
/>
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
@@ -327,7 +374,9 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
id="scan-generate-image-previews"
|
id="scan-generate-image-previews"
|
||||||
checked={scanGenerateImagePreviews}
|
checked={scanGenerateImagePreviews}
|
||||||
disabled={!scanGeneratePreviews}
|
disabled={!scanGeneratePreviews}
|
||||||
label="Generate image previews during scan (animated WebP previews, only required if Preview Type is set to Animated Image)"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_previews_during_scan",
|
||||||
|
})}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
setScanGenerateImagePreviews(!scanGenerateImagePreviews)
|
setScanGenerateImagePreviews(!scanGenerateImagePreviews)
|
||||||
}
|
}
|
||||||
@@ -337,13 +386,17 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="scan-generate-sprites"
|
id="scan-generate-sprites"
|
||||||
checked={scanGenerateSprites}
|
checked={scanGenerateSprites}
|
||||||
label="Generate sprites during scan (for the scene scrubber)"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_sprites_during_scan",
|
||||||
|
})}
|
||||||
onChange={() => setScanGenerateSprites(!scanGenerateSprites)}
|
onChange={() => setScanGenerateSprites(!scanGenerateSprites)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="scan-generate-phashes"
|
id="scan-generate-phashes"
|
||||||
checked={scanGeneratePhashes}
|
checked={scanGeneratePhashes}
|
||||||
label="Generate phashes during scan (for deduplication and scene identification)"
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_phashes_during_scan",
|
||||||
|
})}
|
||||||
onChange={() => setScanGeneratePhashes(!scanGeneratePhashes)}
|
onChange={() => setScanGeneratePhashes(!scanGeneratePhashes)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -354,41 +407,41 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onScan()}
|
onClick={() => onScan()}
|
||||||
>
|
>
|
||||||
Scan
|
<FormattedMessage id="actions.scan" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => setIsScanDialogOpen(true)}
|
onClick={() => setIsScanDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Selective Scan
|
<FormattedMessage id="actions.selective_scan" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Scan for new content and add it to the database.
|
{intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Auto Tagging</h5>
|
<h5>{intl.formatMessage({ id: "config.tasks.auto_tagging" })}</h5>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="autotag-performers"
|
id="autotag-performers"
|
||||||
checked={autoTagPerformers}
|
checked={autoTagPerformers}
|
||||||
label="Performers"
|
label={intl.formatMessage({ id: "performers" })}
|
||||||
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="autotag-studios"
|
id="autotag-studios"
|
||||||
checked={autoTagStudios}
|
checked={autoTagStudios}
|
||||||
label="Studios"
|
label={intl.formatMessage({ id: "studios" })}
|
||||||
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
||||||
/>
|
/>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="autotag-tags"
|
id="autotag-tags"
|
||||||
checked={autoTagTags}
|
checked={autoTagTags}
|
||||||
label="Tags"
|
label={intl.formatMessage({ id: "tags" })}
|
||||||
onChange={() => setAutoTagTags(!autoTagTags)}
|
onChange={() => setAutoTagTags(!autoTagTags)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -399,32 +452,34 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
className="mr-2"
|
className="mr-2"
|
||||||
onClick={() => onAutoTag()}
|
onClick={() => onAutoTag()}
|
||||||
>
|
>
|
||||||
Auto Tag
|
<FormattedMessage id="actions.auto_tag" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => setIsAutoTagDialogOpen(true)}
|
onClick={() => setIsAutoTagDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Selective Auto Tag
|
<FormattedMessage id="actions.selective_auto_tag" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Auto-tag content based on filenames.
|
{intl.formatMessage({
|
||||||
|
id: "config.tasks.auto_tag_based_on_filenames",
|
||||||
|
})}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Generated Content</h5>
|
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
||||||
<GenerateButton />
|
<GenerateButton />
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
<h5>Maintenance</h5>
|
<h5>{intl.formatMessage({ id: "config.tasks.maintenance" })}</h5>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="clean-dryrun"
|
id="clean-dryrun"
|
||||||
checked={cleanDryRun}
|
checked={cleanDryRun}
|
||||||
label="Only perform a dry run. Don't remove anything"
|
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
||||||
onChange={() => setCleanDryRun(!cleanDryRun)}
|
onChange={() => setCleanDryRun(!cleanDryRun)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -434,17 +489,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setIsCleanAlertOpen(true)}
|
onClick={() => setIsCleanAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Clean
|
<FormattedMessage id="actions.clean" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Check for missing files and remove them from the database. This is a
|
{intl.formatMessage({ id: "config.tasks.cleanup_desc" })}
|
||||||
destructive action.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Metadata</h5>
|
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button
|
<Button
|
||||||
id="export"
|
id="export"
|
||||||
@@ -452,11 +506,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onExport()}
|
onClick={() => onExport()}
|
||||||
>
|
>
|
||||||
Full Export
|
<FormattedMessage id="actions.full_export" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Exports the database content into JSON format in the metadata
|
{intl.formatMessage({ id: "config.tasks.export_to_json" })}
|
||||||
directory.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -466,11 +519,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setIsImportAlertOpen(true)}
|
onClick={() => setIsImportAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Full Import
|
<FormattedMessage id="actions.full_import" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Import from exported JSON in the metadata directory. Wipes the
|
{intl.formatMessage({ id: "config.tasks.import_from_exported_json" })}
|
||||||
existing database.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -480,16 +532,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => setIsImportDialogOpen(true)}
|
onClick={() => setIsImportDialogOpen(true)}
|
||||||
>
|
>
|
||||||
Import from file
|
<FormattedMessage id="actions.import_from_file" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Incremental import from a supplied export zip file.
|
{intl.formatMessage({ id: "config.tasks.incremental_import" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Backup</h5>
|
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button
|
<Button
|
||||||
id="backup"
|
id="backup"
|
||||||
@@ -497,12 +549,19 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onBackup()}
|
onClick={() => onBackup()}
|
||||||
>
|
>
|
||||||
Backup
|
<FormattedMessage id="actions.backup" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Performs a backup of the database to the same directory as the
|
{intl.formatMessage(
|
||||||
database, with the filename format{" "}
|
{ id: "config.tasks.backup_database" },
|
||||||
<code>[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]</code>
|
{
|
||||||
|
filename_format: (
|
||||||
|
<code>
|
||||||
|
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
||||||
|
</code>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -513,10 +572,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => onBackup(true)}
|
onClick={() => onBackup(true)}
|
||||||
>
|
>
|
||||||
Download Backup
|
<FormattedMessage id="actions.download_backup" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Performs a backup of the database and downloads the resulting file.
|
{intl.formatMessage({ id: "config.tasks.backup_and_download" })}
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
@@ -524,7 +583,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>Migrations</h5>
|
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button
|
<Button
|
||||||
@@ -532,11 +591,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => onMigrateHashNaming()}
|
onClick={() => onMigrateHashNaming()}
|
||||||
>
|
>
|
||||||
Rename generated files
|
<FormattedMessage id="actions.rename_gen_files" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Used after changing the Generated file naming hash to rename existing
|
{intl.formatMessage({ id: "config.tasks.migrate_hash_files" })}
|
||||||
generated files to the new hash format.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export const SettingsToolsPanel: React.FC = () => {
|
export const SettingsToolsPanel: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>Scene Tools</h4>
|
<h4>
|
||||||
|
<FormattedMessage id="config.tools.scene_tools" />
|
||||||
|
</h4>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Link to="/sceneFilenameParser">Scene Filename Parser</Link>
|
<Link to="/sceneFilenameParser">
|
||||||
|
<FormattedMessage id="config.tools.scene_filename_parser.title" />
|
||||||
|
</Link>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Link to="/sceneDuplicateChecker">Scene Duplicate Checker</Link>
|
<Link to="/sceneDuplicateChecker">
|
||||||
|
<FormattedMessage id="config.tools.scene_duplicate_checker" />
|
||||||
|
</Link>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
|
|
||||||
interface IInstanceProps {
|
interface IInstanceProps {
|
||||||
@@ -15,6 +16,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
isMulti,
|
isMulti,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const handleInput = (key: string, value: string) => {
|
const handleInput = (key: string, value: string) => {
|
||||||
const newObj = {
|
const newObj = {
|
||||||
...instance,
|
...instance,
|
||||||
@@ -27,7 +29,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
|||||||
<Form.Group className="row no-gutters">
|
<Form.Group className="row no-gutters">
|
||||||
<InputGroup className="col">
|
<InputGroup className="col">
|
||||||
<Form.Control
|
<Form.Control
|
||||||
placeholder="Name"
|
placeholder={intl.formatMessage({ id: "config.stashbox.name" })}
|
||||||
className="text-input col-3 stash-box-name"
|
className="text-input col-3 stash-box-name"
|
||||||
value={instance?.name}
|
value={instance?.name}
|
||||||
isValid={!isMulti || (instance?.name?.length ?? 0) > 0}
|
isValid={!isMulti || (instance?.name?.length ?? 0) > 0}
|
||||||
@@ -36,7 +38,9 @@ const Instance: React.FC<IInstanceProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
placeholder="GraphQL endpoint"
|
placeholder={intl.formatMessage({
|
||||||
|
id: "config.stashbox.graphql_endpoint",
|
||||||
|
})}
|
||||||
className="text-input col-3 stash-box-endpoint"
|
className="text-input col-3 stash-box-endpoint"
|
||||||
value={instance?.endpoint}
|
value={instance?.endpoint}
|
||||||
isValid={(instance?.endpoint?.length ?? 0) > 0}
|
isValid={(instance?.endpoint?.length ?? 0) > 0}
|
||||||
@@ -45,7 +49,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
placeholder="API key"
|
placeholder={intl.formatMessage({ id: "config.stashbox.api_key" })}
|
||||||
className="text-input col-3 stash-box-apikey"
|
className="text-input col-3 stash-box-apikey"
|
||||||
value={instance?.api_key}
|
value={instance?.api_key}
|
||||||
isValid={(instance?.api_key?.length ?? 0) > 0}
|
isValid={(instance?.api_key?.length ?? 0) > 0}
|
||||||
@@ -57,7 +61,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className=""
|
className=""
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title="Delete"
|
title={intl.formatMessage({ id: "actions.delete" })}
|
||||||
onClick={() => onDelete(instance.index)}
|
onClick={() => onDelete(instance.index)}
|
||||||
>
|
>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
@@ -84,6 +88,7 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
|||||||
boxes,
|
boxes,
|
||||||
saveBoxes,
|
saveBoxes,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [index, setIndex] = useState(1000);
|
const [index, setIndex] = useState(1000);
|
||||||
|
|
||||||
const handleSave = (instance: IStashBoxInstance) =>
|
const handleSave = (instance: IStashBoxInstance) =>
|
||||||
@@ -99,12 +104,18 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h6>Stash-box Endpoints</h6>
|
<h6>{intl.formatMessage({ id: "config.stashbox.title" })}</h6>
|
||||||
{boxes.length > 0 && (
|
{boxes.length > 0 && (
|
||||||
<div className="row no-gutters">
|
<div className="row no-gutters">
|
||||||
<h6 className="col-3 ml-1">Name</h6>
|
<h6 className="col-3 ml-1">
|
||||||
<h6 className="col-3 ml-1">Endpoint</h6>
|
{intl.formatMessage({ id: "config.stashbox.name" })}
|
||||||
<h6 className="col-3 ml-1">API Key</h6>
|
</h6>
|
||||||
|
<h6 className="col-3 ml-1">
|
||||||
|
{intl.formatMessage({ id: "config.stashbox.endpoint" })}
|
||||||
|
</h6>
|
||||||
|
<h6 className="col-3 ml-1">
|
||||||
|
{intl.formatMessage({ id: "config.general.auth.api_key" })}
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{boxes.map((instance) => (
|
{boxes.map((instance) => (
|
||||||
@@ -118,17 +129,13 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
|||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
className="minimal"
|
className="minimal"
|
||||||
title="Add stash-box instance"
|
title={intl.formatMessage({ id: "config.stashbox.add_instance" })}
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
<Icon icon="plus" />
|
<Icon icon="plus" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Stash-box facilitates automated tagging of scenes and performers based
|
{intl.formatMessage({ id: "config.stashbox.description" })}
|
||||||
on fingerprints and filenames.
|
|
||||||
<br />
|
|
||||||
Endpoint and API key can be found on your account page on the stash-box
|
|
||||||
instance. Names are required when more than one instance is added.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button, Form, Row, Col } from "react-bootstrap";
|
import { Button, Form, Row, Col } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog";
|
import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog";
|
||||||
@@ -21,6 +22,7 @@ const Stash: React.FC<IStashProps> = ({ index, stash, onSave, onDelete }) => {
|
|||||||
onSave(newObj);
|
onSave(newObj);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const classAdd = index % 2 === 1 ? "bg-dark" : "";
|
const classAdd = index % 2 === 1 ? "bg-dark" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +49,7 @@ const Stash: React.FC<IStashProps> = ({ index, stash, onSave, onDelete }) => {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
title="Delete"
|
title={intl.formatMessage({ id: "actions.delete" })}
|
||||||
onClick={() => onDelete()}
|
onClick={() => onDelete()}
|
||||||
>
|
>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
@@ -103,9 +105,15 @@ export const StashConfiguration: React.FC<IStashConfigurationProps> = ({
|
|||||||
<Form.Group>
|
<Form.Group>
|
||||||
{stashes.length > 0 && (
|
{stashes.length > 0 && (
|
||||||
<Row>
|
<Row>
|
||||||
<h6 className="col-4">Path</h6>
|
<h6 className="col-4">
|
||||||
<h6 className="col-3">Exclude Video</h6>
|
<FormattedMessage id="path" />
|
||||||
<h6 className="col-3">Exclude Image</h6>
|
</h6>
|
||||||
|
<h6 className="col-3">
|
||||||
|
<FormattedMessage id="config.general.exclude_video" />
|
||||||
|
</h6>
|
||||||
|
<h6 className="col-3">
|
||||||
|
<FormattedMessage id="config.general.exclude_image" />
|
||||||
|
</h6>
|
||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
{stashes.map((stash, index) => (
|
{stashes.map((stash, index) => (
|
||||||
@@ -122,7 +130,7 @@ export const StashConfiguration: React.FC<IStashConfigurationProps> = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setIsDisplayingDialog(true)}
|
onClick={() => setIsDisplayingDialog(true)}
|
||||||
>
|
>
|
||||||
Add Directory
|
<FormattedMessage id="actions.add_directory" />
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -24,24 +24,16 @@ interface IDeleteEntityDialogProps {
|
|||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
deleteHeader: {
|
deleteHeader: {
|
||||||
id: "delete-header",
|
id: "dialogs.delete_object_title",
|
||||||
defaultMessage:
|
|
||||||
"Delete {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
|
||||||
},
|
},
|
||||||
deleteToast: {
|
deleteToast: {
|
||||||
id: "delete-toast",
|
id: "toast.delete_past_tense",
|
||||||
defaultMessage:
|
|
||||||
"Deleted {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
|
||||||
},
|
},
|
||||||
deleteMessage: {
|
deleteMessage: {
|
||||||
id: "delete-message",
|
id: "dialogs.delete_object_desc",
|
||||||
defaultMessage:
|
|
||||||
"Are you sure you want to delete {count, plural, =1 {this {singularEntity}} other {these {pluralEntity}}}?",
|
|
||||||
},
|
},
|
||||||
overflowMessage: {
|
overflowMessage: {
|
||||||
id: "overflow-message",
|
id: "dialogs.delete_object_overflow",
|
||||||
defaultMessage:
|
|
||||||
"...and {count} other {count, plural, =1 {{ singularEntity}} other {{ pluralEntity }}}.",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,10 +79,14 @@ const DeleteEntityDialog: React.FC<IDeleteEntityDialogProps> = ({
|
|||||||
singularEntity,
|
singularEntity,
|
||||||
pluralEntity,
|
pluralEntity,
|
||||||
})}
|
})}
|
||||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
accept={{
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => onClose(false),
|
onClick: () => onClose(false),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isDeleting}
|
isRunning={isDeleting}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Button, Modal } from "react-bootstrap";
|
import { Button, Modal } from "react-bootstrap";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { ImageInput } from "src/components/Shared";
|
import { ImageInput } from "src/components/Shared";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -21,6 +22,7 @@ interface IProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
function renderEditButton() {
|
function renderEditButton() {
|
||||||
@@ -31,7 +33,9 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
className="edit"
|
className="edit"
|
||||||
onClick={() => props.onToggleEdit()}
|
onClick={() => props.onToggleEdit()}
|
||||||
>
|
>
|
||||||
{props.isEditing ? "Cancel" : "Edit"}
|
{props.isEditing
|
||||||
|
? intl.formatMessage({ id: "actions.cancel" })
|
||||||
|
: intl.formatMessage({ id: "actions.edit" })}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,7 +50,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
disabled={props.saveDisabled}
|
disabled={props.saveDisabled}
|
||||||
onClick={() => props.onSave()}
|
onClick={() => props.onSave()}
|
||||||
>
|
>
|
||||||
Save
|
<FormattedMessage id="actions.save" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,7 +63,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
className="delete d-none d-sm-block"
|
className="delete d-none d-sm-block"
|
||||||
onClick={() => setIsDeleteAlertOpen(true)}
|
onClick={() => setIsDeleteAlertOpen(true)}
|
||||||
>
|
>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -71,7 +75,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
return (
|
return (
|
||||||
<ImageInput
|
<ImageInput
|
||||||
isEditing={props.isEditing}
|
isEditing={props.isEditing}
|
||||||
text="Back image..."
|
text={intl.formatMessage({ id: "actions.set_back_image" })}
|
||||||
onImageChange={props.onBackImageChange}
|
onImageChange={props.onBackImageChange}
|
||||||
onImageURL={props.onBackImageChangeURL}
|
onImageURL={props.onBackImageChangeURL}
|
||||||
/>
|
/>
|
||||||
@@ -91,7 +95,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Auto Tag
|
<FormattedMessage id="actions.auto_tag" />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,17 +105,20 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
return (
|
return (
|
||||||
<Modal show={isDeleteAlertOpen}>
|
<Modal show={isDeleteAlertOpen}>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
Are you sure you want to delete {props.objectName}?
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{ entityName: props.objectName }}
|
||||||
|
/>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="danger" onClick={props.onDelete}>
|
<Button variant="danger" onClick={props.onDelete}>
|
||||||
Delete
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => setIsDeleteAlertOpen(false)}
|
onClick={() => setIsDeleteAlertOpen(false)}
|
||||||
>
|
>
|
||||||
Cancel
|
<FormattedMessage id="actions.cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -123,7 +130,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
{renderEditButton()}
|
{renderEditButton()}
|
||||||
<ImageInput
|
<ImageInput
|
||||||
isEditing={props.isEditing}
|
isEditing={props.isEditing}
|
||||||
text={props.onBackImageChange ? "Front image..." : undefined}
|
text={
|
||||||
|
props.onBackImageChange
|
||||||
|
? intl.formatMessage({ id: "actions.set_front_image" })
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onImageChange={props.onImageChange}
|
onImageChange={props.onImageChange}
|
||||||
onImageURL={props.onImageChangeURL}
|
onImageURL={props.onImageChangeURL}
|
||||||
acceptSVG={props.acceptSVG ?? false}
|
acceptSVG={props.acceptSVG ?? false}
|
||||||
@@ -134,7 +145,9 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => props.onClearImage!()}
|
onClick={() => props.onClearImage!()}
|
||||||
>
|
>
|
||||||
{props.onClearBackImage ? "Clear front image" : "Clear image"}
|
{props.onClearBackImage
|
||||||
|
? intl.formatMessage({ id: "actions.clear_front_image" })
|
||||||
|
: intl.formatMessage({ id: "actions.clear_image" })}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
@@ -146,7 +159,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => props.onClearBackImage!()}
|
onClick={() => props.onClearBackImage!()}
|
||||||
>
|
>
|
||||||
Clear back image
|
{intl.formatMessage({ id: "actions.clear_back_image" })}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Modal } from "src/components/Shared";
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { downloadFile } from "src/utils";
|
import { downloadFile } from "src/utils";
|
||||||
import { ExportObjectsInput } from "src/core/generated-graphql";
|
import { ExportObjectsInput } from "src/core/generated-graphql";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
interface IExportDialogProps {
|
interface IExportDialogProps {
|
||||||
exportInput: ExportObjectsInput;
|
exportInput: ExportObjectsInput;
|
||||||
@@ -19,6 +20,7 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
|||||||
// Network state
|
// Network state
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
async function onExport() {
|
async function onExport() {
|
||||||
@@ -46,11 +48,14 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
|||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
icon="cogs"
|
icon="cogs"
|
||||||
header="Export"
|
header={intl.formatMessage({ id: "dialogs.export_title" })}
|
||||||
accept={{ onClick: onExport, text: "Export" }}
|
accept={{
|
||||||
|
onClick: onExport,
|
||||||
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
@@ -60,7 +65,9 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
|||||||
<Form.Check
|
<Form.Check
|
||||||
id="include-dependencies"
|
id="include-dependencies"
|
||||||
checked={includeDependencies}
|
checked={includeDependencies}
|
||||||
label="Include related objects in export"
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.export_include_related_objects",
|
||||||
|
})}
|
||||||
onChange={() => setIncludeDependencies(!includeDependencies)}
|
onChange={() => setIncludeDependencies(!includeDependencies)}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Button, Modal } from "react-bootstrap";
|
import { Button, Modal } from "react-bootstrap";
|
||||||
import { FolderSelect } from "./FolderSelect";
|
import { FolderSelect } from "./FolderSelect";
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ export const FolderSelectDialog: React.FC<IProps> = (props: IProps) => {
|
|||||||
variant="success"
|
variant="success"
|
||||||
onClick={() => props.onClose(currentDirectory)}
|
onClick={() => props.onClose(currentDirectory)}
|
||||||
>
|
>
|
||||||
Add
|
<FormattedMessage id="actions.add" />
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Popover,
|
Popover,
|
||||||
Row,
|
Row,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { Modal } from ".";
|
import { Modal } from ".";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isShowDialog, setIsShowDialog] = useState(false);
|
const [isShowDialog, setIsShowDialog] = useState(false);
|
||||||
const [url, setURL] = useState("");
|
const [url, setURL] = useState("");
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
if (!isEditing) return <div />;
|
if (!isEditing) return <div />;
|
||||||
|
|
||||||
@@ -58,13 +60,13 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
show={!!isShowDialog}
|
show={!!isShowDialog}
|
||||||
onHide={() => setIsShowDialog(false)}
|
onHide={() => setIsShowDialog(false)}
|
||||||
header="Image URL"
|
header={intl.formatMessage({ id: "dialogs.set_image_url_title" })}
|
||||||
accept={{ onClick: onConfirmURL, text: "Confirm" }}
|
accept={{ onClick: onConfirmURL, text: "Confirm" }}
|
||||||
>
|
>
|
||||||
<div className="dialog-content">
|
<div className="dialog-content">
|
||||||
<Form.Group controlId="url" as={Row}>
|
<Form.Group controlId="url" as={Row}>
|
||||||
<Form.Label column xs={3}>
|
<Form.Label column xs={3}>
|
||||||
URL
|
{intl.formatMessage({ id: "url" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -73,7 +75,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
setURL(event.currentTarget.value)
|
setURL(event.currentTarget.value)
|
||||||
}
|
}
|
||||||
value={url}
|
value={url}
|
||||||
placeholder="URL"
|
placeholder={intl.formatMessage({ id: "url" })}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -90,7 +92,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
<Form.Label className="image-input">
|
<Form.Label className="image-input">
|
||||||
<Button variant="secondary">
|
<Button variant="secondary">
|
||||||
<Icon icon="file" className="fa-fw" />
|
<Icon icon="file" className="fa-fw" />
|
||||||
<span>From file...</span>
|
<span>{intl.formatMessage({ id: "actions.from_file" })}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="file"
|
type="file"
|
||||||
@@ -102,7 +104,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
<div>
|
<div>
|
||||||
<Button className="minimal" onClick={() => setIsShowDialog(true)}>
|
<Button className="minimal" onClick={() => setIsShowDialog(true)}>
|
||||||
<Icon icon="link" className="fa-fw" />
|
<Icon icon="link" className="fa-fw" />
|
||||||
<span>From URL...</span>
|
<span>{intl.formatMessage({ id: "actions.from_url" })}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -120,7 +122,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
|||||||
rootClose
|
rootClose
|
||||||
>
|
>
|
||||||
<Button variant="secondary" className="mr-2">
|
<Button variant="secondary" className="mr-2">
|
||||||
{text ?? "Set image..."}
|
{text ?? intl.formatMessage({ id: "actions.set_image" })}
|
||||||
</Button>
|
</Button>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { Button, Modal, Spinner, ModalProps } from "react-bootstrap";
|
import { Button, Modal, Spinner, ModalProps } from "react-bootstrap";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
interface IButton {
|
interface IButton {
|
||||||
text?: string;
|
text?: string;
|
||||||
@@ -58,7 +59,13 @@ const ModalComponent: React.FC<IModal> = ({
|
|||||||
onClick={cancel.onClick}
|
onClick={cancel.onClick}
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
>
|
>
|
||||||
{cancel.text ?? "Cancel"}
|
{cancel.text ?? (
|
||||||
|
<FormattedMessage
|
||||||
|
id="actions.cancel"
|
||||||
|
defaultMessage="Cancel"
|
||||||
|
description="Cancels the current action and dismisses the modal."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
@@ -72,7 +79,13 @@ const ModalComponent: React.FC<IModal> = ({
|
|||||||
{isRunning ? (
|
{isRunning ? (
|
||||||
<Spinner animation="border" role="status" size="sm" />
|
<Spinner animation="border" role="status" size="sm" />
|
||||||
) : (
|
) : (
|
||||||
accept?.text ?? "Close"
|
accept?.text ?? (
|
||||||
|
<FormattedMessage
|
||||||
|
id="actions.close"
|
||||||
|
defaultMessage="Close"
|
||||||
|
description="Closes the current modal."
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
@@ -22,6 +23,7 @@ interface IMultiSetProps {
|
|||||||
const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||||
props: IMultiSetProps
|
props: IMultiSetProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
const modes = [
|
const modes = [
|
||||||
GQL.BulkUpdateIdMode.Set,
|
GQL.BulkUpdateIdMode.Set,
|
||||||
GQL.BulkUpdateIdMode.Add,
|
GQL.BulkUpdateIdMode.Add,
|
||||||
@@ -35,11 +37,17 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
|||||||
function getModeText(mode: GQL.BulkUpdateIdMode) {
|
function getModeText(mode: GQL.BulkUpdateIdMode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case GQL.BulkUpdateIdMode.Set:
|
case GQL.BulkUpdateIdMode.Set:
|
||||||
return "Overwrite";
|
return intl.formatMessage({
|
||||||
|
id: "actions.overwrite",
|
||||||
|
defaultMessage: "Overwrite",
|
||||||
|
});
|
||||||
case GQL.BulkUpdateIdMode.Add:
|
case GQL.BulkUpdateIdMode.Add:
|
||||||
return "Add";
|
return intl.formatMessage({ id: "actions.add", defaultMessage: "Add" });
|
||||||
case GQL.BulkUpdateIdMode.Remove:
|
case GQL.BulkUpdateIdMode.Remove:
|
||||||
return "Remove";
|
return intl.formatMessage({
|
||||||
|
id: "actions.remove",
|
||||||
|
defaultMessage: "Remove",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { CollapseButton, Icon, Modal } from "src/components/Shared";
|
import { CollapseButton, Icon, Modal } from "src/components/Shared";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
export class ScrapeResult<T> {
|
export class ScrapeResult<T> {
|
||||||
public newValue?: T;
|
public newValue?: T;
|
||||||
@@ -336,6 +337,7 @@ interface IScrapeDialogProps {
|
|||||||
export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
||||||
props: IScrapeDialogProps
|
props: IScrapeDialogProps
|
||||||
) => {
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
show
|
show
|
||||||
@@ -345,11 +347,11 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
props.onClose(true);
|
props.onClose(true);
|
||||||
},
|
},
|
||||||
text: "Apply",
|
text: intl.formatMessage({ id: "actions.apply" }),
|
||||||
}}
|
}}
|
||||||
cancel={{
|
cancel={{
|
||||||
onClick: () => props.onClose(),
|
onClick: () => props.onClose(),
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
}}
|
}}
|
||||||
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }}
|
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }}
|
||||||
@@ -360,10 +362,10 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
|||||||
<Col lg={{ span: 9, offset: 3 }}>
|
<Col lg={{ span: 9, offset: 3 }}>
|
||||||
<Row>
|
<Row>
|
||||||
<Form.Label column xs="6">
|
<Form.Label column xs="6">
|
||||||
Existing
|
<FormattedMessage id="dialogs.scrape_results_existing" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Label column xs="6">
|
<Form.Label column xs="6">
|
||||||
Scraped
|
<FormattedMessage id="dialogs.scrape_results_scraped" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.image_count} />
|
<FormattedNumber value={data.stats.image_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="images" defaultMessage="Images" />
|
<FormattedMessage id="images" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,7 +68,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.movie_count} />
|
<FormattedNumber value={data.stats.movie_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="movies" defaultMessage="Movies" />
|
<FormattedMessage id="movies" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
@@ -76,7 +76,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.gallery_count} />
|
<FormattedNumber value={data.stats.gallery_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="galleries" defaultMessage="Galleries" />
|
<FormattedMessage id="galleries" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
@@ -84,7 +84,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.performer_count} />
|
<FormattedNumber value={data.stats.performer_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="performers" defaultMessage="Performers" />
|
<FormattedMessage id="performers" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
@@ -92,7 +92,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.studio_count} />
|
<FormattedNumber value={data.stats.studio_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="studios" defaultMessage="Studios" />
|
<FormattedMessage id="studios" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
@@ -100,7 +100,7 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedNumber value={data.stats.tag_count} />
|
<FormattedNumber value={data.stats.tag_count} />
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="tags" defaultMessage="Tags" />
|
<FormattedMessage id="tags" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Button, Table, Tabs, Tab } from "react-bootstrap";
|
import { Button, Table, Tabs, Tab } from "react-bootstrap";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useParams, useHistory, Link } from "react-router-dom";
|
import { useParams, useHistory, Link } from "react-router-dom";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ interface IStudioParams {
|
|||||||
export const Studio: React.FC = () => {
|
export const Studio: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const { tab = "details", id = "new" } = useParams<IStudioParams>();
|
const { tab = "details", id = "new" } = useParams<IStudioParams>();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
@@ -194,7 +196,9 @@ export const Studio: React.FC = () => {
|
|||||||
if (!studio.id) return;
|
if (!studio.id) return;
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||||
Toast.success({ content: "Started auto tagging" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -229,10 +233,23 @@ export const Studio: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
<p>Are you sure you want to delete {name ?? "studio"}?</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{
|
||||||
|
entityName:
|
||||||
|
name ??
|
||||||
|
intl.formatMessage({ id: "studio" }).toLocaleLowerCase(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -266,7 +283,10 @@ export const Studio: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
className="mr-2 py-0"
|
className="mr-2 py-0"
|
||||||
title="Delete StashID"
|
title={intl.formatMessage(
|
||||||
|
{ id: "actions.delete_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||||
|
)}
|
||||||
onClick={() => removeStashID(stashID)}
|
onClick={() => removeStashID(stashID)}
|
||||||
>
|
>
|
||||||
<Icon icon="trash-alt" />
|
<Icon icon="trash-alt" />
|
||||||
@@ -339,7 +359,14 @@ export const Studio: React.FC = () => {
|
|||||||
"col-8": isNew,
|
"col-8": isNew,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isNew && <h2>Add Studio</h2>}
|
{isNew && (
|
||||||
|
<h2>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "actions.add_entity" },
|
||||||
|
{ entityType: intl.formatMessage({ id: "studio" }) }
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{imageEncoding ? (
|
{imageEncoding ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
@@ -352,29 +379,29 @@ export const Studio: React.FC = () => {
|
|||||||
<Table>
|
<Table>
|
||||||
<tbody>
|
<tbody>
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Name",
|
title: intl.formatMessage({ id: "name" }),
|
||||||
value: name ?? "",
|
value: name ?? "",
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setName,
|
onChange: setName,
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "URL",
|
title: intl.formatMessage({ id: "url" }),
|
||||||
value: url,
|
value: url,
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setUrl,
|
onChange: setUrl,
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderTextArea({
|
{TableUtils.renderTextArea({
|
||||||
title: "Details",
|
title: intl.formatMessage({ id: "details" }),
|
||||||
value: details,
|
value: details,
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setDetails,
|
onChange: setDetails,
|
||||||
})}
|
})}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent Studio</td>
|
<td>{intl.formatMessage({ id: "parent_studios" })}</td>
|
||||||
<td>{renderStudio()}</td>
|
<td>{renderStudio()}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Rating:</td>
|
<td>{intl.formatMessage({ id: "rating" })}:</td>
|
||||||
<td>
|
<td>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={rating}
|
value={rating}
|
||||||
@@ -411,19 +438,28 @@ export const Studio: React.FC = () => {
|
|||||||
activeKey={activeTabKey}
|
activeKey={activeTabKey}
|
||||||
onSelect={setActiveTabKey}
|
onSelect={setActiveTabKey}
|
||||||
>
|
>
|
||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||||
<StudioScenesPanel studio={studio} />
|
<StudioScenesPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="galleries" title="Galleries">
|
<Tab
|
||||||
|
eventKey="galleries"
|
||||||
|
title={intl.formatMessage({ id: "galleries" })}
|
||||||
|
>
|
||||||
<StudioGalleriesPanel studio={studio} />
|
<StudioGalleriesPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="images" title="Images">
|
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||||
<StudioImagesPanel studio={studio} />
|
<StudioImagesPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="performers" title="Performers">
|
<Tab
|
||||||
|
eventKey="performers"
|
||||||
|
title={intl.formatMessage({ id: "performers" })}
|
||||||
|
>
|
||||||
<StudioPerformersPanel studio={studio} />
|
<StudioPerformersPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="childstudios" title="Child Studios">
|
<Tab
|
||||||
|
eventKey="childstudios"
|
||||||
|
title={intl.formatMessage({ id: "child_studios" })}
|
||||||
|
>
|
||||||
<StudioChildrenPanel studio={studio} />
|
<StudioChildrenPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
@@ -23,22 +24,23 @@ export const StudioList: React.FC<IStudioList> = ({
|
|||||||
fromParent,
|
fromParent,
|
||||||
filterHook,
|
filterHook,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "View Random",
|
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||||
onClick: viewRandom,
|
onClick: viewRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -119,8 +121,8 @@ export const StudioList: React.FC<IStudioList> = ({
|
|||||||
<DeleteEntityDialog
|
<DeleteEntityDialog
|
||||||
selected={selectedStudios}
|
selected={selectedStudios}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
singularEntity="studio"
|
singularEntity={intl.formatMessage({ id: "studio" })}
|
||||||
pluralEntity="studios"
|
pluralEntity={intl.formatMessage({ id: "studios" })}
|
||||||
destroyMutation={useStudiosDestroy}
|
destroyMutation={useStudiosDestroy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import {
|
|||||||
Form,
|
Form,
|
||||||
InputGroup,
|
InputGroup,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Icon } from "src/components/Shared";
|
import { Icon } from "src/components/Shared";
|
||||||
import { useConfiguration } from "src/core/StashService";
|
import { useConfiguration } from "src/core/StashService";
|
||||||
|
|
||||||
import { ITaggerConfig, ParseMode, ModeDesc } from "./constants";
|
import { ITaggerConfig, ParseMode } from "./constants";
|
||||||
|
|
||||||
interface IConfigProps {
|
interface IConfigProps {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -19,6 +20,7 @@ interface IConfigProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const stashConfig = useConfiguration();
|
const stashConfig = useConfiguration();
|
||||||
const blacklistRef = useRef<HTMLInputElement | null>(null);
|
const blacklistRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
@@ -59,24 +61,30 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
|||||||
<Collapse in={show}>
|
<Collapse in={show}>
|
||||||
<Card>
|
<Card>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<h4 className="col-12">Configuration</h4>
|
<h4 className="col-12">
|
||||||
|
<FormattedMessage id="configuration" />
|
||||||
|
</h4>
|
||||||
<hr className="w-100" />
|
<hr className="w-100" />
|
||||||
<Form className="col-md-6">
|
<Form className="col-md-6">
|
||||||
<Form.Group controlId="tag-males" className="align-items-center">
|
<Form.Group controlId="tag-males" className="align-items-center">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
label="Show male performers"
|
label={
|
||||||
|
<FormattedMessage id="component_tagger.config.show_male_label" />
|
||||||
|
}
|
||||||
checked={config.showMales}
|
checked={config.showMales}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setConfig({ ...config, showMales: e.currentTarget.checked })
|
setConfig({ ...config, showMales: e.currentTarget.checked })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text>
|
<Form.Text>
|
||||||
Toggle whether male performers will be available to tag.
|
<FormattedMessage id="component_tagger.config.show_male_desc" />
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group controlId="set-cover" className="align-items-center">
|
<Form.Group controlId="set-cover" className="align-items-center">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
label="Set scene cover image"
|
label={
|
||||||
|
<FormattedMessage id="component_tagger.config.set_cover_label" />
|
||||||
|
}
|
||||||
checked={config.setCoverImage}
|
checked={config.setCoverImage}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setConfig({
|
setConfig({
|
||||||
@@ -85,13 +93,17 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form.Text>Replace the scene cover if one is found.</Form.Text>
|
<Form.Text>
|
||||||
|
<FormattedMessage id="component_tagger.config.set_cover_desc" />
|
||||||
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group className="align-items-center">
|
<Form.Group className="align-items-center">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Form.Check
|
<Form.Check
|
||||||
id="tag-mode"
|
id="tag-mode"
|
||||||
label="Set tags"
|
label={
|
||||||
|
<FormattedMessage id="component_tagger.config.set_tag_label" />
|
||||||
|
}
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
checked={config.setTags}
|
checked={config.setTags}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
@@ -111,19 +123,25 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
|||||||
}
|
}
|
||||||
disabled={!config.setTags}
|
disabled={!config.setTags}
|
||||||
>
|
>
|
||||||
<option value="merge">Merge</option>
|
<option value="merge">
|
||||||
<option value="overwrite">Overwrite</option>
|
{intl.formatMessage({ id: "actions.merge" })}
|
||||||
|
</option>
|
||||||
|
<option value="overwrite">
|
||||||
|
{intl.formatMessage({ id: "actions.overwrite" })}
|
||||||
|
</option>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
<Form.Text>
|
<Form.Text>
|
||||||
Attach tags to scene, either by overwriting or merging with
|
<FormattedMessage id="component_tagger.config.set_tag_desc" />
|
||||||
existing tags on scene.
|
|
||||||
</Form.Text>
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="mode-select">
|
<Form.Group controlId="mode-select">
|
||||||
<div className="row no-gutters">
|
<div className="row no-gutters">
|
||||||
<Form.Label className="mr-4 mt-1">Query Mode:</Form.Label>
|
<Form.Label className="mr-4 mt-1">
|
||||||
|
<FormattedMessage id="component_tagger.config.query_mode_label" />
|
||||||
|
:
|
||||||
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
className="col-md-2 col-3 input-control"
|
className="col-md-2 col-3 input-control"
|
||||||
@@ -135,28 +153,58 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<option value="auto">Auto</option>
|
<option value="auto">
|
||||||
<option value="filename">Filename</option>
|
{intl.formatMessage({
|
||||||
<option value="dir">Dir</option>
|
id: "component_tagger.config.query_mode_auto",
|
||||||
<option value="path">Path</option>
|
})}
|
||||||
<option value="metadata">Metadata</option>
|
</option>
|
||||||
|
<option value="filename">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "component_tagger.config.query_mode_filename",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
|
<option value="dir">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "component_tagger.config.query_mode_dir",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
|
<option value="path">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "component_tagger.config.query_mode_path",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
|
<option value="metadata">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "component_tagger.config.query_mode_metadata",
|
||||||
|
})}
|
||||||
|
</option>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</div>
|
</div>
|
||||||
<Form.Text>{ModeDesc[config.mode]}</Form.Text>
|
<Form.Text>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: `component_tagger.config.query_mode_${config.mode}_desc`,
|
||||||
|
defaultMessage: "Unknown query mode",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</Form>
|
</Form>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<h5>Blacklist</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="component_tagger.config.blacklist_label" />
|
||||||
|
</h5>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<Form.Control className="text-input" ref={blacklistRef} />
|
<Form.Control className="text-input" ref={blacklistRef} />
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
<Button onClick={handleBlacklistAddition}>Add</Button>
|
<Button onClick={handleBlacklistAddition}>
|
||||||
|
<FormattedMessage id="actions.add" />
|
||||||
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
<div>
|
<div>
|
||||||
Blacklist items are excluded from queries. Note that they are
|
{intl.formatMessage(
|
||||||
regular expressions and also case-insensitive. Certain characters
|
{ id: "component_tagger.config.blacklist_desc" },
|
||||||
must be escaped with a backslash: <code>[\^$.|?*+()</code>
|
{ chars_require_escape: <code>[\^$.|?*+()</code> }
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{config.blacklist.map((item, index) => (
|
{config.blacklist.map((item, index) => (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -179,7 +227,7 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
|||||||
className="align-items-center row no-gutters mt-4"
|
className="align-items-center row no-gutters mt-4"
|
||||||
>
|
>
|
||||||
<Form.Label className="mr-4">
|
<Form.Label className="mr-4">
|
||||||
Active stash-box instance:
|
<FormattedMessage id="component_tagger.config.active_instance" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
import { Modal, Icon } from "src/components/Shared";
|
import { Modal, Icon } from "src/components/Shared";
|
||||||
import { TextUtils } from "src/utils";
|
import { TextUtils } from "src/utils";
|
||||||
@@ -17,6 +18,7 @@ const PerformerFieldSelect: React.FC<IProps> = ({
|
|||||||
excludedFields,
|
excludedFields,
|
||||||
onSelect,
|
onSelect,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [excluded, setExcluded] = useState<Record<string, boolean>>(
|
const [excluded, setExcluded] = useState<Record<string, boolean>>(
|
||||||
excludedFields.reduce((dict, field) => ({ ...dict, [field]: true }), {})
|
excludedFields.reduce((dict, field) => ({ ...dict, [field]: true }), {})
|
||||||
);
|
);
|
||||||
@@ -46,7 +48,7 @@ const PerformerFieldSelect: React.FC<IProps> = ({
|
|||||||
icon="list"
|
icon="list"
|
||||||
dialogClassName="FieldSelect"
|
dialogClassName="FieldSelect"
|
||||||
accept={{
|
accept={{
|
||||||
text: "Save",
|
text: intl.formatMessage({ id: "actions.save" }),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
onSelect(Object.keys(excluded).filter((f) => excluded[f])),
|
onSelect(Object.keys(excluded).filter((f) => excluded[f])),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
create = false,
|
create = false,
|
||||||
endpoint,
|
endpoint,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [imageIndex, setImageIndex] = useState(0);
|
const [imageIndex, setImageIndex] = useState(0);
|
||||||
const [imageState, setImageState] = useState<
|
const [imageState, setImageState] = useState<
|
||||||
"loading" | "error" | "loaded" | "empty"
|
"loading" | "error" | "loaded" | "empty"
|
||||||
@@ -109,7 +111,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
<Modal
|
<Modal
|
||||||
show={modalVisible}
|
show={modalVisible}
|
||||||
accept={{
|
accept={{
|
||||||
text: "Save",
|
text: intl.formatMessage({ id: "actions.save" }),
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
handlePerformerCreate(
|
handlePerformerCreate(
|
||||||
imageIndex,
|
imageIndex,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
import { SuccessIcon, PerformerSelect } from "src/components/Shared";
|
import { SuccessIcon, PerformerSelect } from "src/components/Shared";
|
||||||
@@ -112,12 +113,12 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className="row no-gutters my-2">
|
<div className="row no-gutters my-2">
|
||||||
<div className="entity-name">
|
<div className="entity-name">
|
||||||
Performer:
|
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||||
<b className="ml-2">{performer.name}</b>
|
<b className="ml-2">{performer.name}</b>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-auto">
|
<span className="ml-auto">
|
||||||
<SuccessIcon />
|
<SuccessIcon />
|
||||||
Matched:
|
<FormattedMessage id="component_tagger.verb_matched" />:
|
||||||
</span>
|
</span>
|
||||||
<b className="col-3 text-right">
|
<b className="col-3 text-right">
|
||||||
{stashData.findPerformers.performers[0].name}
|
{stashData.findPerformers.performers[0].name}
|
||||||
@@ -138,7 +139,7 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
/>
|
/>
|
||||||
<div className="entity-name">
|
<div className="entity-name">
|
||||||
Performer:
|
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||||
<b className="ml-2">{performer.name}</b>
|
<b className="ml-2">{performer.name}</b>
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
@@ -146,13 +147,13 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||||||
variant={selectedSource === "create" ? "primary" : "secondary"}
|
variant={selectedSource === "create" ? "primary" : "secondary"}
|
||||||
onClick={() => showModal(true)}
|
onClick={() => showModal(true)}
|
||||||
>
|
>
|
||||||
Create
|
<FormattedMessage id="actions.create" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
||||||
onClick={() => handlePerformerSkip()}
|
onClick={() => handlePerformerSkip()}
|
||||||
>
|
>
|
||||||
Skip
|
<FormattedMessage id="actions.skip" />
|
||||||
</Button>
|
</Button>
|
||||||
<PerformerSelect
|
<PerformerSelect
|
||||||
ids={selectedPerformer ? [selectedPerformer] : []}
|
ids={selectedPerformer ? [selectedPerformer] : []}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useReducer } from "react";
|
import React, { useState, useReducer } from "react";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { uniq } from "lodash";
|
import { uniq } from "lodash";
|
||||||
import { blobToBase64 } from "base64-blob";
|
import { blobToBase64 } from "base64-blob";
|
||||||
|
|
||||||
@@ -34,9 +35,14 @@ const getDurationStatus = (
|
|||||||
|
|
||||||
let match;
|
let match;
|
||||||
if (matchCount > 0)
|
if (matchCount > 0)
|
||||||
match = `Duration matches ${matchCount}/${durations.length} fingerprints`;
|
match = (
|
||||||
|
<FormattedMessage
|
||||||
|
id="component_tagger.results.fp_matches_multi"
|
||||||
|
values={{ matchCount, durationsLength: durations.length }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
else if (Math.abs(scene.duration - stashDuration) < 5)
|
else if (Math.abs(scene.duration - stashDuration) < 5)
|
||||||
match = "Duration is a match";
|
match = <FormattedMessage id="component_tagger.results.fp_matches" />;
|
||||||
|
|
||||||
if (match)
|
if (match)
|
||||||
return (
|
return (
|
||||||
@@ -67,7 +73,16 @@ const getFingerprintStatus = (
|
|||||||
return (
|
return (
|
||||||
<div className="font-weight-bold">
|
<div className="font-weight-bold">
|
||||||
<SuccessIcon className="mr-2" />
|
<SuccessIcon className="mr-2" />
|
||||||
{phashMatch ? "PHash" : "Checksum"} is a match
|
<FormattedMessage
|
||||||
|
id="component_tagger.results.hash_matches"
|
||||||
|
values={{
|
||||||
|
hash_type: (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`media_info.${phashMatch ? "phash" : "checksum"}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -116,6 +131,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const createStudio = useCreateStudio();
|
const createStudio = useCreateStudio();
|
||||||
const createPerformer = useCreatePerformer();
|
const createPerformer = useCreatePerformer();
|
||||||
const createTag = useCreateTag();
|
const createTag = useCreateTag();
|
||||||
@@ -400,7 +416,11 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
{scene?.studio?.name} • {scene?.date}
|
{scene?.studio?.name} • {scene?.date}
|
||||||
</h5>
|
</h5>
|
||||||
<div>
|
<div>
|
||||||
Performers: {scene?.performers?.map((p) => p.name).join(", ")}
|
{intl.formatMessage(
|
||||||
|
{ id: "countables.performers" },
|
||||||
|
{ count: scene?.performers?.length }
|
||||||
|
)}
|
||||||
|
: {scene?.performers?.map((p) => p.name).join(", ")}
|
||||||
</div>
|
</div>
|
||||||
{getDurationStatus(scene, stashScene.file?.duration)}
|
{getDurationStatus(scene, stashScene.file?.duration)}
|
||||||
{getFingerprintStatus(scene, stashScene)}
|
{getFingerprintStatus(scene, stashScene)}
|
||||||
@@ -440,7 +460,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
{saveState ? (
|
{saveState ? (
|
||||||
<LoadingIndicator inline small message="" />
|
<LoadingIndicator inline small message="" />
|
||||||
) : (
|
) : (
|
||||||
"Save"
|
<FormattedMessage id="actions.save" />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState, Dispatch, SetStateAction } from "react";
|
import React, { useEffect, useState, Dispatch, SetStateAction } from "react";
|
||||||
import { Button, ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
import { SuccessIcon, Modal, StudioSelect } from "src/components/Shared";
|
import { SuccessIcon, Modal, StudioSelect } from "src/components/Shared";
|
||||||
@@ -19,6 +20,7 @@ interface IStudioResultProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [selectedStudio, setSelectedStudio] = useState<string | null>();
|
const [selectedStudio, setSelectedStudio] = useState<string | null>();
|
||||||
const [modalVisible, showModal] = useState(false);
|
const [modalVisible, showModal] = useState(false);
|
||||||
const [selectedSource, setSelectedSource] = useState<
|
const [selectedSource, setSelectedSource] = useState<
|
||||||
@@ -94,8 +96,11 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="row no-gutters my-2">
|
<div className="row no-gutters my-2">
|
||||||
<div className="entity-name">
|
<div className="entity-name">
|
||||||
Studio:
|
<FormattedMessage
|
||||||
<b className="ml-2">{studio?.name}</b>
|
id="countables.studios"
|
||||||
|
values={{ count: stashIDData?.findStudios.studios.length }}
|
||||||
|
/>
|
||||||
|
:<b className="ml-2">{studio?.name}</b>
|
||||||
</div>
|
</div>
|
||||||
<span className="ml-auto">
|
<span className="ml-auto">
|
||||||
<SuccessIcon className="mr-2" />
|
<SuccessIcon className="mr-2" />
|
||||||
@@ -112,15 +117,22 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
|||||||
<div className="row no-gutters align-items-center mt-2">
|
<div className="row no-gutters align-items-center mt-2">
|
||||||
<Modal
|
<Modal
|
||||||
show={modalVisible}
|
show={modalVisible}
|
||||||
accept={{ text: "Save", onClick: handleStudioCreate }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.save" }),
|
||||||
|
onClick: handleStudioCreate,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => showModal(false), variant: "secondary" }}
|
cancel={{ onClick: () => showModal(false), variant: "secondary" }}
|
||||||
>
|
>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<strong className="col-2">Name:</strong>
|
<strong className="col-2">
|
||||||
|
<FormattedMessage id="name" />:
|
||||||
|
</strong>
|
||||||
<span className="col-10">{studio?.name}</span>
|
<span className="col-10">{studio?.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<strong className="col-2">URL:</strong>
|
<strong className="col-2">
|
||||||
|
<FormattedMessage id="url" />:
|
||||||
|
</strong>
|
||||||
<span className="col-10">{studio?.url ?? ""}</span>
|
<span className="col-10">{studio?.url ?? ""}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
@@ -132,21 +144,20 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<div className="entity-name">
|
<div className="entity-name">
|
||||||
Studio:
|
<FormattedMessage id="studios" />:<b className="ml-2">{studio?.name}</b>
|
||||||
<b className="ml-2">{studio?.name}</b>
|
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<Button
|
<Button
|
||||||
variant={selectedSource === "create" ? "primary" : "secondary"}
|
variant={selectedSource === "create" ? "primary" : "secondary"}
|
||||||
onClick={() => showModal(true)}
|
onClick={() => showModal(true)}
|
||||||
>
|
>
|
||||||
Create
|
<FormattedMessage id="actions.create" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
||||||
onClick={() => handleStudioSkip()}
|
onClick={() => handleStudioSkip()}
|
||||||
>
|
>
|
||||||
Skip
|
<FormattedMessage id="actions.skip" />
|
||||||
</Button>
|
</Button>
|
||||||
<StudioSelect
|
<StudioSelect
|
||||||
ids={selectedStudio ? [selectedStudio] : []}
|
ids={selectedStudio ? [selectedStudio] : []}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Card, Form, InputGroup } from "react-bootstrap";
|
import { Button, Card, Form, InputGroup } from "react-bootstrap";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { HashLink } from "react-router-hash-link";
|
import { HashLink } from "react-router-hash-link";
|
||||||
import { uniqBy } from "lodash";
|
import { uniqBy } from "lodash";
|
||||||
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
||||||
@@ -161,6 +162,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
queueFingerprintSubmission,
|
queueFingerprintSubmission,
|
||||||
clearSubmissionQueue,
|
clearSubmissionQueue,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [fingerprintError, setFingerprintError] = useState("");
|
const [fingerprintError, setFingerprintError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const queryString = useRef<Record<string, string>>({});
|
const queryString = useRef<Record<string, string>>({});
|
||||||
@@ -310,7 +312,10 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
|
|
||||||
const getFingerprintCountMessage = () => {
|
const getFingerprintCountMessage = () => {
|
||||||
const count = getFingerprintCount();
|
const count = getFingerprintCount();
|
||||||
return `${count > 0 ? count : "No"} new fingerprint matches found`;
|
return intl.formatMessage(
|
||||||
|
{ id: "component_tagger.results.fp_found" },
|
||||||
|
{ fpCount: count }
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleHideUnmatchedScenes = () => {
|
const toggleHideUnmatchedScenes = () => {
|
||||||
@@ -359,14 +364,18 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
if (!isTagged && hasStashIDs) {
|
if (!isTagged && hasStashIDs) {
|
||||||
mainContent = (
|
mainContent = (
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<h5 className="text-bold">Scene already tagged</h5>
|
<h5 className="text-bold">
|
||||||
|
<FormattedMessage id="component_tagger.results.match_failed_already_tagged" />
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (!isTagged && !hasStashIDs) {
|
} else if (!isTagged && !hasStashIDs) {
|
||||||
mainContent = (
|
mainContent = (
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
<InputGroup.Prepend>
|
<InputGroup.Prepend>
|
||||||
<InputGroup.Text>Query</InputGroup.Text>
|
<InputGroup.Text>
|
||||||
|
<FormattedMessage id="component_tagger.noun_query" />
|
||||||
|
</InputGroup.Text>
|
||||||
</InputGroup.Prepend>
|
</InputGroup.Prepend>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -392,7 +401,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Search
|
<FormattedMessage id="actions.search" />
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
@@ -400,7 +409,9 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
} else if (isTagged) {
|
} else if (isTagged) {
|
||||||
mainContent = (
|
mainContent = (
|
||||||
<div className="d-flex flex-column text-right">
|
<div className="d-flex flex-column text-right">
|
||||||
<h5>Scene successfully tagged:</h5>
|
<h5>
|
||||||
|
<FormattedMessage id="component_tagger.results.match_success" />
|
||||||
|
</h5>
|
||||||
<h6>
|
<h6>
|
||||||
<Link className="bold" to={sceneLink}>
|
<Link className="bold" to={sceneLink}>
|
||||||
{taggedScenes[scene.id].title}
|
{taggedScenes[scene.id].title}
|
||||||
@@ -438,7 +449,9 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
);
|
);
|
||||||
} else if (searchResults[scene.id]?.length === 0) {
|
} else if (searchResults[scene.id]?.length === 0) {
|
||||||
subContent = (
|
subContent = (
|
||||||
<div className="text-danger font-weight-bold">No results found.</div>
|
<div className="text-danger font-weight-bold">
|
||||||
|
<FormattedMessage id="component_tagger.results.match_failed_no_result" />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -544,7 +557,16 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
<div className="mr-2">
|
<div className="mr-2">
|
||||||
{(getFingerprintCount() > 0 || hideUnmatched) && (
|
{(getFingerprintCount() > 0 || hideUnmatched) && (
|
||||||
<Button onClick={toggleHideUnmatchedScenes}>
|
<Button onClick={toggleHideUnmatchedScenes}>
|
||||||
{hideUnmatched ? "Show" : "Hide"} unmatched scenes
|
<FormattedMessage
|
||||||
|
id="component_tagger.verb_toggle_unmatched"
|
||||||
|
values={{
|
||||||
|
toggle: (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`actions.${hideUnmatched ? "hide" : "show"}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -558,7 +580,10 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
<LoadingIndicator message="" inline small />
|
<LoadingIndicator message="" inline small />
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
Submit <b>{fingerprintQueue.length}</b> Fingerprints
|
<FormattedMessage
|
||||||
|
id="component_tagger.verb_submit_fp"
|
||||||
|
values={{ fpCount: fingerprintQueue.length }}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -568,7 +593,11 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
|||||||
onClick={handleFingerprintSearch}
|
onClick={handleFingerprintSearch}
|
||||||
disabled={!canFingerprintSearch() && !loadingFingerprints}
|
disabled={!canFingerprintSearch() && !loadingFingerprints}
|
||||||
>
|
>
|
||||||
{canFingerprintSearch() && <span>Match Fingerprints</span>}
|
{canFingerprintSearch() && (
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({ id: "component_tagger.verb_match_fp" })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{!canFingerprintSearch() && getFingerprintCountMessage()}
|
{!canFingerprintSearch() && getFingerprintCountMessage()}
|
||||||
{loadingFingerprints && <LoadingIndicator message="" inline small />}
|
{loadingFingerprints && <LoadingIndicator message="" inline small />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -638,7 +667,17 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||||||
<>
|
<>
|
||||||
<div className="row mb-2 no-gutters">
|
<div className="row mb-2 no-gutters">
|
||||||
<Button onClick={() => setShowConfig(!showConfig)} variant="link">
|
<Button onClick={() => setShowConfig(!showConfig)} variant="link">
|
||||||
{showConfig ? "Hide" : "Show"} Configuration
|
<FormattedMessage
|
||||||
|
id="component_tagger.verb_toggle_config"
|
||||||
|
values={{
|
||||||
|
toggle: (
|
||||||
|
<FormattedMessage
|
||||||
|
id={`actions.${showConfig ? "hide" : "show"}`}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
configuration: <FormattedMessage id="configuration" />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="ml-auto"
|
className="ml-auto"
|
||||||
@@ -646,7 +685,7 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
|||||||
title="Help"
|
title="Help"
|
||||||
variant="link"
|
variant="link"
|
||||||
>
|
>
|
||||||
Help
|
<FormattedMessage id="help" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -24,14 +24,6 @@ export const initialConfig: ITaggerConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
|
export type ParseMode = "auto" | "filename" | "dir" | "path" | "metadata";
|
||||||
export const ModeDesc = {
|
|
||||||
auto: "Uses metadata if present, or filename",
|
|
||||||
metadata: "Only uses metadata",
|
|
||||||
filename: "Only uses filename",
|
|
||||||
dir: "Only uses parent directory of video file",
|
|
||||||
path: "Uses entire file path",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ITaggerConfig {
|
export interface ITaggerConfig {
|
||||||
blacklist: string[];
|
blacklist: string[];
|
||||||
showMales: boolean;
|
showMales: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap";
|
import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { HashLink } from "react-router-hash-link";
|
import { HashLink } from "react-router-hash-link";
|
||||||
import { useLocalForage } from "src/hooks";
|
import { useLocalForage } from "src/hooks";
|
||||||
@@ -51,6 +52,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
|||||||
onBatchAdd,
|
onBatchAdd,
|
||||||
onBatchUpdate,
|
onBatchUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [searchResults, setSearchResults] = useState<
|
const [searchResults, setSearchResults] = useState<
|
||||||
Record<string, IStashBoxPerformer[]>
|
Record<string, IStashBoxPerformer[]>
|
||||||
@@ -245,7 +247,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Search
|
<FormattedMessage id="actions.search" />
|
||||||
</Button>
|
</Button>
|
||||||
</InputGroup.Append>
|
</InputGroup.Append>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
@@ -384,7 +386,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
|||||||
header="Update Performers"
|
header="Update Performers"
|
||||||
accept={{ text: "Update Performers", onClick: handleBatchUpdate }}
|
accept={{ text: "Update Performers", onClick: handleBatchUpdate }}
|
||||||
cancel={{
|
cancel={{
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
onClick: () => setShowBatchUpdate(false),
|
onClick: () => setShowBatchUpdate(false),
|
||||||
}}
|
}}
|
||||||
@@ -454,7 +456,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
|||||||
header="Add New Performers"
|
header="Add New Performers"
|
||||||
accept={{ text: "Add Performers", onClick: handleBatchAdd }}
|
accept={{ text: "Add Performers", onClick: handleBatchAdd }}
|
||||||
cancel={{
|
cancel={{
|
||||||
text: "Cancel",
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
onClick: () => setShowBatchAdd(false),
|
onClick: () => setShowBatchAdd(false),
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const parsePath = (filePath: string) => {
|
|||||||
const ext = fileName.match(/\.[a-z0-9]*$/)?.[0] ?? "";
|
const ext = fileName.match(/\.[a-z0-9]*$/)?.[0] ?? "";
|
||||||
const file = fileName.slice(0, ext.length * -1);
|
const file = fileName.slice(0, ext.length * -1);
|
||||||
const paths =
|
const paths =
|
||||||
pathComponents.length > 2
|
pathComponents.length >= 2
|
||||||
? pathComponents.slice(0, pathComponents.length - 2)
|
? pathComponents.slice(0, pathComponents.length - 2)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Tabs, Tab } from "react-bootstrap";
|
import { Tabs, Tab } 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 cx from "classnames";
|
import cx from "classnames";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ interface ITabParams {
|
|||||||
export const Tag: React.FC = () => {
|
export const Tag: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
const intl = useIntl();
|
||||||
const { tab = "scenes", id = "new" } = useParams<ITabParams>();
|
const { tab = "scenes", id = "new" } = useParams<ITabParams>();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
@@ -170,10 +172,23 @@ export const Tag: React.FC = () => {
|
|||||||
<Modal
|
<Modal
|
||||||
show={isDeleteAlertOpen}
|
show={isDeleteAlertOpen}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
accept={{
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
variant: "danger",
|
||||||
|
onClick: onDelete,
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
>
|
>
|
||||||
<p>Are you sure you want to delete {tag?.name ?? "tag"}?</p>
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{
|
||||||
|
entityName:
|
||||||
|
tag?.name ??
|
||||||
|
intl.formatMessage({ id: "tag" }).toLocaleLowerCase(),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -248,19 +263,28 @@ export const Tag: React.FC = () => {
|
|||||||
activeKey={activeTabKey}
|
activeKey={activeTabKey}
|
||||||
onSelect={setActiveTabKey}
|
onSelect={setActiveTabKey}
|
||||||
>
|
>
|
||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||||
<TagScenesPanel tag={tag} />
|
<TagScenesPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="images" title="Images">
|
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||||
<TagImagesPanel tag={tag} />
|
<TagImagesPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="galleries" title="Galleries">
|
<Tab
|
||||||
|
eventKey="galleries"
|
||||||
|
title={intl.formatMessage({ id: "galleries" })}
|
||||||
|
>
|
||||||
<TagGalleriesPanel tag={tag} />
|
<TagGalleriesPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="markers" title="Markers">
|
<Tab
|
||||||
|
eventKey="markers"
|
||||||
|
title={intl.formatMessage({ id: "markers" })}
|
||||||
|
>
|
||||||
<TagMarkersPanel tag={tag} />
|
<TagMarkersPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="performers" title="Performers">
|
<Tab
|
||||||
|
eventKey="performers"
|
||||||
|
title={intl.formatMessage({ id: "performers" })}
|
||||||
|
>
|
||||||
<TagPerformersPanel tag={tag} />
|
<TagPerformersPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Badge } from "react-bootstrap";
|
import { Badge } from "react-bootstrap";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
|
||||||
interface ITagDetails {
|
interface ITagDetails {
|
||||||
@@ -14,7 +15,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<dl className="row">
|
<dl className="row">
|
||||||
<dt className="col-3 col-xl-2">Aliases</dt>
|
<dt className="col-3 col-xl-2">
|
||||||
|
<FormattedMessage id="aliases" />
|
||||||
|
</dt>
|
||||||
<dd className="col-9 col-xl-10">
|
<dd className="col-9 col-xl-10">
|
||||||
{tag.aliases.map((a) => (
|
{tag.aliases.map((a) => (
|
||||||
<Badge className="tag-item" variant="secondary">
|
<Badge className="tag-item" variant="secondary">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared";
|
import { DetailsEditNavbar } from "src/components/Shared";
|
||||||
@@ -27,6 +28,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
setImage,
|
setImage,
|
||||||
}) => {
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const isNew = tag === undefined;
|
const isNew = tag === undefined;
|
||||||
@@ -102,7 +104,14 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isNew && <h2>Add Tag</h2>}
|
{isNew && (
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage
|
||||||
|
id="actions.add_entity"
|
||||||
|
values={{ entityType: intl.formatMessage({ id: "tag" }) }}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
|
||||||
<Prompt
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
@@ -117,7 +126,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
||||||
<Form.Group controlId="name" as={Row}>
|
<Form.Group controlId="name" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Name
|
<FormattedMessage id="name" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
@@ -134,7 +143,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
|
|
||||||
<Form.Group controlId="aliases" as={Row}>
|
<Form.Group controlId="aliases" as={Row}>
|
||||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||||
Aliases
|
<FormattedMessage id="aliases" />
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<StringListInput
|
<StringListInput
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
useTagsDestroy,
|
useTagsDestroy,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
import { NavUtils } from "src/utils";
|
import { NavUtils } from "src/utils";
|
||||||
import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared";
|
import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared";
|
||||||
import { TagCard } from "./TagCard";
|
import { TagCard } from "./TagCard";
|
||||||
@@ -38,22 +38,23 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
|
|
||||||
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||||
const [isExportAll, setIsExportAll] = useState(false);
|
const [isExportAll, setIsExportAll] = useState(false);
|
||||||
|
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
text: "View Random",
|
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||||
onClick: viewRandom,
|
onClick: viewRandom,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export...",
|
text: intl.formatMessage({ id: "actions.export" }),
|
||||||
onClick: onExport,
|
onClick: onExport,
|
||||||
isDisplayed: showWhenSelected,
|
isDisplayed: showWhenSelected,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: "Export all...",
|
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||||
onClick: onExportAll,
|
onClick: onExportAll,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -134,8 +135,8 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
<DeleteEntityDialog
|
<DeleteEntityDialog
|
||||||
selected={selectedTags}
|
selected={selectedTags}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
singularEntity="tag"
|
singularEntity={intl.formatMessage({ id: "tag" })}
|
||||||
pluralEntity="tags"
|
pluralEntity={intl.formatMessage({ id: "tags" })}
|
||||||
destroyMutation={useTagsDestroy}
|
destroyMutation={useTagsDestroy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -164,7 +165,9 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||||
Toast.success({ content: "Started auto tagging" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -173,7 +176,16 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
try {
|
try {
|
||||||
await deleteTag();
|
await deleteTag();
|
||||||
Toast.success({ content: "Deleted tag" });
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "toast.delete_past_tense" },
|
||||||
|
{
|
||||||
|
count: 1,
|
||||||
|
singularEntity: intl.formatMessage({ id: "tag" }),
|
||||||
|
pluralEntity: intl.formatMessage({ id: "tags" }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
setDeletingTag(null);
|
setDeletingTag(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -212,11 +224,18 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
onHide={() => {}}
|
onHide={() => {}}
|
||||||
show={!!deletingTag}
|
show={!!deletingTag}
|
||||||
icon="trash-alt"
|
icon="trash-alt"
|
||||||
accept={{ onClick: onDelete, variant: "danger", text: "Delete" }}
|
accept={{
|
||||||
|
onClick: onDelete,
|
||||||
|
variant: "danger",
|
||||||
|
text: intl.formatMessage({ id: "actions.delete" }),
|
||||||
|
}}
|
||||||
cancel={{ onClick: () => setDeletingTag(null) }}
|
cancel={{ onClick: () => setDeletingTag(null) }}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Are you sure you want to delete {deletingTag && deletingTag.name}?
|
<FormattedMessage
|
||||||
|
id="dialogs.delete_confirm"
|
||||||
|
values={{ entityName: deletingTag && deletingTag.name }}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@@ -232,14 +251,20 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
className="tag-list-button"
|
className="tag-list-button"
|
||||||
onClick={() => onAutoTag(tag)}
|
onClick={() => onAutoTag(tag)}
|
||||||
>
|
>
|
||||||
Auto Tag
|
<FormattedMessage id="actions.auto_tag" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" className="tag-list-button">
|
<Button variant="secondary" className="tag-list-button">
|
||||||
<Link
|
<Link
|
||||||
to={NavUtils.makeTagScenesUrl(tag)}
|
to={NavUtils.makeTagScenesUrl(tag)}
|
||||||
className="tag-list-anchor"
|
className="tag-list-anchor"
|
||||||
>
|
>
|
||||||
Scenes: <FormattedNumber value={tag.scene_count ?? 0} />
|
<FormattedMessage
|
||||||
|
id="countables.scenes"
|
||||||
|
values={{
|
||||||
|
count: tag.scene_count ?? 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
: <FormattedNumber value={tag.scene_count ?? 0} />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" className="tag-list-button">
|
<Button variant="secondary" className="tag-list-button">
|
||||||
@@ -247,12 +272,17 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
|||||||
to={NavUtils.makeTagSceneMarkersUrl(tag)}
|
to={NavUtils.makeTagSceneMarkersUrl(tag)}
|
||||||
className="tag-list-anchor"
|
className="tag-list-anchor"
|
||||||
>
|
>
|
||||||
Markers:{" "}
|
<FormattedMessage
|
||||||
<FormattedNumber value={tag.scene_marker_count ?? 0} />
|
id="countables.markers"
|
||||||
|
values={{
|
||||||
|
count: tag.scene_marker_count ?? 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
: <FormattedNumber value={tag.scene_marker_count ?? 0} />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<span className="tag-list-count">
|
<span className="tag-list-count">
|
||||||
Total:{" "}
|
<FormattedMessage id="total" />:{" "}
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={(tag.scene_count || 0) + (tag.scene_marker_count || 0)}
|
value={(tag.scene_count || 0) + (tag.scene_marker_count || 0)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
{
|
|
||||||
"age": "Age",
|
|
||||||
"aliases": "Aliases",
|
|
||||||
"average_resolution": "Average Resolution",
|
|
||||||
"birth_year": "Birth Year",
|
|
||||||
"birthdate": "Birthdate",
|
|
||||||
"bitrate": "Bit Rate",
|
|
||||||
"career_length": "Career Length",
|
|
||||||
"country": "Country",
|
|
||||||
"created_at": "Created At",
|
|
||||||
"criterion_modifier": {
|
|
||||||
"equals": "is",
|
|
||||||
"not_equals": "is not",
|
|
||||||
"greater_than": "is greater than",
|
|
||||||
"less_than": "is less than",
|
|
||||||
"is_null": "is null",
|
|
||||||
"not_null": "is not null",
|
|
||||||
"includes": "includes",
|
|
||||||
"includes_all": "includes all",
|
|
||||||
"excludes": "excludes",
|
|
||||||
"matches_regex": "matches regex",
|
|
||||||
"not_matches_regex": "not matches regex"
|
|
||||||
},
|
|
||||||
"date": "Date",
|
|
||||||
"death_year": "Death Year",
|
|
||||||
"developmentVersion": "Development Version",
|
|
||||||
"donate": "Donate",
|
|
||||||
"duration": "Duration",
|
|
||||||
"ethnicity": "Ethnicity",
|
|
||||||
"eye_color": "Eye Colour",
|
|
||||||
"fake_tits": "Fake Tits",
|
|
||||||
"favourite": "Favourite",
|
|
||||||
"file_mod_time": "File Modification Time",
|
|
||||||
"filesize": "File Size",
|
|
||||||
"framerate": "Frame Rate",
|
|
||||||
"galleries": "Galleries",
|
|
||||||
"gallery_count": "Gallery Count",
|
|
||||||
"gender": "Gender",
|
|
||||||
"hair_color": "Hair Colour",
|
|
||||||
"hasMarkers": "Has Markers",
|
|
||||||
"height": "Height",
|
|
||||||
"image_count": "Image Count",
|
|
||||||
"images": "Images",
|
|
||||||
"images-size": "Images size",
|
|
||||||
"isMissing": "Is Missing",
|
|
||||||
"interactive": "Interactive",
|
|
||||||
"library-size": "Library size",
|
|
||||||
"marker_count": "Marker Count",
|
|
||||||
"markers": "Markers",
|
|
||||||
"measurements": "Measurements",
|
|
||||||
"movie_scene_number": "Movie Scene Number",
|
|
||||||
"movies": "Movies",
|
|
||||||
"name": "Name",
|
|
||||||
"new": "New",
|
|
||||||
"none": "None",
|
|
||||||
"o_counter": "O-Counter",
|
|
||||||
"organized": "Organised",
|
|
||||||
"parent_studios": "Parent Studios",
|
|
||||||
"path": "Path",
|
|
||||||
"performerTags": "Performer Tags",
|
|
||||||
"performer_count": "Performer Count",
|
|
||||||
"performers": "Performers",
|
|
||||||
"piercings": "Piercings",
|
|
||||||
"random": "Random",
|
|
||||||
"rating": "Rating",
|
|
||||||
"resolution": "Resolution",
|
|
||||||
"sceneTagger": "Scene Tagger",
|
|
||||||
"sceneTags": "Scene Tags",
|
|
||||||
"scene_count": "Scene Count",
|
|
||||||
"scene_id": "Scene ID",
|
|
||||||
"scenes": "Scenes",
|
|
||||||
"scenes-size": "Scenes size",
|
|
||||||
"scenes_updated_at": "Scene Updated At",
|
|
||||||
"seconds": "Seconds",
|
|
||||||
"stash_id": "Stash ID",
|
|
||||||
"studio_depth": "Levels (empty for all)",
|
|
||||||
"studios": "Studios",
|
|
||||||
"tag_count": "Tag Count",
|
|
||||||
"tags": "Tags",
|
|
||||||
"tattoos": "Tattoos",
|
|
||||||
"title": "Title",
|
|
||||||
"up-dir": "Up a directory",
|
|
||||||
"updated_at": "Updated At",
|
|
||||||
"url": "URL",
|
|
||||||
"weight": "Weight"
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"developmentVersion": "開發版本",
|
|
||||||
"donate": "贊助",
|
|
||||||
"favourite": "最愛",
|
|
||||||
"images": "圖片",
|
|
||||||
"images-size": "圖片大小",
|
|
||||||
"galleries": "圖庫",
|
|
||||||
"library-size": "收藏庫大小",
|
|
||||||
"markers": "標記",
|
|
||||||
"movies": "影片",
|
|
||||||
"new": "新",
|
|
||||||
"organized": "已整理",
|
|
||||||
"performers": "演員",
|
|
||||||
"scenes": "場景",
|
|
||||||
"scenes-size": "場景收藏大小",
|
|
||||||
"sceneTagger": "標記場景",
|
|
||||||
"studios": "工作室",
|
|
||||||
"tags": "標籤",
|
|
||||||
"up-dir": "往上一層"
|
|
||||||
}
|
|
||||||
581
ui/v2.5/src/locales/en-GB.json
Normal file
581
ui/v2.5/src/locales/en-GB.json
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
{
|
||||||
|
"actions": {
|
||||||
|
"add": "Add",
|
||||||
|
"add_directory": "Add Directory",
|
||||||
|
"add_entity": "Add {entityType}",
|
||||||
|
"add_to_entity": "Add to {entityType}",
|
||||||
|
"allow": "Allow",
|
||||||
|
"allow_temporarily": "Allow temporarily",
|
||||||
|
"apply": "Apply",
|
||||||
|
"auto_tag": "Auto Tag",
|
||||||
|
"backup": "Backup",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"clean": "Clean",
|
||||||
|
"clear_back_image": "Clear back image",
|
||||||
|
"clear_front_image": "Clear front image",
|
||||||
|
"clear_image": "Clear Image",
|
||||||
|
"close": "Close",
|
||||||
|
"create": "Create",
|
||||||
|
"create_entity": "Create {entityType}",
|
||||||
|
"create_marker": "Create Marker",
|
||||||
|
"created_entity": "Created {entity_type}: {entity_name}",
|
||||||
|
"delete": "Delete",
|
||||||
|
"delete_entity": "Delete {entityType}",
|
||||||
|
"delete_file": "Delete file",
|
||||||
|
"delete_generated_supporting_files": "Delete generated supporting files",
|
||||||
|
"disallow": "Disallow",
|
||||||
|
"download": "Download",
|
||||||
|
"download_backup": "Download Backup",
|
||||||
|
"edit": "Edit",
|
||||||
|
"export": "Export…",
|
||||||
|
"export_all": "Export all…",
|
||||||
|
"find": "Find",
|
||||||
|
"from_file": "From file…",
|
||||||
|
"from_url": "From URL…",
|
||||||
|
"full_export": "Full Export",
|
||||||
|
"full_import": "Full Import",
|
||||||
|
"generate": "Generate",
|
||||||
|
"generate_thumb_default": "Generate default thumbnail",
|
||||||
|
"generate_thumb_from_current": "Generate thumbnail from current",
|
||||||
|
"hash_migration": "hash migration",
|
||||||
|
"hide": "Hide",
|
||||||
|
"import": "Import…",
|
||||||
|
"import_from_file": "Import from file",
|
||||||
|
"merge": "Merge",
|
||||||
|
"not_running": "not running",
|
||||||
|
"overwrite": "Overwrite",
|
||||||
|
"play_random": "Play Random",
|
||||||
|
"play_selected": "Play selected",
|
||||||
|
"preview": "Preview",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"reload_plugins": "Reload plugins",
|
||||||
|
"reload_scrapers": "Reload scrapers",
|
||||||
|
"remove": "Remove",
|
||||||
|
"rename_gen_files": "Rename generated files",
|
||||||
|
"rescan": "Rescan",
|
||||||
|
"reshuffle": "Reshuffle",
|
||||||
|
"running": "running",
|
||||||
|
"save": "Save",
|
||||||
|
"scan": "Scan",
|
||||||
|
"scrape_with": "Scrape with…",
|
||||||
|
"search": "Search",
|
||||||
|
"select_all": "Select All",
|
||||||
|
"select_none": "Select None",
|
||||||
|
"selective_auto_tag": "Selective Auto Tag",
|
||||||
|
"selective_scan": "Selective Scan",
|
||||||
|
"set_back_image": "Back image…",
|
||||||
|
"set_front_image": "Front image…",
|
||||||
|
"set_image": "Set image…",
|
||||||
|
"show": "Show",
|
||||||
|
"skip": "Skip",
|
||||||
|
"tasks": {
|
||||||
|
"clean_confirm_message": "Are you sure you want to Clean? This will delete database information and generated content for all scenes and galleries that are no longer found in the filesystem.",
|
||||||
|
"dry_mode_selected": "Dry Mode selected. No actual deleting will take place, only logging.",
|
||||||
|
"import_warning": "Are you sure you want to import? This will delete the database and re-import from your exported metadata."
|
||||||
|
},
|
||||||
|
"temp_disable": "Disable temporarily…",
|
||||||
|
"temp_enable": "Enable temporarily…",
|
||||||
|
"view_random": "View Random"
|
||||||
|
},
|
||||||
|
"actions_name": "Actions",
|
||||||
|
"age": "Age",
|
||||||
|
"aliases": "Aliases",
|
||||||
|
"also_known_as": "Also known as",
|
||||||
|
"ascending": "Ascending",
|
||||||
|
"average_resolution": "Average Resolution",
|
||||||
|
"birth_year": "Birth Year",
|
||||||
|
"birthdate": "Birthdate",
|
||||||
|
"bitrate": "Bit Rate",
|
||||||
|
"career_length": "Career Length",
|
||||||
|
"child_studios": "Child Studios",
|
||||||
|
"component_tagger": {
|
||||||
|
"config": {
|
||||||
|
"active_instance": "Active stash-box instance:",
|
||||||
|
"blacklist_desc": "Blacklist items are excluded from queries. Note that they are regular expressions and also case-insensitive. Certain characters must be escaped with a backslash: {chars_require_escape}",
|
||||||
|
"blacklist_label": "Blacklist",
|
||||||
|
"query_mode_auto": "Auto",
|
||||||
|
"query_mode_auto_desc": "Uses metadata if present, or filename",
|
||||||
|
"query_mode_dir": "Dir",
|
||||||
|
"query_mode_dir_desc": "Only uses parent directory of video file",
|
||||||
|
"query_mode_filename": "Filename",
|
||||||
|
"query_mode_filename_desc": "Only uses filename",
|
||||||
|
"query_mode_label": "Query Mode",
|
||||||
|
"query_mode_metadata": "Metadata",
|
||||||
|
"query_mode_metadata_desc": "Only uses metadata",
|
||||||
|
"query_mode_path": "Path",
|
||||||
|
"query_mode_path_desc": "Uses entire file path",
|
||||||
|
"set_cover_desc": "Replace the scene cover if one is found.",
|
||||||
|
"set_cover_label": "Set scene cover image",
|
||||||
|
"set_tag_desc": "Attach tags to scene, either by overwriting or merging with existing tags on scene.",
|
||||||
|
"set_tag_label": "Set tags",
|
||||||
|
"show_male_desc": "Toggle whether male performers will be available to tag.",
|
||||||
|
"show_male_label": "Show male performers"
|
||||||
|
},
|
||||||
|
"noun_query": "Query",
|
||||||
|
"results": {
|
||||||
|
"fp_found": "{fpCount, plural, =0 {No new fingerprint matches found} other {# new fingerprint matches found}}",
|
||||||
|
"fp_matches": "Duration is a match",
|
||||||
|
"fp_matches_multi": "Duration matches {matchCount}/{durationsLength} fingerprint(s)",
|
||||||
|
"hash_matches": "{hash_type} is a match",
|
||||||
|
"match_failed_already_tagged": "Scene already tagged",
|
||||||
|
"match_failed_no_result": "No results found",
|
||||||
|
"match_success": "Scene successfully tagged"
|
||||||
|
},
|
||||||
|
"verb_match_fp": "Match Fingerprints",
|
||||||
|
"verb_matched": "Matched",
|
||||||
|
"verb_submit_fp": "Submit {fpCount, plural, one{# Fingerprint} other{# Fingerprints}}",
|
||||||
|
"verb_toggle_config": "{toggle} {configuration}",
|
||||||
|
"verb_toggle_unmatched": "{toggle} unmatched scenes"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"about": {
|
||||||
|
"build_hash": "Build hash:",
|
||||||
|
"build_time": "Build time:",
|
||||||
|
"check_for_new_version": "Check for new version",
|
||||||
|
"latest_version_build_hash": "Latest Version Build Hash:",
|
||||||
|
"new_version_notice": "[NEW]",
|
||||||
|
"stash_discord": "Join our {url} channel",
|
||||||
|
"stash_home": "Stash home at {url}",
|
||||||
|
"stash_open_collective": "Support us through {url}",
|
||||||
|
"stash_wiki": "Stash {url} page",
|
||||||
|
"version": "Version"
|
||||||
|
},
|
||||||
|
"categories": {
|
||||||
|
"about": "About",
|
||||||
|
"interface": "Interface",
|
||||||
|
"logs": "Logs",
|
||||||
|
"plugins": "Plugins",
|
||||||
|
"scrapers": "Scrapers",
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"tools": "Tools"
|
||||||
|
},
|
||||||
|
"dlna": {
|
||||||
|
"allow_temp_ip": "Allow {tempIP}",
|
||||||
|
"allowed_ip_addresses": "Allowed IP addresses",
|
||||||
|
"default_ip_whitelist": "Default IP Whitelist",
|
||||||
|
"default_ip_whitelist_desc": "Default IP addresses allow to access DLNA. Use {wildcard} to allow all IP addresses.",
|
||||||
|
"enabled_by_default": "Enabled by default",
|
||||||
|
"network_interfaces": "Interfaces",
|
||||||
|
"network_interfaces_desc": "Interfaces to expose DLNA server on. An empty list results in running on all interfaces. Requires DLNA restart after changing.",
|
||||||
|
"recent_ip_addresses": "Recent IP addresses",
|
||||||
|
"server_display_name": "Server Display Name",
|
||||||
|
"server_display_name_desc": "Display name for the DLNA server. Defaults to {server_name} if empty.",
|
||||||
|
"until_restart": "until restart"
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"auth": {
|
||||||
|
"api_key": "API Key",
|
||||||
|
"api_key_desc": "API key for external systems. Only required when username/password is configured. Username must be saved before generating API key.",
|
||||||
|
"authentication": "Authentication",
|
||||||
|
"clear_api_key": "Clear API key",
|
||||||
|
"generate_api_key": "Generate API key",
|
||||||
|
"log_file": "Log file",
|
||||||
|
"log_file_desc": "Path to the file to output logging to. Blank to disable file logging. Requires restart.",
|
||||||
|
"log_http": "Log http access",
|
||||||
|
"log_http_desc": "Logs http access to the terminal. Requires restart.",
|
||||||
|
"log_to_terminal": "Log to terminal",
|
||||||
|
"log_to_terminal_desc": "Logs to the terminal in addition to a file. Always true if file logging is disabled. Requires restart.",
|
||||||
|
"maximum_session_age": "Maximum Session Age",
|
||||||
|
"maximum_session_age_desc": "Maximum idle time before a login session is expired, in seconds.",
|
||||||
|
"password": "Password",
|
||||||
|
"password_desc": "Password to access Stash. Leave blank to disable user authentication",
|
||||||
|
"stash-box_integration": "Stash-box integration",
|
||||||
|
"username": "Username",
|
||||||
|
"username_desc": "Username to access Stash. Leave blank to disable user authentication"
|
||||||
|
},
|
||||||
|
"cache_location": "Directory location of the cache",
|
||||||
|
"cache_path_head": "Cache Path",
|
||||||
|
"calculate_md5_and_ohash_desc": "Calculate MD5 checksum in addition to oshash. Enabling will cause initial scans to be slower. File naming hash must be set to oshash to disable MD5 calculation.",
|
||||||
|
"calculate_md5_and_ohash_label": "Calculate MD5 for videos",
|
||||||
|
"check_for_insecure_certificates": "Check for insecure certificates",
|
||||||
|
"check_for_insecure_certificates_desc": "Some sites use insecure ssl certificates. When unticked the scraper skips the insecure certificates check and allows scraping of those sites. If you get a certificate error when scraping untick this.",
|
||||||
|
"chrome_cdp_path": "Chrome CDP path",
|
||||||
|
"chrome_cdp_path_desc": "File path to the Chrome executable, or a remote address (starting with http:// or https://, for example http://localhost:9222/json/version) to a Chrome instance.",
|
||||||
|
"create_galleries_from_folders_desc": "If true, creates galleries from folders containing images.",
|
||||||
|
"create_galleries_from_folders_label": "Create galleries from folders containing images",
|
||||||
|
"db_path_head": "Database Path",
|
||||||
|
"directory_locations_to_your_content": "Directory locations to your content",
|
||||||
|
"exclude_image": "Exclude Image",
|
||||||
|
"exclude_video": "Exclude Video",
|
||||||
|
"excluded_image_gallery_patterns_desc": "Regexps of image and gallery files/paths to exclude from Scan and add to Clean",
|
||||||
|
"excluded_image_gallery_patterns_head": "Excluded Image/Gallery Patterns",
|
||||||
|
"excluded_video_patterns_desc": "Regexps of video files/paths to exclude from Scan and add to Clean",
|
||||||
|
"excluded_video_patterns_head": "Excluded Video Patterns",
|
||||||
|
"gallery_ext_desc": "Comma-delimited list of file extensions that will be identified as gallery zip files.",
|
||||||
|
"gallery_ext_head": "Gallery zip Extensions",
|
||||||
|
"generated_file_naming_hash_desc": "Use MD5 or oshash for generated file naming. Changing this requires that all scenes have the applicable MD5/oshash value populated. After changing this value, existing generated files will need to be migrated or regenerated. See Tasks page for migration.",
|
||||||
|
"generated_file_naming_hash_head": "Generated file naming hash",
|
||||||
|
"generated_files_location": "Directory location for the generated files (scene markers, scene previews, sprites, etc)",
|
||||||
|
"generated_path_head": "Generated Path",
|
||||||
|
"hashing": "Hashing",
|
||||||
|
"image_ext_desc": "Comma-delimited list of file extensions that will be identified as images.",
|
||||||
|
"image_ext_head": "Image Extensions",
|
||||||
|
"logging": "Logging",
|
||||||
|
"maximum_streaming_transcode_size_desc": "Maximum size for transcoded streams",
|
||||||
|
"maximum_streaming_transcode_size_head": "Maximum streaming transcode size",
|
||||||
|
"maximum_transcode_size_desc": "Maximum size for generated transcodes",
|
||||||
|
"maximum_transcode_size_head": "Maximum transcode size",
|
||||||
|
"number_of_parallel_task_for_scan_generation_desc": "Set to 0 for auto-detection. Warning running more tasks than is required to achieve 100% cpu utilisation will decrease performance and potentially cause other issues.",
|
||||||
|
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
|
||||||
|
"parallel_scan_head": "Parallel Scan/Generation",
|
||||||
|
"preview_generation": "Preview Generation",
|
||||||
|
"scraper_user_agent": "Scraper User Agent",
|
||||||
|
"scraper_user_agent_desc": "User-Agent string used during scrape http requests",
|
||||||
|
"scraping": "Scraping",
|
||||||
|
"sqlite_location": "File location for the SQLite database (requires restart)",
|
||||||
|
"video_ext_desc": "Comma-delimited list of file extensions that will be identified as videos.",
|
||||||
|
"video_ext_head": "Video Extensions",
|
||||||
|
"video_head": "Video"
|
||||||
|
},
|
||||||
|
"logs": {
|
||||||
|
"log_level": "Log Level"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"hooks": "Hooks",
|
||||||
|
"triggers_on": "Triggers on"
|
||||||
|
},
|
||||||
|
"scrapers": {
|
||||||
|
"entity_metadata": "{entityType} Metadata",
|
||||||
|
"entity_scrapers": "{entityType} scrapers",
|
||||||
|
"search_by_name": "Search by name",
|
||||||
|
"supported_types": "Supported types",
|
||||||
|
"supported_urls": "URLs"
|
||||||
|
},
|
||||||
|
"stashbox": {
|
||||||
|
"add_instance": "Add stash-box instance",
|
||||||
|
"api_key": "API key",
|
||||||
|
"description": "Stash-box facilitates automated tagging of scenes and performers based on fingerprints and filenames.\nEndpoint and API key can be found on your account page on the stash-box instance. Names are required when more than one instance is added.",
|
||||||
|
"endpoint": "Endpoint",
|
||||||
|
"graphql_endpoint": "GraphQL endpoint",
|
||||||
|
"name": "Name",
|
||||||
|
"title": "Stash-box Endpoints"
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"added_job_to_queue": "Added {operation_name} to job queue",
|
||||||
|
"auto_tag_based_on_filenames": "Auto-tag content based on filenames.",
|
||||||
|
"auto_tagging": "Auto Tagging",
|
||||||
|
"backing_up_database": "Backing up database",
|
||||||
|
"backup_and_download": "Performs a backup of the database and downloads the resulting file.",
|
||||||
|
"backup_database": "Performs a backup of the database to the same directory as the database, with the filename format {filename_format}",
|
||||||
|
"cleanup_desc": "Check for missing files and remove them from the database. This is a destructive action.",
|
||||||
|
"dont_include_file_extension_as_part_of_the_title": "Don't include file extension as part of the title",
|
||||||
|
"export_to_json": "Exports the database content into JSON format in the metadata directory.",
|
||||||
|
"generate_desc": "Generate supporting image, sprite, video, vtt and other files.",
|
||||||
|
"generate_phashes_during_scan": "Generate phashes during scan (for deduplication and scene identification)",
|
||||||
|
"generate_previews_during_scan": "Generate image previews during scan (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
||||||
|
"generate_sprites_during_scan": "Generate sprites during scan (for the scene scrubber)",
|
||||||
|
"generate_video_previews_during_scan": "Generate previews during scan (video previews which play when hovering over a scene)",
|
||||||
|
"generated_content": "Generated Content",
|
||||||
|
"import_from_exported_json": "Import from exported JSON in the metadata directory. Wipes the existing database.",
|
||||||
|
"incremental_import": "Incremental import from a supplied export zip file.",
|
||||||
|
"job_queue": "Job Queue",
|
||||||
|
"maintenance": "Maintenance",
|
||||||
|
"migrate_hash_files": "Used after changing the Generated file naming hash to rename existing generated files to the new hash format.",
|
||||||
|
"migrations": "Migrations",
|
||||||
|
"only_dry_run": "Only perform a dry run. Don't remove anything",
|
||||||
|
"plugin_tasks": "Plugin Tasks",
|
||||||
|
"scan_for_content_desc": "Scan for new content and add it to the database.",
|
||||||
|
"set_name_date_details_from_metadata_if_present": "Set name, date, details from metadata (if present)"
|
||||||
|
},
|
||||||
|
"tools": {
|
||||||
|
"scene_duplicate_checker": "Scene Duplicate Checker",
|
||||||
|
"scene_filename_parser": {
|
||||||
|
"add_field": "Add Field",
|
||||||
|
"capitalize_title": "Capitalize title",
|
||||||
|
"display_fields": "Display fields",
|
||||||
|
"escape_chars": "Use \\ to escape literal characters",
|
||||||
|
"filename": "Filename",
|
||||||
|
"filename_pattern": "Filename Pattern",
|
||||||
|
"ignored_words": "Ignored words",
|
||||||
|
"matches_with": "Matches with {i}",
|
||||||
|
"select_parser_recipe": "Select Parser Recipe",
|
||||||
|
"title": "Scene Filename Parser",
|
||||||
|
"whitespace_chars": "Whitespace characters",
|
||||||
|
"whitespace_chars_desc": "These characters will be replaced with whitespace in the title"
|
||||||
|
},
|
||||||
|
"scene_tools": "Scene Tools"
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"custom_css": {
|
||||||
|
"description": "Page must be reloaded for changes to take effect.",
|
||||||
|
"heading": "Custom CSS",
|
||||||
|
"option_label": "Custom CSS enabled"
|
||||||
|
},
|
||||||
|
"handy_connection_key": "Handy Connection Key",
|
||||||
|
"handy_connection_key_desc": "Handy connection key to use for interactive scenes.",
|
||||||
|
"language": {
|
||||||
|
"heading": "Language"
|
||||||
|
},
|
||||||
|
"max_loop_duration": {
|
||||||
|
"description": "Maximum scene duration where scene player will loop the video - 0 to disable",
|
||||||
|
"heading": "Maximum loop duration"
|
||||||
|
},
|
||||||
|
"menu_items": {
|
||||||
|
"description": "Show or hide different types of content on the navigation bar",
|
||||||
|
"heading": "Menu Items"
|
||||||
|
},
|
||||||
|
"preview_type": {
|
||||||
|
"description": "Configuration for wall items",
|
||||||
|
"heading": "Preview Type",
|
||||||
|
"options": {
|
||||||
|
"animated": "Animated Image",
|
||||||
|
"static": "Static Image",
|
||||||
|
"video": "Video"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene_list": {
|
||||||
|
"heading": "Scene List",
|
||||||
|
"options": {
|
||||||
|
"show_studio_as_text": "Show Studios as text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene_player": {
|
||||||
|
"heading": "Scene Player",
|
||||||
|
"options": {
|
||||||
|
"auto_start_video": "Auto-start video"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scene_wall": {
|
||||||
|
"heading": "Scene / Marker Wall",
|
||||||
|
"options": {
|
||||||
|
"display_title": "Display title and tags",
|
||||||
|
"toggle_sound": "Enable sound"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slideshow_delay": {
|
||||||
|
"description": "Slideshow is available in galleries when in wall view mode",
|
||||||
|
"heading": "Slideshow Delay"
|
||||||
|
},
|
||||||
|
"title": "User Interface"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": "Configuration",
|
||||||
|
"countables": {
|
||||||
|
"galleries": "{count, plural, one {Gallery} other {Galleries}}",
|
||||||
|
"images": "{count, plural, one {Image} other {Images}}",
|
||||||
|
"markers": "{count, plural, one {Marker} other {Markers}}",
|
||||||
|
"movies": "{count, plural, one {Movie} other {Movies}}",
|
||||||
|
"performers": "{count, plural, one {Performer} other {Performers}}",
|
||||||
|
"scenes": "{count, plural, one {Scene} other {Scenes}}",
|
||||||
|
"studios": "{count, plural, one {Studio} other {Studios}}",
|
||||||
|
"tags": "{count, plural, one {Tag} other {Tags}}"
|
||||||
|
},
|
||||||
|
"country": "Country",
|
||||||
|
"cover_image": "Cover Image",
|
||||||
|
"created_at": "Created At",
|
||||||
|
"criterion_modifier": {
|
||||||
|
"equals": "is",
|
||||||
|
"excludes": "excludes",
|
||||||
|
"format_string": "{criterion} {modifierString} {valueString}",
|
||||||
|
"greater_than": "is greater than",
|
||||||
|
"includes": "includes",
|
||||||
|
"includes_all": "includes all",
|
||||||
|
"is_null": "is null",
|
||||||
|
"less_than": "is less than",
|
||||||
|
"matches_regex": "matches regex",
|
||||||
|
"not_equals": "is not",
|
||||||
|
"not_matches_regex": "not matches regex",
|
||||||
|
"not_null": "is not null"
|
||||||
|
},
|
||||||
|
"date": "Date",
|
||||||
|
"death_date": "Death Date",
|
||||||
|
"death_year": "Death Year",
|
||||||
|
"descending": "Descending",
|
||||||
|
"detail": "Detail",
|
||||||
|
"details": "Details",
|
||||||
|
"developmentVersion": "Development Version",
|
||||||
|
"dialogs": {
|
||||||
|
"delete_confirm": "Are you sure you want to delete {entityName}?",
|
||||||
|
"delete_entity_desc": "{count, plural, one {Are you sure you want to delete this {singularEntity}? Unless the file is also deleted, this {singularEntity} will be re-added when scan is performed.} other {Are you sure you want to delete these {pluralEntity}? Unless the files are also deleted, these {pluralEntity} will be re-added when scan is performed.}}",
|
||||||
|
"delete_entity_title": "{count, plural, one {Delete {singularEntity}} other {Delete {pluralEntity}}}",
|
||||||
|
"delete_object_desc": "Are you sure you want to delete {count, plural, one {this {singularEntity}} other {these {pluralEntity}}}?",
|
||||||
|
"delete_object_overflow": "…and {count} other {count, plural, one {{singularEntity}} other {{pluralEntity}}}.",
|
||||||
|
"delete_object_title": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
"edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
"export_include_related_objects": "Include related objects in export",
|
||||||
|
"export_title": "Export",
|
||||||
|
"scene_gen": {
|
||||||
|
"image_previews": "Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
||||||
|
"markers": "Markers (20 second videos which begin at the given timecode)",
|
||||||
|
"overwrite": "Overwrite existing generated files",
|
||||||
|
"phash": "Perceptual hashes (for deduplication)",
|
||||||
|
"preview_exclude_end_time_desc": "Exclude the last x seconds from scene previews. This can be a value in seconds, or a percentage (eg 2%) of the total scene duration.",
|
||||||
|
"preview_exclude_end_time_head": "Exclude end time",
|
||||||
|
"preview_exclude_start_time_desc": "Exclude the first x seconds from scene previews. This can be a value in seconds, or a percentage (eg 2%) of the total scene duration.",
|
||||||
|
"preview_exclude_start_time_head": "Exclude start time",
|
||||||
|
"preview_options": "Preview Options",
|
||||||
|
"preview_preset_desc": "The preset regulates size, quality and encoding time of preview generation. Presets beyond “slow” have diminishing returns and are not recommended.",
|
||||||
|
"preview_preset_head": "Preview encoding preset",
|
||||||
|
"preview_seg_count_desc": "Number of segments in preview files.",
|
||||||
|
"preview_seg_count_head": "Number of segments in preview",
|
||||||
|
"preview_seg_duration_desc": "Duration of each preview segment, in seconds.",
|
||||||
|
"preview_seg_duration_head": "Preview segment duration",
|
||||||
|
"sprites": "Sprites (for the scene scrubber)",
|
||||||
|
"transcodes": "Transcodes (MP4 conversions of unsupported video formats)",
|
||||||
|
"video_previews": "Previews (video previews which play when hovering over a scene)"
|
||||||
|
},
|
||||||
|
"scrape_entity_title": "{entity_type} Scrape Results",
|
||||||
|
"scrape_results_existing": "Existing",
|
||||||
|
"scrape_results_scraped": "Scraped",
|
||||||
|
"set_image_url_title": "Image URL",
|
||||||
|
"unsaved_changes": "Unsaved changes. Are you sure you want to leave?"
|
||||||
|
},
|
||||||
|
"dimensions": "Dimensions",
|
||||||
|
"director": "Director",
|
||||||
|
"display_mode": {
|
||||||
|
"grid": "Grid",
|
||||||
|
"list": "List",
|
||||||
|
"tagger": "Tagger",
|
||||||
|
"unknown": "Unknown",
|
||||||
|
"wall": "Wall"
|
||||||
|
},
|
||||||
|
"donate": "Donate",
|
||||||
|
"dupe_check": {
|
||||||
|
"description": "Levels below 'Exact' can take longer to calculate. False positives might also be returned on lower accuracy levels.",
|
||||||
|
"found_sets": "{setCount, plural, one{# set of duplicates found.} other {# sets of duplicates found.}}",
|
||||||
|
"options": {
|
||||||
|
"exact": "Exact",
|
||||||
|
"high": "High",
|
||||||
|
"low": "Low",
|
||||||
|
"medium": "Medium"
|
||||||
|
},
|
||||||
|
"search_accuracy_label": "Search Accuracy",
|
||||||
|
"title": "Duplicate Scenes"
|
||||||
|
},
|
||||||
|
"duration": "Duration",
|
||||||
|
"effect_filters": {
|
||||||
|
"aspect": "Aspect",
|
||||||
|
"blue": "Blue",
|
||||||
|
"blur": "Blur",
|
||||||
|
"brightness": "Brightness",
|
||||||
|
"contrast": "Contrast",
|
||||||
|
"gamma": "Gamma",
|
||||||
|
"green": "Green",
|
||||||
|
"hue": "Hue",
|
||||||
|
"name": "Filters",
|
||||||
|
"name_transforms": "Transforms",
|
||||||
|
"red": "Red",
|
||||||
|
"reset_filters": "Reset Filters",
|
||||||
|
"reset_transforms": "Reset Transforms",
|
||||||
|
"rotate": "Rotate",
|
||||||
|
"rotate_left_and_scale": "Rotate Left & Scale",
|
||||||
|
"rotate_right_and_scale": "Rotate Right & Scale",
|
||||||
|
"saturation": "Saturation",
|
||||||
|
"scale": "Scale",
|
||||||
|
"warmth": "Warmth"
|
||||||
|
},
|
||||||
|
"ethnicity": "Ethnicity",
|
||||||
|
"eye_color": "Eye Colour",
|
||||||
|
"fake_tits": "Fake Tits",
|
||||||
|
"favourite": "Favourite",
|
||||||
|
"file_info": "File Info",
|
||||||
|
"file_mod_time": "File Modification Time",
|
||||||
|
"filesize": "File Size",
|
||||||
|
"framerate": "Frame Rate",
|
||||||
|
"galleries": "Galleries",
|
||||||
|
"gallery": "Gallery",
|
||||||
|
"gallery_count": "Gallery Count",
|
||||||
|
"gender": "Gender",
|
||||||
|
"hair_color": "Hair Colour",
|
||||||
|
"hasMarkers": "Has Markers",
|
||||||
|
"height": "Height",
|
||||||
|
"help": "Help",
|
||||||
|
"image": "Image",
|
||||||
|
"image_count": "Image Count",
|
||||||
|
"images": "Images",
|
||||||
|
"images-size": "Images size",
|
||||||
|
"instagram": "Instagram",
|
||||||
|
"interactive": "Interactive",
|
||||||
|
"isMissing": "Is Missing",
|
||||||
|
"library": "Library",
|
||||||
|
"loading": {
|
||||||
|
"generic": "Loading…"
|
||||||
|
},
|
||||||
|
"marker_count": "Marker Count",
|
||||||
|
"markers": "Markers",
|
||||||
|
"measurements": "Measurements",
|
||||||
|
"media_info": {
|
||||||
|
"audio_codec": "Audio Codec",
|
||||||
|
"checksum": "Checksum",
|
||||||
|
"downloaded_from": "Downloaded From",
|
||||||
|
"hash": "Hash",
|
||||||
|
"performer_card": {
|
||||||
|
"age": "{age} {years_old}",
|
||||||
|
"age_context": "{age} {years_old} in this scene"
|
||||||
|
},
|
||||||
|
"phash": "PHash",
|
||||||
|
"stream": "Stream",
|
||||||
|
"video_codec": "Video Codec"
|
||||||
|
},
|
||||||
|
"metadata": "Metadata",
|
||||||
|
"movie": "Movie",
|
||||||
|
"movie_scene_number": "Movie Scene Number",
|
||||||
|
"movies": "Movies",
|
||||||
|
"name": "Name",
|
||||||
|
"new": "New",
|
||||||
|
"none": "None",
|
||||||
|
"o_counter": "O-Counter",
|
||||||
|
"operations": "Operations",
|
||||||
|
"organized": "Organised",
|
||||||
|
"pagination": {
|
||||||
|
"first": "First",
|
||||||
|
"last": "Last",
|
||||||
|
"next": "Next",
|
||||||
|
"previous": "Previous"
|
||||||
|
},
|
||||||
|
"parent_studios": "Parent Studios",
|
||||||
|
"path": "Path",
|
||||||
|
"performer": "Performer",
|
||||||
|
"performer_count": "Performer Count",
|
||||||
|
"performer_image": "Performer Image",
|
||||||
|
"performers": "Performers",
|
||||||
|
"performerTags": "Performer Tags",
|
||||||
|
"piercings": "Piercings",
|
||||||
|
"queue": "Queue",
|
||||||
|
"random": "Random",
|
||||||
|
"rating": "Rating",
|
||||||
|
"resolution": "Resolution",
|
||||||
|
"scene": "Scene",
|
||||||
|
"scene_count": "Scene Count",
|
||||||
|
"scene_id": "Scene ID",
|
||||||
|
"scenes": "Scenes",
|
||||||
|
"scenes-size": "Scenes size",
|
||||||
|
"scenes_updated_at": "Scene Updated At",
|
||||||
|
"sceneTagger": "Scene Tagger",
|
||||||
|
"sceneTags": "Scene Tags",
|
||||||
|
"search_filter": {
|
||||||
|
"add_filter": "Add Filter",
|
||||||
|
"name": "Filter",
|
||||||
|
"update_filter": "Update Filter"
|
||||||
|
},
|
||||||
|
"seconds": "Seconds",
|
||||||
|
"settings": "Settings",
|
||||||
|
"stash_id": "Stash ID",
|
||||||
|
"status": "Status: {statusText}",
|
||||||
|
"studio": "Studio",
|
||||||
|
"studio_depth": "Levels (empty for all)",
|
||||||
|
"studios": "Studios",
|
||||||
|
"synopsis": "Synopsis",
|
||||||
|
"tag": "Tag",
|
||||||
|
"tag_count": "Tag Count",
|
||||||
|
"tags": "Tags",
|
||||||
|
"tattoos": "Tattoos",
|
||||||
|
"title": "Title",
|
||||||
|
"toast": {
|
||||||
|
"added_entity": "Added {entity}",
|
||||||
|
"added_generation_job_to_queue": "Added generation job to queue",
|
||||||
|
"delete_entity": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
"delete_past_tense": "Deleted {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
|
||||||
|
"generating_screenshot": "Generating screenshot…",
|
||||||
|
"rescanning_entity": "Rescanning {count, plural, one {{singularEntity}} other {{pluralEntity}}}…",
|
||||||
|
"started_auto_tagging": "Started auto tagging",
|
||||||
|
"updated_entity": "Updated {entity}"
|
||||||
|
},
|
||||||
|
"total": "Total",
|
||||||
|
"twitter": "Twitter",
|
||||||
|
"up-dir": "Up a directory",
|
||||||
|
"updated_at": "Updated At",
|
||||||
|
"url": "URL",
|
||||||
|
"weight": "Weight",
|
||||||
|
"years_old": "years old"
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user