mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Tasks page refactor (#1949)
* Add scan dialog * Add Auto Tag dialog * Refactor and combine Generate dialog * Add clean dialog * Support scan task default setting * Support saving auto tag defaults * Support for generate defaults
This commit is contained in:
@@ -106,6 +106,16 @@ fragment ScraperSourceData on ScraperSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
||||||
|
scan {
|
||||||
|
useFileMetadata
|
||||||
|
stripFileExtension
|
||||||
|
scanGeneratePreviews
|
||||||
|
scanGenerateImagePreviews
|
||||||
|
scanGenerateSprites
|
||||||
|
scanGeneratePhashes
|
||||||
|
scanGenerateThumbnails
|
||||||
|
}
|
||||||
|
|
||||||
identify {
|
identify {
|
||||||
sources {
|
sources {
|
||||||
source {
|
source {
|
||||||
@@ -120,6 +130,30 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autoTag {
|
||||||
|
performers
|
||||||
|
studios
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
|
||||||
|
generate {
|
||||||
|
sprites
|
||||||
|
previews
|
||||||
|
imagePreviews
|
||||||
|
previewOptions {
|
||||||
|
previewSegments
|
||||||
|
previewSegmentDuration
|
||||||
|
previewExcludeStart
|
||||||
|
previewExcludeEnd
|
||||||
|
previewPreset
|
||||||
|
}
|
||||||
|
markers
|
||||||
|
markerImagePreviews
|
||||||
|
markerScreenshots
|
||||||
|
transcodes
|
||||||
|
phashes
|
||||||
|
}
|
||||||
|
|
||||||
deleteFile
|
deleteFile
|
||||||
deleteGenerated
|
deleteGenerated
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -333,7 +333,10 @@ type ConfigScrapingResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ConfigDefaultSettingsResult {
|
type ConfigDefaultSettingsResult {
|
||||||
|
scan: ScanMetadataOptions
|
||||||
identify: IdentifyMetadataTaskOptions
|
identify: IdentifyMetadataTaskOptions
|
||||||
|
autoTag: AutoTagMetadataOptions
|
||||||
|
generate: GenerateMetadataOptions
|
||||||
|
|
||||||
"""If true, delete file checkbox will be checked by default"""
|
"""If true, delete file checkbox will be checked by default"""
|
||||||
deleteFile: Boolean
|
deleteFile: Boolean
|
||||||
@@ -342,7 +345,10 @@ type ConfigDefaultSettingsResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input ConfigDefaultSettingsInput {
|
input ConfigDefaultSettingsInput {
|
||||||
|
scan: ScanMetadataInput
|
||||||
identify: IdentifyMetadataInput
|
identify: IdentifyMetadataInput
|
||||||
|
autoTag: AutoTagMetadataInput
|
||||||
|
generate: GenerateMetadataInput
|
||||||
|
|
||||||
"""If true, delete file checkbox will be checked by default"""
|
"""If true, delete file checkbox will be checked by default"""
|
||||||
deleteFile: Boolean
|
deleteFile: Boolean
|
||||||
|
|||||||
@@ -33,6 +33,31 @@ input GeneratePreviewOptionsInput {
|
|||||||
previewPreset: PreviewPreset
|
previewPreset: PreviewPreset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GenerateMetadataOptions {
|
||||||
|
sprites: Boolean
|
||||||
|
previews: Boolean
|
||||||
|
imagePreviews: Boolean
|
||||||
|
previewOptions: GeneratePreviewOptions
|
||||||
|
markers: Boolean
|
||||||
|
markerImagePreviews: Boolean
|
||||||
|
markerScreenshots: Boolean
|
||||||
|
transcodes: Boolean
|
||||||
|
phashes: Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type GeneratePreviewOptions {
|
||||||
|
"""Number of segments in a preview file"""
|
||||||
|
previewSegments: Int
|
||||||
|
"""Preview segment duration, in seconds"""
|
||||||
|
previewSegmentDuration: Float
|
||||||
|
"""Duration of start of video to exclude when generating previews"""
|
||||||
|
previewExcludeStart: String
|
||||||
|
"""Duration of end of video to exclude when generating previews"""
|
||||||
|
previewExcludeEnd: String
|
||||||
|
"""Preset when generating preview"""
|
||||||
|
previewPreset: PreviewPreset
|
||||||
|
}
|
||||||
|
|
||||||
input ScanMetadataInput {
|
input ScanMetadataInput {
|
||||||
paths: [String!]
|
paths: [String!]
|
||||||
"""Set name, date, details from metadata (if present)"""
|
"""Set name, date, details from metadata (if present)"""
|
||||||
@@ -51,6 +76,23 @@ input ScanMetadataInput {
|
|||||||
scanGenerateThumbnails: Boolean
|
scanGenerateThumbnails: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScanMetadataOptions {
|
||||||
|
"""Set name, date, details from metadata (if present)"""
|
||||||
|
useFileMetadata: Boolean!
|
||||||
|
"""Strip file extension from title"""
|
||||||
|
stripFileExtension: Boolean!
|
||||||
|
"""Generate previews during scan"""
|
||||||
|
scanGeneratePreviews: Boolean!
|
||||||
|
"""Generate image previews during scan"""
|
||||||
|
scanGenerateImagePreviews: Boolean!
|
||||||
|
"""Generate sprites during scan"""
|
||||||
|
scanGenerateSprites: Boolean!
|
||||||
|
"""Generate phashes during scan"""
|
||||||
|
scanGeneratePhashes: Boolean!
|
||||||
|
"""Generate image thumbnails during scan"""
|
||||||
|
scanGenerateThumbnails: Boolean!
|
||||||
|
}
|
||||||
|
|
||||||
input CleanMetadataInput {
|
input CleanMetadataInput {
|
||||||
"""Do a dry run. Don't delete any files"""
|
"""Do a dry run. Don't delete any files"""
|
||||||
dryRun: Boolean!
|
dryRun: Boolean!
|
||||||
@@ -67,6 +109,15 @@ input AutoTagMetadataInput {
|
|||||||
tags: [String!]
|
tags: [String!]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AutoTagMetadataOptions {
|
||||||
|
"""IDs of performers to tag files with, or "*" for all"""
|
||||||
|
performers: [String!]
|
||||||
|
"""IDs of studios to tag files with, or "*" for all"""
|
||||||
|
studios: [String!]
|
||||||
|
"""IDs of tags to tag files with, or "*" for all"""
|
||||||
|
tags: [String!]
|
||||||
|
}
|
||||||
|
|
||||||
enum IdentifyFieldStrategy {
|
enum IdentifyFieldStrategy {
|
||||||
"""Never sets the field value"""
|
"""Never sets the field value"""
|
||||||
IGNORE
|
IGNORE
|
||||||
|
|||||||
@@ -394,6 +394,18 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
|
|||||||
c.Set(config.DefaultIdentifySettings, input.Identify)
|
c.Set(config.DefaultIdentifySettings, input.Identify)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.Scan != nil {
|
||||||
|
c.Set(config.DefaultScanSettings, input.Scan)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.AutoTag != nil {
|
||||||
|
c.Set(config.DefaultAutoTagSettings, input.AutoTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Generate != nil {
|
||||||
|
c.Set(config.DefaultGenerateSettings, input.Generate)
|
||||||
|
}
|
||||||
|
|
||||||
if input.DeleteFile != nil {
|
if input.DeleteFile != nil {
|
||||||
c.Set(config.DeleteFileDefault, *input.DeleteFile)
|
c.Set(config.DeleteFileDefault, *input.DeleteFile)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
|
|||||||
|
|
||||||
return &models.ConfigDefaultSettingsResult{
|
return &models.ConfigDefaultSettingsResult{
|
||||||
Identify: config.GetDefaultIdentifySettings(),
|
Identify: config.GetDefaultIdentifySettings(),
|
||||||
|
Scan: config.GetDefaultScanSettings(),
|
||||||
|
AutoTag: config.GetDefaultAutoTagSettings(),
|
||||||
|
Generate: config.GetDefaultGenerateSettings(),
|
||||||
DeleteFile: &deleteFileDefault,
|
DeleteFile: &deleteFileDefault,
|
||||||
DeleteGenerated: &deleteGeneratedDefault,
|
DeleteGenerated: &deleteGeneratedDefault,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,11 @@ const (
|
|||||||
LogAccess = "logAccess"
|
LogAccess = "logAccess"
|
||||||
defaultLogAccess = true
|
defaultLogAccess = true
|
||||||
|
|
||||||
|
// Default settings
|
||||||
|
DefaultScanSettings = "defaults.scan_task"
|
||||||
DefaultIdentifySettings = "defaults.identify_task"
|
DefaultIdentifySettings = "defaults.identify_task"
|
||||||
|
DefaultAutoTagSettings = "defaults.auto_tag_task"
|
||||||
|
DefaultGenerateSettings = "defaults.generate_task"
|
||||||
|
|
||||||
DeleteFileDefault = "defaults.delete_file"
|
DeleteFileDefault = "defaults.delete_file"
|
||||||
DeleteGeneratedDefault = "defaults.delete_generated"
|
DeleteGeneratedDefault = "defaults.delete_generated"
|
||||||
@@ -953,6 +957,63 @@ func (i *Instance) GetDefaultIdentifySettings() *models.IdentifyMetadataTaskOpti
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDefaultScanSettings returns the default Scan task settings.
|
||||||
|
// Returns nil if the settings could not be unmarshalled, or if it
|
||||||
|
// has not been set.
|
||||||
|
func (i *Instance) GetDefaultScanSettings() *models.ScanMetadataOptions {
|
||||||
|
i.RLock()
|
||||||
|
defer i.RUnlock()
|
||||||
|
v := i.viper(DefaultScanSettings)
|
||||||
|
|
||||||
|
if v.IsSet(DefaultScanSettings) {
|
||||||
|
var ret models.ScanMetadataOptions
|
||||||
|
if err := v.UnmarshalKey(DefaultScanSettings, &ret); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultAutoTagSettings returns the default Scan task settings.
|
||||||
|
// Returns nil if the settings could not be unmarshalled, or if it
|
||||||
|
// has not been set.
|
||||||
|
func (i *Instance) GetDefaultAutoTagSettings() *models.AutoTagMetadataOptions {
|
||||||
|
i.RLock()
|
||||||
|
defer i.RUnlock()
|
||||||
|
v := i.viper(DefaultAutoTagSettings)
|
||||||
|
|
||||||
|
if v.IsSet(DefaultAutoTagSettings) {
|
||||||
|
var ret models.AutoTagMetadataOptions
|
||||||
|
if err := v.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultGenerateSettings returns the default Scan task settings.
|
||||||
|
// Returns nil if the settings could not be unmarshalled, or if it
|
||||||
|
// has not been set.
|
||||||
|
func (i *Instance) GetDefaultGenerateSettings() *models.GenerateMetadataOptions {
|
||||||
|
i.RLock()
|
||||||
|
defer i.RUnlock()
|
||||||
|
v := i.viper(DefaultGenerateSettings)
|
||||||
|
|
||||||
|
if v.IsSet(DefaultGenerateSettings) {
|
||||||
|
var ret models.GenerateMetadataOptions
|
||||||
|
if err := v.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &ret
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetTrustedProxies returns a comma separated list of ip addresses that should allow proxying.
|
// GetTrustedProxies returns a comma separated list of ip addresses that should allow proxying.
|
||||||
// When empty, allow from any private network
|
// When empty, allow from any private network
|
||||||
func (i *Instance) GetTrustedProxies() []string {
|
func (i *Instance) GetTrustedProxies() []string {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Refactored tasks page and dialogs and added support for saving default options for scanning, generating and auto-tagging. ([#1949](https://github.com/stashapp/stash/pull/1949))
|
||||||
* Changed query string parsing behaviour to require all words by default, with the option to `or` keywords and exclude keywords. See the `Browsing` section of the manual for details. ([#1982](https://github.com/stashapp/stash/pull/1982))
|
* Changed query string parsing behaviour to require all words by default, with the option to `or` keywords and exclude keywords. See the `Browsing` section of the manual for details. ([#1982](https://github.com/stashapp/stash/pull/1982))
|
||||||
* Add forward jump 10 second button to video player. ([#1973](https://github.com/stashapp/stash/pull/1973))
|
* Add forward jump 10 second button to video player. ([#1973](https://github.com/stashapp/stash/pull/1973))
|
||||||
|
|
||||||
|
|||||||
262
ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx
Normal file
262
ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
mutateMetadataAutoTag,
|
||||||
|
useConfiguration,
|
||||||
|
useConfigureDefaults,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Icon, Modal, OperationButton } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||||
|
import { Manual } from "src/components/Help/Manual";
|
||||||
|
import { withoutTypename } from "src/utils";
|
||||||
|
|
||||||
|
interface IAutoTagOptions {
|
||||||
|
options: GQL.AutoTagMetadataInput;
|
||||||
|
setOptions: (s: GQL.AutoTagMetadataInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoTagOptions: React.FC<IAutoTagOptions> = ({
|
||||||
|
options,
|
||||||
|
setOptions: setOptionsState,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const { performers, studios, tags } = options;
|
||||||
|
const wildcard = ["*"];
|
||||||
|
|
||||||
|
function toggle(v?: GQL.Maybe<string[]>) {
|
||||||
|
if (!v?.length) {
|
||||||
|
return wildcard;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptions(input: Partial<GQL.AutoTagMetadataInput>) {
|
||||||
|
setOptionsState({ ...options, ...input });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="autotag-performers"
|
||||||
|
checked={!!performers?.length}
|
||||||
|
label={intl.formatMessage({ id: "performers" })}
|
||||||
|
onChange={() => setOptions({ performers: toggle(performers) })}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="autotag-studios"
|
||||||
|
checked={!!studios?.length}
|
||||||
|
label={intl.formatMessage({ id: "studios" })}
|
||||||
|
onChange={() => setOptions({ studios: toggle(studios) })}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="autotag-tags"
|
||||||
|
checked={!!tags?.length}
|
||||||
|
label={intl.formatMessage({ id: "tags" })}
|
||||||
|
onChange={() => setOptions({ tags: toggle(tags) })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAutoTagDialogProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AutoTagDialog: React.FC<IAutoTagDialogProps> = ({ onClose }) => {
|
||||||
|
const [configureDefaults] = useConfigureDefaults();
|
||||||
|
|
||||||
|
const [options, setOptions] = useState<GQL.AutoTagMetadataInput>({
|
||||||
|
performers: ["*"],
|
||||||
|
studios: ["*"],
|
||||||
|
tags: ["*"],
|
||||||
|
});
|
||||||
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
const [settingPaths, setSettingPaths] = useState(false);
|
||||||
|
const [animation, setAnimation] = useState(true);
|
||||||
|
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const { data: configData, error: configError } = useConfiguration();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!configData?.configuration.defaults) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { autoTag } = configData.configuration.defaults;
|
||||||
|
|
||||||
|
if (autoTag) {
|
||||||
|
setOptions(withoutTypename(autoTag));
|
||||||
|
}
|
||||||
|
}, [configData]);
|
||||||
|
|
||||||
|
const selectionStatus = useMemo(() => {
|
||||||
|
const message = paths.length ? (
|
||||||
|
<div>
|
||||||
|
<FormattedMessage id="config.tasks.auto_tag.auto_tagging_paths" />:
|
||||||
|
<ul>
|
||||||
|
{paths.map((p) => (
|
||||||
|
<li key={p}>{p}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage id="config.tasks.auto_tag.auto_tagging_all_paths" />.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
setAnimation(false);
|
||||||
|
setSettingPaths(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group className="dialog-selected-folders">
|
||||||
|
<div>
|
||||||
|
{message}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
title={intl.formatMessage({ id: "actions.select_folders" })}
|
||||||
|
onClick={() => onClick()}
|
||||||
|
>
|
||||||
|
<Icon icon="folder-open" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}, [intl, paths]);
|
||||||
|
|
||||||
|
if (configError) return <div>{configError}</div>;
|
||||||
|
if (!configData) return <div />;
|
||||||
|
|
||||||
|
function makeDefaultAutoTagInput() {
|
||||||
|
const ret = options;
|
||||||
|
const { paths: _paths, ...withoutSpecifics } = ret;
|
||||||
|
return withoutSpecifics;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAutoTag() {
|
||||||
|
try {
|
||||||
|
await mutateMetadataAutoTag({
|
||||||
|
...options,
|
||||||
|
paths: paths.length ? paths : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
} finally {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowManual() {
|
||||||
|
setAnimation(false);
|
||||||
|
setShowManual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAsDefault() {
|
||||||
|
try {
|
||||||
|
setSavingDefaults(true);
|
||||||
|
await configureDefaults({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
autoTag: makeDefaultAutoTagInput(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.defaults_set" },
|
||||||
|
{ action: intl.formatMessage({ id: "actions.auto_tag" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
setSavingDefaults(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingPaths) {
|
||||||
|
return (
|
||||||
|
<DirectorySelectionDialog
|
||||||
|
animation={false}
|
||||||
|
allowEmpty
|
||||||
|
initialPaths={paths}
|
||||||
|
onClose={(p) => {
|
||||||
|
if (p) {
|
||||||
|
setPaths(p);
|
||||||
|
}
|
||||||
|
setSettingPaths(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showManual) {
|
||||||
|
return (
|
||||||
|
<Manual
|
||||||
|
animation={false}
|
||||||
|
show
|
||||||
|
onClose={() => setShowManual(false)}
|
||||||
|
defaultActiveTab="AutoTagging.md"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
modalProps={{ animation }}
|
||||||
|
show
|
||||||
|
icon="cogs"
|
||||||
|
header={intl.formatMessage({ id: "actions.auto_tag" })}
|
||||||
|
accept={{
|
||||||
|
onClick: onAutoTag,
|
||||||
|
text: intl.formatMessage({ id: "actions.auto_tag" }),
|
||||||
|
}}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => onClose(),
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
disabled={savingDefaults}
|
||||||
|
footerButtons={
|
||||||
|
<OperationButton variant="secondary" operation={setAsDefault}>
|
||||||
|
<FormattedMessage id="actions.set_as_default" />
|
||||||
|
</OperationButton>
|
||||||
|
}
|
||||||
|
leftFooterButtons={
|
||||||
|
<Button
|
||||||
|
title="Help"
|
||||||
|
className="minimal help-button"
|
||||||
|
onClick={() => onShowManual()}
|
||||||
|
>
|
||||||
|
<Icon icon="question-circle" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
{selectionStatus}
|
||||||
|
<AutoTagOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AutoTagDialog;
|
||||||
198
ui/v2.5/src/components/Dialogs/CleanDialog.tsx
Normal file
198
ui/v2.5/src/components/Dialogs/CleanDialog.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
mutateMetadataClean,
|
||||||
|
useConfiguration,
|
||||||
|
// useConfigureDefaults,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Icon, Modal } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { Manual } from "src/components/Help/Manual";
|
||||||
|
|
||||||
|
interface ICleanOptions {
|
||||||
|
options: GQL.CleanMetadataInput;
|
||||||
|
setOptions: (s: GQL.CleanMetadataInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CleanOptions: React.FC<ICleanOptions> = ({
|
||||||
|
options,
|
||||||
|
setOptions: setOptionsState,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
function setOptions(input: Partial<GQL.CleanMetadataInput>) {
|
||||||
|
setOptionsState({ ...options, ...input });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="clean-dryrun"
|
||||||
|
checked={options.dryRun}
|
||||||
|
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
||||||
|
onChange={() => setOptions({ dryRun: !options.dryRun })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ICleanDialog {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CleanDialog: React.FC<ICleanDialog> = ({ onClose }) => {
|
||||||
|
const [options, setOptions] = useState<GQL.CleanMetadataInput>({
|
||||||
|
dryRun: false,
|
||||||
|
});
|
||||||
|
// TODO - selective clean
|
||||||
|
// const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
// const [settingPaths, setSettingPaths] = useState(false);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
const [animation, setAnimation] = useState(true);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const { data: configData, error: configError } = useConfiguration();
|
||||||
|
|
||||||
|
const message = useMemo(() => {
|
||||||
|
if (options.dryRun) {
|
||||||
|
return (
|
||||||
|
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [options.dryRun, intl]);
|
||||||
|
|
||||||
|
// const selectionStatus = useMemo(() => {
|
||||||
|
// const message = paths.length ? (
|
||||||
|
// <div>
|
||||||
|
// <FormattedMessage id="config.tasks.auto_tag.auto_tagging_paths" />:
|
||||||
|
// <ul>
|
||||||
|
// {paths.map((p) => (
|
||||||
|
// <li key={p}>{p}</li>
|
||||||
|
// ))}
|
||||||
|
// </ul>
|
||||||
|
// </div>
|
||||||
|
// ) : (
|
||||||
|
// <span>
|
||||||
|
// <FormattedMessage id="config.tasks.auto_tag.auto_tagging_all_paths" />.
|
||||||
|
// </span>
|
||||||
|
// );
|
||||||
|
|
||||||
|
// function onClick() {
|
||||||
|
// setAnimation(false);
|
||||||
|
// setSettingPaths(true);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Form.Group className="dialog-selected-folders">
|
||||||
|
// <div>
|
||||||
|
// {message}
|
||||||
|
// <div>
|
||||||
|
// <Button
|
||||||
|
// title={intl.formatMessage({ id: "actions.select_folders" })}
|
||||||
|
// onClick={() => onClick()}
|
||||||
|
// >
|
||||||
|
// <Icon icon="folder-open" />
|
||||||
|
// </Button>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
// </Form.Group>
|
||||||
|
// );
|
||||||
|
// }, [intl, paths]);
|
||||||
|
|
||||||
|
if (configError) return <div>{configError}</div>;
|
||||||
|
if (!configData) return <div />;
|
||||||
|
|
||||||
|
async function onClean() {
|
||||||
|
try {
|
||||||
|
await mutateMetadataClean(options);
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.clean" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowManual() {
|
||||||
|
setAnimation(false);
|
||||||
|
setShowManual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (settingPaths) {
|
||||||
|
// return (
|
||||||
|
// <DirectorySelectionDialog
|
||||||
|
// animation={false}
|
||||||
|
// allowEmpty
|
||||||
|
// initialPaths={paths}
|
||||||
|
// onClose={(p) => {
|
||||||
|
// if (p) {
|
||||||
|
// setPaths(p);
|
||||||
|
// }
|
||||||
|
// setSettingPaths(false);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (showManual) {
|
||||||
|
return (
|
||||||
|
<Manual
|
||||||
|
animation={false}
|
||||||
|
show
|
||||||
|
onClose={() => setShowManual(false)}
|
||||||
|
defaultActiveTab="Tasks.md"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
modalProps={{ animation }}
|
||||||
|
show
|
||||||
|
icon="cogs"
|
||||||
|
header={intl.formatMessage({ id: "actions.clean" })}
|
||||||
|
accept={{
|
||||||
|
onClick: onClean,
|
||||||
|
variant: "danger",
|
||||||
|
text: intl.formatMessage({ id: "actions.clean" }),
|
||||||
|
}}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => onClose(),
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
leftFooterButtons={
|
||||||
|
<Button
|
||||||
|
title="Help"
|
||||||
|
className="minimal help-button"
|
||||||
|
onClick={() => onShowManual()}
|
||||||
|
>
|
||||||
|
<Icon icon="question-circle" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<CleanOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||||
|
{message}
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CleanDialog;
|
||||||
557
ui/v2.5/src/components/Dialogs/GenerateDialog.tsx
Normal file
557
ui/v2.5/src/components/Dialogs/GenerateDialog.tsx
Normal file
@@ -0,0 +1,557 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import { Form, Button, Collapse } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
mutateMetadataGenerate,
|
||||||
|
useConfigureDefaults,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Modal, Icon, OperationButton } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
// import { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||||
|
import { Manual } from "../Help/Manual";
|
||||||
|
import { withoutTypename } from "src/utils";
|
||||||
|
|
||||||
|
interface IGenerateOptions {
|
||||||
|
options: GQL.GenerateMetadataInput;
|
||||||
|
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GenerateOptions: React.FC<IGenerateOptions> = ({
|
||||||
|
options,
|
||||||
|
setOptions: setOptionsState,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
||||||
|
|
||||||
|
const previewOptions: GQL.GeneratePreviewOptionsInput =
|
||||||
|
options.previewOptions ?? {};
|
||||||
|
|
||||||
|
function setOptions(input: Partial<GQL.GenerateMetadataInput>) {
|
||||||
|
setOptionsState({ ...options, ...input });
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPreviewOptions(input: Partial<GQL.GeneratePreviewOptionsInput>) {
|
||||||
|
setOptions({
|
||||||
|
previewOptions: {
|
||||||
|
...previewOptions,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="preview-task"
|
||||||
|
checked={options.previews ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.video_previews",
|
||||||
|
})}
|
||||||
|
onChange={() => setOptions({ previews: !options.previews })}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row">
|
||||||
|
<div>↳</div>
|
||||||
|
<Form.Check
|
||||||
|
id="image-preview-task"
|
||||||
|
checked={options.imagePreviews ?? false}
|
||||||
|
disabled={!options.previews}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.image_previews",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ imagePreviews: !options.imagePreviews })
|
||||||
|
}
|
||||||
|
className="ml-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<Button
|
||||||
|
onClick={() => setPreviewOptionsOpen(!previewOptionsOpen)}
|
||||||
|
className="minimal pl-0 no-focus"
|
||||||
|
>
|
||||||
|
<Icon icon={previewOptionsOpen ? "chevron-down" : "chevron-right"} />
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_options",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<Form.Group>
|
||||||
|
<Collapse in={previewOptionsOpen}>
|
||||||
|
<Form.Group className="mt-2">
|
||||||
|
<Form.Group id="preview-preset">
|
||||||
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_preset_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="w-auto input-control"
|
||||||
|
as="select"
|
||||||
|
value={previewOptions.previewPreset ?? GQL.PreviewPreset.Slow}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPreviewOptions({
|
||||||
|
previewPreset: e.currentTarget.value as GQL.PreviewPreset,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Object.keys(GQL.PreviewPreset).map((p) => (
|
||||||
|
<option value={p.toLowerCase()} key={p}>
|
||||||
|
{p}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_preset_desc",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-segments">
|
||||||
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={previewOptions.previewSegments?.toString() ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPreviewOptions({
|
||||||
|
previewSegments: Number.parseInt(
|
||||||
|
e.currentTarget.value,
|
||||||
|
10
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-segment-duration">
|
||||||
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
type="number"
|
||||||
|
value={
|
||||||
|
previewOptions.previewSegmentDuration?.toString() ?? ""
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPreviewOptions({
|
||||||
|
previewSegmentDuration: Number.parseFloat(
|
||||||
|
e.currentTarget.value
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_seg_duration_desc",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_start_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
value={previewOptions.previewExcludeStart ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPreviewOptions({
|
||||||
|
previewExcludeStart: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group id="preview-exclude-start">
|
||||||
|
<h6>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_end_time_head",
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
<Form.Control
|
||||||
|
className="col col-sm-6 text-input"
|
||||||
|
value={previewOptions.previewExcludeEnd ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setPreviewOptions({
|
||||||
|
previewExcludeEnd: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||||
|
})}
|
||||||
|
</Form.Text>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
</Collapse>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="sprite-task"
|
||||||
|
checked={options.sprites ?? false}
|
||||||
|
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||||
|
onChange={() => setOptions({ sprites: !options.sprites })}
|
||||||
|
/>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="marker-task"
|
||||||
|
checked={options.markers ?? false}
|
||||||
|
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||||
|
onChange={() => setOptions({ markers: !options.markers })}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row">
|
||||||
|
<div>↳</div>
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="marker-image-preview-task"
|
||||||
|
checked={options.markerImagePreviews ?? false}
|
||||||
|
disabled={!options.markers}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.marker_image_previews",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({
|
||||||
|
markerImagePreviews: !options.markerImagePreviews,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="ml-2 flex-grow"
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="marker-screenshot-task"
|
||||||
|
checked={options.markerScreenshots ?? false}
|
||||||
|
disabled={!options.markers}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "dialogs.scene_gen.marker_screenshots",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ markerScreenshots: !options.markerScreenshots })
|
||||||
|
}
|
||||||
|
className="ml-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="transcode-task"
|
||||||
|
checked={options.transcodes ?? false}
|
||||||
|
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||||
|
onChange={() => setOptions({ transcodes: !options.transcodes })}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="phash-task"
|
||||||
|
checked={options.phashes ?? false}
|
||||||
|
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||||
|
onChange={() => setOptions({ phashes: !options.phashes })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="overwrite"
|
||||||
|
checked={options.overwrite ?? false}
|
||||||
|
label={intl.formatMessage({ id: "dialogs.scene_gen.overwrite" })}
|
||||||
|
onChange={() => setOptions({ overwrite: !options.overwrite })}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ISceneGenerateDialog {
|
||||||
|
selectedIds?: string[];
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
||||||
|
selectedIds,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const [configureDefaults] = useConfigureDefaults();
|
||||||
|
|
||||||
|
function getDefaultOptions(): GQL.GenerateMetadataInput {
|
||||||
|
return {
|
||||||
|
sprites: true,
|
||||||
|
phashes: true,
|
||||||
|
previews: true,
|
||||||
|
markers: true,
|
||||||
|
previewOptions: {
|
||||||
|
previewSegments: 0,
|
||||||
|
previewSegmentDuration: 0,
|
||||||
|
previewPreset: GQL.PreviewPreset.Slow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [options, setOptions] = useState<GQL.GenerateMetadataInput>(
|
||||||
|
getDefaultOptions()
|
||||||
|
);
|
||||||
|
const [configRead, setConfigRead] = useState(false);
|
||||||
|
const [paths /* , setPaths */] = useState<string[]>([]);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
// const [settingPaths, setSettingPaths] = useState(false);
|
||||||
|
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||||
|
const [animation, setAnimation] = useState(true);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (configRead) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configuration?.defaults.generate) {
|
||||||
|
const { generate } = configuration.defaults;
|
||||||
|
setOptions(withoutTypename(generate));
|
||||||
|
setConfigRead(true);
|
||||||
|
} else if (configuration?.general) {
|
||||||
|
// backwards compatibility
|
||||||
|
const { general } = configuration;
|
||||||
|
setOptions((existing) => ({
|
||||||
|
...existing,
|
||||||
|
previewOptions: {
|
||||||
|
...existing.previewOptions,
|
||||||
|
previewSegments:
|
||||||
|
general.previewSegments ?? existing.previewOptions?.previewSegments,
|
||||||
|
previewSegmentDuration:
|
||||||
|
general.previewSegmentDuration ??
|
||||||
|
existing.previewOptions?.previewSegmentDuration,
|
||||||
|
previewExcludeStart:
|
||||||
|
general.previewExcludeStart ??
|
||||||
|
existing.previewOptions?.previewExcludeStart,
|
||||||
|
previewExcludeEnd:
|
||||||
|
general.previewExcludeEnd ??
|
||||||
|
existing.previewOptions?.previewExcludeEnd,
|
||||||
|
previewPreset:
|
||||||
|
general.previewPreset ?? existing.previewOptions?.previewPreset,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setConfigRead(true);
|
||||||
|
}
|
||||||
|
}, [configuration, configRead]);
|
||||||
|
|
||||||
|
const selectionStatus = useMemo(() => {
|
||||||
|
if (selectedIds) {
|
||||||
|
return (
|
||||||
|
<Form.Group id="selected-generate-ids">
|
||||||
|
<FormattedMessage
|
||||||
|
id="config.tasks.generate.generating_scenes"
|
||||||
|
values={{
|
||||||
|
num: selectedIds.length,
|
||||||
|
scene: intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "countables.scenes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: selectedIds.length,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const message = paths.length ? (
|
||||||
|
<div>
|
||||||
|
<FormattedMessage id="config.tasks.generate.generating_from_paths" />:
|
||||||
|
<ul>
|
||||||
|
{paths.map((p) => (
|
||||||
|
<li key={p}>{p}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage
|
||||||
|
id="config.tasks.generate.generating_scenes"
|
||||||
|
values={{
|
||||||
|
num: intl.formatMessage({ id: "all" }),
|
||||||
|
scene: intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: "countables.scenes",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: 0,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
// function onClick() {
|
||||||
|
// setAnimation(false);
|
||||||
|
// setSettingPaths(true);
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group className="dialog-selected-folders">
|
||||||
|
<div>
|
||||||
|
{message}
|
||||||
|
{/* <div>
|
||||||
|
<Button
|
||||||
|
title={intl.formatMessage({ id: "actions.select_folders" })}
|
||||||
|
onClick={() => onClick()}
|
||||||
|
>
|
||||||
|
<Icon icon="folder-open" />
|
||||||
|
</Button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}, [selectedIds, intl, paths]);
|
||||||
|
|
||||||
|
async function onGenerate() {
|
||||||
|
try {
|
||||||
|
await mutateMetadataGenerate(options);
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.generate" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeDefaultGenerateInput() {
|
||||||
|
const ret = options;
|
||||||
|
// const { paths: _paths, ...withoutSpecifics } = ret;
|
||||||
|
const { overwrite: _overwrite, ...withoutSpecifics } = ret;
|
||||||
|
return withoutSpecifics;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowManual() {
|
||||||
|
setAnimation(false);
|
||||||
|
setShowManual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAsDefault() {
|
||||||
|
try {
|
||||||
|
setSavingDefaults(true);
|
||||||
|
await configureDefaults({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
generate: makeDefaultGenerateInput(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.defaults_set" },
|
||||||
|
{ action: intl.formatMessage({ id: "actions.generate" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
setSavingDefaults(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (settingPaths) {
|
||||||
|
// return (
|
||||||
|
// <DirectorySelectionDialog
|
||||||
|
// animation={false}
|
||||||
|
// allowEmpty
|
||||||
|
// initialPaths={paths}
|
||||||
|
// onClose={(p) => {
|
||||||
|
// if (p) {
|
||||||
|
// setPaths(p);
|
||||||
|
// }
|
||||||
|
// setSettingPaths(false);
|
||||||
|
// }}
|
||||||
|
// />
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (showManual) {
|
||||||
|
return (
|
||||||
|
<Manual
|
||||||
|
animation={false}
|
||||||
|
show
|
||||||
|
onClose={() => setShowManual(false)}
|
||||||
|
defaultActiveTab="Tasks.md"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show
|
||||||
|
modalProps={{ animation, size: "lg" }}
|
||||||
|
icon="cogs"
|
||||||
|
header={intl.formatMessage({ id: "actions.generate" })}
|
||||||
|
accept={{
|
||||||
|
onClick: onGenerate,
|
||||||
|
text: intl.formatMessage({ id: "actions.generate" }),
|
||||||
|
}}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => onClose(),
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
disabled={savingDefaults}
|
||||||
|
footerButtons={
|
||||||
|
<OperationButton variant="secondary" operation={setAsDefault}>
|
||||||
|
<FormattedMessage id="actions.set_as_default" />
|
||||||
|
</OperationButton>
|
||||||
|
}
|
||||||
|
leftFooterButtons={
|
||||||
|
<Button
|
||||||
|
title="Help"
|
||||||
|
className="minimal help-button"
|
||||||
|
onClick={() => onShowManual()}
|
||||||
|
>
|
||||||
|
<Icon icon="question-circle" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
{selectionStatus}
|
||||||
|
<GenerateOptions options={options} setOptions={setOptions} />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import React, { useState, useEffect, useMemo } from "react";
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
import { Button, Form, Spinner } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
mutateMetadataIdentify,
|
mutateMetadataIdentify,
|
||||||
useConfiguration,
|
useConfiguration,
|
||||||
useConfigureDefaults,
|
useConfigureDefaults,
|
||||||
useListSceneScrapers,
|
useListSceneScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { Icon, Modal, OperationButton } from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
@@ -159,7 +159,7 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Group id="selected-identify-folders">
|
<Form.Group className="dialog-selected-folders">
|
||||||
<div>
|
<div>
|
||||||
{message}
|
{message}
|
||||||
<div>
|
<div>
|
||||||
@@ -346,6 +346,13 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.defaults_set" },
|
||||||
|
{ action: intl.formatMessage({ id: "actions.identify" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -409,16 +416,13 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
|||||||
}}
|
}}
|
||||||
disabled={editingField || savingDefaults || sources.length === 0}
|
disabled={editingField || savingDefaults || sources.length === 0}
|
||||||
footerButtons={
|
footerButtons={
|
||||||
<Button
|
<OperationButton
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
disabled={editingField || savingDefaults}
|
disabled={editingField || savingDefaults}
|
||||||
onClick={() => setAsDefault()}
|
operation={setAsDefault}
|
||||||
>
|
>
|
||||||
{savingDefaults && (
|
|
||||||
<Spinner animation="border" role="status" size="sm" />
|
|
||||||
)}
|
|
||||||
<FormattedMessage id="actions.set_as_default" />
|
<FormattedMessage id="actions.set_as_default" />
|
||||||
</Button>
|
</OperationButton>
|
||||||
}
|
}
|
||||||
leftFooterButtons={
|
leftFooterButtons={
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -36,10 +36,3 @@
|
|||||||
.field-options-table td:first-child {
|
.field-options-table td:first-child {
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#selected-identify-folders {
|
|
||||||
& > div {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
108
ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx
Normal file
108
ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
|
||||||
|
interface IScanOptions {
|
||||||
|
options: GQL.ScanMetadataInput;
|
||||||
|
setOptions: (s: GQL.ScanMetadataInput) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScanOptions: React.FC<IScanOptions> = ({
|
||||||
|
options,
|
||||||
|
setOptions: setOptionsState,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const {
|
||||||
|
useFileMetadata,
|
||||||
|
stripFileExtension,
|
||||||
|
scanGeneratePreviews,
|
||||||
|
scanGenerateImagePreviews,
|
||||||
|
scanGenerateSprites,
|
||||||
|
scanGeneratePhashes,
|
||||||
|
scanGenerateThumbnails,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
function setOptions(input: Partial<GQL.ScanMetadataInput>) {
|
||||||
|
setOptionsState({ ...options, ...input });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group>
|
||||||
|
<Form.Check
|
||||||
|
id="use-file-metadata"
|
||||||
|
checked={useFileMetadata ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.set_name_date_details_from_metadata_if_present",
|
||||||
|
})}
|
||||||
|
onChange={() => setOptions({ useFileMetadata: !useFileMetadata })}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="strip-file-extension"
|
||||||
|
checked={stripFileExtension ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.dont_include_file_extension_as_part_of_the_title",
|
||||||
|
})}
|
||||||
|
onChange={() => setOptions({ stripFileExtension: !stripFileExtension })}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="scan-generate-previews"
|
||||||
|
checked={scanGeneratePreviews ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_video_previews_during_scan",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ scanGeneratePreviews: !scanGeneratePreviews })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-row">
|
||||||
|
<div>↳</div>
|
||||||
|
<Form.Check
|
||||||
|
id="scan-generate-image-previews"
|
||||||
|
checked={scanGenerateImagePreviews ?? false}
|
||||||
|
disabled={!scanGeneratePreviews}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_previews_during_scan",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({
|
||||||
|
scanGenerateImagePreviews: !scanGenerateImagePreviews,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="ml-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Form.Check
|
||||||
|
id="scan-generate-sprites"
|
||||||
|
checked={scanGenerateSprites ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_sprites_during_scan",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ scanGenerateSprites: !scanGenerateSprites })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="scan-generate-phashes"
|
||||||
|
checked={scanGeneratePhashes ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_phashes_during_scan",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ scanGeneratePhashes: !scanGeneratePhashes })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Form.Check
|
||||||
|
id="scan-generate-thumbnails"
|
||||||
|
checked={scanGenerateThumbnails ?? false}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_thumbnails_during_scan",
|
||||||
|
})}
|
||||||
|
onChange={() =>
|
||||||
|
setOptions({ scanGenerateThumbnails: !scanGenerateThumbnails })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
209
ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx
Normal file
209
ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
import React, { useState, useMemo, useEffect } from "react";
|
||||||
|
import { Button, Form } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
mutateMetadataScan,
|
||||||
|
useConfigureDefaults,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { Icon, Modal, OperationButton } from "src/components/Shared";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||||
|
import { Manual } from "src/components/Help/Manual";
|
||||||
|
import { ScanOptions } from "./Options";
|
||||||
|
import { withoutTypename } from "src/utils";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
|
||||||
|
interface IScanDialogProps {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScanDialog: React.FC<IScanDialogProps> = ({ onClose }) => {
|
||||||
|
const [configureDefaults] = useConfigureDefaults();
|
||||||
|
|
||||||
|
const [options, setOptions] = useState<GQL.ScanMetadataInput>({});
|
||||||
|
const [paths, setPaths] = useState<string[]>([]);
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
const [settingPaths, setSettingPaths] = useState(false);
|
||||||
|
const [animation, setAnimation] = useState(true);
|
||||||
|
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!configuration?.defaults) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scan } = configuration.defaults;
|
||||||
|
|
||||||
|
if (scan) {
|
||||||
|
setOptions(withoutTypename(scan));
|
||||||
|
}
|
||||||
|
}, [configuration]);
|
||||||
|
|
||||||
|
const selectionStatus = useMemo(() => {
|
||||||
|
const message = paths.length ? (
|
||||||
|
<div>
|
||||||
|
<FormattedMessage id="config.tasks.scan.scanning_paths" />:
|
||||||
|
<ul>
|
||||||
|
{paths.map((p) => (
|
||||||
|
<li key={p}>{p}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
<FormattedMessage id="config.tasks.scan.scanning_all_paths" />.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
function onClick() {
|
||||||
|
setAnimation(false);
|
||||||
|
setSettingPaths(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Group className="dialog-selected-folders">
|
||||||
|
<div>
|
||||||
|
{message}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
title={intl.formatMessage({ id: "actions.select_folders" })}
|
||||||
|
onClick={() => onClick()}
|
||||||
|
>
|
||||||
|
<Icon icon="folder-open" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}, [intl, paths]);
|
||||||
|
|
||||||
|
if (!configuration) return <div />;
|
||||||
|
|
||||||
|
function makeDefaultScanInput() {
|
||||||
|
const ret = options;
|
||||||
|
const { paths: _paths, ...withoutSpecifics } = ret;
|
||||||
|
return withoutSpecifics;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onScan() {
|
||||||
|
try {
|
||||||
|
await mutateMetadataScan({
|
||||||
|
...options,
|
||||||
|
paths: paths.length ? paths : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.added_job_to_queue" },
|
||||||
|
{ operation_name: intl.formatMessage({ id: "actions.scan" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onShowManual() {
|
||||||
|
setAnimation(false);
|
||||||
|
setShowManual(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setAsDefault() {
|
||||||
|
try {
|
||||||
|
setSavingDefaults(true);
|
||||||
|
await configureDefaults({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
scan: makeDefaultScanInput(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success({
|
||||||
|
content: intl.formatMessage(
|
||||||
|
{ id: "config.tasks.defaults_set" },
|
||||||
|
{ action: intl.formatMessage({ id: "actions.scan" }) }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
setSavingDefaults(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingPaths) {
|
||||||
|
return (
|
||||||
|
<DirectorySelectionDialog
|
||||||
|
animation={false}
|
||||||
|
allowEmpty
|
||||||
|
initialPaths={paths}
|
||||||
|
onClose={(p) => {
|
||||||
|
if (p) {
|
||||||
|
setPaths(p);
|
||||||
|
}
|
||||||
|
setSettingPaths(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showManual) {
|
||||||
|
return (
|
||||||
|
<Manual
|
||||||
|
animation={false}
|
||||||
|
show
|
||||||
|
onClose={() => setShowManual(false)}
|
||||||
|
defaultActiveTab="Tasks.md"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
modalProps={{ animation, size: "lg" }}
|
||||||
|
show
|
||||||
|
icon="cogs"
|
||||||
|
header={intl.formatMessage({ id: "actions.scan" })}
|
||||||
|
accept={{
|
||||||
|
onClick: onScan,
|
||||||
|
text: intl.formatMessage({ id: "actions.scan" }),
|
||||||
|
}}
|
||||||
|
cancel={{
|
||||||
|
onClick: () => onClose(),
|
||||||
|
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||||
|
variant: "secondary",
|
||||||
|
}}
|
||||||
|
disabled={savingDefaults}
|
||||||
|
footerButtons={
|
||||||
|
<OperationButton variant="secondary" operation={setAsDefault}>
|
||||||
|
<FormattedMessage id="actions.set_as_default" />
|
||||||
|
</OperationButton>
|
||||||
|
}
|
||||||
|
leftFooterButtons={
|
||||||
|
<Button
|
||||||
|
title="Help"
|
||||||
|
className="minimal help-button"
|
||||||
|
onClick={() => onShowManual()}
|
||||||
|
>
|
||||||
|
<Icon icon="question-circle" />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
{selectionStatus}
|
||||||
|
<ScanOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ScanDialog;
|
||||||
8
ui/v2.5/src/components/Dialogs/styles.scss
Normal file
8
ui/v2.5/src/components/Dialogs/styles.scss
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@import "IdentifyDialog/styles.scss";
|
||||||
|
|
||||||
|
.dialog-selected-folders {
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
&-header,
|
&-header,
|
||||||
&-body {
|
&-body {
|
||||||
background-color: #30404d;
|
background-color: $card-bg;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { ExternalPlayerButton } from "./ExternalPlayerButton";
|
|||||||
import { SceneMoviePanel } from "./SceneMoviePanel";
|
import { SceneMoviePanel } from "./SceneMoviePanel";
|
||||||
import { SceneGalleriesPanel } from "./SceneGalleriesPanel";
|
import { SceneGalleriesPanel } from "./SceneGalleriesPanel";
|
||||||
import { DeleteScenesDialog } from "../DeleteScenesDialog";
|
import { DeleteScenesDialog } from "../DeleteScenesDialog";
|
||||||
import { SceneGenerateDialog } from "../SceneGenerateDialog";
|
import { GenerateDialog } from "../../Dialogs/GenerateDialog";
|
||||||
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
|
import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel";
|
||||||
import { OrganizedButton } from "./OrganizedButton";
|
import { OrganizedButton } from "./OrganizedButton";
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
|
|||||||
function maybeRenderSceneGenerateDialog() {
|
function maybeRenderSceneGenerateDialog() {
|
||||||
if (isGenerateDialogOpen) {
|
if (isGenerateDialogOpen) {
|
||||||
return (
|
return (
|
||||||
<SceneGenerateDialog
|
<GenerateDialog
|
||||||
selectedIds={[scene.id]}
|
selectedIds={[scene.id]}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsGenerateDialogOpen(false);
|
setIsGenerateDialogOpen(false);
|
||||||
|
|||||||
@@ -1,322 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { Form, Button, Collapse } from "react-bootstrap";
|
|
||||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
|
||||||
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";
|
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
|
||||||
|
|
||||||
interface ISceneGenerateDialogProps {
|
|
||||||
selectedIds: string[];
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
|
||||||
props: ISceneGenerateDialogProps
|
|
||||||
) => {
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
|
||||||
|
|
||||||
const [sprites, setSprites] = useState(true);
|
|
||||||
const [phashes, setPhashes] = useState(true);
|
|
||||||
const [previews, setPreviews] = useState(true);
|
|
||||||
const [markers, setMarkers] = useState(true);
|
|
||||||
const [transcodes, setTranscodes] = useState(false);
|
|
||||||
const [overwrite, setOverwrite] = useState(true);
|
|
||||||
const [imagePreviews, setImagePreviews] = useState(false);
|
|
||||||
|
|
||||||
const [previewSegments, setPreviewSegments] = useState<number>(0);
|
|
||||||
const [previewSegmentDuration, setPreviewSegmentDuration] = useState<number>(
|
|
||||||
0
|
|
||||||
);
|
|
||||||
const [previewExcludeStart, setPreviewExcludeStart] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [previewExcludeEnd, setPreviewExcludeEnd] = useState<
|
|
||||||
string | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [previewPreset, setPreviewPreset] = useState<string>(
|
|
||||||
GQL.PreviewPreset.Slow
|
|
||||||
);
|
|
||||||
const [markerImagePreviews, setMarkerImagePreviews] = useState(false);
|
|
||||||
const [markerScreenshots, setMarkerScreenshots] = useState(false);
|
|
||||||
|
|
||||||
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
|
||||||
|
|
||||||
const intl = useIntl();
|
|
||||||
const Toast = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!configuration) return;
|
|
||||||
|
|
||||||
if (configuration.general) {
|
|
||||||
setPreviewSegments(configuration.general.previewSegments);
|
|
||||||
setPreviewSegmentDuration(configuration.general.previewSegmentDuration);
|
|
||||||
setPreviewExcludeStart(configuration.general.previewExcludeStart);
|
|
||||||
setPreviewExcludeEnd(configuration.general.previewExcludeEnd);
|
|
||||||
setPreviewPreset(configuration.general.previewPreset);
|
|
||||||
}
|
|
||||||
}, [configuration]);
|
|
||||||
|
|
||||||
async function onGenerate() {
|
|
||||||
try {
|
|
||||||
await mutateMetadataGenerate({
|
|
||||||
sprites,
|
|
||||||
phashes,
|
|
||||||
previews,
|
|
||||||
imagePreviews: previews && imagePreviews,
|
|
||||||
markers,
|
|
||||||
markerImagePreviews: markers && markerImagePreviews,
|
|
||||||
markerScreenshots: markers && markerScreenshots,
|
|
||||||
transcodes,
|
|
||||||
overwrite,
|
|
||||||
sceneIDs: props.selectedIds,
|
|
||||||
previewOptions: {
|
|
||||||
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
|
||||||
previewSegments,
|
|
||||||
previewSegmentDuration,
|
|
||||||
previewExcludeStart,
|
|
||||||
previewExcludeEnd,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Toast.success({
|
|
||||||
content: intl.formatMessage({ id: "toast.started_generating" }),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
} finally {
|
|
||||||
props.onClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
show
|
|
||||||
icon="cogs"
|
|
||||||
header={intl.formatMessage({ id: "actions.generate" })}
|
|
||||||
accept={{
|
|
||||||
onClick: onGenerate,
|
|
||||||
text: intl.formatMessage({ id: "actions.generate" }),
|
|
||||||
}}
|
|
||||||
cancel={{
|
|
||||||
onClick: () => props.onClose(),
|
|
||||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
|
||||||
variant: "secondary",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="preview-task"
|
|
||||||
checked={previews}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.video_previews",
|
|
||||||
})}
|
|
||||||
onChange={() => setPreviews(!previews)}
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-row">
|
|
||||||
<div>↳</div>
|
|
||||||
<Form.Check
|
|
||||||
id="image-preview-task"
|
|
||||||
checked={imagePreviews}
|
|
||||||
disabled={!previews}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.image_previews",
|
|
||||||
})}
|
|
||||||
onChange={() => setImagePreviews(!imagePreviews)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="my-2">
|
|
||||||
<Button
|
|
||||||
onClick={() => setPreviewOptionsOpen(!previewOptionsOpen)}
|
|
||||||
className="minimal pl-0 no-focus"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={previewOptionsOpen ? "chevron-down" : "chevron-right"}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_options",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
<Collapse in={previewOptionsOpen}>
|
|
||||||
<div>
|
|
||||||
<Form.Group id="transcode-size">
|
|
||||||
<h6>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_preset_head",
|
|
||||||
})}
|
|
||||||
</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="w-auto input-control"
|
|
||||||
as="select"
|
|
||||||
value={previewPreset}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
|
||||||
setPreviewPreset(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Object.keys(GQL.PreviewPreset).map((p) => (
|
|
||||||
<option value={p.toLowerCase()} key={p}>
|
|
||||||
{p}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_preset_desc",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group id="preview-segments">
|
|
||||||
<h6>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_seg_count_head",
|
|
||||||
})}
|
|
||||||
</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="col col-sm-6 text-input"
|
|
||||||
type="number"
|
|
||||||
value={previewSegments.toString()}
|
|
||||||
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
setPreviewSegments(
|
|
||||||
Number.parseInt(e.currentTarget.value, 10)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_seg_count_desc",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group id="preview-segment-duration">
|
|
||||||
<h6>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_seg_duration_head",
|
|
||||||
})}
|
|
||||||
</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="col col-sm-6 text-input"
|
|
||||||
type="number"
|
|
||||||
value={previewSegmentDuration.toString()}
|
|
||||||
onInput={(e: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
setPreviewSegmentDuration(
|
|
||||||
Number.parseFloat(e.currentTarget.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_seg_duration_desc",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
|
||||||
<h6>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_exclude_start_time_head",
|
|
||||||
})}
|
|
||||||
</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="col col-sm-6 text-input"
|
|
||||||
defaultValue={previewExcludeStart}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setPreviewExcludeStart(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group id="preview-exclude-start">
|
|
||||||
<h6>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_exclude_end_time_head",
|
|
||||||
})}
|
|
||||||
</h6>
|
|
||||||
<Form.Control
|
|
||||||
className="col col-sm-6 text-input"
|
|
||||||
defaultValue={previewExcludeEnd}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setPreviewExcludeEnd(e.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
<Form.Check
|
|
||||||
id="sprite-task"
|
|
||||||
checked={sprites}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
|
||||||
onChange={() => setSprites(!sprites)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-task"
|
|
||||||
checked={markers}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
|
||||||
onChange={() => setMarkers(!markers)}
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-row">
|
|
||||||
<div>↳</div>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-image-preview-task"
|
|
||||||
checked={markerImagePreviews}
|
|
||||||
disabled={!markers}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.marker_image_previews",
|
|
||||||
})}
|
|
||||||
onChange={() => setMarkerImagePreviews(!markerImagePreviews)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-screenshot-task"
|
|
||||||
checked={markerScreenshots}
|
|
||||||
disabled={!markers}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.marker_screenshots",
|
|
||||||
})}
|
|
||||||
onChange={() => setMarkerScreenshots(!markerScreenshots)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
<Form.Check
|
|
||||||
id="transcode-task"
|
|
||||||
checked={transcodes}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
|
||||||
onChange={() => setTranscodes(!transcodes)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="phash-task"
|
|
||||||
checked={phashes}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
|
||||||
onChange={() => setPhashes(!phashes)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<Form.Check
|
|
||||||
id="overwrite"
|
|
||||||
checked={overwrite}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.overwrite" })}
|
|
||||||
onChange={() => setOverwrite(!overwrite)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</Form>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -19,7 +19,7 @@ import { WallPanel } from "../Wall/WallPanel";
|
|||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
import { EditScenesDialog } from "./EditScenesDialog";
|
import { EditScenesDialog } from "./EditScenesDialog";
|
||||||
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
||||||
import { SceneGenerateDialog } from "./SceneGenerateDialog";
|
import { GenerateDialog } from "../Dialogs/GenerateDialog";
|
||||||
import { ExportDialog } from "../Shared/ExportDialog";
|
import { ExportDialog } from "../Shared/ExportDialog";
|
||||||
import { SceneCardsGrid } from "./SceneCardsGrid";
|
import { SceneCardsGrid } from "./SceneCardsGrid";
|
||||||
import { TaggerContext } from "../Tagger/context";
|
import { TaggerContext } from "../Tagger/context";
|
||||||
@@ -180,7 +180,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
|||||||
if (isGenerateDialogOpen) {
|
if (isGenerateDialogOpen) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SceneGenerateDialog
|
<GenerateDialog
|
||||||
selectedIds={Array.from(selectedIds.values())}
|
selectedIds={Array.from(selectedIds.values())}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsGenerateDialogOpen(false);
|
setIsGenerateDialogOpen(false);
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
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);
|
|
||||||
const [markers, setMarkers] = useState(true);
|
|
||||||
const [transcodes, setTranscodes] = useState(false);
|
|
||||||
const [imagePreviews, setImagePreviews] = useState(false);
|
|
||||||
const [markerImagePreviews, setMarkerImagePreviews] = useState(false);
|
|
||||||
const [markerScreenshots, setMarkerScreenshots] = useState(false);
|
|
||||||
|
|
||||||
async function onGenerate() {
|
|
||||||
try {
|
|
||||||
await mutateMetadataGenerate({
|
|
||||||
sprites,
|
|
||||||
phashes,
|
|
||||||
previews,
|
|
||||||
imagePreviews: previews && imagePreviews,
|
|
||||||
markers,
|
|
||||||
markerImagePreviews: markers && markerImagePreviews,
|
|
||||||
markerScreenshots: markers && markerScreenshots,
|
|
||||||
transcodes,
|
|
||||||
});
|
|
||||||
Toast.success({
|
|
||||||
content: intl.formatMessage({
|
|
||||||
id: "toast.added_generation_job_to_queue",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="preview-task"
|
|
||||||
checked={previews}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.video_previews" })}
|
|
||||||
onChange={() => setPreviews(!previews)}
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-row">
|
|
||||||
<div>↳</div>
|
|
||||||
<Form.Check
|
|
||||||
id="image-preview-task"
|
|
||||||
checked={imagePreviews}
|
|
||||||
disabled={!previews}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.image_previews",
|
|
||||||
})}
|
|
||||||
onChange={() => setImagePreviews(!imagePreviews)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Form.Check
|
|
||||||
id="sprite-task"
|
|
||||||
checked={sprites}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
|
||||||
onChange={() => setSprites(!sprites)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-task"
|
|
||||||
checked={markers}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
|
||||||
onChange={() => setMarkers(!markers)}
|
|
||||||
/>
|
|
||||||
<div className="d-flex flex-row">
|
|
||||||
<div>↳</div>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-image-preview-task"
|
|
||||||
checked={markerImagePreviews}
|
|
||||||
disabled={!markers}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.marker_image_previews",
|
|
||||||
})}
|
|
||||||
onChange={() => setMarkerImagePreviews(!markerImagePreviews)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="marker-screenshot-task"
|
|
||||||
checked={markerScreenshots}
|
|
||||||
disabled={!markers}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "dialogs.scene_gen.marker_screenshots",
|
|
||||||
})}
|
|
||||||
onChange={() => setMarkerScreenshots(!markerScreenshots)}
|
|
||||||
className="ml-2 flex-grow"
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
<Form.Check
|
|
||||||
id="transcode-task"
|
|
||||||
checked={transcodes}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
|
||||||
onChange={() => setTranscodes(!transcodes)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="phash-task"
|
|
||||||
checked={phashes}
|
|
||||||
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
|
||||||
onChange={() => setPhashes(!phashes)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="generate"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => onGenerate()}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.generate" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.generate_desc" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +1,69 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
|
||||||
import {
|
import {
|
||||||
mutateMetadataImport,
|
mutateMetadataImport,
|
||||||
mutateMetadataClean,
|
|
||||||
mutateMetadataScan,
|
|
||||||
mutateMetadataAutoTag,
|
|
||||||
mutateMetadataExport,
|
mutateMetadataExport,
|
||||||
mutateMigrateHashNaming,
|
mutateMigrateHashNaming,
|
||||||
usePlugins,
|
usePlugins,
|
||||||
mutateRunPluginTask,
|
mutateRunPluginTask,
|
||||||
mutateBackupDatabase,
|
mutateBackupDatabase,
|
||||||
|
mutateMetadataScan,
|
||||||
|
mutateMetadataIdentify,
|
||||||
|
mutateMetadataAutoTag,
|
||||||
|
mutateMetadataGenerate,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { LoadingIndicator, Modal } from "src/components/Shared";
|
import { LoadingIndicator, Modal } from "src/components/Shared";
|
||||||
import { downloadFile } from "src/utils";
|
import { downloadFile, withoutTypename } from "src/utils";
|
||||||
import IdentifyDialog from "src/components/Dialogs/IdentifyDialog/IdentifyDialog";
|
import IdentifyDialog from "src/components/Dialogs/IdentifyDialog/IdentifyDialog";
|
||||||
import { GenerateButton } from "./GenerateButton";
|
|
||||||
import { ImportDialog } from "./ImportDialog";
|
import { ImportDialog } from "./ImportDialog";
|
||||||
import { DirectorySelectionDialog } from "./DirectorySelectionDialog";
|
|
||||||
import { JobTable } from "./JobTable";
|
import { JobTable } from "./JobTable";
|
||||||
|
import ScanDialog from "src/components/Dialogs/ScanDialog/ScanDialog";
|
||||||
|
import AutoTagDialog from "src/components/Dialogs/AutoTagDialog";
|
||||||
|
import { GenerateDialog } from "src/components/Dialogs/GenerateDialog";
|
||||||
|
import CleanDialog from "src/components/Dialogs/CleanDialog";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { PropsWithChildren } from "react-router/node_modules/@types/react";
|
||||||
|
|
||||||
type Plugin = Pick<GQL.Plugin, "id">;
|
type Plugin = Pick<GQL.Plugin, "id">;
|
||||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||||
|
|
||||||
|
interface ITask {
|
||||||
|
description?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Task: React.FC<PropsWithChildren<ITask>> = ({
|
||||||
|
children,
|
||||||
|
description,
|
||||||
|
}) => (
|
||||||
|
<div className="task">
|
||||||
|
{children}
|
||||||
|
{description ? (
|
||||||
|
<Form.Text className="text-muted">{description}</Form.Text>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
export const SettingsTasksPanel: React.FC = () => {
|
export const SettingsTasksPanel: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [dialogOpen, setDialogOpenState] = useState({
|
const [dialogOpen, setDialogOpenState] = useState({
|
||||||
importAlert: false,
|
importAlert: false,
|
||||||
cleanAlert: false,
|
|
||||||
import: false,
|
import: false,
|
||||||
clean: false,
|
clean: false,
|
||||||
scan: false,
|
scan: false,
|
||||||
autoTag: false,
|
autoTag: false,
|
||||||
identify: false,
|
identify: false,
|
||||||
|
generate: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
type DialogOpenState = typeof dialogOpen;
|
type DialogOpenState = typeof dialogOpen;
|
||||||
|
|
||||||
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
||||||
const [useFileMetadata, setUseFileMetadata] = useState<boolean>(false);
|
|
||||||
const [stripFileExtension, setStripFileExtension] = useState<boolean>(false);
|
|
||||||
const [scanGeneratePreviews, setScanGeneratePreviews] = useState<boolean>(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const [scanGenerateSprites, setScanGenerateSprites] = useState<boolean>(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const [scanGeneratePhashes, setScanGeneratePhashes] = useState<boolean>(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const [scanGenerateThumbnails, setScanGenerateThumbnails] = useState<boolean>(
|
|
||||||
false
|
|
||||||
);
|
|
||||||
const [cleanDryRun, setCleanDryRun] = useState<boolean>(false);
|
|
||||||
const [
|
|
||||||
scanGenerateImagePreviews,
|
|
||||||
setScanGenerateImagePreviews,
|
|
||||||
] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [autoTagPerformers, setAutoTagPerformers] = useState<boolean>(true);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
const [autoTagStudios, setAutoTagStudios] = useState<boolean>(true);
|
|
||||||
const [autoTagTags, setAutoTagTags] = useState<boolean>(true);
|
|
||||||
|
|
||||||
const plugins = usePlugins();
|
const plugins = usePlugins();
|
||||||
|
|
||||||
@@ -105,41 +105,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onClean() {
|
function renderCleanDialog() {
|
||||||
setDialogOpen({ cleanAlert: false });
|
if (!dialogOpen.clean) {
|
||||||
mutateMetadataClean({
|
return;
|
||||||
dryRun: cleanDryRun,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCleanAlert() {
|
|
||||||
let msg;
|
|
||||||
if (cleanDryRun) {
|
|
||||||
msg = (
|
|
||||||
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
msg = (
|
|
||||||
<p>
|
|
||||||
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <CleanDialog onClose={() => setDialogOpen({ clean: false })} />;
|
||||||
<Modal
|
|
||||||
show={dialogOpen.cleanAlert}
|
|
||||||
icon="trash-alt"
|
|
||||||
accept={{
|
|
||||||
text: intl.formatMessage({ id: "actions.clean" }),
|
|
||||||
variant: "danger",
|
|
||||||
onClick: onClean,
|
|
||||||
}}
|
|
||||||
cancel={{ onClick: () => setDialogOpen({ cleanAlert: false }) }}
|
|
||||||
>
|
|
||||||
{msg}
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImportDialog() {
|
function renderImportDialog() {
|
||||||
@@ -155,38 +126,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DirectorySelectionDialog onClose={onScanDialogClosed} />;
|
return <ScanDialog onClose={() => setDialogOpen({ scan: false })} />;
|
||||||
}
|
|
||||||
|
|
||||||
function onScanDialogClosed(paths?: string[]) {
|
|
||||||
if (paths) {
|
|
||||||
onScan(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogOpen({ scan: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onScan(paths?: string[]) {
|
|
||||||
try {
|
|
||||||
await mutateMetadataScan({
|
|
||||||
paths,
|
|
||||||
useFileMetadata,
|
|
||||||
stripFileExtension,
|
|
||||||
scanGeneratePreviews,
|
|
||||||
scanGenerateImagePreviews,
|
|
||||||
scanGenerateSprites,
|
|
||||||
scanGeneratePhashes,
|
|
||||||
scanGenerateThumbnails,
|
|
||||||
});
|
|
||||||
Toast.success({
|
|
||||||
content: intl.formatMessage(
|
|
||||||
{ id: "config.tasks.added_job_to_queue" },
|
|
||||||
{ operation_name: intl.formatMessage({ id: "actions.scan" }) }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAutoTagDialog() {
|
function renderAutoTagDialog() {
|
||||||
@@ -194,7 +134,7 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DirectorySelectionDialog onClose={onAutoTagDialogClosed} />;
|
return <AutoTagDialog onClose={() => setDialogOpen({ autoTag: false })} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderIdentifyDialog() {
|
function maybeRenderIdentifyDialog() {
|
||||||
@@ -205,36 +145,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAutoTagDialogClosed(paths?: string[]) {
|
function maybeRenderGenerateDialog() {
|
||||||
if (paths) {
|
if (!dialogOpen.generate) return;
|
||||||
onAutoTag(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
setDialogOpen({ autoTag: false });
|
return (
|
||||||
}
|
<GenerateDialog onClose={() => setDialogOpen({ generate: false })} />
|
||||||
|
);
|
||||||
function getAutoTagInput(paths?: string[]) {
|
|
||||||
const wildcard = ["*"];
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
performers: autoTagPerformers ? wildcard : [],
|
|
||||||
studios: autoTagStudios ? wildcard : [],
|
|
||||||
tags: autoTagTags ? wildcard : [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onAutoTag(paths?: string[]) {
|
|
||||||
try {
|
|
||||||
await mutateMetadataAutoTag(getAutoTagInput(paths));
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
||||||
@@ -254,19 +170,15 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
return pluginTasks.map((o) => {
|
return pluginTasks.map((o) => {
|
||||||
return (
|
return (
|
||||||
<div key={o.name}>
|
<Task description={o.description} key={o.name}>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => onPluginTaskClicked(plugin, o)}
|
onClick={() => onPluginTaskClicked(plugin, o)}
|
||||||
className="mt-3"
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
{o.name}
|
{o.name}
|
||||||
</Button>
|
</Button>
|
||||||
{o.description ? (
|
</Task>
|
||||||
<Form.Text className="text-muted">{o.description}</Form.Text>
|
|
||||||
) : undefined}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -302,16 +214,20 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
|
||||||
{taskPlugins.map((o) => {
|
<Form.Group>
|
||||||
return (
|
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
||||||
<div key={`${o.id}`} className="mb-3">
|
{taskPlugins.map((o) => {
|
||||||
<h6>{o.name}</h6>
|
return (
|
||||||
{renderPluginTasks(o, o.tasks ?? [])}
|
<Form.Group key={`${o.id}`}>
|
||||||
<hr />
|
<h6>{o.name}</h6>
|
||||||
</div>
|
<Card className="task-group">
|
||||||
);
|
{renderPluginTasks(o, o.tasks ?? [])}
|
||||||
})}
|
</Card>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -348,6 +264,66 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onScanClicked() {
|
||||||
|
// check if defaults are set for scan
|
||||||
|
// if not, then open the dialog
|
||||||
|
if (!configuration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scan } = configuration?.defaults;
|
||||||
|
if (!scan) {
|
||||||
|
setDialogOpen({ scan: true });
|
||||||
|
} else {
|
||||||
|
mutateMetadataScan(withoutTypename(scan));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onIdentifyClicked() {
|
||||||
|
// check if defaults are set for identify
|
||||||
|
// if not, then open the dialog
|
||||||
|
if (!configuration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { identify } = configuration?.defaults;
|
||||||
|
if (!identify) {
|
||||||
|
setDialogOpen({ identify: true });
|
||||||
|
} else {
|
||||||
|
mutateMetadataIdentify(withoutTypename(identify));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAutoTagClicked() {
|
||||||
|
// check if defaults are set for auto tag
|
||||||
|
// if not, then open the dialog
|
||||||
|
if (!configuration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { autoTag } = configuration?.defaults;
|
||||||
|
if (!autoTag) {
|
||||||
|
setDialogOpen({ autoTag: true });
|
||||||
|
} else {
|
||||||
|
mutateMetadataAutoTag(withoutTypename(autoTag));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onGenerateClicked() {
|
||||||
|
// check if defaults are set for generate
|
||||||
|
// if not, then open the dialog
|
||||||
|
if (!configuration) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { generate } = configuration?.defaults;
|
||||||
|
if (!generate) {
|
||||||
|
setDialogOpen({ generate: true });
|
||||||
|
} else {
|
||||||
|
mutateMetadataGenerate(withoutTypename(generate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isBackupRunning) {
|
if (isBackupRunning) {
|
||||||
return (
|
return (
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
@@ -359,11 +335,12 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderImportAlert()}
|
{renderImportAlert()}
|
||||||
{renderCleanAlert()}
|
{renderCleanDialog()}
|
||||||
{renderImportDialog()}
|
{renderImportDialog()}
|
||||||
{renderScanDialog()}
|
{renderScanDialog()}
|
||||||
{renderAutoTagDialog()}
|
{renderAutoTagDialog()}
|
||||||
{maybeRenderIdentifyDialog()}
|
{maybeRenderIdentifyDialog()}
|
||||||
|
{maybeRenderGenerateDialog()}
|
||||||
|
|
||||||
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
||||||
|
|
||||||
@@ -373,287 +350,237 @@ export const SettingsTasksPanel: React.FC = () => {
|
|||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
||||||
<Form.Group>
|
<Card className="task-group">
|
||||||
<h6>{intl.formatMessage({ id: "actions.scan" })}</h6>
|
<Task
|
||||||
<Form.Check
|
description={intl.formatMessage({
|
||||||
id="use-file-metadata"
|
id: "config.tasks.scan_for_content_desc",
|
||||||
checked={useFileMetadata}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "config.tasks.set_name_date_details_from_metadata_if_present",
|
|
||||||
})}
|
})}
|
||||||
onChange={() => setUseFileMetadata(!useFileMetadata)}
|
>
|
||||||
/>
|
<ButtonGroup className="ellipsis-button">
|
||||||
<Form.Check
|
<Button
|
||||||
id="strip-file-extension"
|
variant="secondary"
|
||||||
checked={stripFileExtension}
|
type="submit"
|
||||||
label={intl.formatMessage({
|
onClick={() => onScanClicked()}
|
||||||
id:
|
>
|
||||||
"config.tasks.dont_include_file_extension_as_part_of_the_title",
|
<FormattedMessage id="actions.scan" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setDialogOpen({ scan: true })}
|
||||||
|
>
|
||||||
|
…
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.identify.description",
|
||||||
})}
|
})}
|
||||||
onChange={() => setStripFileExtension(!stripFileExtension)}
|
>
|
||||||
/>
|
<ButtonGroup className="ellipsis-button">
|
||||||
<Form.Check
|
<Button
|
||||||
id="scan-generate-previews"
|
variant="secondary"
|
||||||
checked={scanGeneratePreviews}
|
type="submit"
|
||||||
label={intl.formatMessage({
|
onClick={() => onIdentifyClicked()}
|
||||||
id: "config.tasks.generate_video_previews_during_scan",
|
>
|
||||||
|
<FormattedMessage id="actions.identify" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setDialogOpen({ identify: true })}
|
||||||
|
>
|
||||||
|
…
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.auto_tag_based_on_filenames",
|
||||||
})}
|
})}
|
||||||
onChange={() => setScanGeneratePreviews(!scanGeneratePreviews)}
|
>
|
||||||
/>
|
<ButtonGroup className="ellipsis-button">
|
||||||
<div className="d-flex flex-row">
|
<Button
|
||||||
<div>↳</div>
|
variant="secondary"
|
||||||
<Form.Check
|
type="submit"
|
||||||
id="scan-generate-image-previews"
|
onClick={() => onAutoTagClicked()}
|
||||||
checked={scanGenerateImagePreviews}
|
>
|
||||||
disabled={!scanGeneratePreviews}
|
<FormattedMessage id="actions.auto_tag" />
|
||||||
label={intl.formatMessage({
|
</Button>
|
||||||
id: "config.tasks.generate_previews_during_scan",
|
<Button
|
||||||
})}
|
variant="secondary"
|
||||||
onChange={() =>
|
onClick={() => setDialogOpen({ autoTag: true })}
|
||||||
setScanGenerateImagePreviews(!scanGenerateImagePreviews)
|
>
|
||||||
|
…
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.cleanup_desc",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setDialogOpen({ clean: true })}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.clean" />…
|
||||||
|
</Button>
|
||||||
|
</Task>
|
||||||
|
</Card>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
||||||
|
|
||||||
|
<Card className="task-group">
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.generate_desc",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ButtonGroup className="ellipsis-button">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => onGenerateClicked()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.generate" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setDialogOpen({ generate: true })}
|
||||||
|
>
|
||||||
|
…
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Task>
|
||||||
|
</Card>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
||||||
|
<Card className="task-group">
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.export_to_json",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="export"
|
||||||
|
variant="secondary"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => onExport()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.full_export" />
|
||||||
|
</Button>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.import_from_exported_json",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="import"
|
||||||
|
variant="danger"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setDialogOpen({ importAlert: true })}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.full_import" />
|
||||||
|
</Button>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.incremental_import",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id="partial-import"
|
||||||
|
variant="danger"
|
||||||
|
type="submit"
|
||||||
|
onClick={() => setDialogOpen({ import: true })}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.import_from_file" />
|
||||||
|
</Button>
|
||||||
|
</Task>
|
||||||
|
</Card>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<Form.Group>
|
||||||
|
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
||||||
|
<Card className="task-group">
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage(
|
||||||
|
{ id: "config.tasks.backup_database" },
|
||||||
|
{
|
||||||
|
filename_format: (
|
||||||
|
<code>
|
||||||
|
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
||||||
|
</code>
|
||||||
|
),
|
||||||
}
|
}
|
||||||
className="ml-2 flex-grow"
|
)}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Form.Check
|
|
||||||
id="scan-generate-sprites"
|
|
||||||
checked={scanGenerateSprites}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "config.tasks.generate_sprites_during_scan",
|
|
||||||
})}
|
|
||||||
onChange={() => setScanGenerateSprites(!scanGenerateSprites)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="scan-generate-phashes"
|
|
||||||
checked={scanGeneratePhashes}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "config.tasks.generate_phashes_during_scan",
|
|
||||||
})}
|
|
||||||
onChange={() => setScanGeneratePhashes(!scanGeneratePhashes)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="scan-generate-thumbnails"
|
|
||||||
checked={scanGenerateThumbnails}
|
|
||||||
label={intl.formatMessage({
|
|
||||||
id: "config.tasks.generate_thumbnails_during_scan",
|
|
||||||
})}
|
|
||||||
onChange={() => setScanGenerateThumbnails(!scanGenerateThumbnails)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
className="mr-2"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => onScan()}
|
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.scan" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => setDialogOpen({ scan: true })}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.selective_scan" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<h6>
|
|
||||||
<FormattedMessage id="config.tasks.identify.heading" />
|
|
||||||
</h6>
|
|
||||||
<Button
|
|
||||||
className="mr-2"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => setDialogOpen({ identify: true })}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.identify" />…
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
<FormattedMessage id="config.tasks.identify.description" />
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<h6>{intl.formatMessage({ id: "config.tasks.auto_tagging" })}</h6>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="autotag-performers"
|
|
||||||
checked={autoTagPerformers}
|
|
||||||
label={intl.formatMessage({ id: "performers" })}
|
|
||||||
onChange={() => setAutoTagPerformers(!autoTagPerformers)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="autotag-studios"
|
|
||||||
checked={autoTagStudios}
|
|
||||||
label={intl.formatMessage({ id: "studios" })}
|
|
||||||
onChange={() => setAutoTagStudios(!autoTagStudios)}
|
|
||||||
/>
|
|
||||||
<Form.Check
|
|
||||||
id="autotag-tags"
|
|
||||||
checked={autoTagTags}
|
|
||||||
label={intl.formatMessage({ id: "tags" })}
|
|
||||||
onChange={() => setAutoTagTags(!autoTagTags)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
<Button
|
||||||
|
id="backup"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mr-2"
|
onClick={() => onBackup()}
|
||||||
onClick={() => onAutoTag()}
|
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.auto_tag" />
|
<FormattedMessage id="actions.backup" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</Task>
|
||||||
|
|
||||||
|
<Task
|
||||||
|
description={intl.formatMessage({
|
||||||
|
id: "config.tasks.backup_and_download",
|
||||||
|
})}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
|
id="backupDownload"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={() => setDialogOpen({ autoTag: true })}
|
onClick={() => onBackup(true)}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.selective_auto_tag" />
|
<FormattedMessage id="actions.download_backup" />
|
||||||
</Button>
|
</Button>
|
||||||
<Form.Text className="text-muted">
|
</Task>
|
||||||
{intl.formatMessage({
|
</Card>
|
||||||
id: "config.tasks.auto_tag_based_on_filenames",
|
|
||||||
})}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
</Form.Group>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
|
||||||
<GenerateButton />
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
<h5>{intl.formatMessage({ id: "config.tasks.maintenance" })}</h5>
|
|
||||||
<Form.Group>
|
|
||||||
<Form.Check
|
|
||||||
id="clean-dryrun"
|
|
||||||
checked={cleanDryRun}
|
|
||||||
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
|
||||||
onChange={() => setCleanDryRun(!cleanDryRun)}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="clean"
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => setDialogOpen({ cleanAlert: true })}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.clean" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.cleanup_desc" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="export"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => onExport()}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.full_export" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.export_to_json" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="import"
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => setDialogOpen({ importAlert: true })}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.full_import" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.import_from_exported_json" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="partial-import"
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => setDialogOpen({ import: true })}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.import_from_file" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.incremental_import" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="backup"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => onBackup()}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.backup" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "config.tasks.backup_database" },
|
|
||||||
{
|
|
||||||
filename_format: (
|
|
||||||
<code>
|
|
||||||
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
|
||||||
</code>
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group>
|
|
||||||
<Button
|
|
||||||
id="backupDownload"
|
|
||||||
variant="secondary"
|
|
||||||
type="submit"
|
|
||||||
onClick={() => onBackup(true)}
|
|
||||||
>
|
|
||||||
<FormattedMessage id="actions.download_backup" />
|
|
||||||
</Button>
|
|
||||||
<Form.Text className="text-muted">
|
|
||||||
{intl.formatMessage({ id: "config.tasks.backup_and_download" })}
|
|
||||||
</Form.Text>
|
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderPlugins()}
|
{renderPlugins()}
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
|
||||||
|
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Button
|
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
||||||
id="migrateHashNaming"
|
|
||||||
variant="danger"
|
<Card className="task-group">
|
||||||
onClick={() => onMigrateHashNaming()}
|
<Task
|
||||||
>
|
description={intl.formatMessage({
|
||||||
<FormattedMessage id="actions.rename_gen_files" />
|
id: "config.tasks.migrate_hash_files",
|
||||||
</Button>
|
})}
|
||||||
<Form.Text className="text-muted">
|
>
|
||||||
{intl.formatMessage({ id: "config.tasks.migrate_hash_files" })}
|
<Button
|
||||||
</Form.Text>
|
id="migrateHashNaming"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => onMigrateHashNaming()}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="actions.rename_gen_files" />
|
||||||
|
</Button>
|
||||||
|
</Task>
|
||||||
|
</Card>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -161,3 +161,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card.task-group {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
|
||||||
|
.task {
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid $dark-gray2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis-button {
|
||||||
|
.btn:first-child {
|
||||||
|
border-right: 1px solid $card-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:last-child {
|
||||||
|
border-left: 1px solid $card-bg;
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const OperationButton: React.FC<IOperationButton> = (props) => {
|
|||||||
externalLoading !== undefined ? externalLoading : internalLoading;
|
externalLoading !== undefined ? externalLoading : internalLoading;
|
||||||
|
|
||||||
async function handleClick() {
|
async function handleClick() {
|
||||||
if (operation) {
|
if (operation && !loading) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await operation();
|
await operation();
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
@import "src/components/Tagger/styles.scss";
|
@import "src/components/Tagger/styles.scss";
|
||||||
@import "src/hooks/Lightbox/lightbox.scss";
|
@import "src/hooks/Lightbox/lightbox.scss";
|
||||||
@import "src/components/Dialogs/IdentifyDialog/styles.scss";
|
@import "src/components/Dialogs/IdentifyDialog/styles.scss";
|
||||||
|
@import "src/components/Dialogs/styles.scss";
|
||||||
|
|
||||||
/* stylelint-disable */
|
/* stylelint-disable */
|
||||||
#root {
|
#root {
|
||||||
@@ -381,7 +382,7 @@ div.dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background-color: #30404d;
|
background-color: $card-bg;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0),
|
box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0),
|
||||||
0 0 0 rgba(16, 22, 26, 0);
|
0 0 0 rgba(16, 22, 26, 0);
|
||||||
@@ -671,7 +672,7 @@ div.dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
background-color: darken($color: #30404d, $amount: 3);
|
background-color: darken($color: $card-bg, $amount: 3);
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
padding: 0.2em 0.4em;
|
padding: 0.2em 0.4em;
|
||||||
}
|
}
|
||||||
@@ -689,7 +690,7 @@ div.dropdown-menu {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
background-color: darken($color: #30404d, $amount: 3);
|
background-color: darken($color: $card-bg, $amount: 3);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
@@ -711,7 +712,7 @@ div.dropdown-menu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(2n) {
|
tr:nth-child(2n) {
|
||||||
background-color: darken($color: #30404d, $amount: 2);
|
background-color: darken($color: $card-bg, $amount: 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
|
|||||||
@@ -79,8 +79,6 @@
|
|||||||
"select_all": "Alle auswählen",
|
"select_all": "Alle auswählen",
|
||||||
"select_folders": "Ordner auswählen",
|
"select_folders": "Ordner auswählen",
|
||||||
"select_none": "Nichts auswählen",
|
"select_none": "Nichts auswählen",
|
||||||
"selective_auto_tag": "Selektives Auto Tag",
|
|
||||||
"selective_scan": "Selektives Scannen",
|
|
||||||
"set_as_default": "Als Voreinstellung festlegen",
|
"set_as_default": "Als Voreinstellung festlegen",
|
||||||
"set_back_image": "Rückseite…",
|
"set_back_image": "Rückseite…",
|
||||||
"set_front_image": "Vorderseite…",
|
"set_front_image": "Vorderseite…",
|
||||||
|
|||||||
@@ -78,8 +78,6 @@
|
|||||||
"select_all": "Select All",
|
"select_all": "Select All",
|
||||||
"select_folders": "Select folders",
|
"select_folders": "Select folders",
|
||||||
"select_none": "Select None",
|
"select_none": "Select None",
|
||||||
"selective_auto_tag": "Selective Auto Tag",
|
|
||||||
"selective_scan": "Selective Scan",
|
|
||||||
"set_as_default": "Set as default",
|
"set_as_default": "Set as default",
|
||||||
"set_back_image": "Back image…",
|
"set_back_image": "Back image…",
|
||||||
"set_front_image": "Front image…",
|
"set_front_image": "Front image…",
|
||||||
@@ -291,14 +289,23 @@
|
|||||||
},
|
},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"added_job_to_queue": "Added {operation_name} to job queue",
|
"added_job_to_queue": "Added {operation_name} to job queue",
|
||||||
|
"auto_tag": {
|
||||||
|
"auto_tagging_paths": "Auto Tagging the following paths",
|
||||||
|
"auto_tagging_all_paths": "Auto Tagging all paths"
|
||||||
|
},
|
||||||
"auto_tag_based_on_filenames": "Auto-tag content based on filenames.",
|
"auto_tag_based_on_filenames": "Auto-tag content based on filenames.",
|
||||||
"auto_tagging": "Auto Tagging",
|
"auto_tagging": "Auto Tagging",
|
||||||
"backing_up_database": "Backing up database",
|
"backing_up_database": "Backing up database",
|
||||||
"backup_and_download": "Performs a backup of the database and downloads the resulting file.",
|
"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}",
|
"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.",
|
"cleanup_desc": "Check for missing files and remove them from the database. This is a destructive action.",
|
||||||
|
"defaults_set": "Defaults have been set and will be used when clicking the {action} button on the Tasks page.",
|
||||||
"dont_include_file_extension_as_part_of_the_title": "Don't include file extension as part of the title",
|
"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.",
|
"export_to_json": "Exports the database content into JSON format in the metadata directory.",
|
||||||
|
"generate": {
|
||||||
|
"generating_scenes": "Generating for {num} {scene}",
|
||||||
|
"generating_from_paths": "Generating for scenes from the following paths"
|
||||||
|
},
|
||||||
"generate_desc": "Generate supporting image, sprite, video, vtt and other files.",
|
"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_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_previews_during_scan": "Generate image previews during scan (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
||||||
@@ -334,6 +341,10 @@
|
|||||||
"migrations": "Migrations",
|
"migrations": "Migrations",
|
||||||
"only_dry_run": "Only perform a dry run. Don't remove anything",
|
"only_dry_run": "Only perform a dry run. Don't remove anything",
|
||||||
"plugin_tasks": "Plugin Tasks",
|
"plugin_tasks": "Plugin Tasks",
|
||||||
|
"scan": {
|
||||||
|
"scanning_paths": "Scanning the following paths",
|
||||||
|
"scanning_all_paths": "Scanning all paths"
|
||||||
|
},
|
||||||
"scan_for_content_desc": "Scan for new content and add it to the database.",
|
"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)"
|
"set_name_date_details_from_metadata_if_present": "Set name, date, details from metadata (if present)"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,8 +79,6 @@
|
|||||||
"select_all": "Seleccionar todo",
|
"select_all": "Seleccionar todo",
|
||||||
"select_folders": "Seleccionar carpetas",
|
"select_folders": "Seleccionar carpetas",
|
||||||
"select_none": "Deseleccionar todo",
|
"select_none": "Deseleccionar todo",
|
||||||
"selective_auto_tag": "Etiquetado automático selectivo",
|
|
||||||
"selective_scan": "Búsqueda selectiva",
|
|
||||||
"set_as_default": "Establecer por defecto",
|
"set_as_default": "Establecer por defecto",
|
||||||
"set_back_image": "Contraportada…",
|
"set_back_image": "Contraportada…",
|
||||||
"set_front_image": "Portada…",
|
"set_front_image": "Portada…",
|
||||||
|
|||||||
@@ -79,8 +79,6 @@
|
|||||||
"select_all": "Seleziona Tutto",
|
"select_all": "Seleziona Tutto",
|
||||||
"select_folders": "Seleziona cartelle",
|
"select_folders": "Seleziona cartelle",
|
||||||
"select_none": "Deseleziona Tutto",
|
"select_none": "Deseleziona Tutto",
|
||||||
"selective_auto_tag": "Tag Automatico Selettivo",
|
|
||||||
"selective_scan": "Scansione Selettiva",
|
|
||||||
"set_as_default": "Imposta come Predefinito",
|
"set_as_default": "Imposta come Predefinito",
|
||||||
"set_back_image": "Immagine Retro…",
|
"set_back_image": "Immagine Retro…",
|
||||||
"set_front_image": "Immagine Frontale…",
|
"set_front_image": "Immagine Frontale…",
|
||||||
|
|||||||
@@ -64,8 +64,6 @@
|
|||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"select_all": "Selecionar todos",
|
"select_all": "Selecionar todos",
|
||||||
"select_none": "Selecionar nenhum",
|
"select_none": "Selecionar nenhum",
|
||||||
"selective_auto_tag": "Auto Tag seletivo",
|
|
||||||
"selective_scan": "Escaneamento seletivo",
|
|
||||||
"set_as_default": "Aplicar como padrão",
|
"set_as_default": "Aplicar como padrão",
|
||||||
"set_back_image": "Imagem de fundo…",
|
"set_back_image": "Imagem de fundo…",
|
||||||
"set_front_image": "Imagem frontal…",
|
"set_front_image": "Imagem frontal…",
|
||||||
|
|||||||
@@ -68,8 +68,6 @@
|
|||||||
"select_all": "Välj alla",
|
"select_all": "Välj alla",
|
||||||
"select_folders": "Välj mappar",
|
"select_folders": "Välj mappar",
|
||||||
"select_none": "Välj inga",
|
"select_none": "Välj inga",
|
||||||
"selective_auto_tag": "Selektiv Auto Tag",
|
|
||||||
"selective_scan": "Selektiv skanning",
|
|
||||||
"set_as_default": "Välj som standard",
|
"set_as_default": "Välj som standard",
|
||||||
"set_back_image": "Bakbild…",
|
"set_back_image": "Bakbild…",
|
||||||
"set_front_image": "Frambild…",
|
"set_front_image": "Frambild…",
|
||||||
|
|||||||
@@ -79,8 +79,6 @@
|
|||||||
"select_all": "选择所有",
|
"select_all": "选择所有",
|
||||||
"select_folders": "选择目录",
|
"select_folders": "选择目录",
|
||||||
"select_none": "清除选择",
|
"select_none": "清除选择",
|
||||||
"selective_auto_tag": "选择性自动生成标签",
|
|
||||||
"selective_scan": "选择性扫描",
|
|
||||||
"set_as_default": "设置为默认",
|
"set_as_default": "设置为默认",
|
||||||
"set_back_image": "设置背面图…",
|
"set_back_image": "设置背面图…",
|
||||||
"set_front_image": "设置正面图…",
|
"set_front_image": "设置正面图…",
|
||||||
|
|||||||
@@ -79,8 +79,6 @@
|
|||||||
"select_all": "全選",
|
"select_all": "全選",
|
||||||
"select_folders": "選擇資料夾",
|
"select_folders": "選擇資料夾",
|
||||||
"select_none": "清除選擇",
|
"select_none": "清除選擇",
|
||||||
"selective_auto_tag": "選擇性套用標籤",
|
|
||||||
"selective_scan": "選擇性掃描",
|
|
||||||
"set_as_default": "設為預設",
|
"set_as_default": "設為預設",
|
||||||
"set_back_image": "設定背面圖…",
|
"set_back_image": "設定背面圖…",
|
||||||
"set_front_image": "設定正面圖…",
|
"set_front_image": "設定正面圖…",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ $navbar-dark-color: rgb(245, 248, 250);
|
|||||||
$popover-bg: $secondary;
|
$popover-bg: $secondary;
|
||||||
$dark-text: #182026;
|
$dark-text: #182026;
|
||||||
$textfield-bg: rgba(16, 22, 26, 0.3);
|
$textfield-bg: rgba(16, 22, 26, 0.3);
|
||||||
|
$card-bg: #30404d;
|
||||||
|
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
@@ -186,7 +187,7 @@ hr {
|
|||||||
&-header,
|
&-header,
|
||||||
&-body,
|
&-body,
|
||||||
&-footer {
|
&-footer {
|
||||||
background-color: #30404d;
|
background-color: $card-bg;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user