mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +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 {
|
fragment ScrapedScenePerformerData on ScrapedScenePerformer {
|
||||||
id
|
stored_id
|
||||||
name
|
name
|
||||||
gender
|
gender
|
||||||
url
|
url
|
||||||
@@ -62,7 +62,7 @@ fragment ScrapedMovieData on ScrapedMovie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
||||||
id
|
stored_id
|
||||||
name
|
name
|
||||||
aliases
|
aliases
|
||||||
duration
|
duration
|
||||||
@@ -74,13 +74,13 @@ fragment ScrapedSceneMovieData on ScrapedSceneMovie {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
|
fragment ScrapedSceneStudioData on ScrapedSceneStudio {
|
||||||
id
|
stored_id
|
||||||
name
|
name
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
fragment ScrapedSceneTagData on ScrapedSceneTag {
|
fragment ScrapedSceneTagData on ScrapedSceneTag {
|
||||||
id
|
stored_id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ type Scraper {
|
|||||||
|
|
||||||
type ScrapedScenePerformer {
|
type ScrapedScenePerformer {
|
||||||
"""Set if performer matched"""
|
"""Set if performer matched"""
|
||||||
id: ID
|
stored_id: ID
|
||||||
name: String!
|
name: String!
|
||||||
gender: String
|
gender: String
|
||||||
url: String
|
url: String
|
||||||
@@ -48,7 +48,7 @@ type ScrapedScenePerformer {
|
|||||||
|
|
||||||
type ScrapedSceneMovie {
|
type ScrapedSceneMovie {
|
||||||
"""Set if movie matched"""
|
"""Set if movie matched"""
|
||||||
id: ID
|
stored_id: ID
|
||||||
name: String!
|
name: String!
|
||||||
aliases: String
|
aliases: String
|
||||||
duration: String
|
duration: String
|
||||||
@@ -61,14 +61,14 @@ type ScrapedSceneMovie {
|
|||||||
|
|
||||||
type ScrapedSceneStudio {
|
type ScrapedSceneStudio {
|
||||||
"""Set if studio matched"""
|
"""Set if studio matched"""
|
||||||
id: ID
|
stored_id: ID
|
||||||
name: String!
|
name: String!
|
||||||
url: String
|
url: String
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrapedSceneTag {
|
type ScrapedSceneTag {
|
||||||
"""Set if tag matched"""
|
"""Set if tag matched"""
|
||||||
id: ID
|
stored_id: ID
|
||||||
name: String!
|
name: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,22 @@ func (r *Resolver) Tag() models.TagResolver {
|
|||||||
return &tagResolver{r}
|
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 mutationResolver struct{ *Resolver }
|
||||||
type queryResolver struct{ *Resolver }
|
type queryResolver struct{ *Resolver }
|
||||||
type subscriptionResolver struct{ *Resolver }
|
type subscriptionResolver struct{ *Resolver }
|
||||||
@@ -54,6 +70,10 @@ type sceneMarkerResolver struct{ *Resolver }
|
|||||||
type studioResolver struct{ *Resolver }
|
type studioResolver struct{ *Resolver }
|
||||||
type movieResolver struct{ *Resolver }
|
type movieResolver struct{ *Resolver }
|
||||||
type tagResolver 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) {
|
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) ([]*models.SceneMarker, error) {
|
||||||
qb := models.NewSceneMarkerQueryBuilder()
|
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 {
|
type ScrapedSceneTag struct {
|
||||||
// Set if tag matched
|
// Set if tag matched
|
||||||
ID *string `graphql:"id" json:"id"`
|
ID *string `graphql:"stored_id" json:"stored_id"`
|
||||||
Name string `graphql:"name" json:"name"`
|
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. **
|
#### 💥 **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
|
### ✨ 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 scraping movie details.
|
||||||
* Add support for JSON scrapers.
|
* Add support for JSON scrapers.
|
||||||
* Add support for plugin tasks.
|
* Add support for plugin tasks.
|
||||||
|
|||||||
@@ -351,39 +351,39 @@ export const SceneEditPanel: React.FC<IProps> = (props: IProps) => {
|
|||||||
setUrl(scene.url);
|
setUrl(scene.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.studio && scene.studio.id) {
|
if (scene.studio && scene.studio.stored_id) {
|
||||||
setStudioId(scene.studio.id);
|
setStudioId(scene.studio.stored_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.performers && scene.performers.length > 0) {
|
if (scene.performers && scene.performers.length > 0) {
|
||||||
const idPerfs = scene.performers.filter((p) => {
|
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) {
|
if (idPerfs.length > 0) {
|
||||||
const newIds = idPerfs.map((p) => p.id);
|
const newIds = idPerfs.map((p) => p.stored_id);
|
||||||
setPerformerIds(newIds as string[]);
|
setPerformerIds(newIds as string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene.movies && scene.movies.length > 0) {
|
if (scene.movies && scene.movies.length > 0) {
|
||||||
const idMovis = scene.movies.filter((p) => {
|
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) {
|
if (idMovis.length > 0) {
|
||||||
const newIds = idMovis.map((p) => p.id);
|
const newIds = idMovis.map((p) => p.stored_id);
|
||||||
setMovieIds(newIds as string[]);
|
setMovieIds(newIds as string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scene?.tags?.length) {
|
if (scene?.tags?.length) {
|
||||||
const idTags = scene.tags.filter((p) => {
|
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) {
|
if (idTags.length > 0) {
|
||||||
const newIds = idTags.map((p) => p.id);
|
const newIds = idTags.map((p) => p.stored_id);
|
||||||
setTagIds(newIds as string[]);
|
setTagIds(newIds as string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ import {
|
|||||||
ScrapedImageRow,
|
ScrapedImageRow,
|
||||||
} from "src/components/Shared/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
import {
|
||||||
|
useStudioCreate,
|
||||||
|
usePerformerCreate,
|
||||||
|
useMovieCreate,
|
||||||
|
useTagCreate,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
function renderScrapedStudio(
|
function renderScrapedStudio(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
@@ -36,7 +43,9 @@ function renderScrapedStudio(
|
|||||||
|
|
||||||
function renderScrapedStudioRow(
|
function renderScrapedStudioRow(
|
||||||
result: ScrapeResult<string>,
|
result: ScrapeResult<string>,
|
||||||
onChange: (value: ScrapeResult<string>) => void
|
onChange: (value: ScrapeResult<string>) => void,
|
||||||
|
newStudio?: GQL.ScrapedSceneStudio,
|
||||||
|
onCreateNew?: (value: GQL.ScrapedSceneStudio) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
@@ -49,6 +58,8 @@ function renderScrapedStudioRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
newValues={newStudio ? [newStudio] : undefined}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -78,7 +89,9 @@ function renderScrapedPerformers(
|
|||||||
|
|
||||||
function renderScrapedPerformersRow(
|
function renderScrapedPerformersRow(
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
|
newPerformers: GQL.ScrapedScenePerformer[],
|
||||||
|
onCreateNew?: (value: GQL.ScrapedScenePerformer) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
@@ -91,6 +104,8 @@ function renderScrapedPerformersRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
newValues={newPerformers}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -120,7 +135,9 @@ function renderScrapedMovies(
|
|||||||
|
|
||||||
function renderScrapedMoviesRow(
|
function renderScrapedMoviesRow(
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
|
newMovies: GQL.ScrapedSceneMovie[],
|
||||||
|
onCreateNew?: (value: GQL.ScrapedSceneMovie) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
@@ -133,6 +150,8 @@ function renderScrapedMoviesRow(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
newValues={newMovies}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -162,7 +181,9 @@ function renderScrapedTags(
|
|||||||
|
|
||||||
function renderScrapedTagsRow(
|
function renderScrapedTagsRow(
|
||||||
result: ScrapeResult<string[]>,
|
result: ScrapeResult<string[]>,
|
||||||
onChange: (value: ScrapeResult<string[]>) => void
|
onChange: (value: ScrapeResult<string[]>) => void,
|
||||||
|
newTags: GQL.ScrapedSceneTag[],
|
||||||
|
onCreateNew?: (value: GQL.ScrapedSceneTag) => void
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
@@ -174,7 +195,9 @@ function renderScrapedTagsRow(
|
|||||||
onChange(result.cloneWithValue(value))
|
onChange(result.cloneWithValue(value))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
newValues={newTags}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -186,8 +209,8 @@ interface ISceneScrapeDialogProps {
|
|||||||
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
onClose: (scrapedScene?: GQL.ScrapedScene) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHasID {
|
interface IHasStoredID {
|
||||||
id?: string | null;
|
stored_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
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)
|
new ScrapeResult<string>(props.scene.date, props.scraped.date)
|
||||||
);
|
);
|
||||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
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) {
|
if (!scrapedObjects) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const ret = scrapedObjects
|
const ret = scrapedObjects
|
||||||
.map((p) => p.id)
|
.map((p) => p.stored_id)
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
return p !== undefined && p !== null;
|
return p !== undefined && p !== null;
|
||||||
}) as string[];
|
}) as string[];
|
||||||
@@ -245,21 +280,33 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
const [performers, setPerformers] = useState<ScrapeResult<string[]>>(
|
||||||
new ScrapeResult<string[]>(
|
new ScrapeResult<string[]>(
|
||||||
sortIdList(props.scene.performer_ids),
|
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[]>>(
|
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
||||||
new ScrapeResult<string[]>(
|
new ScrapeResult<string[]>(
|
||||||
sortIdList(props.scene.movies?.map((p) => p.movie_id)),
|
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[]>>(
|
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||||
new ScrapeResult<string[]>(
|
new ScrapeResult<string[]>(
|
||||||
sortIdList(props.scene.tag_ids),
|
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>>(
|
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(props.scene.details, props.scraped.details)
|
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)
|
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
|
// don't show the dialog if nothing was scraped
|
||||||
if (
|
if (
|
||||||
[title, url, date, studio, performers, movies, tags, details, image].every(
|
[title, url, date, studio, performers, movies, tags, details, image].every(
|
||||||
@@ -277,34 +331,164 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNewScrapedItem() {
|
async function createNewStudio(toCreate: GQL.ScrapedSceneStudio) {
|
||||||
const newStudio = studio.getNewValue();
|
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 {
|
return {
|
||||||
title: title.getNewValue(),
|
title: title.getNewValue(),
|
||||||
url: url.getNewValue(),
|
url: url.getNewValue(),
|
||||||
date: date.getNewValue(),
|
date: date.getNewValue(),
|
||||||
studio: newStudio
|
studio: newStudioValue
|
||||||
? {
|
? {
|
||||||
id: newStudio,
|
stored_id: newStudioValue,
|
||||||
name: "",
|
name: "",
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
performers: performers.getNewValue()?.map((p) => {
|
performers: performers.getNewValue()?.map((p) => {
|
||||||
return {
|
return {
|
||||||
id: p,
|
stored_id: p,
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue()?.map((m) => {
|
||||||
return {
|
return {
|
||||||
id: m,
|
stored_id: m,
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
tags: tags.getNewValue()?.map((m) => {
|
tags: tags.getNewValue()?.map((m) => {
|
||||||
return {
|
return {
|
||||||
id: m,
|
stored_id: m,
|
||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -332,12 +516,30 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = (
|
|||||||
result={date}
|
result={date}
|
||||||
onChange={(value) => setDate(value)}
|
onChange={(value) => setDate(value)}
|
||||||
/>
|
/>
|
||||||
{renderScrapedStudioRow(studio, (value) => setStudio(value))}
|
{renderScrapedStudioRow(
|
||||||
{renderScrapedPerformersRow(performers, (value) =>
|
studio,
|
||||||
setPerformers(value)
|
(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
|
<ScrapedTextAreaRow
|
||||||
title="Details"
|
title="Details"
|
||||||
result={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,
|
InputGroup,
|
||||||
Button,
|
Button,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
Badge,
|
||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import { Icon, Modal } from "src/components/Shared";
|
import { CollapseButton, Icon, Modal } from "src/components/Shared";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
export class ScrapeResult<T> {
|
export class ScrapeResult<T> {
|
||||||
@@ -35,6 +36,7 @@ export class ScrapeResult<T> {
|
|||||||
|
|
||||||
ret.newValue = value;
|
ret.newValue = value;
|
||||||
ret.useNewValue = !_.isEqual(ret.newValue, ret.originalValue);
|
ret.useNewValue = !_.isEqual(ret.newValue, ret.originalValue);
|
||||||
|
ret.scraped = ret.useNewValue;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@@ -46,15 +48,22 @@ export class ScrapeResult<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IHasName {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface IScrapedFieldProps<T> {
|
interface IScrapedFieldProps<T> {
|
||||||
result: ScrapeResult<T>;
|
result: ScrapeResult<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IScrapedRowProps<T> extends IScrapedFieldProps<T> {
|
interface IScrapedRowProps<T, V extends IHasName>
|
||||||
|
extends IScrapedFieldProps<T> {
|
||||||
title: string;
|
title: string;
|
||||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||||
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||||
onChange: (value: ScrapeResult<T>) => void;
|
onChange: (value: ScrapeResult<T>) => void;
|
||||||
|
newValues?: V[];
|
||||||
|
onCreateNew?: (newValue: V) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderButtonIcon(selected: boolean) {
|
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) {
|
function handleSelectClick(isNew: boolean) {
|
||||||
const ret = _.clone(props.result);
|
const ret = _.clone(props.result);
|
||||||
ret.useNewValue = isNew;
|
ret.useNewValue = isNew;
|
||||||
props.onChange(ret);
|
props.onChange(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.result.scraped) {
|
function hasNewValues() {
|
||||||
|
return props.newValues && props.newValues.length > 0 && props.onCreateNew;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.result.scraped && !hasNewValues()) {
|
||||||
return <></>;
|
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 (
|
return (
|
||||||
<Row className="px-3 pt-3">
|
<Row className="px-3 pt-3">
|
||||||
<Form.Label column lg="3">
|
<Form.Label column lg="3">
|
||||||
@@ -112,6 +163,7 @@ export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
|
|||||||
</InputGroup.Prepend>
|
</InputGroup.Prepend>
|
||||||
{props.renderNewField(props.result)}
|
{props.renderNewField(props.result)}
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
{renderNewValues()}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export {
|
|||||||
|
|
||||||
export { default as Icon } from "./Icon";
|
export { default as Icon } from "./Icon";
|
||||||
export { default as Modal } from "./Modal";
|
export { default as Modal } from "./Modal";
|
||||||
|
export { CollapseButton } from "./CollapseButton";
|
||||||
export { DetailsEditNavbar } from "./DetailsEditNavbar";
|
export { DetailsEditNavbar } from "./DetailsEditNavbar";
|
||||||
export { DurationInput } from "./DurationInput";
|
export { DurationInput } from "./DurationInput";
|
||||||
export { TagLink } from "./TagLink";
|
export { TagLink } from "./TagLink";
|
||||||
|
|||||||
@@ -119,3 +119,12 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 15px;
|
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: {
|
variables: {
|
||||||
performer_name: performerName,
|
performer_name: performerName,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryScrapePerformer = (
|
export const queryScrapePerformer = (
|
||||||
@@ -417,6 +418,7 @@ export const queryScrapePerformer = (
|
|||||||
scraper_id: scraperId,
|
scraper_id: scraperId,
|
||||||
scraped_performer: scrapedPerformer,
|
scraped_performer: scrapedPerformer,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryScrapePerformerURL = (url: string) =>
|
export const queryScrapePerformerURL = (url: string) =>
|
||||||
@@ -425,6 +427,7 @@ export const queryScrapePerformerURL = (url: string) =>
|
|||||||
variables: {
|
variables: {
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryScrapeSceneURL = (url: string) =>
|
export const queryScrapeSceneURL = (url: string) =>
|
||||||
@@ -433,6 +436,7 @@ export const queryScrapeSceneURL = (url: string) =>
|
|||||||
variables: {
|
variables: {
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryScrapeMovieURL = (url: string) =>
|
export const queryScrapeMovieURL = (url: string) =>
|
||||||
@@ -441,6 +445,7 @@ export const queryScrapeMovieURL = (url: string) =>
|
|||||||
variables: {
|
variables: {
|
||||||
url,
|
url,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryScrapeScene = (
|
export const queryScrapeScene = (
|
||||||
@@ -453,6 +458,7 @@ export const queryScrapeScene = (
|
|||||||
scraper_id: scraperId,
|
scraper_id: scraperId,
|
||||||
scene,
|
scene,
|
||||||
},
|
},
|
||||||
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mutateReloadScrapers = () =>
|
export const mutateReloadScrapers = () =>
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ div.dropdown-menu {
|
|||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 2rem;
|
top: 2rem;
|
||||||
z-index: 1031;
|
z-index: 1051;
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
background-color: $success;
|
background-color: $success;
|
||||||
|
|||||||
Reference in New Issue
Block a user