mirror of
https://github.com/stashapp/stash.git
synced 2026-04-17 20:42:26 +02:00
Exclude zip folders when browsing scenes and galleries (#6740)
* Add short cuts when only getting zip/folder ids * Don't show zip folders when viewing scenes and galleries. Zip folders have no results for scenes and galleries, but will for images.
This commit is contained in:
parent
2e48dbfc63
commit
fd480c5a3e
5 changed files with 114 additions and 24 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/stashapp/stash/internal/build"
|
||||
"github.com/stashapp/stash/internal/manager"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
|
|
@ -145,6 +146,13 @@ func (r *Resolver) withReadTxn(ctx context.Context, fn func(ctx context.Context)
|
|||
return r.repository.WithReadTxn(ctx, fn)
|
||||
}
|
||||
|
||||
// idOnly returns true if the query is only asking for the id field.
|
||||
// This can be used to optimize certain queries where we don't need to load the full object if we're only getting the id.
|
||||
func (r *Resolver) idOnly(ctx context.Context) bool {
|
||||
fields := graphql.CollectAllFields(ctx)
|
||||
return len(fields) == 1 && fields[0] == "id"
|
||||
}
|
||||
|
||||
func (r *queryResolver) MarkerWall(ctx context.Context, q *string) (ret []*models.SceneMarker, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.SceneMarker.Wall(ctx, q)
|
||||
|
|
|
|||
|
|
@ -17,15 +17,31 @@ func (r *folderResolver) ParentFolder(ctx context.Context, obj *models.Folder) (
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
if r.idOnly(ctx) {
|
||||
return &models.Folder{ID: *obj.ParentFolderID}, nil
|
||||
}
|
||||
|
||||
return loaders.From(ctx).FolderByID.Load(*obj.ParentFolderID)
|
||||
}
|
||||
|
||||
func foldersFromIDs(ids []models.FolderID) []*models.Folder {
|
||||
ret := make([]*models.Folder, len(ids))
|
||||
for i, id := range ids {
|
||||
ret[i] = &models.Folder{ID: id}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *folderResolver) ParentFolders(ctx context.Context, obj *models.Folder) ([]*models.Folder, error) {
|
||||
ids, err := loaders.From(ctx).FolderParentFolderIDs.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r.idOnly(ctx) {
|
||||
return foldersFromIDs(ids), nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs := loaders.From(ctx).FolderByID.LoadAll(ids)
|
||||
return ret, firstError(errs)
|
||||
|
|
@ -37,11 +53,26 @@ func (r *folderResolver) SubFolders(ctx context.Context, obj *models.Folder) ([]
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if r.idOnly(ctx) {
|
||||
return foldersFromIDs(ids), nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
ret, errs := loaders.From(ctx).FolderByID.LoadAll(ids)
|
||||
return ret, firstError(errs)
|
||||
}
|
||||
|
||||
func (r *folderResolver) ZipFile(ctx context.Context, obj *models.Folder) (*BasicFile, error) {
|
||||
// shortcut for id only queries
|
||||
if r.idOnly(ctx) {
|
||||
if obj.ZipFileID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &BasicFile{
|
||||
BaseFile: &models.BaseFile{ID: *obj.ZipFileID},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return zipFileResolver(ctx, obj.ZipFileID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
query FindRootFoldersForSelect {
|
||||
query FindRootFoldersForSelect($zip_file_filter: MultiCriterionInput) {
|
||||
findFolders(
|
||||
filter: { per_page: -1, sort: "path", direction: ASC }
|
||||
folder_filter: { parent_folder: { modifier: IS_NULL } }
|
||||
folder_filter: {
|
||||
parent_folder: { modifier: IS_NULL }
|
||||
zip_file: $zip_file_filter
|
||||
}
|
||||
) {
|
||||
count
|
||||
folders {
|
||||
|
|
@ -34,6 +37,10 @@ query FindFolderHierarchyForIDs($ids: [ID!]!) {
|
|||
# the parent folders will be expanded, so we need the child folders
|
||||
sub_folders {
|
||||
...SelectFolderData
|
||||
# get zip file so we can filter out zip folders if needed
|
||||
zip_file {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CriterionModifier,
|
||||
FilterMode,
|
||||
FolderDataFragment,
|
||||
MultiCriterionInput,
|
||||
useFindFolderHierarchyForIDsQuery,
|
||||
useFindFoldersForQueryQuery,
|
||||
useFindRootFoldersForSelectQuery,
|
||||
|
|
@ -159,21 +162,47 @@ function mergeFolderMaps(base: IFolder[], update: IFolder[]): IFolder[] {
|
|||
return ret;
|
||||
}
|
||||
|
||||
function useFolderMap(
|
||||
query: string,
|
||||
skip?: boolean,
|
||||
initialSelected?: string[]
|
||||
) {
|
||||
function useFolderMap(props: {
|
||||
query: string;
|
||||
skip?: boolean;
|
||||
initialSelected?: string[];
|
||||
mode?: FilterMode;
|
||||
}) {
|
||||
const { query, skip = false, initialSelected, mode } = props;
|
||||
|
||||
const [cachedInitialSelected] = useState<string[]>(initialSelected ?? []);
|
||||
|
||||
// exclude zip folders for scenes and galleries
|
||||
const excludeZipFolders =
|
||||
mode === FilterMode.Scenes || mode === FilterMode.Galleries;
|
||||
|
||||
const zipFileFilter: MultiCriterionInput | undefined = useMemo(
|
||||
() =>
|
||||
excludeZipFolders
|
||||
? {
|
||||
modifier: CriterionModifier.IsNull,
|
||||
}
|
||||
: undefined,
|
||||
[excludeZipFolders]
|
||||
);
|
||||
|
||||
const folderFilterForQuery = useMemo(
|
||||
() => (zipFileFilter ? { zip_file: zipFileFilter } : undefined),
|
||||
[zipFileFilter]
|
||||
);
|
||||
|
||||
const { data: rootFoldersResult } = useFindRootFoldersForSelectQuery({
|
||||
skip,
|
||||
variables: {
|
||||
zip_file_filter: zipFileFilter,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: queryFoldersResult } = useFindFoldersForQueryQuery({
|
||||
skip: !query,
|
||||
variables: {
|
||||
filter: { q: query, per_page: 200 },
|
||||
folder_filter: folderFilterForQuery,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -213,11 +242,14 @@ function useFolderMap(
|
|||
existing = {
|
||||
...folder.parent_folders[i],
|
||||
expanded: true,
|
||||
children: folder.parent_folders[i].sub_folders.map((f) => ({
|
||||
...f,
|
||||
expanded: false,
|
||||
children: undefined,
|
||||
})),
|
||||
children: folder.parent_folders[i].sub_folders
|
||||
// filter out zip folders if needed
|
||||
.filter((f) => f.zip_file === null || !excludeZipFolders)
|
||||
.map((f) => ({
|
||||
...f,
|
||||
expanded: false,
|
||||
children: undefined,
|
||||
})),
|
||||
};
|
||||
ret.push(existing);
|
||||
}
|
||||
|
|
@ -243,11 +275,14 @@ function useFolderMap(
|
|||
existing = {
|
||||
...existing,
|
||||
expanded: true,
|
||||
children: thisFolder.sub_folders.map((f) => ({
|
||||
...f,
|
||||
expanded: false,
|
||||
children: undefined,
|
||||
})),
|
||||
// filter out zip folders if needed
|
||||
children: thisFolder.sub_folders
|
||||
.filter((f) => f.zip_file === null || !excludeZipFolders)
|
||||
.map((f) => ({
|
||||
...f,
|
||||
expanded: false,
|
||||
children: undefined,
|
||||
})),
|
||||
};
|
||||
|
||||
currentParent!.children![existingIndex] = existing;
|
||||
|
|
@ -255,7 +290,7 @@ function useFolderMap(
|
|||
}
|
||||
});
|
||||
return ret;
|
||||
}, [initialSelectedResult]);
|
||||
}, [initialSelectedResult, excludeZipFolders]);
|
||||
|
||||
const mergedRootFolders = useMemo(() => {
|
||||
if (query) {
|
||||
|
|
@ -347,7 +382,10 @@ function useFolderMap(
|
|||
|
||||
// query children folders if not already loaded
|
||||
if (folder.children === undefined) {
|
||||
const subFolderResult = await queryFindSubFolders(folder.id);
|
||||
const subFolderResult = await queryFindSubFolders(
|
||||
folder.id,
|
||||
excludeZipFolders
|
||||
);
|
||||
setFolderMap((current) =>
|
||||
current.map(
|
||||
replaceFolder({
|
||||
|
|
@ -419,17 +457,19 @@ export const FolderSelector: React.FC<{
|
|||
interface IInputFilterProps {
|
||||
criterion: FolderCriterion;
|
||||
setCriterion: (c: FolderCriterion) => void;
|
||||
mode?: FilterMode;
|
||||
}
|
||||
|
||||
export const FolderFilter: React.FC<IInputFilterProps> = ({
|
||||
criterion,
|
||||
setCriterion,
|
||||
mode,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [query, setQuery] = useState("");
|
||||
const [displayQuery, onQueryChange] = useDebouncedState(query, setQuery, 250);
|
||||
|
||||
const { folderMap, onToggleExpanded } = useFolderMap(query);
|
||||
const { folderMap, onToggleExpanded } = useFolderMap({ query, mode });
|
||||
|
||||
const messages = defineMessages({
|
||||
sub_folder_depth: {
|
||||
|
|
@ -599,11 +639,12 @@ export const SidebarFolderFilter: React.FC<
|
|||
const multipleSelected =
|
||||
criterion.value.items.length > 1 || criterion.value.excluded.length > 0;
|
||||
|
||||
const { folderMap, onToggleExpanded } = useFolderMap(
|
||||
const { folderMap, onToggleExpanded } = useFolderMap({
|
||||
query,
|
||||
skip,
|
||||
criterion.value.items.map((i) => i.id)
|
||||
);
|
||||
initialSelected: criterion.value.items.map((i) => i.id),
|
||||
mode: filter.mode,
|
||||
});
|
||||
|
||||
function onSelect(folder: IFolder) {
|
||||
// maintain sub-folder select if present
|
||||
|
|
|
|||
|
|
@ -515,12 +515,15 @@ export const useFindSavedFilters = (mode?: GQL.FilterMode) =>
|
|||
variables: { mode },
|
||||
});
|
||||
|
||||
export const queryFindSubFolders = (id: string) =>
|
||||
export const queryFindSubFolders = (id: string, excludeZipFolders?: boolean) =>
|
||||
client.query<GQL.FindFoldersForQueryQuery>({
|
||||
query: GQL.FindFoldersForQueryDocument,
|
||||
variables: {
|
||||
folder_filter: {
|
||||
parent_folder: { value: id, modifier: GQL.CriterionModifier.Equals },
|
||||
zip_file: excludeZipFolders
|
||||
? { modifier: GQL.CriterionModifier.IsNull }
|
||||
: undefined,
|
||||
},
|
||||
filter: {
|
||||
per_page: -1,
|
||||
|
|
|
|||
Loading…
Reference in a new issue