mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Merge pull request #6242 from stashapp/releases/0.29.3
Merge 0.29.3 to master
This commit is contained in:
commit
e92a0cb126
42 changed files with 766 additions and 256 deletions
|
|
@ -201,7 +201,7 @@ func (r *queryResolver) ScrapeSingleScene(ctx context.Context, source scraper.So
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO - this should happen after any scene is scraped
|
// TODO - this should happen after any scene is scraped
|
||||||
if err := r.matchScenesRelationships(ctx, ret, *source.StashBoxEndpoint); err != nil {
|
if err := r.matchScenesRelationships(ctx, ret, b.Endpoint); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -245,7 +245,7 @@ func (r *queryResolver) ScrapeMultiScenes(ctx context.Context, source scraper.So
|
||||||
// just flatten the slice and pass it in
|
// just flatten the slice and pass it in
|
||||||
flat := sliceutil.Flatten(ret)
|
flat := sliceutil.Flatten(ret)
|
||||||
|
|
||||||
if err := r.matchScenesRelationships(ctx, flat, *source.StashBoxEndpoint); err != nil {
|
if err := r.matchScenesRelationships(ctx, flat, b.Endpoint); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -335,7 +335,7 @@ func (r *queryResolver) ScrapeSingleStudio(ctx context.Context, source scraper.S
|
||||||
if len(ret) > 0 {
|
if len(ret) > 0 {
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
for _, studio := range ret {
|
for _, studio := range ret {
|
||||||
if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, *source.StashBoxEndpoint); err != nil {
|
if err := match.ScrapedStudioHierarchy(ctx, r.repository.Studio, studio, b.Endpoint); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ func (t *GenerateCoverTask) Start(ctx context.Context) {
|
||||||
return t.Scene.LoadPrimaryFile(ctx, r.File)
|
return t.Scene.LoadPrimaryFile(ctx, r.File)
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !required {
|
if !required {
|
||||||
|
|
|
||||||
|
|
@ -462,6 +462,7 @@ type ScrapedGroup struct {
|
||||||
Date *string `json:"date"`
|
Date *string `json:"date"`
|
||||||
Rating *string `json:"rating"`
|
Rating *string `json:"rating"`
|
||||||
Director *string `json:"director"`
|
Director *string `json:"director"`
|
||||||
|
URL *string `json:"url"` // included for backward compatibility
|
||||||
URLs []string `json:"urls"`
|
URLs []string `json:"urls"`
|
||||||
Synopsis *string `json:"synopsis"`
|
Synopsis *string `json:"synopsis"`
|
||||||
Studio *ScrapedStudio `json:"studio"`
|
Studio *ScrapedStudio `json:"studio"`
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,7 @@ type mappedSceneScraperConfig struct {
|
||||||
Performers mappedPerformerScraperConfig `yaml:"Performers"`
|
Performers mappedPerformerScraperConfig `yaml:"Performers"`
|
||||||
Studio mappedConfig `yaml:"Studio"`
|
Studio mappedConfig `yaml:"Studio"`
|
||||||
Movies mappedConfig `yaml:"Movies"`
|
Movies mappedConfig `yaml:"Movies"`
|
||||||
|
Groups mappedConfig `yaml:"Groups"`
|
||||||
}
|
}
|
||||||
type _mappedSceneScraperConfig mappedSceneScraperConfig
|
type _mappedSceneScraperConfig mappedSceneScraperConfig
|
||||||
|
|
||||||
|
|
@ -134,6 +135,7 @@ const (
|
||||||
mappedScraperConfigScenePerformers = "Performers"
|
mappedScraperConfigScenePerformers = "Performers"
|
||||||
mappedScraperConfigSceneStudio = "Studio"
|
mappedScraperConfigSceneStudio = "Studio"
|
||||||
mappedScraperConfigSceneMovies = "Movies"
|
mappedScraperConfigSceneMovies = "Movies"
|
||||||
|
mappedScraperConfigSceneGroups = "Groups"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *mappedSceneScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
func (s *mappedSceneScraperConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
|
@ -151,11 +153,13 @@ func (s *mappedSceneScraperConfig) UnmarshalYAML(unmarshal func(interface{}) err
|
||||||
thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers]
|
thisMap[mappedScraperConfigScenePerformers] = parentMap[mappedScraperConfigScenePerformers]
|
||||||
thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio]
|
thisMap[mappedScraperConfigSceneStudio] = parentMap[mappedScraperConfigSceneStudio]
|
||||||
thisMap[mappedScraperConfigSceneMovies] = parentMap[mappedScraperConfigSceneMovies]
|
thisMap[mappedScraperConfigSceneMovies] = parentMap[mappedScraperConfigSceneMovies]
|
||||||
|
thisMap[mappedScraperConfigSceneGroups] = parentMap[mappedScraperConfigSceneGroups]
|
||||||
|
|
||||||
delete(parentMap, mappedScraperConfigSceneTags)
|
delete(parentMap, mappedScraperConfigSceneTags)
|
||||||
delete(parentMap, mappedScraperConfigScenePerformers)
|
delete(parentMap, mappedScraperConfigScenePerformers)
|
||||||
delete(parentMap, mappedScraperConfigSceneStudio)
|
delete(parentMap, mappedScraperConfigSceneStudio)
|
||||||
delete(parentMap, mappedScraperConfigSceneMovies)
|
delete(parentMap, mappedScraperConfigSceneMovies)
|
||||||
|
delete(parentMap, mappedScraperConfigSceneGroups)
|
||||||
|
|
||||||
// re-unmarshal the sub-fields
|
// re-unmarshal the sub-fields
|
||||||
yml, err := yaml.Marshal(thisMap)
|
yml, err := yaml.Marshal(thisMap)
|
||||||
|
|
@ -873,50 +877,55 @@ func (r mappedResult) apply(dest interface{}) {
|
||||||
|
|
||||||
func mapFieldValue(destVal reflect.Value, key string, value interface{}) error {
|
func mapFieldValue(destVal reflect.Value, key string, value interface{}) error {
|
||||||
field := destVal.FieldByName(key)
|
field := destVal.FieldByName(key)
|
||||||
|
|
||||||
|
if !field.IsValid() {
|
||||||
|
return fmt.Errorf("field %s does not exist on %s", key, destVal.Type().Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !field.CanSet() {
|
||||||
|
return fmt.Errorf("field %s cannot be set on %s", key, destVal.Type().Name())
|
||||||
|
}
|
||||||
|
|
||||||
fieldType := field.Type()
|
fieldType := field.Type()
|
||||||
|
|
||||||
if field.IsValid() && field.CanSet() {
|
switch v := value.(type) {
|
||||||
switch v := value.(type) {
|
case string:
|
||||||
case string:
|
// if the field is a pointer to a string, then we need to convert the string to a pointer
|
||||||
// if the field is a pointer to a string, then we need to convert the string to a pointer
|
// if the field is a string slice, then we need to convert the string to a slice
|
||||||
// if the field is a string slice, then we need to convert the string to a slice
|
switch {
|
||||||
switch {
|
case fieldType.Kind() == reflect.String:
|
||||||
case fieldType.Kind() == reflect.String:
|
field.SetString(v)
|
||||||
field.SetString(v)
|
case fieldType.Kind() == reflect.Ptr && fieldType.Elem().Kind() == reflect.String:
|
||||||
case fieldType.Kind() == reflect.Ptr && fieldType.Elem().Kind() == reflect.String:
|
ptr := reflect.New(fieldType.Elem())
|
||||||
ptr := reflect.New(fieldType.Elem())
|
ptr.Elem().SetString(v)
|
||||||
ptr.Elem().SetString(v)
|
field.Set(ptr)
|
||||||
field.Set(ptr)
|
case fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String:
|
||||||
case fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String:
|
field.Set(reflect.ValueOf([]string{v}))
|
||||||
field.Set(reflect.ValueOf([]string{v}))
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
|
||||||
}
|
|
||||||
case []string:
|
|
||||||
// expect the field to be a string slice
|
|
||||||
if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String {
|
|
||||||
field.Set(reflect.ValueOf(v))
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
// fallback to reflection
|
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
||||||
reflectValue := reflect.ValueOf(value)
|
}
|
||||||
reflectValueType := reflectValue.Type()
|
case []string:
|
||||||
|
// expect the field to be a string slice
|
||||||
switch {
|
if fieldType.Kind() == reflect.Slice && fieldType.Elem().Kind() == reflect.String {
|
||||||
case reflectValueType.ConvertibleTo(fieldType):
|
field.Set(reflect.ValueOf(v))
|
||||||
field.Set(reflectValue.Convert(fieldType))
|
} else {
|
||||||
case fieldType.Kind() == reflect.Pointer && reflectValueType.ConvertibleTo(fieldType.Elem()):
|
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
||||||
ptr := reflect.New(fieldType.Elem())
|
}
|
||||||
ptr.Elem().Set(reflectValue.Convert(fieldType.Elem()))
|
default:
|
||||||
field.Set(ptr)
|
// fallback to reflection
|
||||||
default:
|
reflectValue := reflect.ValueOf(value)
|
||||||
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
reflectValueType := reflectValue.Type()
|
||||||
}
|
|
||||||
|
switch {
|
||||||
|
case reflectValueType.ConvertibleTo(fieldType):
|
||||||
|
field.Set(reflectValue.Convert(fieldType))
|
||||||
|
case fieldType.Kind() == reflect.Pointer && reflectValueType.ConvertibleTo(fieldType.Elem()):
|
||||||
|
ptr := reflect.New(fieldType.Elem())
|
||||||
|
ptr.Elem().Set(reflectValue.Convert(fieldType.Elem()))
|
||||||
|
field.Set(ptr)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot convert %T to %s", value, fieldType)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return fmt.Errorf("field does not exist or cannot be set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -1008,6 +1017,7 @@ func (s mappedScraper) processSceneRelationships(ctx context.Context, q mappedQu
|
||||||
sceneTagsMap := sceneScraperConfig.Tags
|
sceneTagsMap := sceneScraperConfig.Tags
|
||||||
sceneStudioMap := sceneScraperConfig.Studio
|
sceneStudioMap := sceneScraperConfig.Studio
|
||||||
sceneMoviesMap := sceneScraperConfig.Movies
|
sceneMoviesMap := sceneScraperConfig.Movies
|
||||||
|
sceneGroupsMap := sceneScraperConfig.Groups
|
||||||
|
|
||||||
ret.Performers = s.processPerformers(ctx, scenePerformersMap, q)
|
ret.Performers = s.processPerformers(ctx, scenePerformersMap, q)
|
||||||
|
|
||||||
|
|
@ -1034,7 +1044,12 @@ func (s mappedScraper) processSceneRelationships(ctx context.Context, q mappedQu
|
||||||
ret.Movies = processRelationships[models.ScrapedMovie](ctx, s, sceneMoviesMap, q)
|
ret.Movies = processRelationships[models.ScrapedMovie](ctx, s, sceneMoviesMap, q)
|
||||||
}
|
}
|
||||||
|
|
||||||
return len(ret.Performers) > 0 || len(ret.Tags) > 0 || ret.Studio != nil || len(ret.Movies) > 0
|
if sceneGroupsMap != nil {
|
||||||
|
logger.Debug(`Processing scene groups:`)
|
||||||
|
ret.Groups = processRelationships[models.ScrapedGroup](ctx, s, sceneGroupsMap, q)
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(ret.Performers) > 0 || len(ret.Tags) > 0 || ret.Studio != nil || len(ret.Movies) > 0 || len(ret.Groups) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s mappedScraper) processPerformers(ctx context.Context, performersMap mappedPerformerScraperConfig, q mappedQuery) []*models.ScrapedPerformer {
|
func (s mappedScraper) processPerformers(ctx context.Context, performersMap mappedPerformerScraperConfig, q mappedQuery) []*models.ScrapedPerformer {
|
||||||
|
|
|
||||||
|
|
@ -143,6 +143,21 @@ func (c Cache) postScrapeMovie(ctx context.Context, m models.ScrapedMovie, exclu
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populate URL/URLs
|
||||||
|
// if URLs are provided, only use those
|
||||||
|
if len(m.URLs) > 0 {
|
||||||
|
m.URL = &m.URLs[0]
|
||||||
|
} else {
|
||||||
|
urls := []string{}
|
||||||
|
if m.URL != nil {
|
||||||
|
urls = append(urls, *m.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urls) > 0 {
|
||||||
|
m.URLs = urls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// post-process - set the image if applicable
|
// post-process - set the image if applicable
|
||||||
if err := setMovieFrontImage(ctx, c.client, &m, c.globalConfig); err != nil {
|
if err := setMovieFrontImage(ctx, c.client, &m, c.globalConfig); err != nil {
|
||||||
logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err)
|
logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err)
|
||||||
|
|
@ -175,6 +190,21 @@ func (c Cache) postScrapeGroup(ctx context.Context, m models.ScrapedGroup, exclu
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// populate URL/URLs
|
||||||
|
// if URLs are provided, only use those
|
||||||
|
if len(m.URLs) > 0 {
|
||||||
|
m.URL = &m.URLs[0]
|
||||||
|
} else {
|
||||||
|
urls := []string{}
|
||||||
|
if m.URL != nil {
|
||||||
|
urls = append(urls, *m.URL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(urls) > 0 {
|
||||||
|
m.URLs = urls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// post-process - set the image if applicable
|
// post-process - set the image if applicable
|
||||||
if err := setGroupFrontImage(ctx, c.client, &m, c.globalConfig); err != nil {
|
if err := setGroupFrontImage(ctx, c.client, &m, c.globalConfig); err != nil {
|
||||||
logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err)
|
logger.Warnf("could not set front image using URL %s: %v", *m.FrontImage, err)
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,7 @@ func (qb *sceneFilterHandler) isMissingCriterionHandler(isMissing *string) crite
|
||||||
f.addWhere("galleries_join.scene_id IS NULL")
|
f.addWhere("galleries_join.scene_id IS NULL")
|
||||||
case "studio":
|
case "studio":
|
||||||
f.addWhere("scenes.studio_id IS NULL")
|
f.addWhere("scenes.studio_id IS NULL")
|
||||||
case "movie":
|
case "movie", "group":
|
||||||
sceneRepository.groups.join(f, "groups_join", "scenes.id")
|
sceneRepository.groups.join(f, "groups_join", "scenes.id")
|
||||||
f.addWhere("groups_join.scene_id IS NULL")
|
f.addWhere("groups_join.scene_id IS NULL")
|
||||||
case "performers":
|
case "performers":
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import cx from "classnames";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: GQL.GalleryDataFragment;
|
gallery: GQL.GalleryDataFragment;
|
||||||
|
|
@ -167,7 +168,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
||||||
function onDeleteDialogClosed(deleted: boolean) {
|
function onDeleteDialogClosed(deleted: boolean) {
|
||||||
setIsDeleteAlertOpen(false);
|
setIsDeleteAlertOpen(false);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
history.goBack();
|
goBackOrReplace(history, "/galleries");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@ export const AddSubGroupsDialog: React.FC<IListOperationProps> = (
|
||||||
onUpdate={(input) => setEntries(input)}
|
onUpdate={(input) => setEntries(input)}
|
||||||
excludeIDs={excludeIDs}
|
excludeIDs={excludeIDs}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
menuPortalTarget={document.body}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import { Button, Tab, Tabs } from "react-bootstrap";
|
||||||
import { GroupSubGroupsPanel } from "./GroupSubGroupsPanel";
|
import { GroupSubGroupsPanel } from "./GroupSubGroupsPanel";
|
||||||
import { GroupPerformersPanel } from "./GroupPerformersPanel";
|
import { GroupPerformersPanel } from "./GroupPerformersPanel";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
const validTabs = ["default", "scenes", "performers", "subgroups"] as const;
|
const validTabs = ["default", "scenes", "performers", "subgroups"] as const;
|
||||||
type TabKey = (typeof validTabs)[number];
|
type TabKey = (typeof validTabs)[number];
|
||||||
|
|
@ -276,7 +277,7 @@ const GroupPage: React.FC<IProps> = ({ group, tabKey }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.goBack();
|
goBackOrReplace(history, "/groups");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEditing(value?: boolean) {
|
function toggleEditing(value?: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useMemo } from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { GroupList } from "../GroupList";
|
import { GroupList } from "../GroupList";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
|
@ -101,6 +101,18 @@ interface IGroupSubGroupsPanel {
|
||||||
group: GQL.GroupDataFragment;
|
group: GQL.GroupDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const defaultFilter = (() => {
|
||||||
|
const sortBy = "sub_group_order";
|
||||||
|
const ret = new ListFilterModel(GQL.FilterMode.Groups, undefined, {
|
||||||
|
defaultSortBy: sortBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
// unset the sort by so that its not included in the URL
|
||||||
|
ret.sortBy = undefined;
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
})();
|
||||||
|
|
||||||
export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> = ({
|
export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> = ({
|
||||||
active,
|
active,
|
||||||
group,
|
group,
|
||||||
|
|
@ -114,18 +126,6 @@ export const GroupSubGroupsPanel: React.FC<IGroupSubGroupsPanel> = ({
|
||||||
|
|
||||||
const filterHook = useContainingGroupFilterHook(group);
|
const filterHook = useContainingGroupFilterHook(group);
|
||||||
|
|
||||||
const defaultFilter = useMemo(() => {
|
|
||||||
const sortBy = "sub_group_order";
|
|
||||||
const ret = new ListFilterModel(GQL.FilterMode.Groups, undefined, {
|
|
||||||
defaultSortBy: sortBy,
|
|
||||||
});
|
|
||||||
|
|
||||||
// unset the sort by so that its not included in the URL
|
|
||||||
ret.sortBy = undefined;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function removeSubGroups(
|
async function removeSubGroups(
|
||||||
result: GQL.FindGroupsQueryResult,
|
result: GQL.FindGroupsQueryResult,
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ import TextUtils from "src/utils/text";
|
||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
|
|
@ -156,7 +157,7 @@ const ImagePage: React.FC<IProps> = ({ image }) => {
|
||||||
function onDeleteDialogClosed(deleted: boolean) {
|
function onDeleteDialogClosed(deleted: boolean) {
|
||||||
setIsDeleteAlertOpen(false);
|
setIsDeleteAlertOpen(false);
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
history.goBack();
|
goBackOrReplace(history, "/images");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ interface ICriterionList {
|
||||||
optionSelected: (o?: CriterionOption) => void;
|
optionSelected: (o?: CriterionOption) => void;
|
||||||
onRemoveCriterion: (c: string) => void;
|
onRemoveCriterion: (c: string) => void;
|
||||||
onTogglePin: (c: CriterionOption) => void;
|
onTogglePin: (c: CriterionOption) => void;
|
||||||
|
externallySelected?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CriterionOptionList: React.FC<ICriterionList> = ({
|
const CriterionOptionList: React.FC<ICriterionList> = ({
|
||||||
|
|
@ -62,6 +63,7 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
|
||||||
optionSelected,
|
optionSelected,
|
||||||
onRemoveCriterion,
|
onRemoveCriterion,
|
||||||
onTogglePin,
|
onTogglePin,
|
||||||
|
externallySelected = false,
|
||||||
}) => {
|
}) => {
|
||||||
const prevCriterion = usePrevious(currentCriterion);
|
const prevCriterion = usePrevious(currentCriterion);
|
||||||
|
|
||||||
|
|
@ -101,14 +103,19 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
|
||||||
// scrolling to the current criterion doesn't work well when the
|
// scrolling to the current criterion doesn't work well when the
|
||||||
// dialog is already open, so limit to when we click on the
|
// dialog is already open, so limit to when we click on the
|
||||||
// criterion from the external tags
|
// criterion from the external tags
|
||||||
if (!scrolled.current && type && criteriaRefs[type]?.current) {
|
if (
|
||||||
|
externallySelected &&
|
||||||
|
!scrolled.current &&
|
||||||
|
type &&
|
||||||
|
criteriaRefs[type]?.current
|
||||||
|
) {
|
||||||
criteriaRefs[type].current!.scrollIntoView({
|
criteriaRefs[type].current!.scrollIntoView({
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "start",
|
block: "start",
|
||||||
});
|
});
|
||||||
scrolled.current = true;
|
scrolled.current = true;
|
||||||
}
|
}
|
||||||
}, [currentCriterion, criteriaRefs, type]);
|
}, [externallySelected, currentCriterion, criteriaRefs, type]);
|
||||||
|
|
||||||
function getReleventCriterion(t: CriterionType) {
|
function getReleventCriterion(t: CriterionType) {
|
||||||
if (currentCriterion?.criterionOption.type === t) {
|
if (currentCriterion?.criterionOption.type === t) {
|
||||||
|
|
@ -549,6 +556,7 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
||||||
selected={criterion?.criterionOption}
|
selected={criterion?.criterionOption}
|
||||||
onRemoveCriterion={(c) => removeCriterionString(c)}
|
onRemoveCriterion={(c) => removeCriterionString(c)}
|
||||||
onTogglePin={(c) => onTogglePinFilter(c)}
|
onTogglePin={(c) => onTogglePinFilter(c)}
|
||||||
|
externallySelected={!!editingCriterion}
|
||||||
/>
|
/>
|
||||||
{criteria.length > 0 && (
|
{criteria.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ interface ISidebarFilter {
|
||||||
option: CriterionOption;
|
option: CriterionOption;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
sectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
|
|
@ -61,6 +62,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
option,
|
option,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
@ -127,6 +129,7 @@ export const SidebarBooleanFilter: React.FC<ISidebarFilter> = ({
|
||||||
onUnselect={onUnselect}
|
onUnselect={onUnselect}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
singleValue
|
singleValue
|
||||||
|
sectionID={sectionID}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ import ScreenUtils from "src/utils/screen";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
|
|
||||||
|
const savedFiltersSectionID = "saved-filters";
|
||||||
|
|
||||||
export const FilteredSidebarHeader: React.FC<{
|
export const FilteredSidebarHeader: React.FC<{
|
||||||
sidebarOpen: boolean;
|
sidebarOpen: boolean;
|
||||||
showEditFilter: () => void;
|
showEditFilter: () => void;
|
||||||
|
|
@ -60,6 +62,7 @@ export const FilteredSidebarHeader: React.FC<{
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
className="sidebar-saved-filters"
|
className="sidebar-saved-filters"
|
||||||
text={<FormattedMessage id="search_filter.saved_filters" />}
|
text={<FormattedMessage id="search_filter.saved_filters" />}
|
||||||
|
sectionID={savedFiltersSectionID}
|
||||||
>
|
>
|
||||||
<SidebarSavedFilterList
|
<SidebarSavedFilterList
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,8 @@ export const SidebarPerformersFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -119,7 +120,7 @@ export const SidebarPerformersFilter: React.FC<{
|
||||||
useQuery: usePerformerQueryFilter,
|
useQuery: usePerformerQueryFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PerformersFilter;
|
export default PerformersFilter;
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ interface ISidebarFilter {
|
||||||
option: CriterionOption;
|
option: CriterionOption;
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
|
sectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const any = "any";
|
const any = "any";
|
||||||
|
|
@ -87,6 +88,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
||||||
option,
|
option,
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
|
@ -199,6 +201,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
||||||
singleValue
|
singleValue
|
||||||
preCandidates={ratingValue === null ? ratingStars : undefined}
|
preCandidates={ratingValue === null ? ratingStars : undefined}
|
||||||
preSelected={ratingValue !== null ? ratingStars : undefined}
|
preSelected={ratingValue !== null ? ratingStars : undefined}
|
||||||
|
sectionID={sectionID}
|
||||||
/>
|
/>
|
||||||
<div></div>
|
<div></div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,8 @@ export const SidebarListFilter: React.FC<{
|
||||||
preCandidates?: React.ReactNode;
|
preCandidates?: React.ReactNode;
|
||||||
postCandidates?: React.ReactNode;
|
postCandidates?: React.ReactNode;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
|
// used to store open/closed state in SidebarStateContext
|
||||||
|
sectionID?: string;
|
||||||
}> = ({
|
}> = ({
|
||||||
title,
|
title,
|
||||||
selected,
|
selected,
|
||||||
|
|
@ -292,6 +294,7 @@ export const SidebarListFilter: React.FC<{
|
||||||
preSelected,
|
preSelected,
|
||||||
postSelected,
|
postSelected,
|
||||||
onOpen,
|
onOpen,
|
||||||
|
sectionID,
|
||||||
}) => {
|
}) => {
|
||||||
// TODO - sort items?
|
// TODO - sort items?
|
||||||
|
|
||||||
|
|
@ -325,6 +328,7 @@ export const SidebarListFilter: React.FC<{
|
||||||
<SidebarSection
|
<SidebarSection
|
||||||
className="sidebar-list-filter"
|
className="sidebar-list-filter"
|
||||||
text={title}
|
text={title}
|
||||||
|
sectionID={sectionID}
|
||||||
outsideCollapse={
|
outsideCollapse={
|
||||||
<>
|
<>
|
||||||
{preSelected ? <div className="extra">{preSelected}</div> : null}
|
{preSelected ? <div className="extra">{preSelected}</div> : null}
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,8 @@ export const SidebarStudiosFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -110,7 +111,7 @@ export const SidebarStudiosFilter: React.FC<{
|
||||||
includeSubMessageID: "subsidiary_studios",
|
includeSubMessageID: "subsidiary_studios",
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StudiosFilter;
|
export default StudiosFilter;
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,8 @@ export const SidebarTagsFilter: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
setFilter: (f: ListFilterModel) => void;
|
setFilter: (f: ListFilterModel) => void;
|
||||||
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
filterHook?: (f: ListFilterModel) => ListFilterModel;
|
||||||
}> = ({ title, option, filter, setFilter, filterHook }) => {
|
sectionID?: string;
|
||||||
|
}> = ({ title, option, filter, setFilter, filterHook, sectionID }) => {
|
||||||
const state = useLabeledIdFilterState({
|
const state = useLabeledIdFilterState({
|
||||||
filter,
|
filter,
|
||||||
setFilter,
|
setFilter,
|
||||||
|
|
@ -114,7 +115,7 @@ export const SidebarTagsFilter: React.FC<{
|
||||||
includeSubMessageID: "sub_tags",
|
includeSubMessageID: "sub_tags",
|
||||||
});
|
});
|
||||||
|
|
||||||
return <SidebarListFilter {...state} title={title} />;
|
return <SidebarListFilter {...state} title={title} sectionID={sectionID} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TagsFilter;
|
export default TagsFilter;
|
||||||
|
|
|
||||||
|
|
@ -16,22 +16,28 @@ import {
|
||||||
faTrash,
|
faTrash,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
export const OperationDropdown: React.FC<
|
export const OperationDropdown: React.FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
className?: string;
|
className?: string;
|
||||||
|
menuPortalTarget?: HTMLElement;
|
||||||
}>
|
}>
|
||||||
> = ({ className, children }) => {
|
> = ({ className, menuPortalTarget, children }) => {
|
||||||
if (!children) return null;
|
if (!children) return null;
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Dropdown.Menu className="bg-secondary text-white">
|
||||||
|
{children}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown className={className} as={ButtonGroup}>
|
<Dropdown className={className} as={ButtonGroup}>
|
||||||
<Dropdown.Toggle variant="secondary" id="more-menu">
|
<Dropdown.Toggle variant="secondary" id="more-menu">
|
||||||
<Icon icon={faEllipsisH} />
|
<Icon icon={faEllipsisH} />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
<Dropdown.Menu className="bg-secondary text-white">
|
{menuPortalTarget ? createPortal(menu, menuPortalTarget) : menu}
|
||||||
{children}
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { PaginationIndex } from "../List/Pagination";
|
import { Pagination, PaginationIndex } from "../List/Pagination";
|
||||||
import { ButtonToolbar } from "react-bootstrap";
|
import { ButtonToolbar } from "react-bootstrap";
|
||||||
import { ListViewOptions } from "../List/ListViewOptions";
|
import { ListViewOptions } from "../List/ListViewOptions";
|
||||||
import { PageSizeSelector, SortBySelect } from "../List/ListFilter";
|
import { PageSizeSelector, SortBySelect } from "../List/ListFilter";
|
||||||
|
|
@ -23,15 +23,6 @@ export const ListResultsHeader: React.FC<{
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ButtonToolbar className={cx(className, "list-results-header")}>
|
<ButtonToolbar className={cx(className, "list-results-header")}>
|
||||||
<div>
|
|
||||||
<PaginationIndex
|
|
||||||
loading={loading}
|
|
||||||
itemsPerPage={filter.itemsPerPage}
|
|
||||||
currentPage={filter.currentPage}
|
|
||||||
totalItems={totalCount}
|
|
||||||
metadataByline={metadataByline}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<SortBySelect
|
<SortBySelect
|
||||||
options={filter.options.sortByOptions}
|
options={filter.options.sortByOptions}
|
||||||
|
|
@ -61,6 +52,22 @@ export const ListResultsHeader: React.FC<{
|
||||||
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
|
onSetZoom={(zoom) => onChangeFilter(filter.setZoom(zoom))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="pagination-index-container">
|
||||||
|
<Pagination
|
||||||
|
currentPage={filter.currentPage}
|
||||||
|
itemsPerPage={filter.itemsPerPage}
|
||||||
|
totalItems={totalCount}
|
||||||
|
onChangePage={(page) => onChangeFilter(filter.changePage(page))}
|
||||||
|
/>
|
||||||
|
<PaginationIndex
|
||||||
|
loading={loading}
|
||||||
|
itemsPerPage={filter.itemsPerPage}
|
||||||
|
currentPage={filter.currentPage}
|
||||||
|
totalItems={totalCount}
|
||||||
|
metadataByline={metadataByline}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="empty-space"></div>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
import { faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FilterTags } from "../List/FilterTags";
|
import { FilterTags } from "../List/FilterTags";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { Button, ButtonToolbar } from "react-bootstrap";
|
import { Button, ButtonGroup, ButtonToolbar } from "react-bootstrap";
|
||||||
import { FilterButton } from "../List/Filters/FilterButton";
|
import { FilterButton } from "../List/Filters/FilterButton";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { SearchTermInput } from "../List/ListFilter";
|
import { SearchTermInput } from "../List/ListFilter";
|
||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { SidebarToggleButton } from "../Shared/Sidebar";
|
import { SidebarToggleButton } from "../Shared/Sidebar";
|
||||||
import { PatchComponent } from "src/patch";
|
import { PatchComponent } from "src/patch";
|
||||||
|
import { SavedFilterDropdown } from "./SavedFilterList";
|
||||||
|
import { View } from "./views";
|
||||||
|
|
||||||
export const ToolbarFilterSection: React.FC<{
|
export const ToolbarFilterSection: React.FC<{
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
|
|
@ -21,6 +23,7 @@ export const ToolbarFilterSection: React.FC<{
|
||||||
onRemoveAllCriterion: () => void;
|
onRemoveAllCriterion: () => void;
|
||||||
onEditSearchTerm: () => void;
|
onEditSearchTerm: () => void;
|
||||||
onRemoveSearchTerm: () => void;
|
onRemoveSearchTerm: () => void;
|
||||||
|
view?: View;
|
||||||
}> = PatchComponent(
|
}> = PatchComponent(
|
||||||
"ToolbarFilterSection",
|
"ToolbarFilterSection",
|
||||||
({
|
({
|
||||||
|
|
@ -32,6 +35,7 @@ export const ToolbarFilterSection: React.FC<{
|
||||||
onRemoveAllCriterion,
|
onRemoveAllCriterion,
|
||||||
onEditSearchTerm,
|
onEditSearchTerm,
|
||||||
onRemoveSearchTerm,
|
onRemoveSearchTerm,
|
||||||
|
view,
|
||||||
}) => {
|
}) => {
|
||||||
const { criteria, searchTerm } = filter;
|
const { criteria, searchTerm } = filter;
|
||||||
|
|
||||||
|
|
@ -41,10 +45,19 @@ export const ToolbarFilterSection: React.FC<{
|
||||||
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
|
<SearchTermInput filter={filter} onFilterUpdate={onSetFilter} />
|
||||||
</div>
|
</div>
|
||||||
<div className="filter-section">
|
<div className="filter-section">
|
||||||
<FilterButton
|
<ButtonGroup>
|
||||||
onClick={() => onEditCriterion()}
|
<SidebarToggleButton onClick={onToggleSidebar} />
|
||||||
count={criteria.length}
|
<SavedFilterDropdown
|
||||||
/>
|
filter={filter}
|
||||||
|
onSetFilter={onSetFilter}
|
||||||
|
view={view}
|
||||||
|
menuPortalTarget={document.body}
|
||||||
|
/>
|
||||||
|
<FilterButton
|
||||||
|
onClick={() => onEditCriterion()}
|
||||||
|
count={criteria.length}
|
||||||
|
/>
|
||||||
|
</ButtonGroup>
|
||||||
<FilterTags
|
<FilterTags
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
criteria={criteria}
|
criteria={criteria}
|
||||||
|
|
@ -55,7 +68,6 @@ export const ToolbarFilterSection: React.FC<{
|
||||||
onRemoveSearchTerm={onRemoveSearchTerm}
|
onRemoveSearchTerm={onRemoveSearchTerm}
|
||||||
truncateOnOverflow
|
truncateOnOverflow
|
||||||
/>
|
/>
|
||||||
<SidebarToggleButton onClick={onToggleSidebar} />
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
@ -65,28 +77,33 @@ export const ToolbarFilterSection: React.FC<{
|
||||||
export const ToolbarSelectionSection: React.FC<{
|
export const ToolbarSelectionSection: React.FC<{
|
||||||
selected: number;
|
selected: number;
|
||||||
onToggleSidebar: () => void;
|
onToggleSidebar: () => void;
|
||||||
|
operations?: React.ReactNode;
|
||||||
onSelectAll: () => void;
|
onSelectAll: () => void;
|
||||||
onSelectNone: () => void;
|
onSelectNone: () => void;
|
||||||
}> = PatchComponent(
|
}> = PatchComponent(
|
||||||
"ToolbarSelectionSection",
|
"ToolbarSelectionSection",
|
||||||
({ selected, onToggleSidebar, onSelectAll, onSelectNone }) => {
|
({ selected, onToggleSidebar, operations, onSelectAll, onSelectNone }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="selected-items-info">
|
<div className="toolbar-selection-section">
|
||||||
<Button
|
<div className="selected-items-info">
|
||||||
variant="secondary"
|
<SidebarToggleButton onClick={onToggleSidebar} />
|
||||||
className="minimal"
|
<Button
|
||||||
onClick={() => onSelectNone()}
|
variant="secondary"
|
||||||
title={intl.formatMessage({ id: "actions.select_none" })}
|
className="minimal"
|
||||||
>
|
onClick={() => onSelectNone()}
|
||||||
<Icon icon={faTimes} />
|
title={intl.formatMessage({ id: "actions.select_none" })}
|
||||||
</Button>
|
>
|
||||||
<span>{selected} selected</span>
|
<Icon icon={faTimes} />
|
||||||
<Button variant="link" onClick={() => onSelectAll()}>
|
</Button>
|
||||||
<FormattedMessage id="actions.select_all" />
|
<span>{selected} selected</span>
|
||||||
</Button>
|
<Button variant="link" onClick={() => onSelectAll()}>
|
||||||
<SidebarToggleButton onClick={onToggleSidebar} />
|
<FormattedMessage id="actions.select_all" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{operations}
|
||||||
|
<div className="empty-space" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +131,11 @@ export const FilteredListToolbar2: React.FC<{
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!hasSelection ? filterSection : selectionSection}
|
{!hasSelection ? filterSection : selectionSection}
|
||||||
<div className="filtered-list-toolbar-operations">{operationSection}</div>
|
{!hasSelection ? (
|
||||||
|
<div className="filtered-list-toolbar-operations">
|
||||||
|
{operationSection}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const PageCount: React.FC<{
|
||||||
useStopWheelScroll(pageInput);
|
useStopWheelScroll(pageInput);
|
||||||
|
|
||||||
const pageOptions = useMemo(() => {
|
const pageOptions = useMemo(() => {
|
||||||
const maxPagesToShow = 10;
|
const maxPagesToShow = 1000;
|
||||||
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
||||||
const max = Math.min(min + maxPagesToShow, totalPages);
|
const max = Math.min(min + maxPagesToShow, totalPages);
|
||||||
const pages = [];
|
const pages = [];
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { AlertModal } from "../Shared/Alert";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { TruncatedInlineText } from "../Shared/TruncatedText";
|
import { TruncatedInlineText } from "../Shared/TruncatedText";
|
||||||
import { OperationButton } from "../Shared/OperationButton";
|
import { OperationButton } from "../Shared/OperationButton";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
const ExistingSavedFilterList: React.FC<{
|
const ExistingSavedFilterList: React.FC<{
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -243,6 +244,7 @@ interface ISavedFilterListProps {
|
||||||
filter: ListFilterModel;
|
filter: ListFilterModel;
|
||||||
onSetFilter: (f: ListFilterModel) => void;
|
onSetFilter: (f: ListFilterModel) => void;
|
||||||
view?: View;
|
view?: View;
|
||||||
|
menuPortalTarget?: Element | DocumentFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
export const SavedFilterList: React.FC<ISavedFilterListProps> = ({
|
||||||
|
|
@ -841,8 +843,15 @@ export const SavedFilterDropdown: React.FC<ISavedFilterListProps> = (props) => {
|
||||||
));
|
));
|
||||||
SavedFilterDropdownRef.displayName = "SavedFilterDropdown";
|
SavedFilterDropdownRef.displayName = "SavedFilterDropdown";
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<Dropdown.Menu
|
||||||
|
as={SavedFilterDropdownRef}
|
||||||
|
className="saved-filter-list-menu"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown as={ButtonGroup}>
|
<Dropdown as={ButtonGroup} className="saved-filter-dropdown">
|
||||||
<OverlayTrigger
|
<OverlayTrigger
|
||||||
placement="top"
|
placement="top"
|
||||||
overlay={
|
overlay={
|
||||||
|
|
@ -855,10 +864,9 @@ export const SavedFilterDropdown: React.FC<ISavedFilterListProps> = (props) => {
|
||||||
<Icon icon={faBookmark} />
|
<Icon icon={faBookmark} />
|
||||||
</Dropdown.Toggle>
|
</Dropdown.Toggle>
|
||||||
</OverlayTrigger>
|
</OverlayTrigger>
|
||||||
<Dropdown.Menu
|
{props.menuPortalTarget
|
||||||
as={SavedFilterDropdownRef}
|
? createPortal(menu, props.menuPortalTarget)
|
||||||
className="saved-filter-list-menu"
|
: menu}
|
||||||
/>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hide zoom slider in xs viewport
|
||||||
|
@include media-breakpoint-down(xs) {
|
||||||
|
.display-mode-menu .zoom-slider-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.display-mode-popover {
|
.display-mode-popover {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|
@ -1048,7 +1055,7 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide sidebar Edit Filter button on larger screens
|
// hide sidebar Edit Filter button on larger screens
|
||||||
@include media-breakpoint-up(lg) {
|
@include media-breakpoint-up(md) {
|
||||||
.sidebar .edit-filter-button {
|
.sidebar .edit-filter-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
@ -1064,6 +1071,7 @@ input[type="range"].zoom-slider {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 0;
|
||||||
row-gap: 1rem;
|
row-gap: 1rem;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
|
@ -1094,10 +1102,6 @@ input[type="range"].zoom-slider {
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-items-info .btn {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hide drop down menu items for play and create new
|
// hide drop down menu items for play and create new
|
||||||
// when the buttons are visible
|
// when the buttons are visible
|
||||||
@include media-breakpoint-up(sm) {
|
@include media-breakpoint-up(sm) {
|
||||||
|
|
@ -1118,7 +1122,7 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.selected-items-info,
|
.toolbar-selection-section,
|
||||||
div.filter-section {
|
div.filter-section {
|
||||||
border: 1px solid $secondary;
|
border: 1px solid $secondary;
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
@ -1126,13 +1130,69 @@ input[type="range"].zoom-slider {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-toggle-button {
|
div.toolbar-selection-section {
|
||||||
margin-left: auto;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.sidebar-toggle-button {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-items-info {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:first-child,
|
||||||
|
> div:last-child {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-list-operations {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// on smaller viewports move the operation buttons to the right
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
div.scene-list-operations {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div:last-child {
|
||||||
|
flex: 0;
|
||||||
|
order: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on larger viewports, move the operation buttons to the center
|
||||||
|
@include media-breakpoint-up(lg) {
|
||||||
|
div.toolbar-selection-section div.scene-list-operations {
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
> .btn-group {
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.toolbar-selection-section .empty-space {
|
||||||
|
flex: 1;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
border-right: 1px solid $secondary;
|
border-right: 1px solid $secondary;
|
||||||
display: block;
|
display: flex;
|
||||||
margin-right: -0.5rem;
|
margin-right: -0.5rem;
|
||||||
min-width: calc($sidebar-width - 15px);
|
min-width: calc($sidebar-width - 15px);
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
|
@ -1168,21 +1228,27 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
// hide the search box in the toolbar when sidebar is shown on larger screens
|
||||||
|
// larger screens don't overlap the sidebar
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-container {
|
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .search-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// hide the search box when sidebar is hidden on smaller screens
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(md) {
|
||||||
.sidebar-pane.hide-sidebar .filtered-list-toolbar .search-container {
|
.sidebar-pane.hide-sidebar .filtered-list-toolbar .search-container {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide the filter icon button when sidebar is shown on smaller screens
|
// hide the filter and saved filters icon buttons when sidebar is shown on smaller screens
|
||||||
@include media-breakpoint-down(md) {
|
@include media-breakpoint-down(sm) {
|
||||||
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar .filter-button {
|
.sidebar-pane:not(.hide-sidebar) .filtered-list-toolbar {
|
||||||
display: none;
|
.filter-button,
|
||||||
|
.saved-filter-dropdown {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust the width of the filter-tags as well
|
// adjust the width of the filter-tags as well
|
||||||
|
|
@ -1191,8 +1257,8 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// move the sidebar toggle to the left on xl viewports
|
// move the sidebar toggle to the left on larger viewports
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(md) {
|
||||||
.filtered-list-toolbar .filter-section {
|
.filtered-list-toolbar .filter-section {
|
||||||
.sidebar-toggle-button {
|
.sidebar-toggle-button {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
@ -1239,17 +1305,21 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-results-header {
|
.list-results-header {
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
background-color: $body-bg;
|
background-color: $body-bg;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&.pagination-index-container {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
@ -1257,19 +1327,72 @@ input[type="range"].zoom-slider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-results-header {
|
.list-results-header .pagination-index-container {
|
||||||
flex-wrap: wrap-reverse;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
// hidden by default. Can be shown via css override if needed
|
||||||
|
display: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-results-header {
|
||||||
|
gap: 0.25rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
.paginationIndex {
|
.paginationIndex {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// move pagination info to right on medium screens
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
& > .empty-space {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.pagination-index-container {
|
||||||
|
align-items: flex-end;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// center the header on smaller screens
|
// center the header on smaller screens
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
& > div,
|
& > div,
|
||||||
& > div:last-child {
|
& > div.pagination-index-container {
|
||||||
|
flex-basis: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.pagination-index-container {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sidebar visible styling
|
||||||
|
.sidebar-pane:not(.hide-sidebar) .list-results-header {
|
||||||
|
// move pagination info to right on medium screens when sidebar
|
||||||
|
@include media-breakpoint-down(lg) {
|
||||||
|
& > .empty-space {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div.pagination-index-container {
|
||||||
|
justify-content: flex-end;
|
||||||
|
order: 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// center the header on smaller screens when sidebar is visible
|
||||||
|
@include media-breakpoint-down(md) {
|
||||||
|
& > div,
|
||||||
|
& > div.pagination-index-container {
|
||||||
flex-basis: 100%;
|
flex-basis: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,13 @@ import * as GQL from "src/core/generated-graphql";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
|
|
||||||
|
function locationEquals(
|
||||||
|
loc1: ReturnType<typeof useLocation> | undefined,
|
||||||
|
loc2: ReturnType<typeof useLocation>
|
||||||
|
) {
|
||||||
|
return loc1 && loc1.pathname === loc2.pathname && loc1.search === loc2.search;
|
||||||
|
}
|
||||||
|
|
||||||
export function useFilterURL(
|
export function useFilterURL(
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
setFilter: React.Dispatch<React.SetStateAction<ListFilterModel>>,
|
setFilter: React.Dispatch<React.SetStateAction<ListFilterModel>>,
|
||||||
|
|
@ -49,7 +56,7 @@ export function useFilterURL(
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// don't apply if active is false
|
// don't apply if active is false
|
||||||
// also don't apply if location is unchanged
|
// also don't apply if location is unchanged
|
||||||
if (!active || prevLocation === location) return;
|
if (!active || locationEquals(prevLocation, location)) return;
|
||||||
|
|
||||||
// re-init to load default filter on empty new query params
|
// re-init to load default filter on empty new query params
|
||||||
if (!location.search) {
|
if (!location.search) {
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
||||||
import { LightboxLink } from "src/hooks/Lightbox/LightboxLink";
|
import { LightboxLink } from "src/hooks/Lightbox/LightboxLink";
|
||||||
import { PatchComponent } from "src/patch";
|
import { PatchComponent } from "src/patch";
|
||||||
import { ILightboxImage } from "src/hooks/Lightbox/types";
|
import { ILightboxImage } from "src/hooks/Lightbox/types";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
|
|
@ -330,7 +331,7 @@ const PerformerPage: React.FC<IProps> = PatchComponent(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.goBack();
|
goBackOrReplace(history, "/performers");
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleEditing(value?: boolean) {
|
function toggleEditing(value?: boolean) {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ import { lazyComponent } from "src/utils/lazyComponent";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
const SubmitStashBoxDraft = lazyComponent(
|
const SubmitStashBoxDraft = lazyComponent(
|
||||||
() => import("src/components/Dialogs/SubmitDraft")
|
() => import("src/components/Dialogs/SubmitDraft")
|
||||||
|
|
@ -909,7 +910,7 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
|
||||||
) {
|
) {
|
||||||
loadScene(queueScenes[currentQueueIndex + 1].id);
|
loadScene(queueScenes[currentQueueIndex + 1].id);
|
||||||
} else {
|
} else {
|
||||||
history.goBack();
|
goBackOrReplace(history, "/scenes");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,13 @@ import {
|
||||||
OperationDropdownItem,
|
OperationDropdownItem,
|
||||||
} from "../List/ListOperationButtons";
|
} from "../List/ListOperationButtons";
|
||||||
import { useFilteredItemList } from "../List/ItemList";
|
import { useFilteredItemList } from "../List/ItemList";
|
||||||
import { Sidebar, SidebarPane, useSidebarState } from "../Shared/Sidebar";
|
import {
|
||||||
|
Sidebar,
|
||||||
|
SidebarPane,
|
||||||
|
SidebarPaneContent,
|
||||||
|
SidebarStateContext,
|
||||||
|
useSidebarState,
|
||||||
|
} from "../Shared/Sidebar";
|
||||||
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
import { SidebarPerformersFilter } from "../List/Filters/PerformersFilter";
|
||||||
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
|
import { SidebarStudiosFilter } from "../List/Filters/StudiosFilter";
|
||||||
import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers";
|
import { PerformersCriterionOption } from "src/models/list-filter/criteria/performers";
|
||||||
|
|
@ -285,6 +291,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="studios"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SidebarPerformersFilter
|
<SidebarPerformersFilter
|
||||||
|
|
@ -294,6 +301,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="performers"
|
||||||
/>
|
/>
|
||||||
<SidebarTagsFilter
|
<SidebarTagsFilter
|
||||||
title={<FormattedMessage id="tags" />}
|
title={<FormattedMessage id="tags" />}
|
||||||
|
|
@ -302,6 +310,7 @@ const SidebarContent: React.FC<{
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
filterHook={filterHook}
|
filterHook={filterHook}
|
||||||
|
sectionID="tags"
|
||||||
/>
|
/>
|
||||||
<SidebarRatingFilter
|
<SidebarRatingFilter
|
||||||
title={<FormattedMessage id="rating" />}
|
title={<FormattedMessage id="rating" />}
|
||||||
|
|
@ -309,6 +318,7 @@ const SidebarContent: React.FC<{
|
||||||
option={RatingCriterionOption}
|
option={RatingCriterionOption}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
|
sectionID="rating"
|
||||||
/>
|
/>
|
||||||
<SidebarBooleanFilter
|
<SidebarBooleanFilter
|
||||||
title={<FormattedMessage id="organized" />}
|
title={<FormattedMessage id="organized" />}
|
||||||
|
|
@ -316,6 +326,7 @@ const SidebarContent: React.FC<{
|
||||||
option={OrganizedCriterionOption}
|
option={OrganizedCriterionOption}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
setFilter={setFilter}
|
setFilter={setFilter}
|
||||||
|
sectionID="organized"
|
||||||
/>
|
/>
|
||||||
</ScenesFilterSidebarSections>
|
</ScenesFilterSidebarSections>
|
||||||
|
|
||||||
|
|
@ -355,7 +366,7 @@ const SceneListOperations: React.FC<{
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="scene-list-operations">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{!!items && (
|
{!!items && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -396,7 +407,10 @@ const SceneListOperations: React.FC<{
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<OperationDropdown className="scene-list-operations">
|
<OperationDropdown
|
||||||
|
className="scene-list-operations"
|
||||||
|
menuPortalTarget={document.body}
|
||||||
|
>
|
||||||
{operations.map((o) => {
|
{operations.map((o) => {
|
||||||
if (o.isDisplayed && !o.isDisplayed()) {
|
if (o.isDisplayed && !o.isDisplayed()) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -439,6 +453,8 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
showSidebar,
|
showSidebar,
|
||||||
setShowSidebar,
|
setShowSidebar,
|
||||||
loading: sidebarStateLoading,
|
loading: sidebarStateLoading,
|
||||||
|
sectionOpen,
|
||||||
|
setSectionOpen,
|
||||||
} = useSidebarState(view);
|
} = useSidebarState(view);
|
||||||
|
|
||||||
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
const { filterState, queryResult, modalState, listSelect, showEditFilter } =
|
||||||
|
|
@ -518,7 +534,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
|
|
||||||
const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]);
|
const queue = useMemo(() => SceneQueue.fromListFilterModel(filter), [filter]);
|
||||||
|
|
||||||
const playRandom = usePlayRandom(filter, totalCount);
|
const playRandom = usePlayRandom(effectiveFilter, totalCount);
|
||||||
const playSelected = usePlaySelected(selectedIds);
|
const playSelected = usePlaySelected(selectedIds);
|
||||||
const playFirst = usePlayFirst();
|
const playFirst = usePlayFirst();
|
||||||
|
|
||||||
|
|
@ -666,6 +682,18 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
// render
|
// render
|
||||||
if (filterLoading || sidebarStateLoading) return null;
|
if (filterLoading || sidebarStateLoading) return null;
|
||||||
|
|
||||||
|
const operations = (
|
||||||
|
<SceneListOperations
|
||||||
|
items={items.length}
|
||||||
|
hasSelection={hasSelection}
|
||||||
|
operations={otherOperations}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
onPlay={onPlay}
|
||||||
|
onCreateNew={onCreateNew}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TaggerContext>
|
<TaggerContext>
|
||||||
<div
|
<div
|
||||||
|
|
@ -675,94 +703,90 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
||||||
>
|
>
|
||||||
{modal}
|
{modal}
|
||||||
|
|
||||||
<SidebarPane hideSidebar={!showSidebar}>
|
<SidebarStateContext.Provider value={{ sectionOpen, setSectionOpen }}>
|
||||||
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
<SidebarPane hideSidebar={!showSidebar}>
|
||||||
<SidebarContent
|
<Sidebar hide={!showSidebar} onHide={() => setShowSidebar(false)}>
|
||||||
filter={filter}
|
<SidebarContent
|
||||||
setFilter={setFilter}
|
filter={filter}
|
||||||
filterHook={filterHook}
|
setFilter={setFilter}
|
||||||
showEditFilter={showEditFilter}
|
filterHook={filterHook}
|
||||||
view={view}
|
showEditFilter={showEditFilter}
|
||||||
sidebarOpen={showSidebar}
|
view={view}
|
||||||
onClose={() => setShowSidebar(false)}
|
sidebarOpen={showSidebar}
|
||||||
count={cachedResult.loading ? undefined : totalCount}
|
onClose={() => setShowSidebar(false)}
|
||||||
focus={searchFocus}
|
count={cachedResult.loading ? undefined : totalCount}
|
||||||
/>
|
focus={searchFocus}
|
||||||
</Sidebar>
|
/>
|
||||||
<div>
|
</Sidebar>
|
||||||
<FilteredListToolbar2
|
<SidebarPaneContent>
|
||||||
className="scene-list-toolbar"
|
<FilteredListToolbar2
|
||||||
hasSelection={hasSelection}
|
className="scene-list-toolbar"
|
||||||
filterSection={
|
hasSelection={hasSelection}
|
||||||
<ToolbarFilterSection
|
filterSection={
|
||||||
filter={filter}
|
<ToolbarFilterSection
|
||||||
onSetFilter={setFilter}
|
filter={filter}
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
onSetFilter={setFilter}
|
||||||
onEditCriterion={(c) =>
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||||
showEditFilter(c?.criterionOption.type)
|
onEditCriterion={(c) =>
|
||||||
}
|
showEditFilter(c?.criterionOption.type)
|
||||||
onRemoveCriterion={removeCriterion}
|
}
|
||||||
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
onRemoveCriterion={removeCriterion}
|
||||||
onEditSearchTerm={() => {
|
onRemoveAllCriterion={() => clearAllCriteria(true)}
|
||||||
setShowSidebar(true);
|
onEditSearchTerm={() => {
|
||||||
setSearchFocus(true);
|
setShowSidebar(true);
|
||||||
}}
|
setSearchFocus(true);
|
||||||
onRemoveSearchTerm={() => setFilter(filter.clearSearchTerm())}
|
}}
|
||||||
/>
|
onRemoveSearchTerm={() =>
|
||||||
}
|
setFilter(filter.clearSearchTerm())
|
||||||
selectionSection={
|
}
|
||||||
<ToolbarSelectionSection
|
view={view}
|
||||||
selected={selectedIds.size}
|
/>
|
||||||
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
}
|
||||||
onSelectAll={() => onSelectAll()}
|
selectionSection={
|
||||||
onSelectNone={() => onSelectNone()}
|
<ToolbarSelectionSection
|
||||||
/>
|
selected={selectedIds.size}
|
||||||
}
|
onToggleSidebar={() => setShowSidebar(!showSidebar)}
|
||||||
operationSection={
|
onSelectAll={() => onSelectAll()}
|
||||||
<SceneListOperations
|
onSelectNone={() => onSelectNone()}
|
||||||
items={items.length}
|
operations={operations}
|
||||||
hasSelection={hasSelection}
|
/>
|
||||||
operations={otherOperations}
|
}
|
||||||
onEdit={onEdit}
|
operationSection={operations}
|
||||||
onDelete={onDelete}
|
|
||||||
onPlay={onPlay}
|
|
||||||
onCreateNew={onCreateNew}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ListResultsHeader
|
|
||||||
loading={cachedResult.loading}
|
|
||||||
filter={filter}
|
|
||||||
totalCount={totalCount}
|
|
||||||
metadataByline={metadataByline}
|
|
||||||
onChangeFilter={(newFilter) => setFilter(newFilter)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LoadedContent loading={result.loading} error={result.error}>
|
|
||||||
<SceneList
|
|
||||||
filter={effectiveFilter}
|
|
||||||
scenes={items}
|
|
||||||
selectedIds={selectedIds}
|
|
||||||
onSelectChange={onSelectChange}
|
|
||||||
fromGroupId={fromGroupId}
|
|
||||||
/>
|
/>
|
||||||
</LoadedContent>
|
|
||||||
|
|
||||||
{totalCount > filter.itemsPerPage && (
|
<ListResultsHeader
|
||||||
<div className="pagination-footer">
|
loading={cachedResult.loading}
|
||||||
<Pagination
|
filter={filter}
|
||||||
itemsPerPage={filter.itemsPerPage}
|
totalCount={totalCount}
|
||||||
currentPage={filter.currentPage}
|
metadataByline={metadataByline}
|
||||||
totalItems={totalCount}
|
onChangeFilter={(newFilter) => setFilter(newFilter)}
|
||||||
metadataByline={metadataByline}
|
/>
|
||||||
onChangePage={setPage}
|
|
||||||
pagePopupPlacement="top"
|
<LoadedContent loading={result.loading} error={result.error}>
|
||||||
|
<SceneList
|
||||||
|
filter={effectiveFilter}
|
||||||
|
scenes={items}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectChange={onSelectChange}
|
||||||
|
fromGroupId={fromGroupId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</LoadedContent>
|
||||||
)}
|
|
||||||
</div>
|
{totalCount > filter.itemsPerPage && (
|
||||||
</SidebarPane>
|
<div className="pagination-footer">
|
||||||
|
<Pagination
|
||||||
|
itemsPerPage={filter.itemsPerPage}
|
||||||
|
currentPage={filter.currentPage}
|
||||||
|
totalItems={totalCount}
|
||||||
|
metadataByline={metadataByline}
|
||||||
|
onChangePage={setPage}
|
||||||
|
pagePopupPlacement="top"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SidebarPaneContent>
|
||||||
|
</SidebarPane>
|
||||||
|
</SidebarStateContext.Provider>
|
||||||
</div>
|
</div>
|
||||||
</TaggerContext>
|
</TaggerContext>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faChevronUp,
|
faChevronUp,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Collapse, CollapseProps } from "react-bootstrap";
|
import { Button, Collapse, CollapseProps } from "react-bootstrap";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
|
||||||
|
|
@ -12,22 +12,27 @@ interface IProps {
|
||||||
text: React.ReactNode;
|
text: React.ReactNode;
|
||||||
collapseProps?: Partial<CollapseProps>;
|
collapseProps?: Partial<CollapseProps>;
|
||||||
outsideCollapse?: React.ReactNode;
|
outsideCollapse?: React.ReactNode;
|
||||||
onOpen?: () => void;
|
onOpenChanged?: (o: boolean) => void;
|
||||||
|
open?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
export const CollapseButton: React.FC<React.PropsWithChildren<IProps>> = (
|
||||||
props: React.PropsWithChildren<IProps>
|
props: React.PropsWithChildren<IProps>
|
||||||
) => {
|
) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(props.open ?? false);
|
||||||
|
|
||||||
function toggleOpen() {
|
function toggleOpen() {
|
||||||
const nv = !open;
|
const nv = !open;
|
||||||
setOpen(nv);
|
setOpen(nv);
|
||||||
if (props.onOpen && nv) {
|
props.onOpenChanged?.(nv);
|
||||||
props.onOpen();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.open !== undefined) {
|
||||||
|
setOpen(props.open);
|
||||||
|
}
|
||||||
|
}, [props.open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={props.className}>
|
<div className={props.className}>
|
||||||
<div className="collapse-header">
|
<div className="collapse-header">
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,12 @@ import { Button, CollapseProps } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
import { faSliders } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
const fixedSidebarMediaQuery = "only screen and (max-width: 1199px)";
|
export type SidebarSectionStates = Record<string, boolean>;
|
||||||
|
|
||||||
|
// this needs to correspond to the CSS media query that overlaps the sidebar over content
|
||||||
|
const fixedSidebarMediaQuery = "only screen and (max-width: 767px)";
|
||||||
|
|
||||||
export const Sidebar: React.FC<
|
export const Sidebar: React.FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
|
|
@ -56,14 +60,53 @@ export const SidebarPane: React.FC<
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const SidebarPaneContent: React.FC = ({ children }) => {
|
||||||
|
return <div className="sidebar-pane-content">{children}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IContext {
|
||||||
|
sectionOpen: SidebarSectionStates;
|
||||||
|
setSectionOpen: (section: string, open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarStateContext = React.createContext<IContext | null>(null);
|
||||||
|
|
||||||
export const SidebarSection: React.FC<
|
export const SidebarSection: React.FC<
|
||||||
PropsWithChildren<{
|
PropsWithChildren<{
|
||||||
text: React.ReactNode;
|
text: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
outsideCollapse?: React.ReactNode;
|
outsideCollapse?: React.ReactNode;
|
||||||
onOpen?: () => void;
|
onOpen?: () => void;
|
||||||
|
// used to store open/closed state in SidebarStateContext
|
||||||
|
sectionID?: string;
|
||||||
}>
|
}>
|
||||||
> = ({ className = "", text, outsideCollapse, onOpen, children }) => {
|
> = ({
|
||||||
|
className = "",
|
||||||
|
text,
|
||||||
|
outsideCollapse,
|
||||||
|
onOpen,
|
||||||
|
sectionID = "",
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
// this is optional
|
||||||
|
const contextState = React.useContext(SidebarStateContext);
|
||||||
|
const openState =
|
||||||
|
!contextState || !sectionID
|
||||||
|
? undefined
|
||||||
|
: contextState.sectionOpen[sectionID] ?? undefined;
|
||||||
|
|
||||||
|
function onOpenInternal(open: boolean) {
|
||||||
|
if (contextState && sectionID) {
|
||||||
|
contextState.setSectionOpen(sectionID, open);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openState && onOpen) {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
}, [openState, onOpen]);
|
||||||
|
|
||||||
const collapseProps: Partial<CollapseProps> = {
|
const collapseProps: Partial<CollapseProps> = {
|
||||||
mountOnEnter: true,
|
mountOnEnter: true,
|
||||||
unmountOnExit: true,
|
unmountOnExit: true,
|
||||||
|
|
@ -74,7 +117,8 @@ export const SidebarSection: React.FC<
|
||||||
collapseProps={collapseProps}
|
collapseProps={collapseProps}
|
||||||
text={text}
|
text={text}
|
||||||
outsideCollapse={outsideCollapse}
|
outsideCollapse={outsideCollapse}
|
||||||
onOpen={onOpen}
|
onOpenChanged={onOpenInternal}
|
||||||
|
open={openState}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</CollapseButton>
|
</CollapseButton>
|
||||||
|
|
@ -87,7 +131,7 @@ export const SidebarToggleButton: React.FC<{
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="minimal sidebar-toggle-button ignore-sidebar-outside-click"
|
className="sidebar-toggle-button ignore-sidebar-outside-click"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={intl.formatMessage({ id: "actions.sidebar.toggle" })}
|
title={intl.formatMessage({ id: "actions.sidebar.toggle" })}
|
||||||
|
|
@ -105,6 +149,7 @@ export function defaultShowSidebar() {
|
||||||
export function useSidebarState(view?: View) {
|
export function useSidebarState(view?: View) {
|
||||||
const [interfaceLocalForage, setInterfaceLocalForage] =
|
const [interfaceLocalForage, setInterfaceLocalForage] =
|
||||||
useInterfaceLocalForage();
|
useInterfaceLocalForage();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const { data: interfaceLocalForageData, loading } = interfaceLocalForage;
|
const { data: interfaceLocalForageData, loading } = interfaceLocalForage;
|
||||||
|
|
||||||
|
|
@ -113,6 +158,7 @@ export function useSidebarState(view?: View) {
|
||||||
}, [view, interfaceLocalForageData]);
|
}, [view, interfaceLocalForageData]);
|
||||||
|
|
||||||
const [showSidebar, setShowSidebar] = useState<boolean>();
|
const [showSidebar, setShowSidebar] = useState<boolean>();
|
||||||
|
const [sectionOpen, setSectionOpen] = useState<SidebarSectionStates>();
|
||||||
|
|
||||||
// set initial state once loading is done
|
// set initial state once loading is done
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -127,7 +173,17 @@ export function useSidebarState(view?: View) {
|
||||||
|
|
||||||
// only show sidebar by default on large screens
|
// only show sidebar by default on large screens
|
||||||
setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar());
|
setShowSidebar(!!viewConfig.showSidebar && defaultShowSidebar());
|
||||||
}, [view, loading, showSidebar, viewConfig.showSidebar]);
|
setSectionOpen(
|
||||||
|
(history.location.state as { sectionOpen?: SidebarSectionStates })
|
||||||
|
?.sectionOpen || {}
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
view,
|
||||||
|
loading,
|
||||||
|
showSidebar,
|
||||||
|
viewConfig.showSidebar,
|
||||||
|
history.location.state,
|
||||||
|
]);
|
||||||
|
|
||||||
const onSetShowSidebar = useCallback(
|
const onSetShowSidebar = useCallback(
|
||||||
(show: boolean | ((prevState: boolean | undefined) => boolean)) => {
|
(show: boolean | ((prevState: boolean | undefined) => boolean)) => {
|
||||||
|
|
@ -149,9 +205,28 @@ export function useSidebarState(view?: View) {
|
||||||
[showSidebar, setInterfaceLocalForage, view, viewConfig]
|
[showSidebar, setInterfaceLocalForage, view, viewConfig]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSetSectionOpen = useCallback(
|
||||||
|
(section: string, open: boolean) => {
|
||||||
|
const newSectionOpen = { ...sectionOpen, [section]: open };
|
||||||
|
setSectionOpen(newSectionOpen);
|
||||||
|
if (view === undefined) return;
|
||||||
|
|
||||||
|
history.replace({
|
||||||
|
...history.location,
|
||||||
|
state: {
|
||||||
|
...(history.location.state as {}),
|
||||||
|
sectionOpen: newSectionOpen,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[sectionOpen, view, history]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showSidebar: showSidebar ?? defaultShowSidebar(),
|
showSidebar: showSidebar ?? defaultShowSidebar(),
|
||||||
|
sectionOpen: sectionOpen || {},
|
||||||
setShowSidebar: onSetShowSidebar,
|
setShowSidebar: onSetShowSidebar,
|
||||||
|
setSectionOpen: onSetSectionOpen,
|
||||||
loading: showSidebar === undefined,
|
loading: showSidebar === undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -805,7 +805,7 @@ button.btn.favorite-button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(md) {
|
||||||
transition: margin-left 0.1s;
|
transition: margin-left 0.1s;
|
||||||
|
|
||||||
&:not(.hide-sidebar) {
|
&:not(.hide-sidebar) {
|
||||||
|
|
@ -910,12 +910,12 @@ $sticky-header-height: calc(50px + 3.3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
// on smaller viewports we want the sidebar to overlap content
|
// on smaller viewports we want the sidebar to overlap content
|
||||||
@include media-breakpoint-down(lg) {
|
@include media-breakpoint-down(sm) {
|
||||||
.sidebar-pane:not(.hide-sidebar) .sidebar {
|
.sidebar-pane:not(.hide-sidebar) .sidebar {
|
||||||
margin-right: -$sidebar-width;
|
margin-right: -$sidebar-width;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-pane > :nth-child(2) {
|
.sidebar-pane > .sidebar-pane-content {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -935,7 +935,7 @@ $sticky-header-height: calc(50px + 3.3rem);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@include media-breakpoint-up(xl) {
|
@include media-breakpoint-up(md) {
|
||||||
.sidebar-pane:not(.hide-sidebar) {
|
.sidebar-pane:not(.hide-sidebar) {
|
||||||
> :nth-child(2) {
|
> :nth-child(2) {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
|
||||||
import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton";
|
import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton";
|
||||||
import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
||||||
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
|
|
@ -378,7 +379,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.goBack();
|
goBackOrReplace(history, "/studios");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ import { ExpandCollapseButton } from "src/components/Shared/CollapseButton";
|
||||||
import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
|
import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
|
||||||
import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
||||||
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
||||||
|
import { goBackOrReplace } from "src/utils/history";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment;
|
||||||
|
|
@ -420,7 +421,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
history.goBack();
|
goBackOrReplace(history, "/tags");
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDeleteAlert() {
|
function renderDeleteAlert() {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@
|
||||||
|
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* **[0.29.2]** Returned saved filters button to the top toolbar in the Scene list. ([#6215](https://github.com/stashapp/stash/pull/6215))
|
||||||
|
* **[0.29.2]** Top pagination can now be optionally shown in the scene list with [custom css](https://github.com/stashapp/stash/pull/6234#issue-3593190476). ([#6234](https://github.com/stashapp/stash/pull/6234))
|
||||||
|
* **[0.29.2]** Restyled the scene list toolbar based on user feedback. ([#6215](https://github.com/stashapp/stash/pull/6215))
|
||||||
|
* **[0.29.2]** Sidebar section collapsed state is now saved in the browser history. ([#6217](https://github.com/stashapp/stash/pull/6217))
|
||||||
|
* **[0.29.2]** Increased the number of pages in pagination dropdown to 1000. ([#6207](https://github.com/stashapp/stash/pull/6207))
|
||||||
* Revamped the scene and marker wall views. ([#5816](https://github.com/stashapp/stash/pull/5816))
|
* Revamped the scene and marker wall views. ([#5816](https://github.com/stashapp/stash/pull/5816))
|
||||||
* Added zoom functionality to wall views. ([#6011](https://github.com/stashapp/stash/pull/6011))
|
* Added zoom functionality to wall views. ([#6011](https://github.com/stashapp/stash/pull/6011))
|
||||||
* Added search term field to the Edit Filter dialog. ([#6082](https://github.com/stashapp/stash/pull/6082))
|
* Added search term field to the Edit Filter dialog. ([#6082](https://github.com/stashapp/stash/pull/6082))
|
||||||
|
|
@ -28,6 +33,16 @@
|
||||||
* Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760))
|
* Include IP address in login errors in log. ([#5760](https://github.com/stashapp/stash/pull/5760))
|
||||||
|
|
||||||
### 🐛 Bug fixes
|
### 🐛 Bug fixes
|
||||||
|
* **[0.29.3]** Fixed sidebar filter contents not loading. ([#6240](https://github.com/stashapp/stash/pull/6240))
|
||||||
|
* **[0.29.2]** Fixed Play Random not playing from the current filtered scenes on scene list sub-pages. ([#6202](https://github.com/stashapp/stash/pull/6202))
|
||||||
|
* **[0.29.2]** Fixed infinite loop in Group Sub-Groups panel. ([#6212](https://github.com/stashapp/stash/pull/6212))
|
||||||
|
* **[0.29.2]** Page no longer scrolls when selecting criterion for the first time in the Edit Filter dialog. ([#6205](https://github.com/stashapp/stash/pull/6205))
|
||||||
|
* **[0.29.2]** Zoom slider is no longer shown on mobile devices. ([#6206](https://github.com/stashapp/stash/pull/6206))
|
||||||
|
* **[0.29.2]** Fixed trailing space sometimes being trimmed from query string when querying. ([#6211](https://github.com/stashapp/stash/pull/6211))
|
||||||
|
* **[0.29.2]** Page now redirects to list page when deleting an object in a new browser tab. ([#6203](https://github.com/stashapp/stash/pull/6203))
|
||||||
|
* **[0.29.2]** Related groups can now be scraped when scraping a scene. ([#6228](https://github.com/stashapp/stash/pull/6228))
|
||||||
|
* **[0.29.2]** Fixed panic when a scraper configuration contains an unknown field. ([#6220](https://github.com/stashapp/stash/pull/6220))
|
||||||
|
* **[0.29.2]** Fixed panic when using `stash_box_index` input in scrape API calls. ([#6201](https://github.com/stashapp/stash/pull/6201))
|
||||||
* **[0.29.1]** Fixed password with special characters not allowing login. ([#6163](https://github.com/stashapp/stash/pull/6163))
|
* **[0.29.1]** Fixed password with special characters not allowing login. ([#6163](https://github.com/stashapp/stash/pull/6163))
|
||||||
* **[0.29.1]** Fixed layout issues using column direction for image wall. ([#6168](https://github.com/stashapp/stash/pull/6168))
|
* **[0.29.1]** Fixed layout issues using column direction for image wall. ([#6168](https://github.com/stashapp/stash/pull/6168))
|
||||||
* **[0.29.1]** Fixed layout issues for scene list table. ([#6169](https://github.com/stashapp/stash/pull/6169))
|
* **[0.29.1]** Fixed layout issues for scene list table. ([#6169](https://github.com/stashapp/stash/pull/6169))
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
## Library
|
## Library
|
||||||
|
|
||||||
This section allows you to add and remove directories from your library list. Files in these directories will be included when scanning. Files that are outside of these directories will be removed when running the Clean task.
|
This section enables you to add or remove directories that will be discoverable by Stash. The directories you add will be utilized for scanning new files and for updating their locations in Stash database.
|
||||||
|
|
||||||
|
You can configure these directories to apply specifically to:
|
||||||
|
|
||||||
|
- **Videos**
|
||||||
|
- **Images**
|
||||||
|
- **Both**
|
||||||
|
|
||||||
> **⚠️ Note:** Don't forget to click `Save` after updating these directories!
|
> **⚠️ Note:** Don't forget to click `Save` after updating these directories!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,86 @@
|
||||||
"blobs_path": {
|
"blobs_path": {
|
||||||
"description": "Къде във файловата система да се пазят бинарни данни. Позва се само ако се ползва Файлова система за блоб пазене. ВНИМАНИЕ: промяната ще изисква ръчно местене на съществуващи данни."
|
"description": "Къде във файловата система да се пазят бинарни данни. Позва се само ако се ползва Файлова система за блоб пазене. ВНИМАНИЕ: промяната ще изисква ръчно местене на съществуващи данни."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"custom_locales": {
|
||||||
|
"option_label": "Персонализирана локализация е активирана"
|
||||||
|
},
|
||||||
|
"delete_options": {
|
||||||
|
"description": "Настройки по подразбиране при триене на картини, галерий и сцени.",
|
||||||
|
"heading": "Изтриване на настройки",
|
||||||
|
"options": {
|
||||||
|
"delete_file": "Изтриване на файлове по подразбиране",
|
||||||
|
"delete_generated_supporting_files": "Изтриване на генерираните поддържащи файлове по подразбиране"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"desktop_integration": {
|
||||||
|
"desktop_integration": "Десктоп Интеграция",
|
||||||
|
"notifications_enabled": "Включване на известяването",
|
||||||
|
"send_desktop_notifications_for_events": "Изпащане на десктоп известявания за събития",
|
||||||
|
"skip_opening_browser": "Пропускане на отваряне на браузер",
|
||||||
|
"skip_opening_browser_on_startup": "Пропускане на автоматично отваряне на броузер по време на стартиране"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"compact_expanded_details": {
|
||||||
|
"description": "Когато е включена, тази настройка ще предосвати разширени детайли запавайки компактна презентация",
|
||||||
|
"heading": "Компактни разширени детайли"
|
||||||
|
},
|
||||||
|
"enable_background_image": {
|
||||||
|
"description": "Покажи фонова картина на станицата с детайли.",
|
||||||
|
"heading": "Включи фонова картина"
|
||||||
|
},
|
||||||
|
"heading": "Станица с детайли",
|
||||||
|
"show_all_details": {
|
||||||
|
"description": "Когато е включена, всичкото съдържание ще бъде показано по подразбиране и всеки детайл ще бъде в собствена колона",
|
||||||
|
"heading": "Покажи всички детайли"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editing": {
|
||||||
|
"disable_dropdown_create": {
|
||||||
|
"description": "Премахни възможноста да се създават нови обекти от падащият селектор",
|
||||||
|
"heading": "Изключи падащо създаване"
|
||||||
|
},
|
||||||
|
"heading": "Редактиране",
|
||||||
|
"max_options_shown": {
|
||||||
|
"label": "Максимален брой неща който се показват в падащ селектор"
|
||||||
|
},
|
||||||
|
"rating_system": {
|
||||||
|
"star_precision": {
|
||||||
|
"label": "Точност на звездния рейтинг",
|
||||||
|
"options": {
|
||||||
|
"full": "Цели",
|
||||||
|
"half": "Половинки",
|
||||||
|
"quarter": "Четвъртинки",
|
||||||
|
"tenth": "Десетици"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"label": "Тип система за рейтинг",
|
||||||
|
"options": {
|
||||||
|
"decimal": "Десетична",
|
||||||
|
"stars": "Звезди"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"funscript_offset": {
|
||||||
|
"description": "Време за разминаване в милисекунди за пускане на интерактивни скриптове.",
|
||||||
|
"heading": "Funscript разминаване (ms)"
|
||||||
|
},
|
||||||
|
"handy_connection": {
|
||||||
|
"connect": "Свързване",
|
||||||
|
"server_offset": {
|
||||||
|
"heading": "Сървърно разминаване"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"heading": "Статус на връзка с Handy"
|
||||||
|
},
|
||||||
|
"sync": "Синхронизиране"
|
||||||
|
},
|
||||||
|
"handy_connection_key": {
|
||||||
|
"description": "Handy connection key за ползване със интерактивни сцени. Слагането на този ключ ще позволи на Stash да сподели иформация за текущата сцена със handyfeeling.com"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,8 @@
|
||||||
},
|
},
|
||||||
"show_results": "Resultaat tonen",
|
"show_results": "Resultaat tonen",
|
||||||
"show_count_results": "{count} resultaten tonen",
|
"show_count_results": "{count} resultaten tonen",
|
||||||
"play": "Afspelen"
|
"play": "Afspelen",
|
||||||
|
"load_filter": "Laad filter"
|
||||||
},
|
},
|
||||||
"actions_name": "Acties",
|
"actions_name": "Acties",
|
||||||
"age": "Leeftijd",
|
"age": "Leeftijd",
|
||||||
|
|
@ -498,7 +499,8 @@
|
||||||
"source": "Bron",
|
"source": "Bron",
|
||||||
"source_options": "{source} Opties",
|
"source_options": "{source} Opties",
|
||||||
"sources": "Bronnen",
|
"sources": "Bronnen",
|
||||||
"strategy": "Strategie"
|
"strategy": "Strategie",
|
||||||
|
"skip_multiple_matches": "Sla overeenkomsten met meer dan één resultaat over"
|
||||||
},
|
},
|
||||||
"import_from_exported_json": "Import van geëxporteerde JSON in de map metadata. Maakt de bestaande database leeg.",
|
"import_from_exported_json": "Import van geëxporteerde JSON in de map metadata. Maakt de bestaande database leeg.",
|
||||||
"incremental_import": "Incrementele import uit een meegeleverde export zip-bestand.",
|
"incremental_import": "Incrementele import uit een meegeleverde export zip-bestand.",
|
||||||
|
|
@ -528,7 +530,13 @@
|
||||||
"anonymising_database": "Anonimiseer database",
|
"anonymising_database": "Anonimiseer database",
|
||||||
"anonymise_database": "Maakt een kopie van de database naar de backups-map, waarbij alle gevoelige gegevens anoniem worden gemaakt. Deze kan vervolgens aan anderen worden verstrekt voor probleemoplossing en debugging. De originele database wordt niet gewijzigd. De geanonimiseerde database gebruikt de bestandsnaamindeling {filename_format}.",
|
"anonymise_database": "Maakt een kopie van de database naar de backups-map, waarbij alle gevoelige gegevens anoniem worden gemaakt. Deze kan vervolgens aan anderen worden verstrekt voor probleemoplossing en debugging. De originele database wordt niet gewijzigd. De geanonimiseerde database gebruikt de bestandsnaamindeling {filename_format}.",
|
||||||
"anonymise_and_download": "Maakt een geanonimiseerde kopie van de database en downloadt het resulterende bestand.",
|
"anonymise_and_download": "Maakt een geanonimiseerde kopie van de database en downloadt het resulterende bestand.",
|
||||||
"generate_clip_previews_during_scan": "Genereer voorbeelden van Afbeelingsfragmenten"
|
"generate_clip_previews_during_scan": "Genereer voorbeelden van Afbeelingsfragmenten",
|
||||||
|
"migrate_blobs": {
|
||||||
|
"delete_old": "Verwijder oude gegevens"
|
||||||
|
},
|
||||||
|
"migrate_scene_screenshots": {
|
||||||
|
"delete_files": "Verwijder screenshotbestanden"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"tools": {
|
"tools": {
|
||||||
"scene_duplicate_checker": "Scène Duplicator Checker",
|
"scene_duplicate_checker": "Scène Duplicator Checker",
|
||||||
|
|
@ -547,7 +555,9 @@
|
||||||
"whitespace_chars": "WhiteSpace-tekens",
|
"whitespace_chars": "WhiteSpace-tekens",
|
||||||
"whitespace_chars_desc": "Deze tekens worden vervangen door witruimte in de titel"
|
"whitespace_chars_desc": "Deze tekens worden vervangen door witruimte in de titel"
|
||||||
},
|
},
|
||||||
"scene_tools": "Scene gereedschap"
|
"scene_tools": "Scene gereedschap",
|
||||||
|
"graphql_playground": "GraphQL speeltuin",
|
||||||
|
"heading": "Hulpmiddelen"
|
||||||
},
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"basic_settings": "Basis instellingen",
|
"basic_settings": "Basis instellingen",
|
||||||
|
|
@ -576,7 +586,15 @@
|
||||||
"description": "Verwijder de mogelijkheid om nieuwe objecten te maken uit de dropdown menu",
|
"description": "Verwijder de mogelijkheid om nieuwe objecten te maken uit de dropdown menu",
|
||||||
"heading": "Schakel het maken van dropdowns uit"
|
"heading": "Schakel het maken van dropdowns uit"
|
||||||
},
|
},
|
||||||
"heading": "Aanpassen"
|
"heading": "Aanpassen",
|
||||||
|
"rating_system": {
|
||||||
|
"type": {
|
||||||
|
"options": {
|
||||||
|
"decimal": "Decimaal",
|
||||||
|
"stars": "Sterren"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"funscript_offset": {
|
"funscript_offset": {
|
||||||
"description": "Time Offset in milliseconden voor het afspelen van interactieve scripts.",
|
"description": "Time Offset in milliseconden voor het afspelen van interactieve scripts.",
|
||||||
|
|
@ -654,7 +672,9 @@
|
||||||
"continue_playlist_default": {
|
"continue_playlist_default": {
|
||||||
"description": "Speel de volgende scène in de wachtrij wanneer video is voltooid",
|
"description": "Speel de volgende scène in de wachtrij wanneer video is voltooid",
|
||||||
"heading": "Ga Standaard door met de afspeellijst"
|
"heading": "Ga Standaard door met de afspeellijst"
|
||||||
}
|
},
|
||||||
|
"always_start_from_beginning": "Start video altijd vanaf het begin",
|
||||||
|
"enable_chromecast": "Chromecast inschakelen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scene_wall": {
|
"scene_wall": {
|
||||||
|
|
@ -672,7 +692,28 @@
|
||||||
"description": "Diavoorstelling is beschikbaar in galerijen in de muurweergavemodus",
|
"description": "Diavoorstelling is beschikbaar in galerijen in de muurweergavemodus",
|
||||||
"heading": "Diavoorstellingsvertraging (in seconden)"
|
"heading": "Diavoorstellingsvertraging (in seconden)"
|
||||||
},
|
},
|
||||||
"title": "Gebruikers interface"
|
"title": "Gebruikers interface",
|
||||||
|
"custom_javascript": {
|
||||||
|
"heading": "Aangepaste JavaScript",
|
||||||
|
"option_label": "Aangepaste JavaScript ingeschakeld"
|
||||||
|
},
|
||||||
|
"custom_locales": {
|
||||||
|
"heading": "Aangepaste lokalisatie",
|
||||||
|
"option_label": "Aangepaste lokalisatie ingeschakeld"
|
||||||
|
},
|
||||||
|
"detail": {
|
||||||
|
"enable_background_image": {
|
||||||
|
"description": "Achtergrondfoto op detailscherm weergeven.",
|
||||||
|
"heading": "Achtergrondfoto inschakelen"
|
||||||
|
},
|
||||||
|
"heading": "Detailscherm",
|
||||||
|
"show_all_details": {
|
||||||
|
"heading": "Alle details weergeven"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image_wall": {
|
||||||
|
"margin": "Marge (pixels)"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"advanced_mode": "Geavanceerde modus"
|
"advanced_mode": "Geavanceerde modus"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,8 @@
|
||||||
"play": "Oynat",
|
"play": "Oynat",
|
||||||
"show_results": "Sonuçları göster",
|
"show_results": "Sonuçları göster",
|
||||||
"show_count_results": "{count} sonucu göster",
|
"show_count_results": "{count} sonucu göster",
|
||||||
"load": "Yükle"
|
"load": "Yükle",
|
||||||
|
"load_filter": "Filtre yükle"
|
||||||
},
|
},
|
||||||
"actions_name": "Eylemler",
|
"actions_name": "Eylemler",
|
||||||
"age": "Yaş",
|
"age": "Yaş",
|
||||||
|
|
@ -1021,7 +1022,7 @@
|
||||||
"include_sub_tags": "Alt etiketleri dahil et",
|
"include_sub_tags": "Alt etiketleri dahil et",
|
||||||
"instagram": "Instagram",
|
"instagram": "Instagram",
|
||||||
"interactive": "Etkileşimli",
|
"interactive": "Etkileşimli",
|
||||||
"interactive_speed": "Etkileşim hızı",
|
"interactive_speed": "Etkileşimli Hız",
|
||||||
"isMissing": "Eksik",
|
"isMissing": "Eksik",
|
||||||
"library": "Kütüphane",
|
"library": "Kütüphane",
|
||||||
"loading": {
|
"loading": {
|
||||||
|
|
@ -1036,7 +1037,7 @@
|
||||||
"checksum": "Sağlama Toplamı (checksum)",
|
"checksum": "Sağlama Toplamı (checksum)",
|
||||||
"downloaded_from": "İndirildiği Yer",
|
"downloaded_from": "İndirildiği Yer",
|
||||||
"hash": "Dosya İmzası (Hash)",
|
"hash": "Dosya İmzası (Hash)",
|
||||||
"interactive_speed": "Etkileşim hızı",
|
"interactive_speed": "Etkileşimli Hız",
|
||||||
"performer_card": {
|
"performer_card": {
|
||||||
"age": "{age} {years_old}",
|
"age": "{age} {years_old}",
|
||||||
"age_context": "Bu sahnede {age} {years_old}"
|
"age_context": "Bu sahnede {age} {years_old}"
|
||||||
|
|
@ -1090,7 +1091,8 @@
|
||||||
"name": "Filtre",
|
"name": "Filtre",
|
||||||
"saved_filters": "Kaydedilmiş filtreler",
|
"saved_filters": "Kaydedilmiş filtreler",
|
||||||
"update_filter": "Filtreyi Güncelle",
|
"update_filter": "Filtreyi Güncelle",
|
||||||
"edit_filter": "Filtreyi Düzenle"
|
"edit_filter": "Filtreyi Düzenle",
|
||||||
|
"search_term": "Arama terimi"
|
||||||
},
|
},
|
||||||
"seconds": "Saniye",
|
"seconds": "Saniye",
|
||||||
"settings": "Ayarlar",
|
"settings": "Ayarlar",
|
||||||
|
|
@ -1135,7 +1137,7 @@
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"database_filename_empty_for_default": "veritabanı adı (varsayılan için boş bırakın)",
|
"database_filename_empty_for_default": "veritabanı adı (varsayılan için boş bırakın)",
|
||||||
"description": "Sırada porno koleksiyonunuzun hangi dizinde olduğunun, stash veritabanının ve oluşturulan ek dosyaların nereye kaydedileceğinin belirlenmesi var. Bu ayarları sonradan değiştirebilirsiniz.",
|
"description": "Sırada, porno koleksiyonunuzun nerede bulunacağını ve Stash veritabanının, oluşturulan dosyaların ve önbellek dosyalarının nerede depolanacağını belirlememiz gerekiyor. Bu ayarlar daha sonra gerekirse değiştirilebilir.",
|
||||||
"path_to_generated_directory_empty_for_default": "oluşturulan ek dosyalar için dizin konumu (varsayılan için boş bırakın)",
|
"path_to_generated_directory_empty_for_default": "oluşturulan ek dosyalar için dizin konumu (varsayılan için boş bırakın)",
|
||||||
"set_up_your_paths": "Yollarınızı ayarlayın",
|
"set_up_your_paths": "Yollarınızı ayarlayın",
|
||||||
"stash_alert": "Herhangi bir kütüphane konumu seçilmedi. Hiçbir medya Stash'e taranamayacak. Emin misiniz?",
|
"stash_alert": "Herhangi bir kütüphane konumu seçilmedi. Hiçbir medya Stash'e taranamayacak. Emin misiniz?",
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export const SceneIsMissingCriterionOption = new IsMissingCriterionOption(
|
||||||
"date",
|
"date",
|
||||||
"galleries",
|
"galleries",
|
||||||
"studio",
|
"studio",
|
||||||
"movie",
|
"group",
|
||||||
"performers",
|
"performers",
|
||||||
"tags",
|
"tags",
|
||||||
"stash_id",
|
"stash_id",
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ export class ListFilterModel {
|
||||||
ret.disp = Number.parseInt(params.disp, 10);
|
ret.disp = Number.parseInt(params.disp, 10);
|
||||||
}
|
}
|
||||||
if (params.q) {
|
if (params.q) {
|
||||||
ret.q = params.q.trim();
|
ret.q = params.q;
|
||||||
}
|
}
|
||||||
if (params.p) {
|
if (params.p) {
|
||||||
ret.p = Number.parseInt(params.p, 10);
|
ret.p = Number.parseInt(params.p, 10);
|
||||||
|
|
|
||||||
11
ui/v2.5/src/utils/history.ts
Normal file
11
ui/v2.5/src/utils/history.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
type History = ReturnType<typeof useHistory>;
|
||||||
|
|
||||||
|
export function goBackOrReplace(history: History, defaultPath: string) {
|
||||||
|
if (history.length > 1) {
|
||||||
|
history.goBack();
|
||||||
|
} else {
|
||||||
|
history.replace(defaultPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue