mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Show and allow creation of unknown performers/tags/studios/movies from scraper dialog (#741)
* Fix toast container z-index * Make scrape operations network only * Add CollapseButton component
This commit is contained in:
@@ -19,7 +19,7 @@ fragment ScrapedPerformerData on ScrapedPerformer {
|
||||
}
|
||||
|
||||
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||
id
|
||||
stored_id
|
||||
name
|
||||
gender
|
||||
url
|
||||
@@ -62,7 +62,7 @@ fragment ScrapedMovieData on ScrapedMovie {
|
||||
}
|
||||
|
||||
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||
id
|
||||
stored_id
|
||||
name
|
||||
aliases
|
||||
duration
|
||||
@@ -74,13 +74,13 @@ fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||
}
|
||||
|
||||
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
|
||||
id
|
||||
stored_id
|
||||
name
|
||||
url
|
||||
}
|
||||
|
||||
fragment ScrapedSceneTagData on ScrapedSceneTag {
|
||||
id
|
||||
stored_id
|
||||
name
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ type Scraper {
|
||||
|
||||
type ScrapedScenePerformer {
|
||||
"""Set if performer matched"""
|
||||
id: ID
|
||||
stored_id: ID
|
||||
name: String!
|
||||
gender: String
|
||||
url: String
|
||||
@@ -48,7 +48,7 @@ type ScrapedScenePerformer {
|
||||
|
||||
type ScrapedSceneMovie {
|
||||
"""Set if movie matched"""
|
||||
id: ID
|
||||
stored_id: ID
|
||||
name: String!
|
||||
aliases: String
|
||||
duration: String
|
||||
@@ -61,14 +61,14 @@ type ScrapedSceneMovie {
|
||||
|
||||
type ScrapedSceneStudio {
|
||||
"""Set if studio matched"""
|
||||
id: ID
|
||||
stored_id: ID
|
||||
name: String!
|
||||
url: String
|
||||
}
|
||||
|
||||
type ScrapedSceneTag {
|
||||
"""Set if tag matched"""
|
||||
id: ID
|
||||
stored_id: ID
|
||||
name: String!
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,22 @@ func (r *Resolver) Tag() models.TagResolver {
|
||||
return &tagResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneTag() models.ScrapedSceneTagResolver {
|
||||
return &scrapedSceneTagResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneMovie() models.ScrapedSceneMovieResolver {
|
||||
return &scrapedSceneMovieResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedScenePerformer() models.ScrapedScenePerformerResolver {
|
||||
return &scrapedScenePerformerResolver{r}
|
||||
}
|
||||
|
||||
func (r *Resolver) ScrapedSceneStudio() models.ScrapedSceneStudioResolver {
|
||||
return &scrapedSceneStudioResolver{r}
|
||||
}
|
||||
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
type subscriptionResolver struct{ *Resolver }
|
||||
@@ -54,6 +70,10 @@ type sceneMarkerResolver struct{ *Resolver }
|
||||
type studioResolver struct{ *Resolver }
|
||||
type movieResolver struct{ *Resolver }
|
||||
type tagResolver struct{ *Resolver }
|
||||
type scrapedSceneTagResolver struct{ *Resolver }
|
||||
type scrapedSceneMovieResolver struct{ *Resolver }
|
||||
type scrapedScenePerformerResolver struct{ *Resolver }
|
||||
type scrapedSceneStudioResolver struct{ *Resolver }
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) ([]*models.SceneMarker, error) {
|
||||
qb := models.NewSceneMarkerQueryBuilder()
|
||||
|
||||
23
pkg/api/resolver_model_scraper.go
Normal file
23
pkg/api/resolver_model_scraper.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
func (r *scrapedSceneTagResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneTag) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedSceneMovieResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneMovie) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedScenePerformerResolver) StoredID(ctx context.Context, obj *models.ScrapedScenePerformer) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
|
||||
func (r *scrapedSceneStudioResolver) StoredID(ctx context.Context, obj *models.ScrapedSceneStudio) (*string, error) {
|
||||
return obj.ID, nil
|
||||
}
|
||||
@@ -132,7 +132,7 @@ type ScrapedSceneMovie struct {
|
||||
|
||||
type ScrapedSceneTag struct {
|
||||
// Set if tag matched
|
||||
ID *string `graphql:"id" json:"id"`
|
||||
ID *string `graphql:"stored_id" json:"stored_id"`
|
||||
Name string `graphql:"name" json:"name"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const markup = `
|
||||
#### 💥 **Note: After upgrading, the next scan will populate all scenes with oshash hashes. MD5 calculation can be disabled after populating the oshash for all scenes. See \`Hashing Algorithms\` in the \`Configuration\` section of the manual for details. **
|
||||
|
||||
### ✨ New Features
|
||||
* Show and allow creation of unknown performers/tags/studios/movies in the scraper dialog.
|
||||
* Add support for scraping movie details.
|
||||
* Add support for JSON scrapers.
|
||||
* Add support for plugin tasks.
|
||||
|
||||
@@ -351,39 +351,39 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
||||
setUrl(scene.url);
|
||||
}
|
||||
|
||||
if (scene.studio && scene.studio.id) {
|
||||
setStudioId(scene.studio.id);
|
||||
if (scene.studio && scene.studio.stored_id) {
|
||||
setStudioId(scene.studio.stored_id);
|
||||
}
|
||||
|
||||
if (scene.performers && scene.performers.length > 0) {
|
||||
const idPerfs = scene.performers.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
return p.stored_id !== undefined && p.stored_id !== null;
|
||||
});
|
||||
|
||||
if (idPerfs.length > 0) {
|
||||
const newIds = idPerfs.map((p) => p.id);
|
||||
const newIds = idPerfs.map((p) => p.stored_id);
|
||||
setPerformerIds(newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.movies && scene.movies.length > 0) {
|
||||
const idMovis = scene.movies.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
return p.stored_id !== undefined && p.stored_id !== null;
|
||||
});
|
||||
|
||||
if (idMovis.length > 0) {
|
||||
const newIds = idMovis.map((p) => p.id);
|
||||
const newIds = idMovis.map((p) => p.stored_id);
|
||||
setMovieIds(newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
if (scene?.tags?.length) {
|
||||
const idTags = scene.tags.filter((p) => {
|
||||
return p.id !== undefined && p.id !== null;
|
||||
return p.stored_id !== undefined && p.stored_id !== null;
|
||||
});
|
||||
|
||||
if (idTags.length > 0) {
|
||||
const newIds = idTags.map((p) => p.id);
|
||||
const newIds = idTags.map((p) => p.stored_id);
|
||||
setTagIds(newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,13 @@ import {
|
||||
ScrapedImageRow,
|
||||
} from "src/components/Shared/ScrapeDialog";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
useStudioCreate,
|
||||
usePerformerCreate,
|
||||
useMovieCreate,
|
||||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
function renderScrapedStudio(
|
||||
result: ScrapeResult<string>,
|
||||
@@ -36,7 +43,9 @@ function renderScrapedStudio(
|
||||
|
||||
function renderScrapedStudioRow(
|
||||
result: ScrapeResult<string>,
|
||||
onChange: (value: ScrapeResult<string>) => void
|
||||
onChange: (value: ScrapeResult<string>) => void,
|
||||
newStudio?: GQL.ScrapedSceneStudio,
|
||||
onCreateNew?: (value: GQL.ScrapedSceneStudio) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -49,6 +58,8 @@ function renderScrapedStudioRow(
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newStudio ? [newStudio] : undefined}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -78,7 +89,9 @@ function renderScrapedPerformers(
|
||||
|
||||
function renderScrapedPerformersRow(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newPerformers: GQL.ScrapedScenePerformer[],
|
||||
onCreateNew?: (value: GQL.ScrapedScenePerformer) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -91,6 +104,8 @@ function renderScrapedPerformersRow(
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newPerformers}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -120,7 +135,9 @@ function renderScrapedMovies(
|
||||
|
||||
function renderScrapedMoviesRow(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newMovies: GQL.ScrapedSceneMovie[],
|
||||
onCreateNew?: (value: GQL.ScrapedSceneMovie) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -133,6 +150,8 @@ function renderScrapedMoviesRow(
|
||||
)
|
||||
}
|
||||
onChange={onChange}
|
||||
newValues={newMovies}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -162,7 +181,9 @@ function renderScrapedTags(
|
||||
|
||||
function renderScrapedTagsRow(
|
||||
result: ScrapeResult<string[]>,
|
||||
onChange: (value: ScrapeResult<string[]>) => void
|
||||
onChange: (value: ScrapeResult<string[]>) => void,
|
||||
newTags: GQL.ScrapedSceneTag[],
|
||||
onCreateNew?: (value: GQL.ScrapedSceneTag) => void
|
||||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
@@ -174,7 +195,9 @@ function renderScrapedTagsRow(
|
||||
onChange(result.cloneWithValue(value))
|
||||
)
|
||||
}
|
||||
newValues={newTags}
|
||||
onChange={onChange}
|
||||
onCreateNew={onCreateNew}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -186,8 +209,8 @@ interface ISceneScrapeDialogProps {
|
||||
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
||||
}
|
||||
|
||||
interface IHasID {
|
||||
id?: string | null;
|
||||
interface IHasStoredID {
|
||||
stored_id?: string | null;
|
||||
}
|
||||
|
||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
@@ -203,15 +226,27 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
new ScrapeResult<string>(props.scene.date, props.scraped.date)
|
||||
);
|
||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.studio_id, props.scraped.studio?.id)
|
||||
new ScrapeResult<string>(
|
||||
props.scene.studio_id,
|
||||
props.scraped.studio?.stored_id
|
||||
)
|
||||
);
|
||||
const [newStudio, setNewStudio] = useState<
|
||||
GQL.ScrapedSceneStudio | undefined
|
||||
>(
|
||||
props.scraped.studio && !props.scraped.studio.stored_id
|
||||
? props.scraped.studio
|
||||
: undefined
|
||||
);
|
||||
|
||||
function mapIdObjects(scrapedObjects?: IHasID[]): string[] | undefined {
|
||||
function mapStoredIdObjects(
|
||||
scrapedObjects?: IHasStoredID[]
|
||||
): string[] | undefined {
|
||||
if (!scrapedObjects) {
|
||||
return undefined;
|
||||
}
|
||||
const ret = scrapedObjects
|
||||
.map((p) => p.id)
|
||||
.map((p) => p.stored_id)
|
||||
.filter((p) => {
|
||||
return p !== undefined && p !== null;
|
||||
}) as string[];
|
||||
@@ -245,21 +280,33 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.performer_ids),
|
||||
mapIdObjects(props.scraped.performers ?? undefined)
|
||||
mapStoredIdObjects(props.scraped.performers ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newPerformers, setNewPerformers] = useState<
|
||||
GQL.ScrapedScenePerformer[]
|
||||
>(props.scraped.performers?.filter((t) => !t.stored_id) ?? []);
|
||||
|
||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.movies?.map((p) => p.movie_id)),
|
||||
mapIdObjects(props.scraped.movies ?? undefined)
|
||||
mapStoredIdObjects(props.scraped.movies ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newMovies, setNewMovies] = useState<GQL.ScrapedSceneMovie[]>(
|
||||
props.scraped.movies?.filter((t) => !t.stored_id) ?? []
|
||||
);
|
||||
|
||||
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.scene.tag_ids),
|
||||
mapIdObjects(props.scraped.tags ?? undefined)
|
||||
mapStoredIdObjects(props.scraped.tags ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>(
|
||||
props.scraped.tags?.filter((t) => !t.stored_id) ?? []
|
||||
);
|
||||
|
||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.scene.details, props.scraped.details)
|
||||
);
|
||||
@@ -267,6 +314,13 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
new ScrapeResult<string>(props.scene.cover_image, props.scraped.image)
|
||||
);
|
||||
|
||||
const [createStudio] = useStudioCreate({ name: "" });
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
const [createMovie] = useMovieCreate({ name: "" });
|
||||
const [createTag] = useTagCreate({ name: "" });
|
||||
|
||||
const Toast = useToast();
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (
|
||||
[title, url, date, studio, performers, movies, tags, details, image].every(
|
||||
@@ -277,34 +331,164 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function makeNewScrapedItem() {
|
||||
const newStudio = studio.getNewValue();
|
||||
async function createNewStudio(toCreate: GQL.ScrapedSceneStudio) {
|
||||
try {
|
||||
const result = await createStudio({
|
||||
variables: {
|
||||
name: toCreate.name,
|
||||
url: toCreate.url,
|
||||
},
|
||||
});
|
||||
|
||||
// set the new studio as the value
|
||||
setStudio(studio.cloneWithValue(result.data!.studioCreate!.id));
|
||||
setNewStudio(undefined);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created studio: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewPerformer(toCreate: GQL.ScrapedScenePerformer) {
|
||||
let performerInput: GQL.PerformerCreateInput = {};
|
||||
try {
|
||||
performerInput = Object.assign(performerInput, toCreate);
|
||||
const result = await createPerformer({
|
||||
variables: performerInput,
|
||||
});
|
||||
|
||||
// add the new performer to the new performers value
|
||||
const performerClone = performers.cloneWithValue(performers.newValue);
|
||||
if (!performerClone.newValue) {
|
||||
performerClone.newValue = [];
|
||||
}
|
||||
performerClone.newValue.push(result.data!.performerCreate!.id);
|
||||
setPerformers(performerClone);
|
||||
|
||||
// remove the performer from the list
|
||||
const newPerformersClone = newPerformers.concat();
|
||||
const pIndex = newPerformersClone.indexOf(toCreate);
|
||||
newPerformersClone.splice(pIndex, 1);
|
||||
|
||||
setNewPerformers(newPerformersClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created performer: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewMovie(toCreate: GQL.ScrapedSceneMovie) {
|
||||
let movieInput: GQL.MovieCreateInput = { name: "" };
|
||||
try {
|
||||
movieInput = Object.assign(movieInput, toCreate);
|
||||
const result = await createMovie({
|
||||
variables: movieInput,
|
||||
});
|
||||
|
||||
// add the new movie to the new movies value
|
||||
const movieClone = movies.cloneWithValue(movies.newValue);
|
||||
if (!movieClone.newValue) {
|
||||
movieClone.newValue = [];
|
||||
}
|
||||
movieClone.newValue.push(result.data!.movieCreate!.id);
|
||||
setMovies(movieClone);
|
||||
|
||||
// remove the movie from the list
|
||||
const newMoviesClone = newMovies.concat();
|
||||
const pIndex = newMoviesClone.indexOf(toCreate);
|
||||
newMoviesClone.splice(pIndex, 1);
|
||||
|
||||
setNewMovies(newMoviesClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created movie: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewTag(toCreate: GQL.ScrapedSceneTag) {
|
||||
let tagInput: GQL.TagCreateInput = { name: "" };
|
||||
try {
|
||||
tagInput = Object.assign(tagInput, toCreate);
|
||||
const result = await createTag({
|
||||
variables: tagInput,
|
||||
});
|
||||
|
||||
// add the new tag to the new tags value
|
||||
const tagClone = tags.cloneWithValue(tags.newValue);
|
||||
if (!tagClone.newValue) {
|
||||
tagClone.newValue = [];
|
||||
}
|
||||
tagClone.newValue.push(result.data!.tagCreate!.id);
|
||||
setTags(tagClone);
|
||||
|
||||
// remove the tag from the list
|
||||
const newTagsClone = newTags.concat();
|
||||
const pIndex = newTagsClone.indexOf(toCreate);
|
||||
newTagsClone.splice(pIndex, 1);
|
||||
|
||||
setNewTags(newTagsClone);
|
||||
|
||||
Toast.success({
|
||||
content: (
|
||||
<span>
|
||||
Created tag: <b>{toCreate.name}</b>
|
||||
</span>
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function makeNewScrapedItem(): GQL.ScrapedSceneDataFragment {
|
||||
const newStudioValue = studio.getNewValue();
|
||||
|
||||
return {
|
||||
title: title.getNewValue(),
|
||||
url: url.getNewValue(),
|
||||
date: date.getNewValue(),
|
||||
studio: newStudio
|
||||
studio: newStudioValue
|
||||
? {
|
||||
id: newStudio,
|
||||
stored_id: newStudioValue,
|
||||
name: "",
|
||||
}
|
||||
: undefined,
|
||||
performers: performers.getNewValue()?.map((p) => {
|
||||
return {
|
||||
id: p,
|
||||
stored_id: p,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
movies: movies.getNewValue()?.map((m) => {
|
||||
return {
|
||||
id: m,
|
||||
stored_id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
tags: tags.getNewValue()?.map((m) => {
|
||||
return {
|
||||
id: m,
|
||||
stored_id: m,
|
||||
name: "",
|
||||
};
|
||||
}),
|
||||
@@ -332,12 +516,30 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
{renderScrapedStudioRow(studio, (value) => setStudio(value))}
|
||||
{renderScrapedPerformersRow(performers, (value) =>
|
||||
setPerformers(value)
|
||||
{renderScrapedStudioRow(
|
||||
studio,
|
||||
(value) => setStudio(value),
|
||||
newStudio,
|
||||
createNewStudio
|
||||
)}
|
||||
{renderScrapedPerformersRow(
|
||||
performers,
|
||||
(value) => setPerformers(value),
|
||||
newPerformers,
|
||||
createNewPerformer
|
||||
)}
|
||||
{renderScrapedMoviesRow(
|
||||
movies,
|
||||
(value) => setMovies(value),
|
||||
newMovies,
|
||||
createNewMovie
|
||||
)}
|
||||
{renderScrapedTagsRow(
|
||||
tags,
|
||||
(value) => setTags(value),
|
||||
newTags,
|
||||
createNewTag
|
||||
)}
|
||||
{renderScrapedMoviesRow(movies, (value) => setMovies(value))}
|
||||
{renderScrapedTagsRow(tags, (value) => setTags(value))}
|
||||
<ScrapedTextAreaRow
|
||||
title="Details"
|
||||
result={details}
|
||||
|
||||
28
ui/v2.5/src/components/Shared/CollapseButton.tsx
Normal file
28
ui/v2.5/src/components/Shared/CollapseButton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Collapse } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
|
||||
interface IProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
||||
props: React.PropsWithChildren<IProps>
|
||||
) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => setOpen(!open)}
|
||||
className="minimal collapse-button"
|
||||
>
|
||||
<Icon icon={open ? "chevron-down" : "chevron-right"} />
|
||||
<span>{props.text}</span>
|
||||
</Button>
|
||||
<Collapse in={open}>
|
||||
<div>{props.children}</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -6,8 +6,9 @@ import {
|
||||
InputGroup,
|
||||
Button,
|
||||
FormControl,
|
||||
Badge,
|
||||
} from "react-bootstrap";
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import { CollapseButton, Icon, Modal } from "src/components/Shared";
|
||||
import _ from "lodash";
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
@@ -35,6 +36,7 @@ export class ScrapeResult<T> {
|
||||
|
||||
ret.newValue = value;
|
||||
ret.useNewValue = !_.isEqual(ret.newValue, ret.originalValue);
|
||||
ret.scraped = ret.useNewValue;
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -46,15 +48,22 @@ export class ScrapeResult<T> {
|
||||
}
|
||||
}
|
||||
|
||||
interface IHasName {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface IScrapedFieldProps<T> {
|
||||
result: ScrapeResult<T>;
|
||||
}
|
||||
|
||||
interface IScrapedRowProps<T> extends IScrapedFieldProps<T> {
|
||||
interface IScrapedRowProps<T, V extends IHasName>
|
||||
extends IScrapedFieldProps<T> {
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
onChange: (value: ScrapeResult<T>) => void;
|
||||
newValues?: V[];
|
||||
onCreateNew?: (newValue: V) => void;
|
||||
}
|
||||
|
||||
function renderButtonIcon(selected: boolean) {
|
||||
@@ -68,17 +77,59 @@ function renderButtonIcon(selected: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
|
||||
export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
props: IScrapedRowProps<T, V>
|
||||
) => {
|
||||
function handleSelectClick(isNew: boolean) {
|
||||
const ret = _.clone(props.result);
|
||||
ret.useNewValue = isNew;
|
||||
props.onChange(ret);
|
||||
}
|
||||
|
||||
if (!props.result.scraped) {
|
||||
function hasNewValues() {
|
||||
return props.newValues && props.newValues.length > 0 && props.onCreateNew;
|
||||
}
|
||||
|
||||
if (!props.result.scraped && !hasNewValues()) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function renderNewValues() {
|
||||
if (!hasNewValues()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = (
|
||||
<>
|
||||
{props.newValues!.map((t) => (
|
||||
<Badge
|
||||
className="tag-item"
|
||||
variant="secondary"
|
||||
key={t.name}
|
||||
onClick={() => props.onCreateNew!(t)}
|
||||
>
|
||||
{t.name}
|
||||
<Button className="minimal ml-2">
|
||||
<Icon className="fa-fw" icon="plus" />
|
||||
</Button>
|
||||
</Badge>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const minCollapseLength = 10;
|
||||
|
||||
if (props.newValues!.length >= minCollapseLength) {
|
||||
return (
|
||||
<CollapseButton text={`Missing (${props.newValues!.length})`}>
|
||||
{ret}
|
||||
</CollapseButton>
|
||||
);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="px-3 pt-3">
|
||||
<Form.Label column lg="3">
|
||||
@@ -112,6 +163,7 @@ export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
|
||||
</InputGroup.Prepend>
|
||||
{props.renderNewField(props.result)}
|
||||
</InputGroup>
|
||||
{renderNewValues()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
@@ -10,6 +10,7 @@ export {
|
||||
|
||||
export { default as Icon } from "./Icon";
|
||||
export { default as Modal } from "./Modal";
|
||||
export { CollapseButton } from "./CollapseButton";
|
||||
export { DetailsEditNavbar } from "./DetailsEditNavbar";
|
||||
export { DurationInput } from "./DurationInput";
|
||||
export { TagLink } from "./TagLink";
|
||||
|
||||
@@ -119,3 +119,12 @@
|
||||
overflow-y: auto;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
button.collapse-button.btn-primary:not(:disabled):not(.disabled):hover,
|
||||
button.collapse-button.btn-primary:not(:disabled):not(.disabled):focus,
|
||||
button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
||||
background: none;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
color: #f5f8fa;
|
||||
}
|
||||
|
||||
@@ -405,6 +405,7 @@ export const queryScrapeFreeones = (performerName: string) =>
|
||||
variables: {
|
||||
performer_name: performerName,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const queryScrapePerformer = (
|
||||
@@ -417,6 +418,7 @@ export const queryScrapePerformer = (
|
||||
scraper_id: scraperId,
|
||||
scraped_performer: scrapedPerformer,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const queryScrapePerformerURL = (url: string) =>
|
||||
@@ -425,6 +427,7 @@ export const queryScrapePerformerURL = (url: string) =>
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const queryScrapeSceneURL = (url: string) =>
|
||||
@@ -433,6 +436,7 @@ export const queryScrapeSceneURL = (url: string) =>
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const queryScrapeMovieURL = (url: string) =>
|
||||
@@ -441,6 +445,7 @@ export const queryScrapeMovieURL = (url: string) =>
|
||||
variables: {
|
||||
url,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const queryScrapeScene = (
|
||||
@@ -453,6 +458,7 @@ export const queryScrapeScene = (
|
||||
scraper_id: scraperId,
|
||||
scene,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const mutateReloadScrapers = () =>
|
||||
|
||||
@@ -343,7 +343,7 @@ div.dropdown-menu {
|
||||
max-width: 350px;
|
||||
position: fixed;
|
||||
top: 2rem;
|
||||
z-index: 1031;
|
||||
z-index: 1051;
|
||||
|
||||
.success {
|
||||
background-color: $success;
|
||||
|
||||
Reference in New Issue
Block a user