Add delete file and generated files by default config options (#1852)

* add delete file and generated files by default config options
* add alert message with files to be deleted

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
7dJx1qP
2021-10-28 01:45:44 -04:00
committed by GitHub
parent 27cdefbb10
commit 96fce90cc3
12 changed files with 288 additions and 34 deletions

View File

@@ -116,6 +116,9 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult {
...IdentifyMetadataOptionsData
}
}
deleteFile
deleteGenerated
}
fragment ConfigData on ConfigResult {

View File

@@ -304,10 +304,20 @@ type ConfigScrapingResult {
type ConfigDefaultSettingsResult {
identify: IdentifyMetadataTaskOptions
"""If true, delete file checkbox will be checked by default"""
deleteFile: Boolean
"""If true, delete generated supporting files checkbox will be checked by default"""
deleteGenerated: Boolean
}
input ConfigDefaultSettingsInput {
identify: IdentifyMetadataInput
"""If true, delete file checkbox will be checked by default"""
deleteFile: Boolean
"""If true, delete generated files checkbox will be checked by default"""
deleteGenerated: Boolean
}
"""All configuration settings"""

View File

@@ -359,6 +359,14 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C
c.Set(config.DefaultIdentifySettings, input.Identify)
}
if input.DeleteFile != nil {
c.Set(config.DeleteFileDefault, *input.DeleteFile)
}
if input.DeleteGenerated != nil {
c.Set(config.DeleteGeneratedDefault, *input.DeleteGenerated)
}
if err := c.Write(); err != nil {
return makeConfigDefaultsResult(), err
}

View File

@@ -163,8 +163,12 @@ func makeConfigScrapingResult() *models.ConfigScrapingResult {
func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult {
config := config.GetInstance()
deleteFileDefault := config.GetDeleteFileDefault()
deleteGeneratedDefault := config.GetDeleteGeneratedDefault()
return &models.ConfigDefaultSettingsResult{
Identify: config.GetDefaultIdentifySettings(),
Identify: config.GetDefaultIdentifySettings(),
DeleteFile: &deleteFileDefault,
DeleteGenerated: &deleteGeneratedDefault,
}
}

View File

@@ -147,6 +147,9 @@ const FunscriptOffset = "funscript_offset"
// Default settings
const (
DefaultIdentifySettings = "defaults.identify_task"
DeleteFileDefault = "defaults.delete_file"
DeleteGeneratedDefault = "defaults.delete_generated"
)
// Security
@@ -880,6 +883,20 @@ func (i *Instance) GetFunscriptOffset() int {
return viper.GetInt(FunscriptOffset)
}
func (i *Instance) GetDeleteFileDefault() bool {
i.Lock()
defer i.Unlock()
viper.SetDefault(DeleteFileDefault, false)
return viper.GetBool(DeleteFileDefault)
}
func (i *Instance) GetDeleteGeneratedDefault() bool {
i.Lock()
defer i.Unlock()
viper.SetDefault(DeleteGeneratedDefault, true)
return viper.GetBool(DeleteGeneratedDefault)
}
// GetDefaultIdentifySettings returns the default Identify task settings.
// Returns nil if the settings could not be unmarshalled, or if it
// has not been set.

View File

@@ -1,4 +1,5 @@
### ✨ New Features
* Support setting defaults for Delete File and Delete Generated Files in the Interface Settings. ([#1852](https://github.com/stashapp/stash/pull/1852))
* Added Identify task to automatically identify scenes from stash-box/scraper sources. See manual entry for details. ([#1839](https://github.com/stashapp/stash/pull/1839))
* Added support for matching scenes using perceptual hashes when querying stash-box. ([#1858](https://github.com/stashapp/stash/pull/1858))
* Generalised Tagger view to support tagging using supported scene scrapers. ([#1812](https://github.com/stashapp/stash/pull/1812))
@@ -6,6 +7,7 @@
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))
### 🎨 Improvements
* Show files being deleted in the Delete dialogs. ([#1852](https://github.com/stashapp/stash/pull/1852))
* Added specific page titles. ([#1831](https://github.com/stashapp/stash/pull/1831))
* Added es-ES language option. ([#1886](https://github.com/stashapp/stash/pull/1886))
* Show pagination at top and bottom of page. ([#1776](https://github.com/stashapp/stash/pull/1776))

View File

@@ -4,10 +4,11 @@ import { useGalleryDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks";
import { useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
import { FormattedMessage, useIntl } from "react-intl";
interface IDeleteGalleryDialogProps {
selected: Pick<GQL.Gallery, "id">[];
selected: GQL.SlimGalleryDataFragment[];
onClose: (confirmed: boolean) => void;
}
@@ -31,8 +32,14 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity }
);
const [deleteFile, setDeleteFile] = useState<boolean>(false);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
const { configuration: config } = React.useContext(ConfigurationContext);
const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false
);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(
config?.defaults.deleteGenerated ?? true
);
const Toast = useToast();
const [deleteGallery] = useGalleryDestroy(getGalleriesDeleteInput());
@@ -60,6 +67,50 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
props.onClose(true);
}
function maybeRenderDeleteFileAlert() {
if (!deleteFile) {
return;
}
const fsGalleries = props.selected.filter((g) => g.path);
if (fsGalleries.length === 0) {
return;
}
return (
<div className="delete-dialog alert alert-danger text-break">
<p className="font-weight-bold">
<FormattedMessage
values={{
count: fsGalleries.length,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_alert"
/>
</p>
<ul>
{fsGalleries.slice(0, 5).map((s) => (
<li key={s.path}>{s.path}</li>
))}
{fsGalleries.length > 5 && (
<FormattedMessage
values={{
count: fsGalleries.length - 5,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_object_overflow"
/>
)}
<li>
<FormattedMessage id="dialogs.delete_galleries_extra" />
</li>
</ul>
</div>
);
}
return (
<Modal
show
@@ -78,6 +129,7 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
isRunning={isDeleting}
>
<p>{message}</p>
{maybeRenderDeleteFileAlert()}
<Form>
<Form.Check
id="delete-file"

View File

@@ -100,7 +100,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
if (isDeleteAlertOpen && gallery) {
return (
<DeleteGalleriesDialog
selected={[gallery]}
selected={[{ ...gallery, image_count: NaN }]}
onClose={onDeleteDialogClosed}
/>
);

View File

@@ -4,7 +4,8 @@ import { useImagesDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks";
import { useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
import { FormattedMessage, useIntl } from "react-intl";
interface IDeleteImageDialogProps {
selected: GQL.SlimImageDataFragment[];
@@ -31,8 +32,14 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity }
);
const [deleteFile, setDeleteFile] = useState<boolean>(false);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
const { configuration: config } = React.useContext(ConfigurationContext);
const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false
);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(
config?.defaults.deleteGenerated ?? true
);
const Toast = useToast();
const [deleteImage] = useImagesDestroy(getImagesDeleteInput());
@@ -60,6 +67,42 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
props.onClose(true);
}
function maybeRenderDeleteFileAlert() {
if (!deleteFile) {
return;
}
return (
<div className="delete-dialog alert alert-danger text-break">
<p className="font-weight-bold">
<FormattedMessage
values={{
count: props.selected.length,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_alert"
/>
</p>
<ul>
{props.selected.slice(0, 5).map((s) => (
<li key={s.path}>{s.path}</li>
))}
{props.selected.length > 5 && (
<FormattedMessage
values={{
count: props.selected.length - 5,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_object_overflow"
/>
)}
</ul>
</div>
);
}
return (
<Modal
show
@@ -78,6 +121,7 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
isRunning={isDeleting}
>
<p>{message}</p>
{maybeRenderDeleteFileAlert()}
<Form>
<Form.Check
id="delete-image"

View File

@@ -4,7 +4,8 @@ import { useScenesDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { Modal } from "src/components/Shared";
import { useToast } from "src/hooks";
import { useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
import { FormattedMessage, useIntl } from "react-intl";
interface IDeleteSceneDialogProps {
selected: GQL.SlimSceneDataFragment[];
@@ -31,8 +32,14 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity }
);
const [deleteFile, setDeleteFile] = useState<boolean>(false);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(true);
const { configuration: config } = React.useContext(ConfigurationContext);
const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false
);
const [deleteGenerated, setDeleteGenerated] = useState<boolean>(
config?.defaults.deleteGenerated ?? true
);
const Toast = useToast();
const [deleteScene] = useScenesDestroy(getScenesDeleteInput());
@@ -60,6 +67,42 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
props.onClose(true);
}
function maybeRenderDeleteFileAlert() {
if (!deleteFile) {
return;
}
return (
<div className="delete-dialog alert alert-danger text-break">
<p className="font-weight-bold">
<FormattedMessage
values={{
count: props.selected.length,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_alert"
/>
</p>
<ul>
{props.selected.slice(0, 5).map((s) => (
<li key={s.path}>{s.path}</li>
))}
{props.selected.length > 5 && (
<FormattedMessage
values={{
count: props.selected.length - 5,
singularEntity: intl.formatMessage({ id: "file" }),
pluralEntity: intl.formatMessage({ id: "files" }),
}}
id="dialogs.delete_object_overflow"
/>
)}
</ul>
</div>
);
}
return (
<Modal
show
@@ -78,6 +121,7 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
isRunning={isDeleting}
>
<p>{message}</p>
{maybeRenderDeleteFileAlert()}
<Form>
<Form.Check
id="delete-file"

View File

@@ -2,10 +2,15 @@ import React, { useEffect, useState } from "react";
import { Button, Form } from "react-bootstrap";
import { useIntl } from "react-intl";
import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
import {
useConfiguration,
useConfigureDefaults,
useConfigureInterface,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import * as GQL from "src/core/generated-graphql";
import { CheckboxGroup } from "./CheckboxGroup";
import { withoutTypename } from "src/utils";
const allMenuItems = [
{ id: "scenes", label: "Scenes" },
@@ -39,6 +44,10 @@ export const SettingsInterfacePanel: React.FC = () => {
const [language, setLanguage] = useState<string>("en");
const [handyKey, setHandyKey] = useState<string>();
const [funscriptOffset, setFunscriptOffset] = useState<number>(0);
const [deleteFileDefault, setDeleteFileDefault] = useState<boolean>(false);
const [deleteGeneratedDefault, setDeleteGeneratedDefault] = useState<boolean>(
true
);
const [
disableDropdownCreate,
setDisableDropdownCreate,
@@ -61,35 +70,51 @@ export const SettingsInterfacePanel: React.FC = () => {
disableDropdownCreate,
});
const [updateDefaultsConfig] = useConfigureDefaults();
useEffect(() => {
const iCfg = config?.configuration?.interface;
setMenuItemIds(iCfg?.menuItems ?? allMenuItems.map((item) => item.id));
setSoundOnPreview(iCfg?.soundOnPreview ?? true);
setWallShowTitle(iCfg?.wallShowTitle ?? true);
setWallPlayback(iCfg?.wallPlayback ?? "video");
setMaximumLoopDuration(iCfg?.maximumLoopDuration ?? 0);
setAutostartVideo(iCfg?.autostartVideo ?? false);
setShowStudioAsText(iCfg?.showStudioAsText ?? false);
setCSS(iCfg?.css ?? "");
setCSSEnabled(iCfg?.cssEnabled ?? false);
setLanguage(iCfg?.language ?? "en-US");
setSlideshowDelay(iCfg?.slideshowDelay ?? 5000);
setHandyKey(iCfg?.handyKey ?? "");
setFunscriptOffset(iCfg?.funscriptOffset ?? 0);
setDisableDropdownCreate({
performer: iCfg?.disabledDropdownCreate.performer,
studio: iCfg?.disabledDropdownCreate.studio,
tag: iCfg?.disabledDropdownCreate.tag,
});
if (config) {
const { interface: iCfg, defaults } = config.configuration;
setMenuItemIds(iCfg.menuItems ?? allMenuItems.map((item) => item.id));
setSoundOnPreview(iCfg.soundOnPreview ?? true);
setWallShowTitle(iCfg.wallShowTitle ?? true);
setWallPlayback(iCfg.wallPlayback ?? "video");
setMaximumLoopDuration(iCfg.maximumLoopDuration ?? 0);
setAutostartVideo(iCfg.autostartVideo ?? false);
setShowStudioAsText(iCfg.showStudioAsText ?? false);
setCSS(iCfg.css ?? "");
setCSSEnabled(iCfg.cssEnabled ?? false);
setLanguage(iCfg.language ?? "en-US");
setSlideshowDelay(iCfg.slideshowDelay ?? 5000);
setHandyKey(iCfg.handyKey ?? "");
setFunscriptOffset(iCfg.funscriptOffset ?? 0);
setDisableDropdownCreate({
performer: iCfg.disabledDropdownCreate.performer,
studio: iCfg.disabledDropdownCreate.studio,
tag: iCfg.disabledDropdownCreate.tag,
});
setDeleteFileDefault(defaults.deleteFile ?? false);
setDeleteGeneratedDefault(defaults.deleteGenerated ?? true);
}
}, [config]);
async function onSave() {
const prevCSS = config?.configuration.interface.css;
const prevCSSenabled = config?.configuration.interface.cssEnabled;
try {
if (config?.configuration.defaults) {
await updateDefaultsConfig({
variables: {
input: {
...withoutTypename(config?.configuration.defaults),
deleteFile: deleteFileDefault,
deleteGenerated: deleteGeneratedDefault,
},
},
});
}
const result = await updateInterfaceConfig();
// eslint-disable-next-line no-console
console.log(result);
// Force refetch of custom css if it was changed
if (
@@ -389,6 +414,38 @@ export const SettingsInterfacePanel: React.FC = () => {
</Form.Text>
</Form.Group>
<Form.Group>
<h5>
{intl.formatMessage({ id: "config.ui.delete_options.heading" })}
</h5>
<Form.Check
id="delete-file-default"
checked={deleteFileDefault}
label={intl.formatMessage({
id: "config.ui.delete_options.options.delete_file",
})}
onChange={() => {
setDeleteFileDefault(!deleteFileDefault);
}}
/>
<Form.Check
id="delete-generated-default"
checked={deleteGeneratedDefault}
label={intl.formatMessage({
id:
"config.ui.delete_options.options.delete_generated_supporting_files",
})}
onChange={() => {
setDeleteGeneratedDefault(!deleteGeneratedDefault);
}}
/>
<Form.Text className="text-muted">
{intl.formatMessage({
id: "config.ui.delete_options.description",
})}
</Form.Text>
</Form.Group>
<hr />
<Button variant="primary" onClick={() => onSave()}>
{intl.formatMessage({ id: "actions.save" })}

View File

@@ -61,6 +61,7 @@
"reshuffle": "Reshuffle",
"running": "running",
"save": "Save",
"save_delete_settings": "Use these options by default when deleting",
"save_filter": "Save filter",
"scan": "Scan",
"scrape_with": "Scrape with…",
@@ -364,6 +365,14 @@
"heading": "Custom CSS",
"option_label": "Custom CSS enabled"
},
"delete_options": {
"description": "Default settings when deleting images, galleries, and scenes.",
"heading": "Delete Options",
"options": {
"delete_file": "Delete file by default",
"delete_generated_supporting_files": "Delete generated supporting files by default"
}
},
"editing": {
"disable_dropdown_create": {
"heading": "Disable dropdown create",
@@ -485,12 +494,14 @@
"details": "Details",
"developmentVersion": "Development Version",
"dialogs": {
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
"delete_confirm": "Are you sure you want to delete {entityName}?",
"delete_entity_desc": "{count, plural, one {Are you sure you want to delete this {singularEntity}? Unless the file is also deleted, this {singularEntity} will be re-added when scan is performed.} other {Are you sure you want to delete these {pluralEntity}? Unless the files are also deleted, these {pluralEntity} will be re-added when scan is performed.}}",
"delete_entity_title": "{count, plural, one {Delete {singularEntity}} other {Delete {pluralEntity}}}",
"delete_object_desc": "Are you sure you want to delete {count, plural, one {this {singularEntity}} other {these {pluralEntity}}}?",
"delete_object_overflow": "…and {count} other {count, plural, one {{singularEntity}} other {{pluralEntity}}}.",
"delete_object_title": "Delete {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
"delete_galleries_extra": "…plus any image files not attached to any other gallery.",
"edit_entity_title": "Edit {count, plural, one {{singularEntity}} other {{pluralEntity}}}",
"export_include_related_objects": "Include related objects in export",
"export_title": "Export",
@@ -600,6 +611,8 @@
"fake_tits": "Fake Tits",
"false": "False",
"favourite": "Favourite",
"file": "file",
"files": "files",
"file_info": "File Info",
"file_mod_time": "File Modification Time",
"filesize": "File Size",