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",
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"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 { Route, Switch, useRouteMatch } from "react-router-dom";
|
||||
import { IntlProvider } from "react-intl";
|
||||
import { merge } from "lodash";
|
||||
import { ToastProvider } from "src/hooks/Toast";
|
||||
import LightboxProvider from "src/hooks/Lightbox/context";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { fas } from "@fortawesome/free-solid-svg-icons";
|
||||
import { initPolyfills } from "src/polyfills";
|
||||
|
||||
import locales from "src/locale";
|
||||
import locales from "src/locales";
|
||||
import { useConfiguration, useSystemStatus } from "src/core/StashService";
|
||||
import { flattenMessages } from "src/utils";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -58,12 +59,12 @@ export const App: React.FC = () => {
|
||||
const messageLanguage = languageMessageString(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
|
||||
...(locales as any)[defaultMessageLanguage],
|
||||
(locales as any)[defaultMessageLanguage],
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
...(locales as any)[messageLanguage],
|
||||
};
|
||||
(locales as any)[messageLanguage]
|
||||
);
|
||||
const messages = flattenMessages(mergedMessages);
|
||||
|
||||
const setupMatch = useRouteMatch(["/setup", "/migrate"]);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
|
||||
|
||||
### 🎨 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))
|
||||
* 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))
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IDeleteGalleryDialogProps {
|
||||
selected: Pick<GQL.Gallery, "id">[];
|
||||
@@ -14,20 +14,22 @@ interface IDeleteGalleryDialogProps {
|
||||
export const DeleteGalleriesDialog: React.FC<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 pluralMessageId = "deleteGallerysText";
|
||||
|
||||
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 pluralMessage =
|
||||
"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.";
|
||||
|
||||
const header = plural ? "Delete Galleries" : "Delete Gallery";
|
||||
const toastMessage = plural ? "Deleted galleries" : "Deleted gallery";
|
||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
||||
const message = plural ? pluralMessage : singleMessage;
|
||||
const header = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_title" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const toastMessage = intl.formatMessage(
|
||||
{ id: "toast.delete_entity" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const message = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_desc" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||
@@ -63,17 +65,19 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
||||
show
|
||||
icon="trash-alt"
|
||||
header={header}
|
||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||
accept={{
|
||||
variant: "danger",
|
||||
onClick: onDelete,
|
||||
text: intl.formatMessage({ id: "actions.delete" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isDeleting}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
||||
</p>
|
||||
<p>{message}</p>
|
||||
<Form>
|
||||
<Form.Check
|
||||
id="delete-file"
|
||||
@@ -84,7 +88,9 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
||||
<Form.Check
|
||||
id="delete-generated"
|
||||
checked={deleteGenerated}
|
||||
label="Delete generated supporting files"
|
||||
label={intl.formatMessage({
|
||||
id: "actions.delete_generated_supporting_files",
|
||||
})}
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useBulkGalleryUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
||||
export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
@@ -138,7 +140,14 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
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);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -347,11 +356,21 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header="Edit Galleries"
|
||||
accept={{ onClick: onSave, text: "Apply" }}
|
||||
header={intl.formatMessage(
|
||||
{ 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={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
@@ -359,7 +378,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
<Form>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -372,7 +391,7 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -386,19 +405,23 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
label="Organized"
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
mutateMetadataScan,
|
||||
useFindGallery,
|
||||
@@ -28,6 +29,7 @@ export const Gallery: React.FC = () => {
|
||||
const { tab = "images", id = "new" } = useParams<IGalleryParams>();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const isNew = id === "new";
|
||||
|
||||
const { data, error, loading } = useFindGallery(id);
|
||||
@@ -73,7 +75,15 @@ export const Gallery: React.FC = () => {
|
||||
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);
|
||||
@@ -103,7 +113,7 @@ export const Gallery: React.FC = () => {
|
||||
variant="secondary"
|
||||
id="operation-menu"
|
||||
className="minimal"
|
||||
title="Operations"
|
||||
title={intl.formatMessage({ id: "operations" })}
|
||||
>
|
||||
<Icon icon="ellipsis-v" />
|
||||
</Dropdown.Toggle>
|
||||
@@ -114,7 +124,7 @@ export const Gallery: React.FC = () => {
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onRescan()}
|
||||
>
|
||||
Rescan
|
||||
<FormattedMessage id="actions.rescan" />
|
||||
</Dropdown.Item>
|
||||
) : undefined}
|
||||
<Dropdown.Item
|
||||
@@ -122,7 +132,10 @@ export const Gallery: React.FC = () => {
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete Gallery
|
||||
<FormattedMessage
|
||||
id="actions.delete_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
@@ -142,22 +155,28 @@ export const Gallery: React.FC = () => {
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="gallery-details-panel">Details</Nav.Link>
|
||||
<Nav.Link eventKey="gallery-details-panel">
|
||||
<FormattedMessage id="details" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{gallery.scenes.length > 0 && (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="gallery-scenes-panel">Scenes</Nav.Link>
|
||||
<Nav.Link eventKey="gallery-scenes-panel">
|
||||
<FormattedMessage id="scenes" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
)}
|
||||
{gallery.path ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="gallery-file-info-panel">
|
||||
File Info
|
||||
<FormattedMessage id="file_info" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<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 className="ml-auto">
|
||||
<OrganizedButton
|
||||
@@ -212,10 +231,14 @@ export const Gallery: React.FC = () => {
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="images">Images</Nav.Link>
|
||||
<Nav.Link eventKey="images">
|
||||
<FormattedMessage id="images" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="add">Add</Nav.Link>
|
||||
<Nav.Link eventKey="add">
|
||||
<FormattedMessage id="actions.add" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
</Nav>
|
||||
</div>
|
||||
@@ -255,7 +278,12 @@ export const Gallery: React.FC = () => {
|
||||
return (
|
||||
<div className="row new-view">
|
||||
<div className="col-6">
|
||||
<h2>Create Gallery</h2>
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="actions.create_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel
|
||||
isNew
|
||||
gallery={undefined}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { showWhenSelected } from "src/hooks/ListHook";
|
||||
import { mutateAddGalleryImages } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IGalleryAddProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
@@ -14,6 +15,7 @@ interface IGalleryAddProps {
|
||||
|
||||
export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
const galleryValue = {
|
||||
@@ -60,8 +62,16 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
gallery_id: gallery.id!,
|
||||
image_ids: Array.from(selectedIds.values()),
|
||||
});
|
||||
const imageCount = selectedIds.size;
|
||||
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) {
|
||||
Toast.error(e);
|
||||
@@ -70,7 +80,10 @@ export const GalleryAddPanel: React.FC<IGalleryAddProps> = ({ gallery }) => {
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Add to Gallery",
|
||||
text: intl.formatMessage(
|
||||
{ id: "actions.add_to_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "gallery" }) }
|
||||
),
|
||||
onClick: addImages,
|
||||
isDisplayed: showWhenSelected,
|
||||
postRefetch: true,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
@@ -49,6 +50,7 @@ interface IExistingProps {
|
||||
export const GalleryEditPanel: React.FC<
|
||||
IProps & (INewProps | IExistingProps)
|
||||
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const [title, setTitle] = useState<string>(gallery?.title ?? "");
|
||||
@@ -173,7 +175,16 @@ export const GalleryEditPanel: React.FC<
|
||||
},
|
||||
});
|
||||
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) {
|
||||
@@ -249,7 +260,7 @@ export const GalleryEditPanel: React.FC<
|
||||
<DropdownButton
|
||||
className="d-inline-block"
|
||||
id="gallery-scrape"
|
||||
title="Scrape with..."
|
||||
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||
>
|
||||
{queryableScrapers.map((s) => (
|
||||
<Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}>
|
||||
@@ -260,7 +271,9 @@ export const GalleryEditPanel: React.FC<
|
||||
<span className="fa-icon">
|
||||
<Icon icon="sync-alt" />
|
||||
</span>
|
||||
<span>Reload scrapers</span>
|
||||
<span>
|
||||
<FormattedMessage id="actions.reload_scrapers" />
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
@@ -359,14 +372,14 @@ export const GalleryEditPanel: React.FC<
|
||||
<div className="form-container row px-3 pt-3">
|
||||
<div className="col edit-buttons mb-3 pl-0">
|
||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
</div>
|
||||
<Col xs={6} className="text-right">
|
||||
@@ -376,21 +389,23 @@ export const GalleryEditPanel: React.FC<
|
||||
<div className="form-container row px-3">
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
{FormUtils.renderInputGroup({
|
||||
title: "Title",
|
||||
title: intl.formatMessage({ id: "title" }),
|
||||
value: title,
|
||||
onChange: setTitle,
|
||||
isEditing: true,
|
||||
})}
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<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">
|
||||
{maybeRenderScrapeButton()}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
title: intl.formatMessage({ id: "url" }),
|
||||
value: url,
|
||||
onChange: setUrl,
|
||||
isEditing: true,
|
||||
@@ -398,7 +413,7 @@ export const GalleryEditPanel: React.FC<
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{FormUtils.renderInputGroup({
|
||||
title: "Date",
|
||||
title: intl.formatMessage({ id: "date" }),
|
||||
value: date,
|
||||
isEditing: true,
|
||||
onChange: setDate,
|
||||
@@ -406,7 +421,7 @@ export const GalleryEditPanel: React.FC<
|
||||
})}
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -418,7 +433,7 @@ export const GalleryEditPanel: React.FC<
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -432,7 +447,7 @@ export const GalleryEditPanel: React.FC<
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Performers",
|
||||
title: intl.formatMessage({ id: "performers" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -452,7 +467,7 @@ export const GalleryEditPanel: React.FC<
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Tags",
|
||||
title: intl.formatMessage({ id: "tags" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -470,7 +485,7 @@ export const GalleryEditPanel: React.FC<
|
||||
|
||||
<Form.Group controlId="scenes" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Scenes",
|
||||
title: intl.formatMessage({ id: "scenes" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -487,7 +502,9 @@ export const GalleryEditPanel: React.FC<
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>Details</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="details" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="gallery-description text-input"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface IGalleryFileInfoPanelProps {
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
@@ -12,7 +13,9 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
||||
function renderChecksum() {
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -23,7 +26,9 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Path</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={filePath} className="col-8">
|
||||
<TruncatedText text={filePath} />
|
||||
</a>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { mutateRemoveGalleryImages } from "src/core/StashService";
|
||||
import { showWhenSelected, PersistanceLevel } from "src/hooks/ListHook";
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IGalleryDetailsProps {
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
@@ -15,6 +16,7 @@ interface IGalleryDetailsProps {
|
||||
export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
||||
gallery,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
function filterHook(filter: ListFilterModel) {
|
||||
@@ -63,7 +65,10 @@ export const GalleryImagesPanel: React.FC<IGalleryDetailsProps> = ({
|
||||
image_ids: Array.from(selectedIds.values()),
|
||||
});
|
||||
Toast.success({
|
||||
content: "Added images",
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.added_entity" },
|
||||
{ entity: intl.formatMessage({ id: "images" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { StudioSelect, PerformerSelect } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TagSelect } from "src/components/Shared/Select";
|
||||
@@ -41,6 +42,7 @@ function renderScrapedStudio(
|
||||
}
|
||||
|
||||
function renderScrapedStudioRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void,
|
||||
newStudio?: GQL.ScrapedSceneStudio,
|
||||
@@ -48,7 +50,7 @@ function renderScrapedStudioRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Studio"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
@@ -87,6 +89,7 @@ function renderScrapedPerformers(
|
||||
}
|
||||
|
||||
function renderScrapedPerformersRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newPerformers: GQL.ScrapedScenePerformer[],
|
||||
@@ -94,7 +97,7 @@ function renderScrapedPerformersRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Performers"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||
renderNewField={() =>
|
||||
@@ -133,6 +136,7 @@ function renderScrapedTags(
|
||||
}
|
||||
|
||||
function renderScrapedTagsRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newTags: GQL.ScrapedSceneTag[],
|
||||
@@ -140,7 +144,7 @@ function renderScrapedTagsRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Tags"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedTags(result)}
|
||||
renderNewField={() =>
|
||||
@@ -169,6 +173,7 @@ interface IHasStoredID {
|
||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
props: IGalleryScrapeDialogProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const [title, setTitle] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
|
||||
);
|
||||
@@ -288,7 +293,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
Toast.success({
|
||||
content: (
|
||||
<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>
|
||||
),
|
||||
});
|
||||
@@ -323,7 +334,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
Toast.success({
|
||||
content: (
|
||||
<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>
|
||||
),
|
||||
});
|
||||
@@ -359,7 +376,13 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
Toast.success({
|
||||
content: (
|
||||
<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>
|
||||
),
|
||||
});
|
||||
@@ -401,41 +424,44 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
title="Title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="URL"
|
||||
title={intl.formatMessage({ id: "url" })}
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
{renderScrapedStudioRow(
|
||||
intl.formatMessage({ id: "studios" }),
|
||||
studio,
|
||||
(value) => setStudio(value),
|
||||
newStudio,
|
||||
createNewStudio
|
||||
)}
|
||||
{renderScrapedPerformersRow(
|
||||
intl.formatMessage({ id: "performers" }),
|
||||
performers,
|
||||
(value) => setPerformers(value),
|
||||
newPerformers,
|
||||
createNewPerformer
|
||||
)}
|
||||
{renderScrapedTagsRow(
|
||||
intl.formatMessage({ id: "tags" }),
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { Table } from "react-bootstrap";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
@@ -28,22 +29,23 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||
filterHook,
|
||||
persistState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "View Random",
|
||||
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
@@ -183,8 +185,10 @@ export const GalleryList: React.FC<IGalleryList> = ({
|
||||
<Table className="col col-sm-6 mx-auto">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Preview</th>
|
||||
<th className="d-none d-sm-none">Title</th>
|
||||
<th>{intl.formatMessage({ id: "actions.preview" })}</th>
|
||||
<th className="d-none d-sm-none">
|
||||
{intl.formatMessage({ id: "title" })}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IDeleteImageDialogProps {
|
||||
selected: GQL.SlimImageDataFragment[];
|
||||
@@ -14,20 +14,22 @@ interface IDeleteImageDialogProps {
|
||||
export const DeleteImagesDialog: React.FC<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 pluralMessageId = "deleteImagesText";
|
||||
|
||||
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 pluralMessage =
|
||||
"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.";
|
||||
|
||||
const header = plural ? "Delete Images" : "Delete Image";
|
||||
const toastMessage = plural ? "Deleted images" : "Deleted image";
|
||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
||||
const message = plural ? pluralMessage : singleMessage;
|
||||
const header = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_title" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const toastMessage = intl.formatMessage(
|
||||
{ id: "toast.delete_entity" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const message = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_desc" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||
@@ -63,28 +65,32 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
||||
show
|
||||
icon="trash-alt"
|
||||
header={header}
|
||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||
accept={{
|
||||
variant: "danger",
|
||||
onClick: onDelete,
|
||||
text: intl.formatMessage({ id: "actions.delete" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isDeleting}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
||||
</p>
|
||||
<p>{message}</p>
|
||||
<Form>
|
||||
<Form.Check
|
||||
id="delete-image"
|
||||
checked={deleteFile}
|
||||
label="Delete file"
|
||||
label={intl.formatMessage({ id: "actions.delete_file" })}
|
||||
onChange={() => setDeleteFile(!deleteFile)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="delete-image-generated"
|
||||
checked={deleteGenerated}
|
||||
label="Delete generated supporting files"
|
||||
label={intl.formatMessage({
|
||||
id: "actions.delete_generated_supporting_files",
|
||||
})}
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useBulkImageUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
||||
export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
@@ -138,7 +140,12 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
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);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -344,11 +351,21 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header="Edit Images"
|
||||
accept={{ onClick: onSave, text: "Apply" }}
|
||||
header={intl.formatMessage(
|
||||
{ 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={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
@@ -356,7 +373,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
<Form>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -369,7 +386,7 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -383,19 +400,23 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
label="Organized"
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import {
|
||||
useFindImage,
|
||||
@@ -28,6 +29,7 @@ export const Image: React.FC = () => {
|
||||
const { id = "new" } = useParams<IImageParams>();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
const { data, error, loading } = useFindImage(id);
|
||||
const image = data?.findImage;
|
||||
@@ -53,7 +55,15 @@ export const Image: React.FC = () => {
|
||||
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 () => {
|
||||
@@ -139,14 +149,17 @@ export const Image: React.FC = () => {
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onRescan()}
|
||||
>
|
||||
Rescan
|
||||
<FormattedMessage id="actions.rescan" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="delete-image"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete Image
|
||||
<FormattedMessage
|
||||
id="actions.delete_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "image" }) }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
@@ -166,13 +179,19 @@ export const Image: React.FC = () => {
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<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.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.Link eventKey="image-edit-panel">Edit</Nav.Link>
|
||||
<Nav.Link eventKey="image-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item className="ml-auto">
|
||||
<OCounterButton
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useImageUpdate } from "src/core/StashService";
|
||||
@@ -24,6 +25,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
isVisible,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [title, setTitle] = useState<string>(image?.title ?? "");
|
||||
const [rating, setRating] = useState<number>(image.rating ?? NaN);
|
||||
@@ -102,7 +104,12 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
},
|
||||
});
|
||||
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) {
|
||||
Toast.error(e);
|
||||
@@ -117,28 +124,28 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
<div className="form-container row px-3 pt-3">
|
||||
<div className="col edit-buttons mb-3 pl-0">
|
||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-container row px-3">
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
{FormUtils.renderInputGroup({
|
||||
title: "Title",
|
||||
title: intl.formatMessage({ id: "title" }),
|
||||
value: title,
|
||||
onChange: setTitle,
|
||||
isEditing: true,
|
||||
})}
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -150,7 +157,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -164,7 +171,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Performers",
|
||||
title: intl.formatMessage({ id: "performers" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -184,7 +191,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Tags",
|
||||
title: intl.formatMessage({ id: "tags" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { FormattedNumber } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
@@ -14,7 +14,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
||||
function renderChecksum() {
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -26,7 +28,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
||||
} = props;
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Path</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={`file://${path}`} className="col-8">
|
||||
<TruncatedText text={`file://${props.image.path}`} />
|
||||
</a>{" "}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -118,22 +119,23 @@ export const ImageList: React.FC<IImageList> = ({
|
||||
persistanceKey,
|
||||
extraOperations,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const otherOperations = (extraOperations ?? []).concat([
|
||||
{
|
||||
text: "View Random",
|
||||
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { NoneCriterion } from "src/models/list-filter/criteria/none";
|
||||
import { makeCriteria } from "src/models/list-filter/criteria/factory";
|
||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { defineMessages, useIntl } from "react-intl";
|
||||
import { defineMessages, FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
criterionIsHierarchicalLabelValue,
|
||||
CriterionType,
|
||||
@@ -339,7 +339,9 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
||||
|
||||
return (
|
||||
<Form.Group controlId="filter">
|
||||
<Form.Label>Filter</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="search_filter.name" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
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 (
|
||||
<>
|
||||
<OverlayTrigger
|
||||
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}>
|
||||
<Icon icon="filter" />
|
||||
|
||||
@@ -20,7 +20,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useFocus } from "src/utils";
|
||||
import { ListFilterOptions } from "src/models/list-filter/filter-options";
|
||||
import { useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
Criterion,
|
||||
CriterionValue,
|
||||
@@ -280,16 +280,22 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
}
|
||||
}
|
||||
function getLabel(option: DisplayMode) {
|
||||
let displayModeId = "unknown";
|
||||
switch (option) {
|
||||
case DisplayMode.Grid:
|
||||
return "Grid";
|
||||
displayModeId = "grid";
|
||||
break;
|
||||
case DisplayMode.List:
|
||||
return "List";
|
||||
displayModeId = "list";
|
||||
break;
|
||||
case DisplayMode.Wall:
|
||||
return "Wall";
|
||||
displayModeId = "wall";
|
||||
break;
|
||||
case DisplayMode.Tagger:
|
||||
return "Tagger";
|
||||
displayModeId = "tagger";
|
||||
break;
|
||||
}
|
||||
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
|
||||
}
|
||||
|
||||
return props.filterOptions.displayModeOptions.map((option) => (
|
||||
@@ -361,7 +367,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectAll()}
|
||||
>
|
||||
Select All
|
||||
<FormattedMessage id="actions.select_all" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
@@ -375,7 +381,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onSelectNone()}
|
||||
>
|
||||
Select None
|
||||
<FormattedMessage id="actions.select_none" />
|
||||
</Dropdown.Item>
|
||||
);
|
||||
}
|
||||
@@ -450,7 +456,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
return (
|
||||
<ButtonGroup className="ml-2">
|
||||
{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}>
|
||||
<Icon icon="pencil-alt" />
|
||||
</Button>
|
||||
@@ -458,7 +470,13 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
)}
|
||||
|
||||
{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}>
|
||||
<Icon icon="trash" />
|
||||
</Button>
|
||||
@@ -481,7 +499,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
<InputGroup className="mr-2 flex-grow-1">
|
||||
<FormControl
|
||||
ref={queryRef}
|
||||
placeholder="Search..."
|
||||
placeholder={`${intl.formatMessage({ id: "actions.search" })}…`}
|
||||
defaultValue={props.filter.searchTerm}
|
||||
onInput={onChangeQuery}
|
||||
className="bg-secondary text-white border-secondary w-50"
|
||||
@@ -510,8 +528,8 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
overlay={
|
||||
<Tooltip id="sort-direction-tooltip">
|
||||
{props.filter.sortDirection === SortDirectionEnum.Asc
|
||||
? "Ascending"
|
||||
: "Descending"}
|
||||
? intl.formatMessage({ id: "ascending" })
|
||||
: intl.formatMessage({ id: "descending" })}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
@@ -528,7 +546,9 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
{props.filter.sortBy === "random" && (
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-reshuffle-tooltip">Reshuffle</Tooltip>
|
||||
<Tooltip id="sort-reshuffle-tooltip">
|
||||
{intl.formatMessage({ id: "actions.reshuffle" })}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" onClick={onReshuffleRandomSort}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { FormattedNumber, useIntl } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
|
||||
interface IPaginationProps {
|
||||
itemsPerPage: number;
|
||||
@@ -75,7 +75,9 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||
disabled={currentPage === 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>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -84,7 +86,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onChangePage(currentPage - 1)}
|
||||
>
|
||||
Previous
|
||||
<FormattedMessage id="pagination.previous" />
|
||||
</Button>
|
||||
{pageButtons}
|
||||
<Button
|
||||
@@ -93,14 +95,16 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => onChangePage(currentPage + 1)}
|
||||
>
|
||||
Next
|
||||
<FormattedMessage id="pagination.next" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={currentPage === 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>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -24,6 +25,7 @@ interface IMovieParams {
|
||||
}
|
||||
|
||||
export const Movie: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const { id = "new" } = useParams<IMovieParams>();
|
||||
@@ -141,10 +143,23 @@ export const Movie: React.FC = () => {
|
||||
<Modal
|
||||
show={isDeleteAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
if (movie.aliases) {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
@@ -31,7 +33,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<RatingStars value={movie.rating} disabled />
|
||||
</dd>
|
||||
@@ -50,31 +54,31 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
||||
|
||||
<div>
|
||||
<TextField
|
||||
name="Duration"
|
||||
id="duration"
|
||||
value={
|
||||
movie.duration ? DurationUtils.secondsToString(movie.duration) : ""
|
||||
}
|
||||
/>
|
||||
<TextField
|
||||
name="Date"
|
||||
id="date"
|
||||
value={movie.date ? TextUtils.formatDate(intl, movie.date) : ""}
|
||||
/>
|
||||
<URLField
|
||||
name="Studio"
|
||||
id="studio"
|
||||
value={movie.studio?.name}
|
||||
url={`/studios/${movie.studio?.id}`}
|
||||
/>
|
||||
<TextField name="Director" value={movie.director} />
|
||||
<TextField id="director" value={movie.director} />
|
||||
|
||||
{renderRatingField()}
|
||||
|
||||
<URLField
|
||||
name="URL"
|
||||
id="url"
|
||||
value={movie.url}
|
||||
url={TextUtils.sanitiseURL(movie.url ?? "")}
|
||||
/>
|
||||
|
||||
<TextField name="Synopsis" value={movie.synopsis} />
|
||||
<TextField id="synopsis" value={movie.synopsis} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -49,6 +50,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
setBackImage,
|
||||
onImageEncoding,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const isNew = movie === undefined;
|
||||
@@ -332,7 +334,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
variant="secondary"
|
||||
onClick={() => setIsImageAlertOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -378,22 +380,29 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
// TODO: CSS class
|
||||
return (
|
||||
<div>
|
||||
{isNew && <h2>Add Movie</h2>}
|
||||
{isNew && (
|
||||
<h2>
|
||||
{intl.formatMessage(
|
||||
{ id: "actions.add_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "movie" }) }
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<Prompt
|
||||
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.Group controlId="name" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Name
|
||||
{intl.formatMessage({ id: "name" })}
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder="Name"
|
||||
placeholder={intl.formatMessage({ id: "name" })}
|
||||
{...formik.getFieldProps("name")}
|
||||
isInvalid={!!formik.errors.name}
|
||||
/>
|
||||
@@ -403,11 +412,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("aliases", "Aliases")}
|
||||
{renderTextField("aliases", intl.formatMessage({ id: "aliases" }))}
|
||||
|
||||
<Form.Group controlId="duration" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Duration
|
||||
{intl.formatMessage({ id: "duration" })}
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<DurationInput
|
||||
@@ -419,11 +428,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("date", "Date (YYYY-MM-DD)")}
|
||||
{renderTextField("date", intl.formatMessage({ id: "date" }))}
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Studio
|
||||
{intl.formatMessage({ id: "studio" })}
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<StudioSelect
|
||||
@@ -438,11 +447,11 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
{renderTextField("director", "Director")}
|
||||
{renderTextField("director", intl.formatMessage({ id: "director" }))}
|
||||
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Rating
|
||||
{intl.formatMessage({ id: "rating" })}
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<RatingStars
|
||||
@@ -456,13 +465,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
URL
|
||||
{intl.formatMessage({ id: "url" })}
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder="URL"
|
||||
placeholder={intl.formatMessage({ id: "url" })}
|
||||
{...formik.getFieldProps("url")}
|
||||
/>
|
||||
<InputGroup.Append>{maybeRenderScrapeButton()}</InputGroup.Append>
|
||||
@@ -472,13 +481,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
|
||||
<Form.Group controlId="synopsis" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Synopsis
|
||||
{intl.formatMessage({ id: "synopsis" })}
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="text-input"
|
||||
placeholder="Synopsis"
|
||||
placeholder={intl.formatMessage({ id: "synopsis" })}
|
||||
{...formik.getFieldProps("synopsis")}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { useHistory } from "react-router-dom";
|
||||
@@ -18,22 +19,23 @@ import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
|
||||
import { MovieCard } from "./MovieCard";
|
||||
|
||||
export const MovieList: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "View Random",
|
||||
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
@@ -58,8 +60,8 @@ export const MovieList: React.FC = () => {
|
||||
<DeleteEntityDialog
|
||||
selected={selectedMovies}
|
||||
onClose={onClose}
|
||||
singularEntity="movie"
|
||||
pluralEntity="movies"
|
||||
singularEntity={intl.formatMessage({ id: "movie" })}
|
||||
pluralEntity={intl.formatMessage({ id: "movies" })}
|
||||
destroyMutation={useMoviesDestroy}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useBulkPerformerUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
||||
export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
@@ -92,7 +94,16 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
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);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -232,17 +243,20 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header="Edit Performers"
|
||||
accept={{ onClick: onSave, text: "Apply" }}
|
||||
accept={{
|
||||
onClick: onSave,
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -254,7 +268,9 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
</Form.Group>
|
||||
<Form>
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="tags" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import {
|
||||
@@ -39,11 +40,22 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
onSelectedChanged,
|
||||
extraCriteria,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const age = TextUtils.age(
|
||||
performer.birthdate,
|
||||
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() {
|
||||
if (performer.favorite === false) {
|
||||
@@ -143,7 +155,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
performer.rating ? `rating-${performer.rating}` : ""
|
||||
}`}
|
||||
>
|
||||
RATING: {performer.rating}
|
||||
<FormattedMessage id="rating" />: {performer.rating}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Button, Tabs, Tab } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -32,6 +33,7 @@ interface IPerformerParams {
|
||||
export const Performer: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const { tab = "details", id = "new" } = useParams<IPerformerParams>();
|
||||
const isNew = id === "new";
|
||||
|
||||
@@ -126,19 +128,19 @@ export const Performer: React.FC = () => {
|
||||
id="performer-details"
|
||||
unmountOnExit
|
||||
>
|
||||
<Tab eventKey="details" title="Details">
|
||||
<Tab eventKey="details" title={intl.formatMessage({ id: "details" })}>
|
||||
<PerformerDetailsPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="scenes" title="Scenes">
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<PerformerScenesPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="galleries" title="Galleries">
|
||||
<Tab eventKey="galleries" title={intl.formatMessage({ id: "galleries" })}>
|
||||
<PerformerGalleriesPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title="Images">
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<PerformerImagesPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="edit" title="Edit">
|
||||
<Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}>
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={activeTabKey === "edit"}
|
||||
@@ -148,7 +150,10 @@ export const Performer: React.FC = () => {
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab eventKey="operations" title="Operations">
|
||||
<Tab
|
||||
eventKey="operations"
|
||||
title={intl.formatMessage({ id: "operations" })}
|
||||
>
|
||||
<PerformerOperationsPanel performer={performer} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -163,7 +168,10 @@ export const Performer: React.FC = () => {
|
||||
<span className="age">
|
||||
{TextUtils.age(performer.birthdate, performer.death_date)}
|
||||
</span>
|
||||
<span className="age-tail"> years old</span>
|
||||
<span className="age-tail">
|
||||
{" "}
|
||||
<FormattedMessage id="years_old" />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -173,7 +181,9 @@ export const Performer: React.FC = () => {
|
||||
if (performer?.aliases) {
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { TagLink } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { genderToString } from "src/core/StashService";
|
||||
@@ -24,7 +24,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<ul className="pl-0">
|
||||
{(performer.tags ?? []).map((tag) => (
|
||||
@@ -43,7 +45,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<RatingStars value={performer.rating} />
|
||||
</dd>
|
||||
@@ -111,36 +115,36 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
name="Gender"
|
||||
id="gender"
|
||||
value={genderToString(performer.gender ?? undefined)}
|
||||
/>
|
||||
<TextField
|
||||
name="Birthdate"
|
||||
id="birthdate"
|
||||
value={TextUtils.formatDate(intl, performer.birthdate ?? undefined)}
|
||||
/>
|
||||
<TextField
|
||||
name="Death Date"
|
||||
id="death_date"
|
||||
value={TextUtils.formatDate(intl, performer.death_date ?? undefined)}
|
||||
/>
|
||||
<TextField name="Ethnicity" value={performer.ethnicity} />
|
||||
<TextField name="Hair Color" value={performer.hair_color} />
|
||||
<TextField name="Eye Color" value={performer.eye_color} />
|
||||
<TextField name="Country" value={performer.country} />
|
||||
<TextField name="Height" value={formatHeight(performer.height)} />
|
||||
<TextField name="Weight" value={formatWeight(performer.weight)} />
|
||||
<TextField name="Measurements" value={performer.measurements} />
|
||||
<TextField name="Fake Tits" value={performer.fake_tits} />
|
||||
<TextField name="Career Length" value={performer.career_length} />
|
||||
<TextField name="Tattoos" value={performer.tattoos} />
|
||||
<TextField name="Piercings" value={performer.piercings} />
|
||||
<TextField name="Details" value={performer.details} />
|
||||
<TextField id="ethnicity" value={performer.ethnicity} />
|
||||
<TextField id="hair_color" value={performer.hair_color} />
|
||||
<TextField id="eye_color" value={performer.eye_color} />
|
||||
<TextField id="country" value={performer.country} />
|
||||
<TextField id="height" value={formatHeight(performer.height)} />
|
||||
<TextField id="weight" value={formatWeight(performer.weight)} />
|
||||
<TextField id="measurements" value={performer.measurements} />
|
||||
<TextField id="fake_tits" value={performer.fake_tits} />
|
||||
<TextField id="career_length" value={performer.career_length} />
|
||||
<TextField id="tattoos" value={performer.tattoos} />
|
||||
<TextField id="piercings" value={performer.piercings} />
|
||||
<TextField id="details" value={performer.details} />
|
||||
<URLField
|
||||
name="URL"
|
||||
id="url"
|
||||
value={performer.url}
|
||||
url={TextUtils.sanitiseURL(performer.url ?? "")}
|
||||
/>
|
||||
<URLField
|
||||
name="Twitter"
|
||||
id="twitter"
|
||||
value={performer.twitter}
|
||||
url={TextUtils.sanitiseURL(
|
||||
performer.twitter ?? "",
|
||||
@@ -148,7 +152,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
)}
|
||||
/>
|
||||
<URLField
|
||||
name="Instagram"
|
||||
id="instagram"
|
||||
value={performer.instagram}
|
||||
url={TextUtils.sanitiseURL(
|
||||
performer.instagram ?? "",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Row,
|
||||
Badge,
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
@@ -64,6 +65,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
onImageChange,
|
||||
onImageEncoding,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
@@ -610,7 +612,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
<span className="fa-icon">
|
||||
<Icon icon="sync-alt" />
|
||||
</span>
|
||||
<span>Reload scrapers</span>
|
||||
<span>
|
||||
<FormattedMessage id="actions.reload_scrapers" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -626,7 +630,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
rootClose
|
||||
>
|
||||
<Button variant="secondary" className="mr-2">
|
||||
Scrape with...
|
||||
<FormattedMessage id="actions.scrape_with" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
@@ -694,7 +698,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
disabled={!formik.dirty}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
{!isNew ? (
|
||||
<Button
|
||||
@@ -702,7 +706,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
variant="danger"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
@@ -718,7 +722,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
variant="danger"
|
||||
onClick={() => formik.setFieldValue("image", null)}
|
||||
>
|
||||
Clear image
|
||||
<FormattedMessage id="actions.clear_image" />
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -747,10 +751,19 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
<Modal
|
||||
show={isDeleteAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
<p>Are you sure you want to delete {performer.name}?</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="dialogs.delete_confirm"
|
||||
values={{ entityName: performer.name }}
|
||||
/>
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -759,7 +772,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
return (
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Tags
|
||||
<FormattedMessage id="tags" defaultMessage="Tags" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<TagSelect
|
||||
@@ -838,7 +851,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
return (
|
||||
<Form.Group controlId={field} as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
{title}
|
||||
<FormattedMessage id={field} defaultMessage={title} />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -866,7 +879,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
<Form noValidate onSubmit={formik.handleSubmit} id="performer-edit">
|
||||
<Form.Group controlId="name" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Name
|
||||
<FormattedMessage id="name" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -883,7 +896,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
<Form.Group controlId="aliases" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Alias
|
||||
<FormattedMessage id="aliases" />
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -897,7 +910,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
<Form.Group as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Gender
|
||||
<FormattedMessage id="gender" />
|
||||
</Form.Label>
|
||||
<Col xs="auto">
|
||||
<Form.Control
|
||||
@@ -927,7 +940,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
<Form.Group controlId="tattoos" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Tattoos
|
||||
<FormattedMessage id="tattoos" />
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -941,7 +954,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
<Form.Group controlId="piercings" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Piercings
|
||||
<FormattedMessage id="piercings" />
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -955,9 +968,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
{renderTextField("career_length", "Career Length")}
|
||||
|
||||
<Form.Group controlId="name" as={Row}>
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
URL
|
||||
<FormattedMessage id="url" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<InputGroup>
|
||||
@@ -975,7 +988,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
{renderTextField("instagram", "Instagram")}
|
||||
<Form.Group controlId="details" as={Row}>
|
||||
<Form.Label column sm={labelXS} xl={labelXL}>
|
||||
Details
|
||||
<FormattedMessage id="details" />
|
||||
</Form.Label>
|
||||
<Col sm={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -990,7 +1003,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Rating
|
||||
<FormattedMessage id="rating" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<RatingStars
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateMetadataAutoTag } from "src/core/StashService";
|
||||
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 { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
@@ -49,12 +50,13 @@ function renderScrapedGender(
|
||||
}
|
||||
|
||||
function renderScrapedGenderRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Gender"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedGender(result)}
|
||||
renderNewField={() =>
|
||||
@@ -91,6 +93,7 @@ function renderScrapedTags(
|
||||
}
|
||||
|
||||
function renderScrapedTagsRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newTags: GQL.ScrapedSceneTag[],
|
||||
@@ -98,7 +101,7 @@ function renderScrapedTagsRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Tags"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedTags(result)}
|
||||
renderNewField={() =>
|
||||
@@ -123,6 +126,8 @@ interface IPerformerScrapeDialogProps {
|
||||
export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
props: IPerformerScrapeDialogProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
|
||||
function translateScrapedGender(scrapedGender?: string | null) {
|
||||
if (!scrapedGender) {
|
||||
return;
|
||||
@@ -385,109 +390,114 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
title="Name"
|
||||
title={intl.formatMessage({ id: "name" })}
|
||||
result={name}
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title="Aliases"
|
||||
title={intl.formatMessage({ id: "aliases" })}
|
||||
result={aliases}
|
||||
onChange={(value) => setAliases(value)}
|
||||
/>
|
||||
{renderScrapedGenderRow(gender, (value) => setGender(value))}
|
||||
{renderScrapedGenderRow(
|
||||
intl.formatMessage({ id: "gender" }),
|
||||
gender,
|
||||
(value) => setGender(value)
|
||||
)}
|
||||
<ScrapedInputGroupRow
|
||||
title="Birthdate"
|
||||
title={intl.formatMessage({ id: "birthdate" })}
|
||||
result={birthdate}
|
||||
onChange={(value) => setBirthdate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Death Date"
|
||||
title={intl.formatMessage({ id: "death_date" })}
|
||||
result={deathDate}
|
||||
onChange={(value) => setDeathDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Ethnicity"
|
||||
title={intl.formatMessage({ id: "ethnicity" })}
|
||||
result={ethnicity}
|
||||
onChange={(value) => setEthnicity(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Country"
|
||||
title={intl.formatMessage({ id: "country" })}
|
||||
result={country}
|
||||
onChange={(value) => setCountry(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Hair Color"
|
||||
title={intl.formatMessage({ id: "hair_color" })}
|
||||
result={hairColor}
|
||||
onChange={(value) => setHairColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Eye Color"
|
||||
title={intl.formatMessage({ id: "eye_color" })}
|
||||
result={eyeColor}
|
||||
onChange={(value) => setEyeColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Weight"
|
||||
title={intl.formatMessage({ id: "weight" })}
|
||||
result={weight}
|
||||
onChange={(value) => setWeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Height"
|
||||
title={intl.formatMessage({ id: "height" })}
|
||||
result={height}
|
||||
onChange={(value) => setHeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Measurements"
|
||||
title={intl.formatMessage({ id: "measurements" })}
|
||||
result={measurements}
|
||||
onChange={(value) => setMeasurements(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Fake Tits"
|
||||
title={intl.formatMessage({ id: "fake_tits" })}
|
||||
result={fakeTits}
|
||||
onChange={(value) => setFakeTits(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Career Length"
|
||||
title={intl.formatMessage({ id: "career_length" })}
|
||||
result={careerLength}
|
||||
onChange={(value) => setCareerLength(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title="Tattoos"
|
||||
title={intl.formatMessage({ id: "tattoos" })}
|
||||
result={tattoos}
|
||||
onChange={(value) => setTattoos(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title="Piercings"
|
||||
title={intl.formatMessage({ id: "piercings" })}
|
||||
result={piercings}
|
||||
onChange={(value) => setPiercings(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="URL"
|
||||
title={intl.formatMessage({ id: "url" })}
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Twitter"
|
||||
title={intl.formatMessage({ id: "twitter" })}
|
||||
result={twitter}
|
||||
onChange={(value) => setTwitter(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Instagram"
|
||||
title={intl.formatMessage({ id: "instagram" })}
|
||||
result={instagram}
|
||||
onChange={(value) => setInstagram(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
{renderScrapedTagsRow(
|
||||
intl.formatMessage({ id: "tags" }),
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
<ScrapedImageRow
|
||||
title="Performer Image"
|
||||
title={intl.formatMessage({ id: "performer_image" })}
|
||||
className="performer-image"
|
||||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
@@ -498,7 +508,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
||||
|
||||
return (
|
||||
<ScrapeDialog
|
||||
title="Performer Scrape Results"
|
||||
title={intl.formatMessage({ id: "dialogs.scrape_entity_title" })}
|
||||
renderScrapeRows={renderScrapeRows}
|
||||
onClose={(apply) => {
|
||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal, LoadingIndicator } from "src/components/Shared";
|
||||
@@ -24,6 +25,7 @@ const PerformerScrapeModal: React.FC<IProps> = ({
|
||||
onHide,
|
||||
onSelectPerformer,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [query, setQuery] = useState<string>(name ?? "");
|
||||
const { data, loading } = useScrapePerformerList(scraper.id, query);
|
||||
@@ -41,7 +43,11 @@ const PerformerScrapeModal: React.FC<IProps> = ({
|
||||
show
|
||||
onHide={onHide}
|
||||
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}>
|
||||
<Form.Control
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { debounce } from "lodash";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal, LoadingIndicator } from "src/components/Shared";
|
||||
@@ -24,6 +25,7 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
||||
onHide,
|
||||
onSelectPerformer,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [query, setQuery] = useState<string>(name ?? "");
|
||||
const { data, loading } = GQL.useQueryStashBoxPerformerQuery({
|
||||
@@ -49,7 +51,11 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
||||
show
|
||||
onHide={onHide}
|
||||
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}>
|
||||
<Form.Control
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import {
|
||||
@@ -31,6 +32,7 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
||||
persistState,
|
||||
extraCriteria,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
@@ -41,12 +43,12 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
||||
onClick: getRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
@@ -112,8 +114,8 @@ export const PerformerList: React.FC<IPerformerList> = ({
|
||||
<DeleteEntityDialog
|
||||
selected={selectedPerformers}
|
||||
onClose={onClose}
|
||||
singularEntity="performer"
|
||||
pluralEntity="performers"
|
||||
singularEntity={intl.formatMessage({ id: "performer" })}
|
||||
pluralEntity={intl.formatMessage({ id: "performers" })}
|
||||
destroyMutation={usePerformersDestroy}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Tooltip,
|
||||
} from "react-bootstrap";
|
||||
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 * as GQL from "src/core/generated-graphql";
|
||||
@@ -32,6 +32,7 @@ import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
const CLASSNAME = "duplicate-checker";
|
||||
|
||||
export const SceneDuplicateChecker: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const { page, size, distance } = querystring.parse(history.location.search);
|
||||
const currentPage = Number.parseInt(
|
||||
@@ -321,10 +322,14 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
{maybeRenderEdit()}
|
||||
<h4>Duplicate Scenes</h4>
|
||||
<h4>
|
||||
<FormattedMessage id="dupe_check.title" />
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Row noGutters>
|
||||
<Form.Label>Search Accuracy</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="dupe_check.search_accuracy_label" />
|
||||
</Form.Label>
|
||||
<Col xs={2}>
|
||||
<Form.Control
|
||||
as="select"
|
||||
@@ -340,31 +345,53 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
defaultValue={distance ?? 0}
|
||||
className="input-control ml-4"
|
||||
>
|
||||
<option value={0}>Exact</option>
|
||||
<option value={4}>High</option>
|
||||
<option value={8}>Medium</option>
|
||||
<option value={10}>Low</option>
|
||||
<option value={0}>
|
||||
{intl.formatMessage({ id: "dupe_check.options.exact" })}
|
||||
</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>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Text>
|
||||
Levels below “Exact” can take longer to calculate. False
|
||||
positives might also be returned on lower accuracy levels.
|
||||
<FormattedMessage id="dupe_check.description" />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
{maybeRenderMissingPhashWarning()}
|
||||
<div className="d-flex mb-2">
|
||||
<h6 className="mr-auto align-self-center">
|
||||
{scenes.length} sets of duplicates found.
|
||||
<FormattedMessage
|
||||
id="dupe_check.found_sets"
|
||||
values={{ setCount: scenes.length }}
|
||||
/>
|
||||
</h6>
|
||||
{checkCount > 0 && (
|
||||
<ButtonGroup>
|
||||
<OverlayTrigger overlay={<Tooltip id="edit">Edit</Tooltip>}>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="edit">
|
||||
{intl.formatMessage({ id: "actions.edit" })}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" onClick={onEdit}>
|
||||
<Icon icon="pencil-alt" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
<OverlayTrigger overlay={<Tooltip id="delete">Delete</Tooltip>}>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="delete">
|
||||
{intl.formatMessage({ id: "actions.delete" })}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button variant="danger" onClick={handleDeleteChecked}>
|
||||
<Icon icon="trash" />
|
||||
</Button>
|
||||
@@ -416,14 +443,14 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
<tr>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th>Details</th>
|
||||
<th>{intl.formatMessage({ id: "details" })}</th>
|
||||
<th> </th>
|
||||
<th>Duration</th>
|
||||
<th>Filesize</th>
|
||||
<th>Resolution</th>
|
||||
<th>Bitrate</th>
|
||||
<th>Codec</th>
|
||||
<th>Delete</th>
|
||||
<th>{intl.formatMessage({ id: "duration" })}</th>
|
||||
<th>{intl.formatMessage({ id: "filesize" })}</th>
|
||||
<th>{intl.formatMessage({ id: "resolution" })}</th>
|
||||
<th>{intl.formatMessage({ id: "bitrate" })}</th>
|
||||
<th>{intl.formatMessage({ id: "media_info.video_codec" })}</th>
|
||||
<th>{intl.formatMessage({ id: "actions.delete" })}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -494,7 +521,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
variant="danger"
|
||||
onClick={() => handleDeleteScene(scene)}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Form,
|
||||
InputGroup,
|
||||
} from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { ParserField } from "./ParserField";
|
||||
import { ShowFields } from "./ShowFields";
|
||||
|
||||
@@ -83,6 +84,7 @@ interface IParserInputProps {
|
||||
export const ParserInput: React.FC<IParserInputProps> = (
|
||||
props: IParserInputProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const [pattern, setPattern] = useState<string>(props.input.pattern);
|
||||
const [ignoreWords, setIgnoreWords] = useState<string>(
|
||||
props.input.ignoreWords.join(" ")
|
||||
@@ -127,7 +129,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
<Form.Group>
|
||||
<Form.Group className="row">
|
||||
<Form.Label htmlFor="filename-pattern" className="col-2">
|
||||
Filename Pattern
|
||||
{intl.formatMessage({
|
||||
id: "config.tools.scene_filename_parser.filename_pattern",
|
||||
})}
|
||||
</Form.Label>
|
||||
<InputGroup className="col-8">
|
||||
<Form.Control
|
||||
@@ -139,7 +143,12 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
value={pattern}
|
||||
/>
|
||||
<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) => (
|
||||
<Dropdown.Item
|
||||
key={item.field}
|
||||
@@ -153,12 +162,18 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
<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.Group>
|
||||
|
||||
<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">
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
@@ -169,14 +184,18 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
/>
|
||||
</InputGroup>
|
||||
<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.Group>
|
||||
|
||||
<h5>Title</h5>
|
||||
<h5>{intl.formatMessage({ id: "title" })}</h5>
|
||||
<Form.Group className="row">
|
||||
<Form.Label htmlFor="whitespace-characters" className="col-2">
|
||||
Whitespace characters:
|
||||
{intl.formatMessage({
|
||||
id: "config.tools.scene_filename_parser.whitespace_chars",
|
||||
})}
|
||||
</Form.Label>
|
||||
<InputGroup className="col-8">
|
||||
<Form.Control
|
||||
@@ -188,7 +207,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
/>
|
||||
</InputGroup>
|
||||
<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.Group>
|
||||
<Form.Group>
|
||||
@@ -199,7 +220,11 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
checked={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>
|
||||
|
||||
{/* TODO - mapping stuff will go here */}
|
||||
@@ -208,7 +233,9 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
<DropdownButton
|
||||
variant="secondary"
|
||||
id="recipe-select"
|
||||
title="Select Parser Recipe"
|
||||
title={intl.formatMessage({
|
||||
id: "config.tools.scene_filename_parser.select_parser_recipe",
|
||||
})}
|
||||
drop="up"
|
||||
>
|
||||
{builtInRecipes.map((item) => (
|
||||
@@ -232,7 +259,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
|
||||
|
||||
<Form.Group className="row">
|
||||
<Button variant="secondary" className="ml-3 col-1" onClick={onFind}>
|
||||
Find
|
||||
{intl.formatMessage({ id: "actions.find" })}
|
||||
</Button>
|
||||
<Form.Control
|
||||
as="select"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from "react";
|
||||
import { Button, Card, Form, Table } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
queryParseSceneFilenames,
|
||||
@@ -35,6 +36,7 @@ const initialShowFieldsState = new Map<string, boolean>([
|
||||
]);
|
||||
|
||||
export const SceneFilenameParser: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
||||
const [parserInput, setParserInput] = useState<IParserInput>(
|
||||
@@ -184,7 +186,12 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
|
||||
try {
|
||||
await updateScenes();
|
||||
Toast.success({ content: "Updated scenes" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "scenes" }).toLocaleLowerCase() }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -333,17 +340,41 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
<Table>
|
||||
<thead>
|
||||
<tr className="scene-parser-row">
|
||||
<th className="parser-field-filename">Filename</th>
|
||||
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
|
||||
{renderHeader("Date", allDateSet, onSelectAllDateSet)}
|
||||
{renderHeader("Rating", allRatingSet, onSelectAllRatingSet)}
|
||||
<th className="parser-field-filename">
|
||||
{intl.formatMessage({
|
||||
id: "config.tools.scene_filename_parser.filename",
|
||||
})}
|
||||
</th>
|
||||
{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,
|
||||
onSelectAllPerformerSet
|
||||
)}
|
||||
{renderHeader("Tags", allTagSet, onSelectAllTagSet)}
|
||||
{renderHeader("Studio", allStudioSet, onSelectAllStudioSet)}
|
||||
{renderHeader(
|
||||
intl.formatMessage({ id: "tags" }),
|
||||
allTagSet,
|
||||
onSelectAllTagSet
|
||||
)}
|
||||
{renderHeader(
|
||||
intl.formatMessage({ id: "studio" }),
|
||||
allStudioSet,
|
||||
onSelectAllStudioSet
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -365,7 +396,7 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
onChangePage={(page) => onPageChanged(page)}
|
||||
/>
|
||||
<Button variant="primary" onClick={onApply}>
|
||||
Apply
|
||||
<FormattedMessage id="actions.apply" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -373,7 +404,9 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
|
||||
return (
|
||||
<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
|
||||
input={parserInput}
|
||||
onFind={(input) => onFindClicked(input)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Collapse } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared";
|
||||
|
||||
interface IShowFieldsProps {
|
||||
@@ -8,6 +9,7 @@ interface IShowFieldsProps {
|
||||
}
|
||||
|
||||
export const ShowFields = (props: IShowFieldsProps) => {
|
||||
const intl = useIntl();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
function handleClick(label: string) {
|
||||
@@ -33,7 +35,11 @@ export const ShowFields = (props: IShowFieldsProps) => {
|
||||
<div>
|
||||
<Button onClick={() => setOpen(!open)} className="minimal">
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||
<span>Display fields</span>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "config.tools.scene_filename_parser.display_fields",
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
<Collapse in={open}>
|
||||
<div>{fieldRows}</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IDeleteSceneDialogProps {
|
||||
selected: GQL.SlimSceneDataFragment[];
|
||||
@@ -14,20 +14,22 @@ interface IDeleteSceneDialogProps {
|
||||
export const DeleteScenesDialog: React.FC<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 pluralMessageId = "deleteScenesText";
|
||||
|
||||
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 pluralMessage =
|
||||
"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.";
|
||||
|
||||
const header = plural ? "Delete Scenes" : "Delete Scene";
|
||||
const toastMessage = plural ? "Deleted scenes" : "Deleted scene";
|
||||
const messageId = plural ? pluralMessageId : singleMessageId;
|
||||
const message = plural ? pluralMessage : singleMessage;
|
||||
const header = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_title" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const toastMessage = intl.formatMessage(
|
||||
{ id: "toast.delete_entity" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
const message = intl.formatMessage(
|
||||
{ id: "dialogs.delete_entity_desc" },
|
||||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(false);
|
||||
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
|
||||
@@ -63,28 +65,32 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
||||
show
|
||||
icon="trash-alt"
|
||||
header={header}
|
||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||
accept={{
|
||||
variant: "danger",
|
||||
onClick: onDelete,
|
||||
text: intl.formatMessage({ id: "actions.delete" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isDeleting}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage id={messageId} defaultMessage={message} />
|
||||
</p>
|
||||
<p>{message}</p>
|
||||
<Form>
|
||||
<Form.Check
|
||||
id="delete-file"
|
||||
checked={deleteFile}
|
||||
label="Delete file"
|
||||
label={intl.formatMessage({ id: "actions.delete_file" })}
|
||||
onChange={() => setDeleteFile(!deleteFile)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="delete-generated"
|
||||
checked={deleteGenerated}
|
||||
label="Delete generated supporting files"
|
||||
label={intl.formatMessage({
|
||||
id: "actions.delete_generated_supporting_files",
|
||||
})}
|
||||
onChange={() => setDeleteGenerated(!deleteGenerated)}
|
||||
/>
|
||||
</Form>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useBulkSceneUpdate } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -17,6 +18,7 @@ interface IListOperationProps {
|
||||
export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
props: IListOperationProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
@@ -134,7 +136,12 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
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);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -340,11 +347,21 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header="Edit Scenes"
|
||||
accept={{ onClick: onSave, text: "Apply" }}
|
||||
header={intl.formatMessage(
|
||||
{ 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={{
|
||||
onClick: () => props.onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isUpdating}
|
||||
@@ -352,7 +369,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
<Form>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -365,7 +382,7 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studio" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -379,19 +396,23 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="performers" />
|
||||
</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="organized">
|
||||
<Form.Check
|
||||
type="checkbox"
|
||||
label="Organized"
|
||||
label={intl.formatMessage({ id: "organized" })}
|
||||
checked={organized}
|
||||
ref={checkboxRef}
|
||||
onChange={() => cycleOrganized()}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DropdownButton,
|
||||
Spinner,
|
||||
} from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon, SweatDrops } from "src/components/Shared";
|
||||
|
||||
export interface IOCounterButtonProps {
|
||||
@@ -21,6 +22,7 @@ export interface IOCounterButtonProps {
|
||||
export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||
props: IOCounterButtonProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
if (props.loading) return <Spinner animation="border" role="status" />;
|
||||
|
||||
const renderButton = () => (
|
||||
@@ -28,7 +30,7 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||
className="minimal pr-1"
|
||||
onClick={props.onIncrement}
|
||||
variant="secondary"
|
||||
title="O-Counter"
|
||||
title={intl.formatMessage({ id: "o_counter" })}
|
||||
>
|
||||
<SweatDrops />
|
||||
<span className="ml-2">{props.value}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Button, Badge, Card } from "react-bootstrap";
|
||||
import { TextUtils } from "src/utils";
|
||||
@@ -46,7 +47,7 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
||||
className="ml-auto"
|
||||
onClick={() => onEdit(marker)}
|
||||
>
|
||||
Edit
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Button>
|
||||
</div>
|
||||
<div>{TextUtils.secondsToTimestamp(marker.seconds)}</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useParams, useLocation, useHistory, Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
@@ -45,6 +46,7 @@ export const Scene: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const [updateScene] = useSceneUpdate();
|
||||
const [generateScreenshot] = useSceneGenerateScreenshot();
|
||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||
@@ -197,7 +199,17 @@ export const Scene: React.FC = () => {
|
||||
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) {
|
||||
@@ -211,7 +223,9 @@ export const Scene: React.FC = () => {
|
||||
at,
|
||||
},
|
||||
});
|
||||
Toast.success({ content: "Generating screenshot" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage({ id: "toast.generating_screenshot" }),
|
||||
});
|
||||
}
|
||||
|
||||
async function onQueueLessScenes() {
|
||||
@@ -343,14 +357,14 @@ export const Scene: React.FC = () => {
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onRescan()}
|
||||
>
|
||||
Rescan
|
||||
<FormattedMessage id="actions.rescan" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsGenerateDialogOpen(true)}
|
||||
>
|
||||
Generate...
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-screenshot"
|
||||
@@ -359,21 +373,24 @@ export const Scene: React.FC = () => {
|
||||
onGenerateScreenshot(JWUtils.getPlayer().getPosition())
|
||||
}
|
||||
>
|
||||
Generate thumbnail from current
|
||||
<FormattedMessage id="actions.generate_thumb_from_current" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-default"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => onGenerateScreenshot()}
|
||||
>
|
||||
Generate default thumbnail
|
||||
<FormattedMessage id="actions.generate_thumb_default" />
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="delete-scene"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete Scene
|
||||
<FormattedMessage
|
||||
id="actions.delete_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "scene" }) }}
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
@@ -393,43 +410,60 @@ export const Scene: React.FC = () => {
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-details-panel">Details</Nav.Link>
|
||||
<Nav.Link eventKey="scene-details-panel">
|
||||
<FormattedMessage id="scenes" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{(queueScenes ?? []).length > 0 ? (
|
||||
<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.Link eventKey="scene-markers-panel">Markers</Nav.Link>
|
||||
<Nav.Link eventKey="scene-markers-panel">
|
||||
<FormattedMessage id="markers" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{scene.movies.length > 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{scene.galleries.length === 1 ? (
|
||||
{scene.galleries.length >= 1 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-gallery-panel">Gallery</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
{scene.galleries.length > 1 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-galleries-panel">Galleries</Nav.Link>
|
||||
<Nav.Link eventKey="scene-galleries-panel">
|
||||
<FormattedMessage
|
||||
id="countables.gallery"
|
||||
values={{ count: scene.galleries.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<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.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.Link eventKey="scene-edit-panel">Edit</Nav.Link>
|
||||
<Nav.Link eventKey="scene-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<ButtonGroup className="ml-auto">
|
||||
<Nav.Item className="ml-auto">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
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 { TextUtils } from "src/utils";
|
||||
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;
|
||||
return (
|
||||
<>
|
||||
<h6>Details</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="details" />
|
||||
</h6>
|
||||
<p className="pre">{props.scene.details}</p>
|
||||
</>
|
||||
);
|
||||
@@ -30,7 +32,12 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
));
|
||||
return (
|
||||
<>
|
||||
<h6>Tags</h6>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="countables.tags"
|
||||
values={{ count: props.scene.tags.length }}
|
||||
/>
|
||||
</h6>
|
||||
{tags}
|
||||
</>
|
||||
);
|
||||
@@ -49,7 +56,12 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h6>Performers</h6>
|
||||
<h6>
|
||||
<FormattedMessage
|
||||
id="countables.performers"
|
||||
values={{ count: props.scene.performers.length }}
|
||||
/>
|
||||
</h6>
|
||||
<div className="row justify-content-center scene-performers">
|
||||
{cards}
|
||||
</div>
|
||||
@@ -85,14 +97,15 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
|
||||
) : undefined}
|
||||
{props.scene.rating ? (
|
||||
<h6>
|
||||
Rating: <RatingStars value={props.scene.rating} />
|
||||
<FormattedMessage id="rating" />:{" "}
|
||||
<RatingStars value={props.scene.rating} />
|
||||
</h6>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{props.scene.file.width && props.scene.file.height && (
|
||||
<h6>
|
||||
Resolution:{" "}
|
||||
<FormattedMessage id="resolution" />:{" "}
|
||||
{TextUtils.resolution(
|
||||
props.scene.file.width,
|
||||
props.scene.file.height
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
@@ -49,6 +50,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
isVisible,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||
scene.galleries.map((g) => ({
|
||||
@@ -229,7 +231,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
},
|
||||
});
|
||||
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
|
||||
formik.resetForm({ values: formik.values });
|
||||
}
|
||||
@@ -360,7 +367,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
<DropdownButton
|
||||
className="d-inline-block"
|
||||
id="scene-scrape"
|
||||
title="Scrape with..."
|
||||
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||
>
|
||||
{stashBoxes.map((s, index) => (
|
||||
<Dropdown.Item
|
||||
@@ -379,7 +386,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
<span className="fa-icon">
|
||||
<Icon icon="sync-alt" />
|
||||
</span>
|
||||
<span>Reload scrapers</span>
|
||||
<span>
|
||||
<FormattedMessage id="actions.reload_scrapers" />
|
||||
</span>
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
@@ -549,7 +558,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
<div id="scene-edit-details">
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
message="Unsaved changes. Are you sure you want to leave?"
|
||||
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||
/>
|
||||
|
||||
{maybeRenderScrapeDialog()}
|
||||
@@ -562,14 +571,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
disabled={!formik.dirty}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
</div>
|
||||
<Col xs={6} className="text-right">
|
||||
@@ -579,10 +588,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
</div>
|
||||
<div className="form-container row px-3">
|
||||
<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}>
|
||||
<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">
|
||||
{maybeRenderScrapeButton()}
|
||||
</div>
|
||||
@@ -590,16 +601,20 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
<Col xs={9}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder="URL"
|
||||
placeholder={intl.formatMessage({ id: "url" })}
|
||||
{...formik.getFieldProps("url")}
|
||||
isInvalid={!!formik.getFieldMeta("url").error}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField("date", "Date", "YYYY-MM-DD")}
|
||||
{renderTextField(
|
||||
"date",
|
||||
intl.formatMessage({ id: "date" }),
|
||||
"YYYY-MM-DD"
|
||||
)}
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
@@ -612,7 +627,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
</Form.Group>
|
||||
<Form.Group controlId="galleries" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Galleries",
|
||||
title: intl.formatMessage({ id: "galleries" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<GallerySelect
|
||||
@@ -624,7 +639,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
title: intl.formatMessage({ id: "studios" }),
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
@@ -641,7 +656,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Performers",
|
||||
title: intl.formatMessage({ id: "performers" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -664,7 +679,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="moviesScenes" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Movies/Scenes",
|
||||
title: `${intl.formatMessage({
|
||||
id: "movies",
|
||||
})}/${intl.formatMessage({ id: "scenes" })}`,
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -685,7 +702,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Tags",
|
||||
title: intl.formatMessage({ id: "tags" }),
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
@@ -726,7 +743,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
<Button
|
||||
variant="danger"
|
||||
className="mr-2 py-0"
|
||||
title="Delete StashID"
|
||||
title={intl.formatMessage(
|
||||
{ id: "actions.delete_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||
)}
|
||||
onClick={() => removeStashID(stashID)}
|
||||
>
|
||||
<Icon icon="trash-alt" />
|
||||
@@ -740,7 +760,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>Details</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="details" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="scene-description text-input"
|
||||
@@ -752,14 +774,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
</Form.Group>
|
||||
<div>
|
||||
<Form.Group controlId="cover">
|
||||
<Form.Label>Cover Image</Form.Label>
|
||||
<Form.Label>
|
||||
<FormattedMessage id="cover_image" />
|
||||
</Form.Label>
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<img
|
||||
className="scene-cover"
|
||||
src={coverImagePreview}
|
||||
alt="Scene cover"
|
||||
alt={intl.formatMessage({ id: "cover_image" })}
|
||||
/>
|
||||
)}
|
||||
<ImageInput
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { FormattedNumber } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
@@ -15,7 +15,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
if (props.scene.oshash) {
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -26,7 +28,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
if (props.scene.checksum) {
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -39,7 +43,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
} = props;
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Path</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={`file://${path}`} className="col-8">
|
||||
<TruncatedText text={`file://${props.scene.path}`} />
|
||||
</a>{" "}
|
||||
@@ -50,7 +56,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
function renderStream() {
|
||||
return (
|
||||
<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">
|
||||
<TruncatedText text={props.scene.paths.stream} />
|
||||
</a>{" "}
|
||||
@@ -69,7 +77,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">File Size</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="filesize" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<FormattedNumber
|
||||
value={size}
|
||||
@@ -90,7 +100,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Duration</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="duration" />
|
||||
</span>
|
||||
<TruncatedText
|
||||
className="col-8"
|
||||
text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
|
||||
@@ -105,7 +117,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Dimensions</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="dimensions" />
|
||||
</span>
|
||||
<TruncatedText
|
||||
className="col-8"
|
||||
text={`${props.scene.file.width} x ${props.scene.file.height}`}
|
||||
@@ -120,7 +134,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Frame Rate</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="framerate" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
|
||||
second
|
||||
@@ -136,7 +152,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Bit Rate</span>
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="bitrate" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<FormattedNumber
|
||||
value={(props.scene.file.bitrate ?? 0) / 1000000}
|
||||
@@ -154,7 +172,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -166,7 +186,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@@ -178,7 +200,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
}
|
||||
return (
|
||||
<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">
|
||||
<TruncatedText text={props.scene.url} />
|
||||
</a>
|
||||
@@ -224,7 +248,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
return (
|
||||
<div className="row">
|
||||
<abbr className="col-4" title="Perceptual hash">
|
||||
PHash
|
||||
<FormattedMessage id="media_info.phash" />
|
||||
</abbr>
|
||||
<TruncatedText className="col-8" text={props.scene.phash} />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Field, FieldProps, Form as FormikForm, Formik } from "formik";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
@@ -169,7 +170,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
onClick={onClose}
|
||||
className="ml-2"
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
{editingMarker && (
|
||||
<Button
|
||||
@@ -177,7 +178,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
||||
className="ml-auto"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { WallPanel } from "src/components/Wall/WallPanel";
|
||||
@@ -57,7 +58,9 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
|
||||
return (
|
||||
<div className="scene-markers-panel">
|
||||
<Button onClick={() => onOpenEditor()}>Create Marker</Button>
|
||||
<Button onClick={() => onOpenEditor()}>
|
||||
<FormattedMessage id="actions.create_marker" />
|
||||
</Button>
|
||||
<div className="container">
|
||||
<PrimaryTags
|
||||
sceneMarkers={props.scene.scene_markers ?? []}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useAllMoviesForFilter } from "src/core/StashService";
|
||||
import { Form, Row, Col } from "react-bootstrap";
|
||||
@@ -13,6 +14,7 @@ export interface IProps {
|
||||
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { data } = useAllMoviesForFilter();
|
||||
|
||||
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
||||
@@ -72,10 +74,10 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||
<div className="movie-table">
|
||||
<Row>
|
||||
<Form.Label column xs={9}>
|
||||
Movie
|
||||
{intl.formatMessage({ id: "movie" })}
|
||||
</Form.Label>
|
||||
<Form.Label column xs={3}>
|
||||
Scene #
|
||||
{intl.formatMessage({ id: "movie_scene_number" })}
|
||||
</Form.Label>
|
||||
</Row>
|
||||
{renderTableData()}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { DurationUtils } from "src/utils";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
@@ -44,6 +45,7 @@ function renderScrapedStudio(
|
||||
}
|
||||
|
||||
function renderScrapedStudioRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void,
|
||||
newStudio?: GQL.ScrapedSceneStudio,
|
||||
@@ -51,7 +53,7 @@ function renderScrapedStudioRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Studio"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
@@ -90,6 +92,7 @@ function renderScrapedPerformers(
|
||||
}
|
||||
|
||||
function renderScrapedPerformersRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newPerformers: GQL.ScrapedScenePerformer[],
|
||||
@@ -97,7 +100,7 @@ function renderScrapedPerformersRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Performers"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedPerformers(result)}
|
||||
renderNewField={() =>
|
||||
@@ -136,6 +139,7 @@ function renderScrapedMovies(
|
||||
}
|
||||
|
||||
function renderScrapedMoviesRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newMovies: GQL.ScrapedSceneMovie[],
|
||||
@@ -143,7 +147,7 @@ function renderScrapedMoviesRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Movies"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedMovies(result)}
|
||||
renderNewField={() =>
|
||||
@@ -182,6 +186,7 @@ function renderScrapedTags(
|
||||
}
|
||||
|
||||
function renderScrapedTagsRow(
|
||||
title: string,
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newTags: GQL.ScrapedSceneTag[],
|
||||
@@ -189,7 +194,7 @@ function renderScrapedTagsRow(
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
title="Tags"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedTags(result)}
|
||||
renderNewField={() =>
|
||||
@@ -321,6 +326,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
const [createMovie] = useMovieCreate();
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
@@ -520,52 +526,56 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
title="Title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="URL"
|
||||
title={intl.formatMessage({ id: "url" })}
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title="Date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
{renderScrapedStudioRow(
|
||||
intl.formatMessage({ id: "studios" }),
|
||||
studio,
|
||||
(value) => setStudio(value),
|
||||
newStudio,
|
||||
createNewStudio
|
||||
)}
|
||||
{renderScrapedPerformersRow(
|
||||
intl.formatMessage({ id: "performers" }),
|
||||
performers,
|
||||
(value) => setPerformers(value),
|
||||
newPerformers,
|
||||
createNewPerformer
|
||||
)}
|
||||
{renderScrapedMoviesRow(
|
||||
intl.formatMessage({ id: "movies" }),
|
||||
movies,
|
||||
(value) => setMovies(value),
|
||||
newMovies,
|
||||
createNewMovie
|
||||
)}
|
||||
{renderScrapedTagsRow(
|
||||
intl.formatMessage({ id: "tags" }),
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
title="Cover Image"
|
||||
title={intl.formatMessage({ id: "cover_image" })}
|
||||
className="scene-cover"
|
||||
result={image}
|
||||
onChange={(value) => setImage(value)}
|
||||
@@ -576,7 +586,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
|
||||
return (
|
||||
<ScrapeDialog
|
||||
title="Scene Scrape Results"
|
||||
title={intl.formatMessage({ id: "dialogs.scrape_entity_title" })}
|
||||
renderScrapeRows={renderScrapeRows}
|
||||
onClose={(apply) => {
|
||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { JWUtils } from "src/utils";
|
||||
@@ -85,6 +86,8 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
divider: 100,
|
||||
};
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const [contrastValue, setContrastValue] = useState(contrastRange.default);
|
||||
const [brightnessValue, setBrightnessValue] = useState(
|
||||
brightnessRange.default
|
||||
@@ -342,7 +345,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderBlur() {
|
||||
return renderSlider({
|
||||
title: "Blur",
|
||||
title: intl.formatMessage({ id: "effect_filters.blur" }),
|
||||
range: blurRange,
|
||||
value: blurValue,
|
||||
setValue: setBlurValue,
|
||||
@@ -352,7 +355,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderContrast() {
|
||||
return renderSlider({
|
||||
title: "Contrast",
|
||||
title: intl.formatMessage({ id: "effect_filters.contrast" }),
|
||||
className: "contrast-slider",
|
||||
range: contrastRange,
|
||||
value: contrastValue,
|
||||
@@ -363,7 +366,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderBrightness() {
|
||||
return renderSlider({
|
||||
title: "Brightness",
|
||||
title: intl.formatMessage({ id: "effect_filters.brightness" }),
|
||||
className: "brightness-slider",
|
||||
range: brightnessRange,
|
||||
value: brightnessValue,
|
||||
@@ -374,7 +377,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderGammaSlider() {
|
||||
return renderSlider({
|
||||
title: "Gamma",
|
||||
title: intl.formatMessage({ id: "effect_filters.gamma" }),
|
||||
className: "gamma-slider",
|
||||
range: gammaRange,
|
||||
value: gammaValue,
|
||||
@@ -385,7 +388,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderSaturate() {
|
||||
return renderSlider({
|
||||
title: "Saturation",
|
||||
title: intl.formatMessage({ id: "effect_filters.saturation" }),
|
||||
className: "saturation-slider",
|
||||
range: saturateRange,
|
||||
value: saturateValue,
|
||||
@@ -396,7 +399,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderHueRotateSlider() {
|
||||
return renderSlider({
|
||||
title: "Hue",
|
||||
title: intl.formatMessage({ id: "effect_filters.hue" }),
|
||||
className: "hue-rotate-slider",
|
||||
range: hueRotateRange,
|
||||
value: hueRotateValue,
|
||||
@@ -407,7 +410,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderWhiteBalance() {
|
||||
return renderSlider({
|
||||
title: "Warmth",
|
||||
title: intl.formatMessage({ id: "effect_filters.warmth" }),
|
||||
className: "white-balance-slider",
|
||||
range: whiteBalanceRange,
|
||||
value: whiteBalanceValue,
|
||||
@@ -421,7 +424,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderRedSlider() {
|
||||
return renderSlider({
|
||||
title: "Red",
|
||||
title: intl.formatMessage({ id: "effect_filters.red" }),
|
||||
className: "red-slider",
|
||||
range: colourRange,
|
||||
value: redValue,
|
||||
@@ -434,7 +437,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderGreenSlider() {
|
||||
return renderSlider({
|
||||
title: "Green",
|
||||
title: intl.formatMessage({ id: "effect_filters.green" }),
|
||||
className: "green-slider",
|
||||
range: colourRange,
|
||||
value: greenValue,
|
||||
@@ -447,7 +450,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderBlueSlider() {
|
||||
return renderSlider({
|
||||
title: "Blue",
|
||||
title: intl.formatMessage({ id: "effect_filters.blue" }),
|
||||
className: "blue-slider",
|
||||
range: colourRange,
|
||||
value: blueValue,
|
||||
@@ -460,7 +463,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderRotate() {
|
||||
return renderSlider({
|
||||
title: "Rotate",
|
||||
title: intl.formatMessage({ id: "effect_filters.rotate" }),
|
||||
range: rotateRange,
|
||||
value: rotateValue,
|
||||
setValue: setRotateValue,
|
||||
@@ -472,7 +475,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderScale() {
|
||||
return renderSlider({
|
||||
title: "Scale",
|
||||
title: intl.formatMessage({ id: "effect_filters.scale" }),
|
||||
range: scaleRange,
|
||||
value: scaleValue,
|
||||
setValue: setScaleValue,
|
||||
@@ -482,7 +485,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
|
||||
function renderAspectRatio() {
|
||||
return renderSlider({
|
||||
title: "Aspect",
|
||||
title: intl.formatMessage({ id: "effect_filters.aspect" }),
|
||||
range: aspectRatioRange,
|
||||
value: aspectRatioValue,
|
||||
setValue: setAspectRatioValue,
|
||||
@@ -557,7 +560,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
type="button"
|
||||
onClick={() => onRotateAndScale(0)}
|
||||
>
|
||||
Rotate Left & Scale
|
||||
<FormattedMessage id="effect_filters.rotate_left_and_scale" />
|
||||
</Button>
|
||||
</span>
|
||||
<span className="col-6">
|
||||
@@ -567,7 +570,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
type="button"
|
||||
onClick={() => onRotateAndScale(1)}
|
||||
>
|
||||
Rotate Right & Scale
|
||||
<FormattedMessage id="effect_filters.rotate_right_and_scale" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -603,7 +606,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
type="button"
|
||||
onClick={() => onResetFilters()}
|
||||
>
|
||||
Reset Filters
|
||||
<FormattedMessage id="effect_filters.reset_filters" />
|
||||
</Button>
|
||||
</span>
|
||||
<span className="col-6">
|
||||
@@ -613,7 +616,7 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
type="button"
|
||||
onClick={() => onResetTransforms()}
|
||||
>
|
||||
Reset Transforms
|
||||
<FormattedMessage id="effect_filters.reset_transforms" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -632,7 +635,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
<div className="container scene-video-filter">
|
||||
<div className="row form-group">
|
||||
<span className="col-12">
|
||||
<h5>Filters</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="effect_filters.name" />
|
||||
</h5>
|
||||
</span>
|
||||
</div>
|
||||
{renderBrightness()}
|
||||
@@ -647,7 +652,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
{renderBlur()}
|
||||
<div className="row form-group">
|
||||
<span className="col-12">
|
||||
<h5>Transforms</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="effect_filters.name_transforms" />
|
||||
</h5>
|
||||
</span>
|
||||
</div>
|
||||
{renderRotate()}
|
||||
@@ -655,7 +662,9 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
|
||||
{renderAspectRatio()}
|
||||
<div className="row form-group">
|
||||
<span className="col-12">
|
||||
<h5>Actions</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="actions_name" />
|
||||
</h5>
|
||||
</span>
|
||||
</div>
|
||||
{renderRotateAndScale()}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface ISceneGenerateDialogProps {
|
||||
selectedIds: string[];
|
||||
@@ -42,6 +43,7 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
|
||||
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -97,11 +99,14 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="cogs"
|
||||
header="Generate"
|
||||
accept={{ onClick: onGenerate, text: "Generate" }}
|
||||
header={intl.formatMessage({ id: "actions.generate" })}
|
||||
accept={{
|
||||
onClick: onGenerate,
|
||||
text: intl.formatMessage({ id: "actions.generate" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
>
|
||||
@@ -110,7 +115,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
<Form.Check
|
||||
id="preview-task"
|
||||
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)}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
@@ -119,7 +126,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
id="image-preview-task"
|
||||
checked={imagePreviews}
|
||||
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)}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
@@ -132,12 +141,20 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
<Icon
|
||||
icon={previewOptionsOpen ? "chevron-down" : "chevron-right"}
|
||||
/>
|
||||
<span>Preview Options</span>
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_options",
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
<Collapse in={previewOptionsOpen}>
|
||||
<div>
|
||||
<Form.Group id="transcode-size">
|
||||
<h6>Preview encoding preset</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
@@ -153,14 +170,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
The preset regulates size, quality and encoding time of
|
||||
preview generation. Presets beyond “slow” have diminishing
|
||||
returns and are not recommended.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -172,12 +193,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Number of segments in preview files.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -189,12 +216,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={previewExcludeStart}
|
||||
@@ -203,14 +236,18 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={previewExcludeEnd}
|
||||
@@ -219,9 +256,9 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</div>
|
||||
@@ -230,25 +267,25 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
label="Sprites (for the scene scrubber)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||
onChange={() => setSprites(!sprites)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="marker-task"
|
||||
checked={markers}
|
||||
label="Markers (20 second videos which begin at the given timecode)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||
onChange={() => setMarkers(!markers)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="transcode-task"
|
||||
checked={transcodes}
|
||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||
onChange={() => setTranscodes(!transcodes)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="phash-task"
|
||||
checked={phashes}
|
||||
label="Perceptual hashes (for deduplication)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||
onChange={() => setPhashes(!phashes)}
|
||||
/>
|
||||
|
||||
@@ -256,7 +293,7 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
<Form.Check
|
||||
id="overwrite"
|
||||
checked={overwrite}
|
||||
label="Overwrite existing generated files"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.overwrite" })}
|
||||
onChange={() => setOverwrite(!overwrite)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import {
|
||||
@@ -32,6 +33,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
defaultSort,
|
||||
persistState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
@@ -39,26 +41,26 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Play selected",
|
||||
text: intl.formatMessage({ id: "actions.play_selected" }),
|
||||
onClick: playSelected,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Play Random",
|
||||
text: intl.formatMessage({ id: "actions.play_random" }),
|
||||
onClick: playRandom,
|
||||
},
|
||||
{
|
||||
text: "Generate...",
|
||||
text: intl.formatMessage({ id: "actions.generate" }),
|
||||
onClick: generate,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import { Icon, TruncatedText } from "src/components/Shared";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface ISceneListTableProps {
|
||||
scenes: GQL.SlimSceneDataFragment[];
|
||||
@@ -97,14 +98,30 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th className="text-left">Title</th>
|
||||
<th>Rating</th>
|
||||
<th>Duration</th>
|
||||
<th>Tags</th>
|
||||
<th>Performers</th>
|
||||
<th>Studio</th>
|
||||
<th>Movies</th>
|
||||
<th>Gallery</th>
|
||||
<th className="text-left">
|
||||
<FormattedMessage id="title" />
|
||||
</th>
|
||||
<th>
|
||||
<FormattedMessage id="rating" />
|
||||
</th>
|
||||
<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>
|
||||
</thead>
|
||||
<tbody>{props.scenes.map(renderSceneRow)}</tbody>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { FindSceneMarkersQueryResult } from "src/core/generated-graphql";
|
||||
import { queryFindSceneMarkers } from "src/core/StashService";
|
||||
@@ -16,10 +17,11 @@ interface ISceneMarkerList {
|
||||
}
|
||||
|
||||
export const SceneMarkerList: React.FC<ISceneMarkerList> = ({ filterHook }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Play Random",
|
||||
text: intl.formatMessage({ id: "actions.play_random" }),
|
||||
onClick: playRandom,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import queryString from "query-string";
|
||||
import { Card, Tab, Nav, Row, Col } from "react-bootstrap";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
||||
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
||||
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
|
||||
@@ -30,31 +31,47 @@ export const Settings: React.FC = () => {
|
||||
<Col sm={3} md={2}>
|
||||
<Nav variant="pills" className="flex-column">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="configuration">Configuration</Nav.Link>
|
||||
<Nav.Link eventKey="configuration">
|
||||
<FormattedMessage id="configuration" />
|
||||
</Nav.Link>
|
||||
</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.Link eventKey="tasks">Tasks</Nav.Link>
|
||||
<Nav.Link eventKey="tasks">
|
||||
<FormattedMessage id="config.categories.tasks" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="dlna">DLNA</Nav.Link>
|
||||
</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.Link eventKey="scrapers">Scrapers</Nav.Link>
|
||||
<Nav.Link eventKey="scrapers">
|
||||
<FormattedMessage id="config.categories.scrapers" />
|
||||
</Nav.Link>
|
||||
</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.Link eventKey="logs">Logs</Nav.Link>
|
||||
<Nav.Link eventKey="logs">
|
||||
<FormattedMessage id="config.categories.logs" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="about">About</Nav.Link>
|
||||
<Nav.Link eventKey="about">
|
||||
<FormattedMessage id="config.categories.about" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<hr className="d-sm-none" />
|
||||
</Nav>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button, Table } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useLatestVersion } from "src/core/StashService";
|
||||
|
||||
@@ -8,6 +9,8 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
const stashVersion = process.env.REACT_APP_STASH_VERSION;
|
||||
const buildTime = process.env.REACT_APP_DATE;
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const {
|
||||
data: dataLatest,
|
||||
error: errorLatest,
|
||||
@@ -22,7 +25,7 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td>Version:</td>
|
||||
<td>{intl.formatMessage({ id: "config.about.version" })}:</td>
|
||||
<td>{stashVersion}</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -39,8 +42,13 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
if (gitHash !== dataLatest.latestversion.shorthash) {
|
||||
return (
|
||||
<>
|
||||
<strong>{dataLatest.latestversion.shorthash} [NEW] </strong>
|
||||
<a href={dataLatest.latestversion.url}>Download</a>
|
||||
<strong>
|
||||
{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>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Latest Version Build Hash: </td>
|
||||
<td>
|
||||
{intl.formatMessage({
|
||||
id: "config.about.latest_version_build_hash",
|
||||
})}{" "}
|
||||
</td>
|
||||
<td>{maybeRenderLatestVersion()} </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<Button onClick={() => refetch()}>Check for new version</Button>
|
||||
<Button onClick={() => refetch()}>
|
||||
{intl.formatMessage({
|
||||
id: "config.about.check_for_new_version",
|
||||
})}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -73,11 +89,11 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
<tbody>
|
||||
{maybeRenderTag()}
|
||||
<tr>
|
||||
<td>Build hash:</td>
|
||||
<td>{intl.formatMessage({ id: "config.about.build_hash" })}</td>
|
||||
<td>{gitHash}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Build time:</td>
|
||||
<td>{intl.formatMessage({ id: "config.about.build_time" })}</td>
|
||||
<td>{buildTime}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -87,50 +103,69 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h4>About</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.categories.about" })}</h4>
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
Stash home at{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "config.about.stash_home" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://github.com/stashapp/stash"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Github
|
||||
GitHub
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Stash{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "config.about.stash_wiki" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://github.com/stashapp/stash/wiki"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Wiki
|
||||
</a>{" "}
|
||||
page
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Join our{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "config.about.stash_discord" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://discord.gg/2TsNFKt"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Discord
|
||||
</a>{" "}
|
||||
channel
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
Support us through{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "config.about.stash_open_collective" },
|
||||
{
|
||||
url: (
|
||||
<a
|
||||
href="https://opencollective.com/stashapp"
|
||||
rel="noopener noreferrer"
|
||||
@@ -138,6 +173,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
||||
>
|
||||
Open Collective
|
||||
</a>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
@@ -69,6 +70,7 @@ const ExclusionPatterns: React.FC<IExclusionPatternsProps> = (props) => {
|
||||
};
|
||||
|
||||
export const SettingsConfigurationPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
// Editing config state
|
||||
const [stashes, setStashes] = useState<GQL.StashConfig[]>([]);
|
||||
@@ -278,7 +280,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
const result = await updateGeneralConfig();
|
||||
// eslint-disable-next-line no-console
|
||||
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) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -363,7 +374,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>Library</h4>
|
||||
<h4>
|
||||
<FormattedMessage id="library" />
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Form.Group id="stashes">
|
||||
<h6>Stashes</h6>
|
||||
@@ -372,12 +385,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
setStashes={(s) => setStashes(s)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Directory locations to your content
|
||||
{intl.formatMessage({
|
||||
id: "config.general.directory_locations_to_your_content",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="database-path">
|
||||
<h6>Database Path</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="config.general.db_path_head" />
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={databasePath}
|
||||
@@ -386,12 +403,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
File location for the SQLite database (requires restart)
|
||||
{intl.formatMessage({ id: "config.general.sqlite_location" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="generated-path">
|
||||
<h6>Generated Path</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="config.general.generated_path_head" />
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={generatedPath}
|
||||
@@ -400,13 +419,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Directory location for the generated files (scene markers, scene
|
||||
previews, sprites, etc)
|
||||
{intl.formatMessage({
|
||||
id: "config.general.generated_files_location",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="cache-path">
|
||||
<h6>Cache Path</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="config.general.cache_path_head" />
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={cachePath}
|
||||
@@ -415,12 +437,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Directory location of the cache
|
||||
{intl.formatMessage({ id: "config.general.cache_location" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="video-extensions">
|
||||
<h6>Video Extensions</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="config.general.video_ext_head" />
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={videoExtensions}
|
||||
@@ -429,13 +453,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Comma-delimited list of file extensions that will be identified as
|
||||
videos.
|
||||
{intl.formatMessage({ id: "config.general.video_ext_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="image-extensions">
|
||||
<h6>Image Extensions</h6>
|
||||
<h6>
|
||||
<FormattedMessage id="config.general.image_ext_head" />
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={imageExtensions}
|
||||
@@ -444,13 +469,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Comma-delimited list of file extensions that will be identified as
|
||||
images.
|
||||
{intl.formatMessage({ id: "config.general.image_ext_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="gallery-extensions">
|
||||
<h6>Gallery zip Extensions</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.general.gallery_ext_head" })}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={galleryExtensions}
|
||||
@@ -459,16 +485,21 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Comma-delimited list of file extensions that will be identified as
|
||||
gallery zip files.
|
||||
{intl.formatMessage({ id: "config.general.gallery_ext_desc" })}
|
||||
</Form.Text>
|
||||
</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} />
|
||||
<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
|
||||
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
||||
rel="noopener noreferrer"
|
||||
@@ -480,14 +511,19 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h6>Excluded Image/Gallery Patterns</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "config.general.excluded_image_gallery_patterns_head",
|
||||
})}
|
||||
</h6>
|
||||
<ExclusionPatterns
|
||||
excludes={imageExcludes}
|
||||
setExcludes={setImageExcludes}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Regexps of image and gallery files/paths to exclude from Scan and
|
||||
add to Clean
|
||||
{intl.formatMessage({
|
||||
id: "config.general.excluded_image_gallery_patterns_desc",
|
||||
})}
|
||||
<a
|
||||
href="https://github.com/stashapp/stash/wiki/Exclude-file-configuration"
|
||||
rel="noopener noreferrer"
|
||||
@@ -502,13 +538,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Check
|
||||
id="log-terminal"
|
||||
checked={createGalleriesFromFolders}
|
||||
label="Create galleries from folders containing images"
|
||||
label={intl.formatMessage({
|
||||
id: "config.general.create_galleries_from_folders_label",
|
||||
})}
|
||||
onChange={() =>
|
||||
setCreateGalleriesFromFolders(!createGalleriesFromFolders)
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
</Form.Group>
|
||||
@@ -516,22 +556,28 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h4>Hashing</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.general.hashing" })}</h4>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
checked={calculateMD5}
|
||||
label="Calculate MD5 for videos"
|
||||
label={intl.formatMessage({
|
||||
id: "config.general.calculate_md5_and_ohash_label",
|
||||
})}
|
||||
onChange={() => setCalculateMD5(!calculateMD5)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "config.general.calculate_md5_and_ohash_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="w-auto input-control"
|
||||
@@ -551,10 +597,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
</Form.Control>
|
||||
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "config.general.generated_file_naming_hash_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -562,9 +607,13 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h4>Video</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.general.video_head" })}</h4>
|
||||
<Form.Group id="transcode-size">
|
||||
<h6>Maximum transcode size</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "config.general.maximum_transcode_size_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
@@ -580,11 +629,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
Maximum size for generated transcodes
|
||||
{intl.formatMessage({
|
||||
id: "config.general.maximum_transcode_size_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<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
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
@@ -602,7 +657,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
Maximum size for transcoded streams
|
||||
{intl.formatMessage({
|
||||
id: "config.general.maximum_streaming_transcode_size_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -610,10 +667,17 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h4>Parallel Scan/Generation</h4>
|
||||
<h4>
|
||||
{intl.formatMessage({ id: "config.general.parallel_scan_head" })}
|
||||
</h4>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -625,9 +689,10 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id:
|
||||
"config.general.number_of_parallel_task_for_scan_generation_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -635,10 +700,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h4>Preview Generation</h4>
|
||||
<h4>
|
||||
{intl.formatMessage({ id: "config.general.preview_generation" })}
|
||||
</h4>
|
||||
|
||||
<Form.Group id="transcode-size">
|
||||
<h6>Preview encoding preset</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
@@ -654,9 +725,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
The preset regulates size, quality and encoding time of preview
|
||||
generation. Presets beyond “slow” have diminishing returns and are
|
||||
not recommended.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -673,7 +744,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -685,12 +760,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Number of segments in preview files.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -702,12 +783,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={previewExcludeStart}
|
||||
@@ -716,13 +803,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<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
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={previewExcludeEnd}
|
||||
@@ -731,16 +823,19 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h4>Scraping</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.general.scraping" })}</h4>
|
||||
<Form.Group id="scraperUserAgent">
|
||||
<h6>Scraper User Agent</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.general.scraper_user_agent" })}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={scraperUserAgent}
|
||||
@@ -749,12 +844,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<Form.Group id="scraperCDPPath">
|
||||
<h6>Chrome CDP path</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.general.chrome_cdp_path" })}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={scraperCDPPath}
|
||||
@@ -763,9 +862,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({ id: "config.general.chrome_cdp_path_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -773,13 +870,15 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Check
|
||||
id="scaper-cert-check"
|
||||
checked={scraperCertCheck}
|
||||
label="Check for insecure certificates"
|
||||
label={intl.formatMessage({
|
||||
id: "config.general.check_for_insecure_certificates",
|
||||
})}
|
||||
onChange={() => setScraperCertCheck(!scraperCertCheck)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
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.
|
||||
{intl.formatMessage({
|
||||
id: "config.general.check_for_insecure_certificates_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -787,16 +886,22 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<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} />
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h4>Authentication</h4>
|
||||
<h4>
|
||||
{intl.formatMessage({ id: "config.general.auth.authentication" })}
|
||||
</h4>
|
||||
<Form.Group id="username">
|
||||
<h6>Username</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.general.auth.username" })}</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={username}
|
||||
@@ -805,11 +910,11 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
<Form.Group id="password">
|
||||
<h6>Password</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.general.auth.password" })}</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="password"
|
||||
@@ -819,12 +924,12 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<Form.Group id="apikey">
|
||||
<h6>API Key</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.general.auth.api_key" })}</h6>
|
||||
<InputGroup>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
@@ -834,7 +939,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<InputGroup.Append>
|
||||
<Button
|
||||
className=""
|
||||
title="Generate API key"
|
||||
title={intl.formatMessage({
|
||||
id: "config.general.auth.generate_api_key",
|
||||
})}
|
||||
onClick={() => onGenerateAPIKey()}
|
||||
>
|
||||
<Icon icon="redo" />
|
||||
@@ -842,7 +949,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Button
|
||||
className=""
|
||||
variant="danger"
|
||||
title="Clear API key"
|
||||
title={intl.formatMessage({
|
||||
id: "config.general.auth.clear_api_key",
|
||||
})}
|
||||
onClick={() => onClearAPIKey()}
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
@@ -850,13 +959,16 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
<Form.Text className="text-muted">
|
||||
API key for external systems. Only required when username/password
|
||||
is configured. Username must be saved before generating API key.
|
||||
{intl.formatMessage({ id: "config.general.auth.api_key_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="maxSessionAge">
|
||||
<h6>Maximum Session Age</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "config.general.auth.maximum_session_age",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -868,16 +980,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<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.Group>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h4>Logging</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.general.logging" })}</h4>
|
||||
<Form.Group id="log-file">
|
||||
<h6>Log file</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.general.auth.log_file" })}</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
defaultValue={logFile}
|
||||
@@ -886,8 +1000,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Path to the file to output logging to. Blank to disable file logging.
|
||||
Requires restart.
|
||||
{intl.formatMessage({ id: "config.general.auth.log_file_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -895,17 +1008,20 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Check
|
||||
id="log-terminal"
|
||||
checked={logOut}
|
||||
label="Log to terminal"
|
||||
label={intl.formatMessage({
|
||||
id: "config.general.auth.log_to_terminal",
|
||||
})}
|
||||
onChange={() => setLogOut(!logOut)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Logs to the terminal in addition to a file. Always true if file
|
||||
logging is disabled. Requires restart.
|
||||
{intl.formatMessage({
|
||||
id: "config.general.auth.log_to_terminal_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="log-level">
|
||||
<h6>Log Level</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.logs.log_level" })}</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
as="select"
|
||||
@@ -926,18 +1042,18 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Check
|
||||
id="log-http"
|
||||
checked={logAccess}
|
||||
label="Log http access"
|
||||
label={intl.formatMessage({ id: "config.general.auth.log_http" })}
|
||||
onChange={() => setLogAccess(!logAccess)}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Button variant="primary" onClick={() => onSave()}>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState } from "react";
|
||||
import { Formik, useFormikContext } from "formik";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { Prompt } from "react-router";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as yup from "yup";
|
||||
import {
|
||||
useConfiguration,
|
||||
@@ -17,6 +18,7 @@ import { DurationInput, Icon, LoadingIndicator, Modal } from "../Shared";
|
||||
import { StringListInput } from "../Shared/StringListInput";
|
||||
|
||||
export const SettingsDLNAPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
// undefined to hide dialog, true for enable, false for disable
|
||||
@@ -74,7 +76,16 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
},
|
||||
});
|
||||
configRefetch();
|
||||
Toast.success({ content: "Updated config" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl
|
||||
.formatMessage({ id: "configuration" })
|
||||
.toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
@@ -166,7 +177,9 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
}
|
||||
|
||||
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)}`;
|
||||
}
|
||||
@@ -181,14 +194,14 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
if (data?.configuration.dlna.enabled) {
|
||||
return (
|
||||
<Button onClick={() => setEnableDisable(false)} className="mr-1">
|
||||
Disable temporarily...
|
||||
<FormattedMessage id="actions.temp_disable" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onClick={() => setEnableDisable(true)} className="mr-1">
|
||||
Enable temporarily...
|
||||
<FormattedMessage id="actions.temp_enable" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -267,7 +280,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
checked={enableUntilRestart}
|
||||
label="until restart"
|
||||
label={intl.formatMessage({ id: "config.dlna.until_restart" })}
|
||||
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -290,10 +303,13 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
return (
|
||||
<Modal
|
||||
show={tempIP !== undefined}
|
||||
header={`Allow ${tempIP}`}
|
||||
header={intl.formatMessage(
|
||||
{ id: "config.dlna.allow_temp_ip" },
|
||||
{ tempIP }
|
||||
)}
|
||||
icon="clock"
|
||||
accept={{
|
||||
text: "Allow",
|
||||
text: intl.formatMessage({ id: "actions.allow" }),
|
||||
variant: "primary",
|
||||
onClick: onAllowTempIP,
|
||||
}}
|
||||
@@ -306,7 +322,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
checked={enableUntilRestart}
|
||||
label="until restart"
|
||||
label={intl.formatMessage({ id: "config.dlna.until_restart" })}
|
||||
onChange={() => setEnableUntilRestart(!enableUntilRestart)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -333,7 +349,9 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
const { allowedIPAddresses } = statusData.dlnaStatus;
|
||||
return (
|
||||
<Form.Group>
|
||||
<h6>Allowed IP addresses</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.dlna.allowed_ip_addresses" })}
|
||||
</h6>
|
||||
|
||||
<ul className="addresses">
|
||||
{allowedIPAddresses.map((a) => (
|
||||
@@ -347,7 +365,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<div className="buttons">
|
||||
<Button
|
||||
size="sm"
|
||||
title="Disallow"
|
||||
title={intl.formatMessage({ id: "actions.disallow" })}
|
||||
variant="danger"
|
||||
onClick={() => onDisallowTempIP(a.ipAddress)}
|
||||
>
|
||||
@@ -377,7 +395,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<div>
|
||||
<Button
|
||||
size="sm"
|
||||
title="Allow temporarily"
|
||||
title={intl.formatMessage({ id: "actions.allow_temporarily" })}
|
||||
onClick={() => setTempIP(a)}
|
||||
>
|
||||
<Icon icon="user-clock" />
|
||||
@@ -398,7 +416,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<div className="buttons">
|
||||
<Button
|
||||
size="sm"
|
||||
title="Allow temporarily"
|
||||
title={intl.formatMessage({ id: "actions.allow_temporarily" })}
|
||||
onClick={() => setTempIP(ipEntry)}
|
||||
disabled={!ipEntry}
|
||||
>
|
||||
@@ -422,13 +440,15 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Prompt
|
||||
when={dirty}
|
||||
message="Unsaved changes. Are you sure you want to leave?"
|
||||
message={intl.formatMessage({ id: "dialogs.unsaved_changes" })}
|
||||
/>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Settings</h5>
|
||||
<h5>{intl.formatMessage({ id: "settings" })}</h5>
|
||||
<Form.Group>
|
||||
<Form.Label>Server Display Name</Form.Label>
|
||||
<Form.Label>
|
||||
{intl.formatMessage({ id: "config.dlna.server_display_name" })}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
className="text-input server-name"
|
||||
value={values.serverName}
|
||||
@@ -437,20 +457,26 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Display name for the DLNA server. Defaults to <code>stash</code>{" "}
|
||||
if empty.
|
||||
{intl.formatMessage(
|
||||
{ id: "config.dlna.server_display_name_desc" },
|
||||
{ server_name: <code>stash</code> }
|
||||
)}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
checked={values.enabled}
|
||||
label="Enabled by default"
|
||||
label={intl.formatMessage({
|
||||
id: "config.dlna.enabled_by_default",
|
||||
})}
|
||||
onChange={() => setFieldValue("enabled", !values.enabled)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h6>Interfaces</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.dlna.network_interfaces" })}
|
||||
</h6>
|
||||
<StringListInput
|
||||
value={values.interfaces}
|
||||
setValue={(value) => setFieldValue("interfaces", value)}
|
||||
@@ -458,13 +484,16 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
className="interfaces-input"
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Interfaces to expose DLNA server on. An empty list results in
|
||||
running on all interfaces. Requires DLNA restart after changing.
|
||||
{intl.formatMessage({
|
||||
id: "config.dlna.network_interfaces_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h6>Default IP Whitelist</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.dlna.default_ip_whitelist" })}
|
||||
</h6>
|
||||
<StringListInput
|
||||
value={values.whitelistedIPs}
|
||||
setValue={(value) => setFieldValue("whitelistedIPs", value)}
|
||||
@@ -472,8 +501,10 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
className="ip-whitelist-input"
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Default IP addresses allow to access DLNA. Use <code>*</code> to
|
||||
allow all IP addresses.
|
||||
{intl.formatMessage(
|
||||
{ id: "config.dlna.default_ip_whitelist_desc" },
|
||||
{ wildcard: <code>*</code> }
|
||||
)}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
@@ -481,7 +512,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<hr />
|
||||
|
||||
<Button variant="primary" type="submit" disabled={!dirty}>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
</Form>
|
||||
);
|
||||
@@ -495,11 +526,13 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
<h4>DLNA</h4>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Status: {renderStatus()}</h5>
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "status" }, { statusText: renderStatus() })}
|
||||
</h5>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Actions</h5>
|
||||
<h5>{intl.formatMessage({ id: "actions_name" })}</h5>
|
||||
|
||||
<Form.Group>
|
||||
{renderEnableButton()}
|
||||
@@ -509,10 +542,14 @@ export const SettingsDLNAPanel: React.FC = () => {
|
||||
{renderAllowedIPs()}
|
||||
|
||||
<Form.Group>
|
||||
<h6>Recent IP addresses</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.dlna.recent_ip_addresses" })}
|
||||
</h6>
|
||||
<Form.Group>{renderRecentIPs()}</Form.Group>
|
||||
<Form.Group>
|
||||
<Button onClick={() => statusRefetch()}>Refresh</Button>
|
||||
<Button onClick={() => statusRefetch()}>
|
||||
<FormattedMessage id="actions.refresh" />
|
||||
</Button>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { DurationInput, LoadingIndicator } from "src/components/Shared";
|
||||
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -19,6 +20,7 @@ const allMenuItems = [
|
||||
const SECONDS_TO_MS = 1000;
|
||||
|
||||
export const SettingsInterfacePanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const { data: config, error, loading } = useConfiguration();
|
||||
const [menuItemIds, setMenuItemIds] = useState<string[]>(
|
||||
@@ -84,7 +86,16 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
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) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -95,9 +106,9 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>User Interface</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.ui.title" })}</h4>
|
||||
<Form.Group controlId="language">
|
||||
<h5>Language</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.language.heading" })}</h5>
|
||||
<Form.Control
|
||||
as="select"
|
||||
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-GB">English (United Kingdom)</option>
|
||||
<option value="zh-TW">Chinese (Taiwan)</option>
|
||||
<option value="zh-TW">繁體中文 (台灣)</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<h5>Menu items</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.menu_items.heading" })}</h5>
|
||||
<CheckboxGroup
|
||||
groupId="menu-items"
|
||||
items={allMenuItems}
|
||||
@@ -120,25 +131,31 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
onChange={setMenuItemIds}
|
||||
/>
|
||||
<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.Group>
|
||||
<Form.Group>
|
||||
<h5>Scene / Marker Wall</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.scene_wall.heading" })}</h5>
|
||||
<Form.Check
|
||||
id="wall-show-title"
|
||||
checked={wallShowTitle}
|
||||
label="Display title and tags"
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_wall.options.display_title",
|
||||
})}
|
||||
onChange={() => setWallShowTitle(!wallShowTitle)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="wall-sound-enabled"
|
||||
checked={soundOnPreview}
|
||||
label="Enable sound"
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_wall.options.toggle_sound",
|
||||
})}
|
||||
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
||||
/>
|
||||
<Form.Label htmlFor="wall-preview">
|
||||
<h6>Preview Type</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.heading" })}
|
||||
</h6>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
@@ -149,21 +166,33 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
setWallPlayback(e.currentTarget.value)
|
||||
}
|
||||
>
|
||||
<option value="video">Video</option>
|
||||
<option value="animation">Animated Image</option>
|
||||
<option value="image">Static Image</option>
|
||||
<option value="video">
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.options.video" })}
|
||||
</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.Text className="text-muted">
|
||||
Configuration for wall items
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Scene List</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.scene_list.heading" })}</h5>
|
||||
<Form.Check
|
||||
id="show-text-studios"
|
||||
checked={showStudioAsText}
|
||||
label="Show Studios as text"
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_list.options.show_studio_as_text",
|
||||
})}
|
||||
onChange={() => {
|
||||
setShowStudioAsText(!showStudioAsText);
|
||||
}}
|
||||
@@ -171,11 +200,13 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
</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.Check
|
||||
checked={autostartVideo}
|
||||
label="Auto-start video"
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_player.options.auto_start_video",
|
||||
})}
|
||||
onChange={() => {
|
||||
setAutostartVideo(!autostartVideo);
|
||||
}}
|
||||
@@ -183,21 +214,26 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="max-loop-duration">
|
||||
<h6>Maximum loop duration</h6>
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.ui.max_loop_duration.heading" })}
|
||||
</h6>
|
||||
<DurationInput
|
||||
className="row col col-4"
|
||||
numericValue={maximumLoopDuration}
|
||||
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
Maximum scene duration where scene player will loop the video - 0 to
|
||||
disable
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.max_loop_duration.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="slideshow-delay">
|
||||
<h5>Slideshow Delay</h5>
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "config.ui.slideshow_delay.heading" })}
|
||||
</h5>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
@@ -209,16 +245,18 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Custom CSS</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
|
||||
<Form.Check
|
||||
id="custom-css"
|
||||
checked={cssEnabled}
|
||||
label="Custom CSS enabled"
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.custom_css.option_label",
|
||||
})}
|
||||
onChange={() => {
|
||||
setCSSEnabled(!cssEnabled);
|
||||
}}
|
||||
@@ -234,12 +272,12 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
className="col col-sm-6 text-input code"
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>Handy Connection Key</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.handy_connection_key" })}</h5>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
value={handyKey}
|
||||
@@ -248,13 +286,13 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<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.Group>
|
||||
|
||||
<hr />
|
||||
<Button variant="primary" onClick={() => onSave()}>
|
||||
Save
|
||||
{intl.formatMessage({ id: "actions.save" })}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useReducer, useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useLogs, useLoggingSubscribe } from "src/core/StashService";
|
||||
|
||||
@@ -74,6 +75,7 @@ const logReducer = (existingEntries: LogEntry[], newEntries: LogEntry[]) => [
|
||||
];
|
||||
|
||||
export const SettingsLogsPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { data, error } = useLoggingSubscribe();
|
||||
const { data: existingData } = useLogs();
|
||||
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
|
||||
@@ -106,9 +108,11 @@ export const SettingsLogsPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>Logs</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.categories.logs" })}</h4>
|
||||
<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
|
||||
className="col-6 col-sm-2 input-control"
|
||||
as="select"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateReloadPlugins, usePlugins } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
@@ -8,6 +9,8 @@ import { CollapseButton, Icon, LoadingIndicator } from "src/components/Shared";
|
||||
|
||||
export const SettingsPluginsPanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
const { data, loading } = usePlugins();
|
||||
|
||||
async function onReloadPlugins() {
|
||||
@@ -58,11 +61,15 @@ export const SettingsPluginsPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<h5>Hooks</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="config.plugins.hooks" />
|
||||
</h5>
|
||||
{hooks.map((h) => (
|
||||
<div key={`${h.name}`} className="mb-3">
|
||||
<h6>{h.name}</h6>
|
||||
<CollapseButton text="Triggers on">
|
||||
<CollapseButton
|
||||
text={intl.formatMessage({ id: "config.plugins.triggers_on" })}
|
||||
>
|
||||
<ul>
|
||||
{h.hooks?.map((hh) => (
|
||||
<li>
|
||||
@@ -82,14 +89,18 @@ export const SettingsPluginsPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Plugins</h3>
|
||||
<h3>
|
||||
<FormattedMessage id="config.categories.plugins" />
|
||||
</h3>
|
||||
<hr />
|
||||
{renderPlugins()}
|
||||
<Button onClick={() => onReloadPlugins()}>
|
||||
<span className="fa-icon">
|
||||
<Icon icon="sync-alt" />
|
||||
</span>
|
||||
<span>Reload plugins</span>
|
||||
<span>
|
||||
<FormattedMessage id="actions.reload_plugins" />
|
||||
</span>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button } from "react-bootstrap";
|
||||
import {
|
||||
mutateReloadScrapers,
|
||||
@@ -68,6 +69,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
|
||||
|
||||
export const SettingsScrapersPanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: performerScrapers,
|
||||
loading: loadingPerformers,
|
||||
@@ -95,7 +97,7 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
.map((t) => {
|
||||
switch (t) {
|
||||
case ScrapeType.Name:
|
||||
return "Search by name";
|
||||
return intl.formatMessage({ id: "config.scrapers.search_by_name" });
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
@@ -114,7 +116,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
const typeStrings = types.map((t) => {
|
||||
switch (t) {
|
||||
case ScrapeType.Fragment:
|
||||
return "Scene Metadata";
|
||||
return intl.formatMessage(
|
||||
{ id: "config.scrapers.entity_metadata" },
|
||||
{ entityType: intl.formatMessage({ id: "scene" }) }
|
||||
);
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
@@ -133,7 +138,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
const typeStrings = types.map((t) => {
|
||||
switch (t) {
|
||||
case ScrapeType.Fragment:
|
||||
return "Gallery Metadata";
|
||||
return intl.formatMessage(
|
||||
{ id: "config.scrapers.entity_metadata" },
|
||||
{ entityType: intl.formatMessage({ id: "gallery" }) }
|
||||
);
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
@@ -152,7 +160,10 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
const typeStrings = types.map((t) => {
|
||||
switch (t) {
|
||||
case ScrapeType.Fragment:
|
||||
return "Movie Metadata";
|
||||
return intl.formatMessage(
|
||||
{ id: "config.scrapers.entity_metadata" },
|
||||
{ entityType: intl.formatMessage({ id: "movie" }) }
|
||||
);
|
||||
default:
|
||||
return t;
|
||||
}
|
||||
@@ -182,7 +193,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
</tr>
|
||||
));
|
||||
|
||||
return renderTable("Scene scrapers", elements);
|
||||
return renderTable(
|
||||
intl.formatMessage(
|
||||
{ id: "config.scrapers.entity_scrapers" },
|
||||
{ entityType: intl.formatMessage({ id: "scene" }) }
|
||||
),
|
||||
elements
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -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() {
|
||||
@@ -230,7 +259,13 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
</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[]) {
|
||||
@@ -241,9 +276,15 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
<table className="scraper-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Supported types</th>
|
||||
<th>URLs</th>
|
||||
<th>{intl.formatMessage({ id: "name" })}</th>
|
||||
<th>
|
||||
{intl.formatMessage({
|
||||
id: "config.scrapers.supported_types",
|
||||
})}
|
||||
</th>
|
||||
<th>
|
||||
{intl.formatMessage({ id: "config.scrapers.supported_urls" })}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{elements}</tbody>
|
||||
@@ -258,13 +299,15 @@ export const SettingsScrapersPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>Scrapers</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.categories.scrapers" })}</h4>
|
||||
<div className="mb-3">
|
||||
<Button onClick={() => onReloadScrapers()}>
|
||||
<span className="fa-icon">
|
||||
<Icon icon="sync-alt" />
|
||||
</span>
|
||||
<span>Reload scrapers</span>
|
||||
<span>
|
||||
<FormattedMessage id="actions.reload_scrapers" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Col, Form, Row } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
@@ -11,6 +12,7 @@ interface IDirectorySelectionDialogProps {
|
||||
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = (
|
||||
props: IDirectorySelectionDialogProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { data } = useConfiguration();
|
||||
|
||||
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path);
|
||||
@@ -42,7 +44,7 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
>
|
||||
@@ -57,7 +59,7 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
|
||||
className="ml-auto"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
title="Delete"
|
||||
title={intl.formatMessage({ id: "actions.delete" })}
|
||||
onClick={() => removePath(p)}
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const GenerateButton: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const [sprites, setSprites] = useState(true);
|
||||
const [phashes, setPhashes] = useState(true);
|
||||
const [previews, setPreviews] = useState(true);
|
||||
@@ -22,7 +24,11 @@ export const GenerateButton: React.FC = () => {
|
||||
markers,
|
||||
transcodes,
|
||||
});
|
||||
Toast.success({ content: "Added generation job to queue" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage({
|
||||
id: "toast.added_generation_job_to_queue",
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -34,7 +40,7 @@ export const GenerateButton: React.FC = () => {
|
||||
<Form.Check
|
||||
id="preview-task"
|
||||
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)}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
@@ -43,7 +49,9 @@ export const GenerateButton: React.FC = () => {
|
||||
id="image-preview-task"
|
||||
checked={imagePreviews}
|
||||
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)}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
@@ -51,25 +59,25 @@ export const GenerateButton: React.FC = () => {
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
label="Sprites (for the scene scrubber)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||
onChange={() => setSprites(!sprites)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="marker-task"
|
||||
checked={markers}
|
||||
label="Markers (20 second videos which begin at the given timecode)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||
onChange={() => setMarkers(!markers)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="transcode-task"
|
||||
checked={transcodes}
|
||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||
onChange={() => setTranscodes(!transcodes)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="phash-task"
|
||||
checked={phashes}
|
||||
label="Phashes (for deduplication and scene identification)"
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||
onChange={() => setPhashes(!phashes)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -80,10 +88,10 @@ export const GenerateButton: React.FC = () => {
|
||||
type="submit"
|
||||
onClick={() => onGenerate()}
|
||||
>
|
||||
Generate
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Generate supporting image, sprite, video, vtt and other files.
|
||||
{intl.formatMessage({ id: "config.tasks.generate_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mutateImportObjects } from "src/core/StashService";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useToast } from "src/hooks";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IImportDialogProps {
|
||||
onClose: () => void;
|
||||
@@ -25,6 +26,7 @@ export const ImportDialog: React.FC<IImportDialogProps> = (
|
||||
// Network state
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
function duplicateHandlingToString(
|
||||
@@ -112,16 +114,16 @@ export const ImportDialog: React.FC<IImportDialogProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="pencil-alt"
|
||||
header="Import"
|
||||
header={intl.formatMessage({ id: "actions.import" })}
|
||||
accept={{
|
||||
onClick: () => {
|
||||
onImport();
|
||||
},
|
||||
text: "Import",
|
||||
text: intl.formatMessage({ id: "actions.import" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
disabled={!file}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataImport,
|
||||
@@ -24,6 +25,7 @@ type Plugin = Pick<GQL.Plugin, "id">;
|
||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||
|
||||
export const SettingsTasksPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [isImportAlertOpen, setIsImportAlertOpen] = useState<boolean>(false);
|
||||
const [isCleanAlertOpen, setIsCleanAlertOpen] = useState<boolean>(false);
|
||||
@@ -60,7 +62,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
setIsImportAlertOpen(false);
|
||||
try {
|
||||
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) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -71,13 +78,14 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Modal
|
||||
show={isImportAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
<p>
|
||||
Are you sure you want to import? This will delete the database and
|
||||
re-import from your exported metadata.
|
||||
</p>
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.import_warning" })}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -93,16 +101,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
let msg;
|
||||
if (cleanDryRun) {
|
||||
msg = (
|
||||
<p>
|
||||
Dry Mode selected. No actual deleting will take place, only logging.
|
||||
</p>
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
||||
);
|
||||
} else {
|
||||
msg = (
|
||||
<p>
|
||||
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.
|
||||
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -111,7 +115,11 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Modal
|
||||
show={isCleanAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
{msg}
|
||||
@@ -154,7 +162,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
scanGenerateSprites,
|
||||
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) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -189,7 +202,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
async function onAutoTag(paths?: string[]) {
|
||||
try {
|
||||
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) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -197,7 +215,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
||||
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[]) {
|
||||
@@ -251,7 +274,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<h5>Plugin Tasks</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
||||
{plugins.data.plugins.map((o) => {
|
||||
return (
|
||||
<div key={`${o.id}`} className="mb-3">
|
||||
@@ -268,7 +291,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
async function onMigrateHashNaming() {
|
||||
try {
|
||||
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) {
|
||||
Toast.error(err);
|
||||
}
|
||||
@@ -277,14 +309,23 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
async function onExport() {
|
||||
try {
|
||||
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) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isBackupRunning) {
|
||||
return <LoadingIndicator message="Backup up database" />;
|
||||
return (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "config.tasks.backing_up_database" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -295,30 +336,36 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
{renderScanDialog()}
|
||||
{renderAutoTagDialog()}
|
||||
|
||||
<h4>Job Queue</h4>
|
||||
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
||||
|
||||
<JobTable />
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Library</h5>
|
||||
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="use-file-metadata"
|
||||
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)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="strip-file-extension"
|
||||
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)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="scan-generate-previews"
|
||||
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)}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
@@ -327,7 +374,9 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
id="scan-generate-image-previews"
|
||||
checked={scanGenerateImagePreviews}
|
||||
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={() =>
|
||||
setScanGenerateImagePreviews(!scanGenerateImagePreviews)
|
||||
}
|
||||
@@ -337,13 +386,17 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
<Form.Check
|
||||
id="scan-generate-sprites"
|
||||
checked={scanGenerateSprites}
|
||||
label="Generate sprites during scan (for the scene scrubber)"
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.generate_sprites_during_scan",
|
||||
})}
|
||||
onChange={() => setScanGenerateSprites(!scanGenerateSprites)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="scan-generate-phashes"
|
||||
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)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -354,41 +407,41 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
type="submit"
|
||||
onClick={() => onScan()}
|
||||
>
|
||||
Scan
|
||||
<FormattedMessage id="actions.scan" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => setIsScanDialogOpen(true)}
|
||||
>
|
||||
Selective Scan
|
||||
<FormattedMessage id="actions.selective_scan" />
|
||||
</Button>
|
||||
<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.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Auto Tagging</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.auto_tagging" })}</h5>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="autotag-performers"
|
||||
checked={autoTagPerformers}
|
||||
label="Performers"
|
||||
label={intl.formatMessage({ id: "performers" })}
|
||||
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-studios"
|
||||
checked={autoTagStudios}
|
||||
label="Studios"
|
||||
label={intl.formatMessage({ id: "studios" })}
|
||||
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-tags"
|
||||
checked={autoTagTags}
|
||||
label="Tags"
|
||||
label={intl.formatMessage({ id: "tags" })}
|
||||
onChange={() => setAutoTagTags(!autoTagTags)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -399,32 +452,34 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
className="mr-2"
|
||||
onClick={() => onAutoTag()}
|
||||
>
|
||||
Auto Tag
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => setIsAutoTagDialogOpen(true)}
|
||||
>
|
||||
Selective Auto Tag
|
||||
<FormattedMessage id="actions.selective_auto_tag" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Auto-tag content based on filenames.
|
||||
{intl.formatMessage({
|
||||
id: "config.tasks.auto_tag_based_on_filenames",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Generated Content</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
||||
<GenerateButton />
|
||||
|
||||
<hr />
|
||||
<h5>Maintenance</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.maintenance" })}</h5>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="clean-dryrun"
|
||||
checked={cleanDryRun}
|
||||
label="Only perform a dry run. Don't remove anything"
|
||||
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
||||
onChange={() => setCleanDryRun(!cleanDryRun)}
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -434,17 +489,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
variant="danger"
|
||||
onClick={() => setIsCleanAlertOpen(true)}
|
||||
>
|
||||
Clean
|
||||
<FormattedMessage id="actions.clean" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Check for missing files and remove them from the database. This is a
|
||||
destructive action.
|
||||
{intl.formatMessage({ id: "config.tasks.cleanup_desc" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Metadata</h5>
|
||||
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
||||
<Form.Group>
|
||||
<Button
|
||||
id="export"
|
||||
@@ -452,11 +506,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
type="submit"
|
||||
onClick={() => onExport()}
|
||||
>
|
||||
Full Export
|
||||
<FormattedMessage id="actions.full_export" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Exports the database content into JSON format in the metadata
|
||||
directory.
|
||||
{intl.formatMessage({ id: "config.tasks.export_to_json" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -466,11 +519,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
variant="danger"
|
||||
onClick={() => setIsImportAlertOpen(true)}
|
||||
>
|
||||
Full Import
|
||||
<FormattedMessage id="actions.full_import" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Import from exported JSON in the metadata directory. Wipes the
|
||||
existing database.
|
||||
{intl.formatMessage({ id: "config.tasks.import_from_exported_json" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -480,16 +532,16 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
variant="danger"
|
||||
onClick={() => setIsImportDialogOpen(true)}
|
||||
>
|
||||
Import from file
|
||||
<FormattedMessage id="actions.import_from_file" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Incremental import from a supplied export zip file.
|
||||
{intl.formatMessage({ id: "config.tasks.incremental_import" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Backup</h5>
|
||||
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
||||
<Form.Group>
|
||||
<Button
|
||||
id="backup"
|
||||
@@ -497,12 +549,19 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
type="submit"
|
||||
onClick={() => onBackup()}
|
||||
>
|
||||
Backup
|
||||
<FormattedMessage id="actions.backup" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Performs a backup of the database to the same directory as the
|
||||
database, with the filename format{" "}
|
||||
<code>[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]</code>
|
||||
{intl.formatMessage(
|
||||
{ id: "config.tasks.backup_database" },
|
||||
{
|
||||
filename_format: (
|
||||
<code>
|
||||
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
||||
</code>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
@@ -513,10 +572,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
type="submit"
|
||||
onClick={() => onBackup(true)}
|
||||
>
|
||||
Download Backup
|
||||
<FormattedMessage id="actions.download_backup" />
|
||||
</Button>
|
||||
<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.Group>
|
||||
|
||||
@@ -524,7 +583,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>Migrations</h5>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
||||
|
||||
<Form.Group>
|
||||
<Button
|
||||
@@ -532,11 +591,10 @@ export const SettingsTasksPanel: React.FC = () => {
|
||||
variant="danger"
|
||||
onClick={() => onMigrateHashNaming()}
|
||||
>
|
||||
Rename generated files
|
||||
<FormattedMessage id="actions.rename_gen_files" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Used after changing the Generated file naming hash to rename existing
|
||||
generated files to the new hash format.
|
||||
{intl.formatMessage({ id: "config.tasks.migrate_hash_files" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</>
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const SettingsToolsPanel: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<h4>Scene Tools</h4>
|
||||
<h4>
|
||||
<FormattedMessage id="config.tools.scene_tools" />
|
||||
</h4>
|
||||
|
||||
<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>
|
||||
<Link to="/sceneDuplicateChecker">Scene Duplicate Checker</Link>
|
||||
<Link to="/sceneDuplicateChecker">
|
||||
<FormattedMessage id="config.tools.scene_duplicate_checker" />
|
||||
</Link>
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared";
|
||||
|
||||
interface IInstanceProps {
|
||||
@@ -15,6 +16,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
||||
onDelete,
|
||||
isMulti,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const handleInput = (key: string, value: string) => {
|
||||
const newObj = {
|
||||
...instance,
|
||||
@@ -27,7 +29,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
||||
<Form.Group className="row no-gutters">
|
||||
<InputGroup className="col">
|
||||
<Form.Control
|
||||
placeholder="Name"
|
||||
placeholder={intl.formatMessage({ id: "config.stashbox.name" })}
|
||||
className="text-input col-3 stash-box-name"
|
||||
value={instance?.name}
|
||||
isValid={!isMulti || (instance?.name?.length ?? 0) > 0}
|
||||
@@ -36,7 +38,9 @@ const Instance: React.FC<IInstanceProps> = ({
|
||||
}
|
||||
/>
|
||||
<Form.Control
|
||||
placeholder="GraphQL endpoint"
|
||||
placeholder={intl.formatMessage({
|
||||
id: "config.stashbox.graphql_endpoint",
|
||||
})}
|
||||
className="text-input col-3 stash-box-endpoint"
|
||||
value={instance?.endpoint}
|
||||
isValid={(instance?.endpoint?.length ?? 0) > 0}
|
||||
@@ -45,7 +49,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
||||
}
|
||||
/>
|
||||
<Form.Control
|
||||
placeholder="API key"
|
||||
placeholder={intl.formatMessage({ id: "config.stashbox.api_key" })}
|
||||
className="text-input col-3 stash-box-apikey"
|
||||
value={instance?.api_key}
|
||||
isValid={(instance?.api_key?.length ?? 0) > 0}
|
||||
@@ -57,7 +61,7 @@ const Instance: React.FC<IInstanceProps> = ({
|
||||
<Button
|
||||
className=""
|
||||
variant="danger"
|
||||
title="Delete"
|
||||
title={intl.formatMessage({ id: "actions.delete" })}
|
||||
onClick={() => onDelete(instance.index)}
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
@@ -84,6 +88,7 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
||||
boxes,
|
||||
saveBoxes,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [index, setIndex] = useState(1000);
|
||||
|
||||
const handleSave = (instance: IStashBoxInstance) =>
|
||||
@@ -99,12 +104,18 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<h6>Stash-box Endpoints</h6>
|
||||
<h6>{intl.formatMessage({ id: "config.stashbox.title" })}</h6>
|
||||
{boxes.length > 0 && (
|
||||
<div className="row no-gutters">
|
||||
<h6 className="col-3 ml-1">Name</h6>
|
||||
<h6 className="col-3 ml-1">Endpoint</h6>
|
||||
<h6 className="col-3 ml-1">API Key</h6>
|
||||
<h6 className="col-3 ml-1">
|
||||
{intl.formatMessage({ id: "config.stashbox.name" })}
|
||||
</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>
|
||||
)}
|
||||
{boxes.map((instance) => (
|
||||
@@ -118,17 +129,13 @@ export const StashBoxConfiguration: React.FC<IStashBoxConfigurationProps> = ({
|
||||
))}
|
||||
<Button
|
||||
className="minimal"
|
||||
title="Add stash-box instance"
|
||||
title={intl.formatMessage({ id: "config.stashbox.add_instance" })}
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<Icon icon="plus" />
|
||||
</Button>
|
||||
<Form.Text className="text-muted">
|
||||
Stash-box facilitates automated tagging of scenes and performers based
|
||||
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.
|
||||
{intl.formatMessage({ id: "config.stashbox.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form, Row, Col } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FolderSelectDialog } from "../Shared/FolderSelect/FolderSelectDialog";
|
||||
@@ -21,6 +22,7 @@ const Stash: React.FC<IStashProps> = ({ index, stash, onSave, onDelete }) => {
|
||||
onSave(newObj);
|
||||
};
|
||||
|
||||
const intl = useIntl();
|
||||
const classAdd = index % 2 === 1 ? "bg-dark" : "";
|
||||
|
||||
return (
|
||||
@@ -47,7 +49,7 @@ const Stash: React.FC<IStashProps> = ({ index, stash, onSave, onDelete }) => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
title="Delete"
|
||||
title={intl.formatMessage({ id: "actions.delete" })}
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
@@ -103,9 +105,15 @@ export const StashConfiguration: React.FC<IStashConfigurationProps> = ({
|
||||
<Form.Group>
|
||||
{stashes.length > 0 && (
|
||||
<Row>
|
||||
<h6 className="col-4">Path</h6>
|
||||
<h6 className="col-3">Exclude Video</h6>
|
||||
<h6 className="col-3">Exclude Image</h6>
|
||||
<h6 className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</h6>
|
||||
<h6 className="col-3">
|
||||
<FormattedMessage id="config.general.exclude_video" />
|
||||
</h6>
|
||||
<h6 className="col-3">
|
||||
<FormattedMessage id="config.general.exclude_image" />
|
||||
</h6>
|
||||
</Row>
|
||||
)}
|
||||
{stashes.map((stash, index) => (
|
||||
@@ -122,7 +130,7 @@ export const StashConfiguration: React.FC<IStashConfigurationProps> = ({
|
||||
variant="secondary"
|
||||
onClick={() => setIsDisplayingDialog(true)}
|
||||
>
|
||||
Add Directory
|
||||
<FormattedMessage id="actions.add_directory" />
|
||||
</Button>
|
||||
</Form.Group>
|
||||
</>
|
||||
|
||||
@@ -24,24 +24,16 @@ interface IDeleteEntityDialogProps {
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteHeader: {
|
||||
id: "delete-header",
|
||||
defaultMessage:
|
||||
"Delete {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
||||
id: "dialogs.delete_object_title",
|
||||
},
|
||||
deleteToast: {
|
||||
id: "delete-toast",
|
||||
defaultMessage:
|
||||
"Deleted {count, plural, =1 {{singularEntity}} other {{pluralEntity}}}",
|
||||
id: "toast.delete_past_tense",
|
||||
},
|
||||
deleteMessage: {
|
||||
id: "delete-message",
|
||||
defaultMessage:
|
||||
"Are you sure you want to delete {count, plural, =1 {this {singularEntity}} other {these {pluralEntity}}}?",
|
||||
id: "dialogs.delete_object_desc",
|
||||
},
|
||||
overflowMessage: {
|
||||
id: "overflow-message",
|
||||
defaultMessage:
|
||||
"...and {count} other {count, plural, =1 {{ singularEntity}} other {{ pluralEntity }}}.",
|
||||
id: "dialogs.delete_object_overflow",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -87,10 +79,14 @@ const DeleteEntityDialog: React.FC<IDeleteEntityDialogProps> = ({
|
||||
singularEntity,
|
||||
pluralEntity,
|
||||
})}
|
||||
accept={{ variant: "danger", onClick: onDelete, text: "Delete" }}
|
||||
accept={{
|
||||
variant: "danger",
|
||||
onClick: onDelete,
|
||||
text: intl.formatMessage({ id: "actions.delete" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => onClose(false),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isDeleting}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button, Modal } from "react-bootstrap";
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ImageInput } from "src/components/Shared";
|
||||
|
||||
interface IProps {
|
||||
@@ -21,6 +22,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
const intl = useIntl();
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
function renderEditButton() {
|
||||
@@ -31,7 +33,9 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
className="edit"
|
||||
onClick={() => props.onToggleEdit()}
|
||||
>
|
||||
{props.isEditing ? "Cancel" : "Edit"}
|
||||
{props.isEditing
|
||||
? intl.formatMessage({ id: "actions.cancel" })
|
||||
: intl.formatMessage({ id: "actions.edit" })}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -46,7 +50,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
disabled={props.saveDisabled}
|
||||
onClick={() => props.onSave()}
|
||||
>
|
||||
Save
|
||||
<FormattedMessage id="actions.save" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -59,7 +63,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
className="delete d-none d-sm-block"
|
||||
onClick={() => setIsDeleteAlertOpen(true)}
|
||||
>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -71,7 +75,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
return (
|
||||
<ImageInput
|
||||
isEditing={props.isEditing}
|
||||
text="Back image..."
|
||||
text={intl.formatMessage({ id: "actions.set_back_image" })}
|
||||
onImageChange={props.onBackImageChange}
|
||||
onImageURL={props.onBackImageChangeURL}
|
||||
/>
|
||||
@@ -91,7 +95,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Auto Tag
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -101,17 +105,20 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
return (
|
||||
<Modal show={isDeleteAlertOpen}>
|
||||
<Modal.Body>
|
||||
Are you sure you want to delete {props.objectName}?
|
||||
<FormattedMessage
|
||||
id="dialogs.delete_confirm"
|
||||
values={{ entityName: props.objectName }}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="danger" onClick={props.onDelete}>
|
||||
Delete
|
||||
<FormattedMessage id="actions.delete" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setIsDeleteAlertOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
<FormattedMessage id="actions.cancel" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
@@ -123,7 +130,11 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
{renderEditButton()}
|
||||
<ImageInput
|
||||
isEditing={props.isEditing}
|
||||
text={props.onBackImageChange ? "Front image..." : undefined}
|
||||
text={
|
||||
props.onBackImageChange
|
||||
? intl.formatMessage({ id: "actions.set_front_image" })
|
||||
: undefined
|
||||
}
|
||||
onImageChange={props.onImageChange}
|
||||
onImageURL={props.onImageChangeURL}
|
||||
acceptSVG={props.acceptSVG ?? false}
|
||||
@@ -134,7 +145,9 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
variant="danger"
|
||||
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>
|
||||
) : (
|
||||
""
|
||||
@@ -146,7 +159,7 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
||||
variant="danger"
|
||||
onClick={() => props.onClearBackImage!()}
|
||||
>
|
||||
Clear back image
|
||||
{intl.formatMessage({ id: "actions.clear_back_image" })}
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { downloadFile } from "src/utils";
|
||||
import { ExportObjectsInput } from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IExportDialogProps {
|
||||
exportInput: ExportObjectsInput;
|
||||
@@ -19,6 +20,7 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
||||
// Network state
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
async function onExport() {
|
||||
@@ -46,11 +48,14 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
||||
<Modal
|
||||
show
|
||||
icon="cogs"
|
||||
header="Export"
|
||||
accept={{ onClick: onExport, text: "Export" }}
|
||||
header={intl.formatMessage({ id: "dialogs.export_title" })}
|
||||
accept={{
|
||||
onClick: onExport,
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
isRunning={isRunning}
|
||||
@@ -60,7 +65,9 @@ export const ExportDialog: React.FC<IExportDialogProps> = (
|
||||
<Form.Check
|
||||
id="include-dependencies"
|
||||
checked={includeDependencies}
|
||||
label="Include related objects in export"
|
||||
label={intl.formatMessage({
|
||||
id: "dialogs.export_include_related_objects",
|
||||
})}
|
||||
onChange={() => setIncludeDependencies(!includeDependencies)}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Button, Modal } from "react-bootstrap";
|
||||
import { FolderSelect } from "./FolderSelect";
|
||||
|
||||
@@ -25,7 +26,7 @@ export const FolderSelectDialog: React.FC<IProps> = (props: IProps) => {
|
||||
variant="success"
|
||||
onClick={() => props.onClose(currentDirectory)}
|
||||
>
|
||||
Add
|
||||
<FormattedMessage id="actions.add" />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Popover,
|
||||
Row,
|
||||
} from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Modal } from ".";
|
||||
import Icon from "./Icon";
|
||||
|
||||
@@ -27,6 +28,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
}) => {
|
||||
const [isShowDialog, setIsShowDialog] = useState(false);
|
||||
const [url, setURL] = useState("");
|
||||
const intl = useIntl();
|
||||
|
||||
if (!isEditing) return <div />;
|
||||
|
||||
@@ -58,13 +60,13 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
<Modal
|
||||
show={!!isShowDialog}
|
||||
onHide={() => setIsShowDialog(false)}
|
||||
header="Image URL"
|
||||
header={intl.formatMessage({ id: "dialogs.set_image_url_title" })}
|
||||
accept={{ onClick: onConfirmURL, text: "Confirm" }}
|
||||
>
|
||||
<div className="dialog-content">
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Form.Label column xs={3}>
|
||||
URL
|
||||
{intl.formatMessage({ id: "url" })}
|
||||
</Form.Label>
|
||||
<Col xs={9}>
|
||||
<Form.Control
|
||||
@@ -73,7 +75,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
setURL(event.currentTarget.value)
|
||||
}
|
||||
value={url}
|
||||
placeholder="URL"
|
||||
placeholder={intl.formatMessage({ id: "url" })}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
@@ -90,7 +92,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
<Form.Label className="image-input">
|
||||
<Button variant="secondary">
|
||||
<Icon icon="file" className="fa-fw" />
|
||||
<span>From file...</span>
|
||||
<span>{intl.formatMessage({ id: "actions.from_file" })}</span>
|
||||
</Button>
|
||||
<Form.Control
|
||||
type="file"
|
||||
@@ -102,7 +104,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
<div>
|
||||
<Button className="minimal" onClick={() => setIsShowDialog(true)}>
|
||||
<Icon icon="link" className="fa-fw" />
|
||||
<span>From URL...</span>
|
||||
<span>{intl.formatMessage({ id: "actions.from_url" })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
@@ -120,7 +122,7 @@ export const ImageInput: React.FC<IImageInput> = ({
|
||||
rootClose
|
||||
>
|
||||
<Button variant="secondary" className="mr-2">
|
||||
{text ?? "Set image..."}
|
||||
{text ?? intl.formatMessage({ id: "actions.set_image" })}
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { Button, Modal, Spinner, ModalProps } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface IButton {
|
||||
text?: string;
|
||||
@@ -58,7 +59,13 @@ const ModalComponent: React.FC<IModal> = ({
|
||||
onClick={cancel.onClick}
|
||||
className="mr-2"
|
||||
>
|
||||
{cancel.text ?? "Cancel"}
|
||||
{cancel.text ?? (
|
||||
<FormattedMessage
|
||||
id="actions.cancel"
|
||||
defaultMessage="Cancel"
|
||||
description="Cancels the current action and dismisses the modal."
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
@@ -72,7 +79,13 @@ const ModalComponent: React.FC<IModal> = ({
|
||||
{isRunning ? (
|
||||
<Spinner animation="border" role="status" size="sm" />
|
||||
) : (
|
||||
accept?.text ?? "Close"
|
||||
accept?.text ?? (
|
||||
<FormattedMessage
|
||||
id="actions.close"
|
||||
defaultMessage="Close"
|
||||
description="Closes the current modal."
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
@@ -22,6 +23,7 @@ interface IMultiSetProps {
|
||||
const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||
props: IMultiSetProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const modes = [
|
||||
GQL.BulkUpdateIdMode.Set,
|
||||
GQL.BulkUpdateIdMode.Add,
|
||||
@@ -35,11 +37,17 @@ const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||
function getModeText(mode: GQL.BulkUpdateIdMode) {
|
||||
switch (mode) {
|
||||
case GQL.BulkUpdateIdMode.Set:
|
||||
return "Overwrite";
|
||||
return intl.formatMessage({
|
||||
id: "actions.overwrite",
|
||||
defaultMessage: "Overwrite",
|
||||
});
|
||||
case GQL.BulkUpdateIdMode.Add:
|
||||
return "Add";
|
||||
return intl.formatMessage({ id: "actions.add", defaultMessage: "Add" });
|
||||
case GQL.BulkUpdateIdMode.Remove:
|
||||
return "Remove";
|
||||
return intl.formatMessage({
|
||||
id: "actions.remove",
|
||||
defaultMessage: "Remove",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "react-bootstrap";
|
||||
import { CollapseButton, Icon, Modal } from "src/components/Shared";
|
||||
import _ from "lodash";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
public newValue?: T;
|
||||
@@ -336,6 +337,7 @@ interface IScrapeDialogProps {
|
||||
export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
||||
props: IScrapeDialogProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
@@ -345,11 +347,11 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
||||
onClick: () => {
|
||||
props.onClose(true);
|
||||
},
|
||||
text: "Apply",
|
||||
text: intl.formatMessage({ id: "actions.apply" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }}
|
||||
@@ -360,10 +362,10 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
||||
<Col lg={{ span: 9, offset: 3 }}>
|
||||
<Row>
|
||||
<Form.Label column xs="6">
|
||||
Existing
|
||||
<FormattedMessage id="dialogs.scrape_results_existing" />
|
||||
</Form.Label>
|
||||
<Form.Label column xs="6">
|
||||
Scraped
|
||||
<FormattedMessage id="dialogs.scrape_results_scraped" />
|
||||
</Form.Label>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
@@ -58,7 +58,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.image_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="images" defaultMessage="Images" />
|
||||
<FormattedMessage id="images" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,7 +68,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.movie_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="movies" defaultMessage="Movies" />
|
||||
<FormattedMessage id="movies" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
@@ -76,7 +76,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.gallery_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="galleries" defaultMessage="Galleries" />
|
||||
<FormattedMessage id="galleries" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
@@ -84,7 +84,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.performer_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="performers" defaultMessage="Performers" />
|
||||
<FormattedMessage id="performers" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
@@ -92,7 +92,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.studio_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="studios" defaultMessage="Studios" />
|
||||
<FormattedMessage id="studios" />
|
||||
</p>
|
||||
</div>
|
||||
<div className="stats-element">
|
||||
@@ -100,7 +100,7 @@ export const Stats: React.FC = () => {
|
||||
<FormattedNumber value={data.stats.tag_count} />
|
||||
</p>
|
||||
<p className="heading">
|
||||
<FormattedMessage id="tags" defaultMessage="Tags" />
|
||||
<FormattedMessage id="tags" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Table, Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory, Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
@@ -36,6 +37,7 @@ interface IStudioParams {
|
||||
export const Studio: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { tab = "details", id = "new" } = useParams<IStudioParams>();
|
||||
const isNew = id === "new";
|
||||
|
||||
@@ -194,7 +196,9 @@ export const Studio: React.FC = () => {
|
||||
if (!studio.id) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ studios: [studio.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -229,10 +233,23 @@ export const Studio: React.FC = () => {
|
||||
<Modal
|
||||
show={isDeleteAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -266,7 +283,10 @@ export const Studio: React.FC = () => {
|
||||
<Button
|
||||
variant="danger"
|
||||
className="mr-2 py-0"
|
||||
title="Delete StashID"
|
||||
title={intl.formatMessage(
|
||||
{ id: "actions.delete_entity" },
|
||||
{ entityType: intl.formatMessage({ id: "stash_id" }) }
|
||||
)}
|
||||
onClick={() => removeStashID(stashID)}
|
||||
>
|
||||
<Icon icon="trash-alt" />
|
||||
@@ -339,7 +359,14 @@ export const Studio: React.FC = () => {
|
||||
"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">
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
@@ -352,29 +379,29 @@ export const Studio: React.FC = () => {
|
||||
<Table>
|
||||
<tbody>
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "Name",
|
||||
title: intl.formatMessage({ id: "name" }),
|
||||
value: name ?? "",
|
||||
isEditing: !!isEditing,
|
||||
onChange: setName,
|
||||
})}
|
||||
{TableUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
title: intl.formatMessage({ id: "url" }),
|
||||
value: url,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setUrl,
|
||||
})}
|
||||
{TableUtils.renderTextArea({
|
||||
title: "Details",
|
||||
title: intl.formatMessage({ id: "details" }),
|
||||
value: details,
|
||||
isEditing: !!isEditing,
|
||||
onChange: setDetails,
|
||||
})}
|
||||
<tr>
|
||||
<td>Parent Studio</td>
|
||||
<td>{intl.formatMessage({ id: "parent_studios" })}</td>
|
||||
<td>{renderStudio()}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Rating:</td>
|
||||
<td>{intl.formatMessage({ id: "rating" })}:</td>
|
||||
<td>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
@@ -411,19 +438,28 @@ export const Studio: React.FC = () => {
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
>
|
||||
<Tab eventKey="scenes" title="Scenes">
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<StudioScenesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="galleries" title="Galleries">
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<StudioGalleriesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title="Images">
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<StudioImagesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="performers" title="Performers">
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<StudioPerformersPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="childstudios" title="Child Studios">
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={intl.formatMessage({ id: "child_studios" })}
|
||||
>
|
||||
<StudioChildrenPanel studio={studio} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import _ from "lodash";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
@@ -23,22 +24,23 @@ export const StudioList: React.FC<IStudioList> = ({
|
||||
fromParent,
|
||||
filterHook,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "View Random",
|
||||
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
@@ -119,8 +121,8 @@ export const StudioList: React.FC<IStudioList> = ({
|
||||
<DeleteEntityDialog
|
||||
selected={selectedStudios}
|
||||
onClose={onClose}
|
||||
singularEntity="studio"
|
||||
pluralEntity="studios"
|
||||
singularEntity={intl.formatMessage({ id: "studio" })}
|
||||
pluralEntity={intl.formatMessage({ id: "studios" })}
|
||||
destroyMutation={useStudiosDestroy}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
Form,
|
||||
InputGroup,
|
||||
} from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
|
||||
import { ITaggerConfig, ParseMode, ModeDesc } from "./constants";
|
||||
import { ITaggerConfig, ParseMode } from "./constants";
|
||||
|
||||
interface IConfigProps {
|
||||
show: boolean;
|
||||
@@ -19,6 +20,7 @@ interface IConfigProps {
|
||||
}
|
||||
|
||||
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
const intl = useIntl();
|
||||
const stashConfig = useConfiguration();
|
||||
const blacklistRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
@@ -59,24 +61,30 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
<Collapse in={show}>
|
||||
<Card>
|
||||
<div className="row">
|
||||
<h4 className="col-12">Configuration</h4>
|
||||
<h4 className="col-12">
|
||||
<FormattedMessage id="configuration" />
|
||||
</h4>
|
||||
<hr className="w-100" />
|
||||
<Form className="col-md-6">
|
||||
<Form.Group controlId="tag-males" className="align-items-center">
|
||||
<Form.Check
|
||||
label="Show male performers"
|
||||
label={
|
||||
<FormattedMessage id="component_tagger.config.show_male_label" />
|
||||
}
|
||||
checked={config.showMales}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setConfig({ ...config, showMales: e.currentTarget.checked })
|
||||
}
|
||||
/>
|
||||
<Form.Text>
|
||||
Toggle whether male performers will be available to tag.
|
||||
<FormattedMessage id="component_tagger.config.show_male_desc" />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="set-cover" className="align-items-center">
|
||||
<Form.Check
|
||||
label="Set scene cover image"
|
||||
label={
|
||||
<FormattedMessage id="component_tagger.config.set_cover_label" />
|
||||
}
|
||||
checked={config.setCoverImage}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
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 className="align-items-center">
|
||||
<div className="d-flex align-items-center">
|
||||
<Form.Check
|
||||
id="tag-mode"
|
||||
label="Set tags"
|
||||
label={
|
||||
<FormattedMessage id="component_tagger.config.set_tag_label" />
|
||||
}
|
||||
className="mr-4"
|
||||
checked={config.setTags}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
@@ -111,19 +123,25 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
}
|
||||
disabled={!config.setTags}
|
||||
>
|
||||
<option value="merge">Merge</option>
|
||||
<option value="overwrite">Overwrite</option>
|
||||
<option value="merge">
|
||||
{intl.formatMessage({ id: "actions.merge" })}
|
||||
</option>
|
||||
<option value="overwrite">
|
||||
{intl.formatMessage({ id: "actions.overwrite" })}
|
||||
</option>
|
||||
</Form.Control>
|
||||
</div>
|
||||
<Form.Text>
|
||||
Attach tags to scene, either by overwriting or merging with
|
||||
existing tags on scene.
|
||||
<FormattedMessage id="component_tagger.config.set_tag_desc" />
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="mode-select">
|
||||
<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
|
||||
as="select"
|
||||
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="filename">Filename</option>
|
||||
<option value="dir">Dir</option>
|
||||
<option value="path">Path</option>
|
||||
<option value="metadata">Metadata</option>
|
||||
<option value="auto">
|
||||
{intl.formatMessage({
|
||||
id: "component_tagger.config.query_mode_auto",
|
||||
})}
|
||||
</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>
|
||||
</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>
|
||||
<div className="col-md-6">
|
||||
<h5>Blacklist</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="component_tagger.config.blacklist_label" />
|
||||
</h5>
|
||||
<InputGroup>
|
||||
<Form.Control className="text-input" ref={blacklistRef} />
|
||||
<InputGroup.Append>
|
||||
<Button onClick={handleBlacklistAddition}>Add</Button>
|
||||
<Button onClick={handleBlacklistAddition}>
|
||||
<FormattedMessage id="actions.add" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
<div>
|
||||
Blacklist items are excluded from queries. Note that they are
|
||||
regular expressions and also case-insensitive. Certain characters
|
||||
must be escaped with a backslash: <code>[\^$.|?*+()</code>
|
||||
{intl.formatMessage(
|
||||
{ id: "component_tagger.config.blacklist_desc" },
|
||||
{ chars_require_escape: <code>[\^$.|?*+()</code> }
|
||||
)}
|
||||
</div>
|
||||
{config.blacklist.map((item, index) => (
|
||||
<Badge
|
||||
@@ -179,7 +227,7 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
className="align-items-center row no-gutters mt-4"
|
||||
>
|
||||
<Form.Label className="mr-4">
|
||||
Active stash-box instance:
|
||||
<FormattedMessage id="component_tagger.config.active_instance" />
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
@@ -17,6 +18,7 @@ const PerformerFieldSelect: React.FC<IProps> = ({
|
||||
excludedFields,
|
||||
onSelect,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [excluded, setExcluded] = useState<Record<string, boolean>>(
|
||||
excludedFields.reduce((dict, field) => ({ ...dict, [field]: true }), {})
|
||||
);
|
||||
@@ -46,7 +48,7 @@ const PerformerFieldSelect: React.FC<IProps> = ({
|
||||
icon="list"
|
||||
dialogClassName="FieldSelect"
|
||||
accept={{
|
||||
text: "Save",
|
||||
text: intl.formatMessage({ id: "actions.save" }),
|
||||
onClick: () =>
|
||||
onSelect(Object.keys(excluded).filter((f) => excluded[f])),
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||
|
||||
@@ -37,6 +38,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||
create = false,
|
||||
endpoint,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [imageIndex, setImageIndex] = useState(0);
|
||||
const [imageState, setImageState] = useState<
|
||||
"loading" | "error" | "loaded" | "empty"
|
||||
@@ -109,7 +111,7 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
||||
<Modal
|
||||
show={modalVisible}
|
||||
accept={{
|
||||
text: "Save",
|
||||
text: intl.formatMessage({ id: "actions.save" }),
|
||||
onClick: () =>
|
||||
handlePerformerCreate(
|
||||
imageIndex,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import cx from "classnames";
|
||||
|
||||
import { SuccessIcon, PerformerSelect } from "src/components/Shared";
|
||||
@@ -112,12 +113,12 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||
return (
|
||||
<div className="row no-gutters my-2">
|
||||
<div className="entity-name">
|
||||
Performer:
|
||||
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||
<b className="ml-2">{performer.name}</b>
|
||||
</div>
|
||||
<span className="ml-auto">
|
||||
<SuccessIcon />
|
||||
Matched:
|
||||
<FormattedMessage id="component_tagger.verb_matched" />:
|
||||
</span>
|
||||
<b className="col-3 text-right">
|
||||
{stashData.findPerformers.performers[0].name}
|
||||
@@ -138,7 +139,7 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||
endpoint={endpoint}
|
||||
/>
|
||||
<div className="entity-name">
|
||||
Performer:
|
||||
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||
<b className="ml-2">{performer.name}</b>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
@@ -146,13 +147,13 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||
variant={selectedSource === "create" ? "primary" : "secondary"}
|
||||
onClick={() => showModal(true)}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="actions.create" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
||||
onClick={() => handlePerformerSkip()}
|
||||
>
|
||||
Skip
|
||||
<FormattedMessage id="actions.skip" />
|
||||
</Button>
|
||||
<PerformerSelect
|
||||
ids={selectedPerformer ? [selectedPerformer] : []}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useReducer } from "react";
|
||||
import cx from "classnames";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { uniq } from "lodash";
|
||||
import { blobToBase64 } from "base64-blob";
|
||||
|
||||
@@ -34,9 +35,14 @@ const getDurationStatus = (
|
||||
|
||||
let match;
|
||||
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)
|
||||
match = "Duration is a match";
|
||||
match = <FormattedMessage id="component_tagger.results.fp_matches" />;
|
||||
|
||||
if (match)
|
||||
return (
|
||||
@@ -67,7 +73,16 @@ const getFingerprintStatus = (
|
||||
return (
|
||||
<div className="font-weight-bold">
|
||||
<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>
|
||||
);
|
||||
};
|
||||
@@ -116,6 +131,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
{}
|
||||
);
|
||||
|
||||
const intl = useIntl();
|
||||
const createStudio = useCreateStudio();
|
||||
const createPerformer = useCreatePerformer();
|
||||
const createTag = useCreateTag();
|
||||
@@ -400,7 +416,11 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
{scene?.studio?.name} • {scene?.date}
|
||||
</h5>
|
||||
<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>
|
||||
{getDurationStatus(scene, stashScene.file?.duration)}
|
||||
{getFingerprintStatus(scene, stashScene)}
|
||||
@@ -440,7 +460,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
||||
{saveState ? (
|
||||
<LoadingIndicator inline small message="" />
|
||||
) : (
|
||||
"Save"
|
||||
<FormattedMessage id="actions.save" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState, Dispatch, SetStateAction } from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
|
||||
import { SuccessIcon, Modal, StudioSelect } from "src/components/Shared";
|
||||
@@ -19,6 +20,7 @@ interface IStudioResultProps {
|
||||
}
|
||||
|
||||
const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
||||
const intl = useIntl();
|
||||
const [selectedStudio, setSelectedStudio] = useState<string | null>();
|
||||
const [modalVisible, showModal] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState<
|
||||
@@ -94,8 +96,11 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
||||
return (
|
||||
<div className="row no-gutters my-2">
|
||||
<div className="entity-name">
|
||||
Studio:
|
||||
<b className="ml-2">{studio?.name}</b>
|
||||
<FormattedMessage
|
||||
id="countables.studios"
|
||||
values={{ count: stashIDData?.findStudios.studios.length }}
|
||||
/>
|
||||
:<b className="ml-2">{studio?.name}</b>
|
||||
</div>
|
||||
<span className="ml-auto">
|
||||
<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">
|
||||
<Modal
|
||||
show={modalVisible}
|
||||
accept={{ text: "Save", onClick: handleStudioCreate }}
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.save" }),
|
||||
onClick: handleStudioCreate,
|
||||
}}
|
||||
cancel={{ onClick: () => showModal(false), variant: "secondary" }}
|
||||
>
|
||||
<div className="row">
|
||||
<strong className="col-2">Name:</strong>
|
||||
<strong className="col-2">
|
||||
<FormattedMessage id="name" />:
|
||||
</strong>
|
||||
<span className="col-10">{studio?.name}</span>
|
||||
</div>
|
||||
<div className="row">
|
||||
<strong className="col-2">URL:</strong>
|
||||
<strong className="col-2">
|
||||
<FormattedMessage id="url" />:
|
||||
</strong>
|
||||
<span className="col-10">{studio?.url ?? ""}</span>
|
||||
</div>
|
||||
<div className="row">
|
||||
@@ -132,21 +144,20 @@ const StudioResult: React.FC<IStudioResultProps> = ({ studio, setStudio }) => {
|
||||
</Modal>
|
||||
|
||||
<div className="entity-name">
|
||||
Studio:
|
||||
<b className="ml-2">{studio?.name}</b>
|
||||
<FormattedMessage id="studios" />:<b className="ml-2">{studio?.name}</b>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
<Button
|
||||
variant={selectedSource === "create" ? "primary" : "secondary"}
|
||||
onClick={() => showModal(true)}
|
||||
>
|
||||
Create
|
||||
<FormattedMessage id="actions.create" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedSource === "skip" ? "primary" : "secondary"}
|
||||
onClick={() => handleStudioSkip()}
|
||||
>
|
||||
Skip
|
||||
<FormattedMessage id="actions.skip" />
|
||||
</Button>
|
||||
<StudioSelect
|
||||
ids={selectedStudio ? [selectedStudio] : []}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Card, Form, InputGroup } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
import { uniqBy } from "lodash";
|
||||
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
||||
@@ -161,6 +162,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
queueFingerprintSubmission,
|
||||
clearSubmissionQueue,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [fingerprintError, setFingerprintError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const queryString = useRef<Record<string, string>>({});
|
||||
@@ -310,7 +312,10 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
|
||||
const getFingerprintCountMessage = () => {
|
||||
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 = () => {
|
||||
@@ -359,14 +364,18 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
if (!isTagged && hasStashIDs) {
|
||||
mainContent = (
|
||||
<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>
|
||||
);
|
||||
} else if (!isTagged && !hasStashIDs) {
|
||||
mainContent = (
|
||||
<InputGroup>
|
||||
<InputGroup.Prepend>
|
||||
<InputGroup.Text>Query</InputGroup.Text>
|
||||
<InputGroup.Text>
|
||||
<FormattedMessage id="component_tagger.noun_query" />
|
||||
</InputGroup.Text>
|
||||
</InputGroup.Prepend>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
@@ -392,7 +401,7 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
Search
|
||||
<FormattedMessage id="actions.search" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
@@ -400,7 +409,9 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
} else if (isTagged) {
|
||||
mainContent = (
|
||||
<div className="d-flex flex-column text-right">
|
||||
<h5>Scene successfully tagged:</h5>
|
||||
<h5>
|
||||
<FormattedMessage id="component_tagger.results.match_success" />
|
||||
</h5>
|
||||
<h6>
|
||||
<Link className="bold" to={sceneLink}>
|
||||
{taggedScenes[scene.id].title}
|
||||
@@ -438,7 +449,9 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
);
|
||||
} else if (searchResults[scene.id]?.length === 0) {
|
||||
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">
|
||||
{(getFingerprintCount() > 0 || hideUnmatched) && (
|
||||
<Button onClick={toggleHideUnmatchedScenes}>
|
||||
{hideUnmatched ? "Show" : "Hide"} unmatched scenes
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_toggle_unmatched"
|
||||
values={{
|
||||
toggle: (
|
||||
<FormattedMessage
|
||||
id={`actions.${hideUnmatched ? "hide" : "show"}`}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -558,7 +580,10 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
<LoadingIndicator message="" inline small />
|
||||
) : (
|
||||
<span>
|
||||
Submit <b>{fingerprintQueue.length}</b> Fingerprints
|
||||
<FormattedMessage
|
||||
id="component_tagger.verb_submit_fp"
|
||||
values={{ fpCount: fingerprintQueue.length }}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
@@ -568,7 +593,11 @@ const TaggerList: React.FC<ITaggerListProps> = ({
|
||||
onClick={handleFingerprintSearch}
|
||||
disabled={!canFingerprintSearch() && !loadingFingerprints}
|
||||
>
|
||||
{canFingerprintSearch() && <span>Match Fingerprints</span>}
|
||||
{canFingerprintSearch() && (
|
||||
<span>
|
||||
{intl.formatMessage({ id: "component_tagger.verb_match_fp" })}
|
||||
</span>
|
||||
)}
|
||||
{!canFingerprintSearch() && getFingerprintCountMessage()}
|
||||
{loadingFingerprints && <LoadingIndicator message="" inline small />}
|
||||
</Button>
|
||||
@@ -638,7 +667,17 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
||||
<>
|
||||
<div className="row mb-2 no-gutters">
|
||||
<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
|
||||
className="ml-auto"
|
||||
@@ -646,7 +685,7 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
||||
title="Help"
|
||||
variant="link"
|
||||
>
|
||||
Help
|
||||
<FormattedMessage id="help" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,14 +24,6 @@ export const initialConfig: ITaggerConfig = {
|
||||
};
|
||||
|
||||
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 {
|
||||
blacklist: string[];
|
||||
showMales: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Card, Form, InputGroup, ProgressBar } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import { HashLink } from "react-router-hash-link";
|
||||
import { useLocalForage } from "src/hooks";
|
||||
@@ -51,6 +52,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||
onBatchAdd,
|
||||
onBatchUpdate,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<
|
||||
Record<string, IStashBoxPerformer[]>
|
||||
@@ -245,7 +247,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
Search
|
||||
<FormattedMessage id="actions.search" />
|
||||
</Button>
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
@@ -384,7 +386,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||
header="Update Performers"
|
||||
accept={{ text: "Update Performers", onClick: handleBatchUpdate }}
|
||||
cancel={{
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "danger",
|
||||
onClick: () => setShowBatchUpdate(false),
|
||||
}}
|
||||
@@ -454,7 +456,7 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
||||
header="Add New Performers"
|
||||
accept={{ text: "Add Performers", onClick: handleBatchAdd }}
|
||||
cancel={{
|
||||
text: "Cancel",
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "danger",
|
||||
onClick: () => setShowBatchAdd(false),
|
||||
}}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const parsePath = (filePath: string) => {
|
||||
const ext = fileName.match(/\.[a-z0-9]*$/)?.[0] ?? "";
|
||||
const file = fileName.slice(0, ext.length * -1);
|
||||
const paths =
|
||||
pathComponents.length > 2
|
||||
pathComponents.length >= 2
|
||||
? pathComponents.slice(0, pathComponents.length - 2)
|
||||
: [];
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Tabs, Tab } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useParams, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import cx from "classnames";
|
||||
import Mousetrap from "mousetrap";
|
||||
|
||||
@@ -35,6 +36,7 @@ interface ITabParams {
|
||||
export const Tag: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { tab = "scenes", id = "new" } = useParams<ITabParams>();
|
||||
const isNew = id === "new";
|
||||
|
||||
@@ -170,10 +172,23 @@ export const Tag: React.FC = () => {
|
||||
<Modal
|
||||
show={isDeleteAlertOpen}
|
||||
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) }}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -248,19 +263,28 @@ export const Tag: React.FC = () => {
|
||||
activeKey={activeTabKey}
|
||||
onSelect={setActiveTabKey}
|
||||
>
|
||||
<Tab eventKey="scenes" title="Scenes">
|
||||
<Tab eventKey="scenes" title={intl.formatMessage({ id: "scenes" })}>
|
||||
<TagScenesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="images" title="Images">
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<TagImagesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="galleries" title="Galleries">
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
>
|
||||
<TagGalleriesPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="markers" title="Markers">
|
||||
<Tab
|
||||
eventKey="markers"
|
||||
title={intl.formatMessage({ id: "markers" })}
|
||||
>
|
||||
<TagMarkersPanel tag={tag} />
|
||||
</Tab>
|
||||
<Tab eventKey="performers" title="Performers">
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
>
|
||||
<TagPerformersPanel tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { Badge } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
interface ITagDetails {
|
||||
@@ -14,7 +15,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag }) => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{tag.aliases.map((a) => (
|
||||
<Badge className="tag-item" variant="secondary">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import { DetailsEditNavbar } from "src/components/Shared";
|
||||
@@ -27,6 +28,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
onDelete,
|
||||
setImage,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const isNew = tag === undefined;
|
||||
@@ -102,7 +104,14 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
// TODO: CSS class
|
||||
return (
|
||||
<div>
|
||||
{isNew && <h2>Add Tag</h2>}
|
||||
{isNew && (
|
||||
<h2>
|
||||
<FormattedMessage
|
||||
id="actions.add_entity"
|
||||
values={{ entityType: intl.formatMessage({ id: "tag" }) }}
|
||||
/>
|
||||
</h2>
|
||||
)}
|
||||
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
@@ -117,7 +126,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
<Form noValidate onSubmit={formik.handleSubmit} id="tag-edit">
|
||||
<Form.Group controlId="name" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Name
|
||||
<FormattedMessage id="name" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<Form.Control
|
||||
@@ -134,7 +143,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
|
||||
<Form.Group controlId="aliases" as={Row}>
|
||||
<Form.Label column xs={labelXS} xl={labelXL}>
|
||||
Aliases
|
||||
<FormattedMessage id="aliases" />
|
||||
</Form.Label>
|
||||
<Col xs={fieldXS} xl={fieldXL}>
|
||||
<StringListInput
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
useTagsDestroy,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { FormattedNumber } from "react-intl";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import { NavUtils } from "src/utils";
|
||||
import { Icon, Modal, DeleteEntityDialog } from "src/components/Shared";
|
||||
import { TagCard } from "./TagCard";
|
||||
@@ -38,22 +38,23 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
|
||||
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
||||
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "View Random",
|
||||
text: intl.formatMessage({ id: "actions.view_random" }),
|
||||
onClick: viewRandom,
|
||||
},
|
||||
{
|
||||
text: "Export...",
|
||||
text: intl.formatMessage({ id: "actions.export" }),
|
||||
onClick: onExport,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
{
|
||||
text: "Export all...",
|
||||
text: intl.formatMessage({ id: "actions.export_all" }),
|
||||
onClick: onExportAll,
|
||||
},
|
||||
];
|
||||
@@ -134,8 +135,8 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
<DeleteEntityDialog
|
||||
selected={selectedTags}
|
||||
onClose={onClose}
|
||||
singularEntity="tag"
|
||||
pluralEntity="tags"
|
||||
singularEntity={intl.formatMessage({ id: "tag" })}
|
||||
pluralEntity={intl.formatMessage({ id: "tags" })}
|
||||
destroyMutation={useTagsDestroy}
|
||||
/>
|
||||
);
|
||||
@@ -164,7 +165,9 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
if (!tag) return;
|
||||
try {
|
||||
await mutateMetadataAutoTag({ tags: [tag.id] });
|
||||
Toast.success({ content: "Started auto tagging" });
|
||||
Toast.success({
|
||||
content: intl.formatMessage({ id: "toast.started_auto_tagging" }),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
@@ -173,7 +176,16 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
async function onDelete() {
|
||||
try {
|
||||
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);
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -212,11 +224,18 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
onHide={() => {}}
|
||||
show={!!deletingTag}
|
||||
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) }}
|
||||
>
|
||||
<span>
|
||||
Are you sure you want to delete {deletingTag && deletingTag.name}?
|
||||
<FormattedMessage
|
||||
id="dialogs.delete_confirm"
|
||||
values={{ entityName: deletingTag && deletingTag.name }}
|
||||
/>
|
||||
</span>
|
||||
</Modal>
|
||||
);
|
||||
@@ -232,14 +251,20 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
className="tag-list-button"
|
||||
onClick={() => onAutoTag(tag)}
|
||||
>
|
||||
Auto Tag
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
<Link
|
||||
to={NavUtils.makeTagScenesUrl(tag)}
|
||||
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>
|
||||
</Button>
|
||||
<Button variant="secondary" className="tag-list-button">
|
||||
@@ -247,12 +272,17 @@ export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
to={NavUtils.makeTagSceneMarkersUrl(tag)}
|
||||
className="tag-list-anchor"
|
||||
>
|
||||
Markers:{" "}
|
||||
<FormattedNumber value={tag.scene_marker_count ?? 0} />
|
||||
<FormattedMessage
|
||||
id="countables.markers"
|
||||
values={{
|
||||
count: tag.scene_marker_count ?? 0,
|
||||
}}
|
||||
/>
|
||||
: <FormattedNumber value={tag.scene_marker_count ?? 0} />
|
||||
</Link>
|
||||
</Button>
|
||||
<span className="tag-list-count">
|
||||
Total:{" "}
|
||||
<FormattedMessage id="total" />:{" "}
|
||||
<FormattedNumber
|
||||
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