mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Image improvements (#847)
* Fix image performer filtering * Add performer images tab * Add studio images tab * Rename interface * Add tag images tab * Add path filtering for images * Show image stats on stats page * Fix incorrect scan counts after timeout * Add gallery filters * Relax scene gallery selector
This commit is contained in:
@@ -47,7 +47,9 @@ query ValidGalleriesForScene($scene_id: ID!) {
|
|||||||
query Stats {
|
query Stats {
|
||||||
stats {
|
stats {
|
||||||
scene_count,
|
scene_count,
|
||||||
scene_size_count,
|
scenes_size,
|
||||||
|
image_count,
|
||||||
|
images_size,
|
||||||
gallery_count,
|
gallery_count,
|
||||||
performer_count,
|
performer_count,
|
||||||
studio_count,
|
studio_count,
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ input GalleryFilterType {
|
|||||||
is_zip: Boolean
|
is_zip: Boolean
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
rating: IntCriterionInput
|
rating: IntCriterionInput
|
||||||
|
"""Filter by average image resolution"""
|
||||||
|
average_resolution: ResolutionEnum
|
||||||
"""Filter to only include scenes with this studio"""
|
"""Filter to only include scenes with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: MultiCriterionInput
|
||||||
"""Filter to only include scenes with these tags"""
|
"""Filter to only include scenes with these tags"""
|
||||||
@@ -133,6 +135,8 @@ input TagFilterType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input ImageFilterType {
|
input ImageFilterType {
|
||||||
|
"""Filter by path"""
|
||||||
|
path: StringCriterionInput
|
||||||
"""Filter by rating"""
|
"""Filter by rating"""
|
||||||
rating: IntCriterionInput
|
rating: IntCriterionInput
|
||||||
"""Filter by o-counter"""
|
"""Filter by o-counter"""
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
type StatsResultType {
|
type StatsResultType {
|
||||||
scene_count: Int!
|
scene_count: Int!
|
||||||
scene_size_count: String!
|
scenes_size: Int!
|
||||||
|
image_count: Int!
|
||||||
|
images_size: Int!
|
||||||
gallery_count: Int!
|
gallery_count: Int!
|
||||||
performer_count: Int!
|
performer_count: Int!
|
||||||
studio_count: Int!
|
studio_count: Int!
|
||||||
|
|||||||
@@ -117,7 +117,10 @@ func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *st
|
|||||||
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
|
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
|
||||||
scenesQB := models.NewSceneQueryBuilder()
|
scenesQB := models.NewSceneQueryBuilder()
|
||||||
scenesCount, _ := scenesQB.Count()
|
scenesCount, _ := scenesQB.Count()
|
||||||
scenesSizeCount, _ := scenesQB.SizeCount()
|
scenesSize, _ := scenesQB.Size()
|
||||||
|
imageQB := models.NewImageQueryBuilder()
|
||||||
|
imageCount, _ := imageQB.Count()
|
||||||
|
imageSize, _ := imageQB.Size()
|
||||||
galleryQB := models.NewGalleryQueryBuilder()
|
galleryQB := models.NewGalleryQueryBuilder()
|
||||||
galleryCount, _ := galleryQB.Count()
|
galleryCount, _ := galleryQB.Count()
|
||||||
performersQB := models.NewPerformerQueryBuilder()
|
performersQB := models.NewPerformerQueryBuilder()
|
||||||
@@ -130,7 +133,9 @@ func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, err
|
|||||||
tagsCount, _ := tagsQB.Count()
|
tagsCount, _ := tagsQB.Count()
|
||||||
return &models.StatsResultType{
|
return &models.StatsResultType{
|
||||||
SceneCount: scenesCount,
|
SceneCount: scenesCount,
|
||||||
SceneSizeCount: scenesSizeCount,
|
ScenesSize: int(scenesSize),
|
||||||
|
ImageCount: imageCount,
|
||||||
|
ImagesSize: int(imageSize),
|
||||||
GalleryCount: galleryCount,
|
GalleryCount: galleryCount,
|
||||||
PerformerCount: performersCount,
|
PerformerCount: performersCount,
|
||||||
StudioCount: studiosCount,
|
StudioCount: studiosCount,
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ func (s *singleton) neededScan() (total *int, newFiles *int) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err == timeoutErr {
|
if err == timeoutErr {
|
||||||
break
|
// timeout should return nil counts
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ func (qb *GalleryQueryBuilder) Query(galleryFilter *GalleryFilterType, findFilte
|
|||||||
}
|
}
|
||||||
|
|
||||||
query.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
|
query.handleStringCriterionInput(galleryFilter.Path, "galleries.path")
|
||||||
|
query.handleIntCriterionInput(galleryFilter.Rating, "galleries.rating")
|
||||||
|
qb.handleAverageResolutionFilter(&query, galleryFilter.AverageResolution)
|
||||||
|
|
||||||
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||||
switch *isMissingFilter {
|
switch *isMissingFilter {
|
||||||
@@ -265,6 +267,48 @@ func (qb *GalleryQueryBuilder) Query(galleryFilter *GalleryFilterType, findFilte
|
|||||||
return galleries, countResult
|
return galleries, countResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (qb *GalleryQueryBuilder) handleAverageResolutionFilter(query *queryBuilder, resolutionFilter *ResolutionEnum) {
|
||||||
|
if resolutionFilter == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolution := resolutionFilter.String(); resolutionFilter.IsValid() {
|
||||||
|
var low int
|
||||||
|
var high int
|
||||||
|
|
||||||
|
switch resolution {
|
||||||
|
case "LOW":
|
||||||
|
high = 480
|
||||||
|
case "STANDARD":
|
||||||
|
low = 480
|
||||||
|
high = 720
|
||||||
|
case "STANDARD_HD":
|
||||||
|
low = 720
|
||||||
|
high = 1080
|
||||||
|
case "FULL_HD":
|
||||||
|
low = 1080
|
||||||
|
high = 2160
|
||||||
|
case "FOUR_K":
|
||||||
|
low = 2160
|
||||||
|
}
|
||||||
|
|
||||||
|
havingClause := ""
|
||||||
|
if low != 0 {
|
||||||
|
havingClause = "avg(images.height) >= " + strconv.Itoa(low)
|
||||||
|
}
|
||||||
|
if high != 0 {
|
||||||
|
if havingClause != "" {
|
||||||
|
havingClause += " AND "
|
||||||
|
}
|
||||||
|
havingClause += "avg(images.height) < " + strconv.Itoa(high)
|
||||||
|
}
|
||||||
|
|
||||||
|
if havingClause != "" {
|
||||||
|
query.addHaving(havingClause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (qb *GalleryQueryBuilder) getGallerySort(findFilter *FindFilterType) string {
|
func (qb *GalleryQueryBuilder) getGallerySort(findFilter *FindFilterType) string {
|
||||||
var sort string
|
var sort string
|
||||||
var direction string
|
var direction string
|
||||||
|
|||||||
@@ -153,6 +153,44 @@ func verifyGalleriesPath(t *testing.T, pathCriterion models.StringCriterionInput
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryRating(t *testing.T) {
|
||||||
|
const rating = 3
|
||||||
|
ratingCriterion := models.IntCriterionInput{
|
||||||
|
Value: rating,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierGreaterThan
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierLessThan
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierIsNull
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
|
||||||
|
ratingCriterion.Modifier = models.CriterionModifierNotNull
|
||||||
|
verifyGalleriesRating(t, ratingCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyGalleriesRating(t *testing.T, ratingCriterion models.IntCriterionInput) {
|
||||||
|
sqb := models.NewGalleryQueryBuilder()
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
Rating: &ratingCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries, _ := sqb.Query(&galleryFilter, nil)
|
||||||
|
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
verifyInt64(t, gallery.Rating, ratingCriterion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGalleryQueryIsMissingScene(t *testing.T) {
|
func TestGalleryQueryIsMissingScene(t *testing.T) {
|
||||||
qb := models.NewGalleryQueryBuilder()
|
qb := models.NewGalleryQueryBuilder()
|
||||||
isMissing := "scene"
|
isMissing := "scene"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const imageTable = "images"
|
const imageTable = "images"
|
||||||
@@ -234,12 +233,8 @@ func (qb *ImageQueryBuilder) Count() (int, error) {
|
|||||||
return runCountQuery(buildCountQuery("SELECT images.id FROM images"), nil)
|
return runCountQuery(buildCountQuery("SELECT images.id FROM images"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *ImageQueryBuilder) SizeCount() (string, error) {
|
func (qb *ImageQueryBuilder) Size() (uint64, error) {
|
||||||
sum, err := runSumQuery("SELECT SUM(size) as sum FROM images", nil)
|
return runSumQuery("SELECT SUM(size) as sum FROM images", nil)
|
||||||
if err != nil {
|
|
||||||
return "0 B", err
|
|
||||||
}
|
|
||||||
return utils.HumanizeBytes(sum), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *ImageQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
func (qb *ImageQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
||||||
@@ -283,6 +278,8 @@ func (qb *ImageQueryBuilder) Query(imageFilter *ImageFilterType, findFilter *Fin
|
|||||||
query.addArg(thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query.handleStringCriterionInput(imageFilter.Path, "images.path")
|
||||||
|
|
||||||
if rating := imageFilter.Rating; rating != nil {
|
if rating := imageFilter.Rating; rating != nil {
|
||||||
clause, count := getIntCriterionWhereClause("images.rating", *imageFilter.Rating)
|
clause, count := getIntCriterionWhereClause("images.rating", *imageFilter.Rating)
|
||||||
query.addWhere(clause)
|
query.addWhere(clause)
|
||||||
|
|||||||
@@ -106,6 +106,34 @@ func imageQueryQ(t *testing.T, sqb models.ImageQueryBuilder, q string, expectedI
|
|||||||
assert.Len(t, images, totalImages)
|
assert.Len(t, images, totalImages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestImageQueryPath(t *testing.T) {
|
||||||
|
const imageIdx = 1
|
||||||
|
imagePath := getImageStringValue(imageIdx, "Path")
|
||||||
|
|
||||||
|
pathCriterion := models.StringCriterionInput{
|
||||||
|
Value: imagePath,
|
||||||
|
Modifier: models.CriterionModifierEquals,
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyImagePath(t, pathCriterion)
|
||||||
|
|
||||||
|
pathCriterion.Modifier = models.CriterionModifierNotEquals
|
||||||
|
verifyImagePath(t, pathCriterion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyImagePath(t *testing.T, pathCriterion models.StringCriterionInput) {
|
||||||
|
sqb := models.NewImageQueryBuilder()
|
||||||
|
imageFilter := models.ImageFilterType{
|
||||||
|
Path: &pathCriterion,
|
||||||
|
}
|
||||||
|
|
||||||
|
images, _ := sqb.Query(&imageFilter, nil)
|
||||||
|
|
||||||
|
for _, image := range images {
|
||||||
|
verifyString(t, image.Path, pathCriterion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestImageQueryRating(t *testing.T) {
|
func TestImageQueryRating(t *testing.T) {
|
||||||
const rating = 3
|
const rating = 3
|
||||||
ratingCriterion := models.IntCriterionInput{
|
ratingCriterion := models.IntCriterionInput{
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const sceneTable = "scenes"
|
const sceneTable = "scenes"
|
||||||
@@ -242,12 +241,8 @@ func (qb *SceneQueryBuilder) Count() (int, error) {
|
|||||||
return runCountQuery(buildCountQuery("SELECT scenes.id FROM scenes"), nil)
|
return runCountQuery(buildCountQuery("SELECT scenes.id FROM scenes"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) SizeCount() (string, error) {
|
func (qb *SceneQueryBuilder) Size() (uint64, error) {
|
||||||
sum, err := runSumQuery("SELECT SUM(size) as sum FROM scenes", nil)
|
return runSumQuery("SELECT SUM(size) as sum FROM scenes", nil)
|
||||||
if err != nil {
|
|
||||||
return "0 B", err
|
|
||||||
}
|
|
||||||
return utils.HumanizeBytes(sum), err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *SceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
func (qb *SceneQueryBuilder) CountByStudioID(studioID int) (int, error) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
@@ -188,29 +187,6 @@ func IsZipFileUncompressed(path string) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// humanize code taken from https://github.com/dustin/go-humanize and adjusted
|
|
||||||
|
|
||||||
func logn(n, b float64) float64 {
|
|
||||||
return math.Log(n) / math.Log(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HumanizeBytes returns a human readable bytes string of a uint
|
|
||||||
func HumanizeBytes(s uint64) string {
|
|
||||||
sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"}
|
|
||||||
if s < 10 {
|
|
||||||
return fmt.Sprintf("%d B", s)
|
|
||||||
}
|
|
||||||
e := math.Floor(logn(float64(s), 1024))
|
|
||||||
suffix := sizes[int(e)]
|
|
||||||
val := math.Floor(float64(s)/math.Pow(1024, e)*10+0.5) / 10
|
|
||||||
f := "%.0f %s"
|
|
||||||
if val < 10 {
|
|
||||||
f = "%.1f %s"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf(f, val, suffix)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFile writes file to path creating parent directories if needed
|
// WriteFile writes file to path creating parent directories if needed
|
||||||
func WriteFile(path string, file []byte) error {
|
func WriteFile(path string, file []byte) error {
|
||||||
pathErr := EnsureDirAll(filepath.Dir(path))
|
pathErr := EnsureDirAll(filepath.Dir(path))
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import FsLightbox from "fslightbox-react";
|
|||||||
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
|
||||||
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
|
||||||
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
import { PerformerScenesPanel } from "./PerformerScenesPanel";
|
||||||
|
import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
||||||
|
|
||||||
interface IPerformerParams {
|
interface IPerformerParams {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -57,7 +58,10 @@ export const Performer: React.FC = () => {
|
|||||||
const [deletePerformer] = usePerformerDestroy();
|
const [deletePerformer] = usePerformerDestroy();
|
||||||
|
|
||||||
const activeTabKey =
|
const activeTabKey =
|
||||||
tab === "scenes" || tab === "edit" || tab === "operations"
|
tab === "scenes" ||
|
||||||
|
tab === "images" ||
|
||||||
|
tab === "edit" ||
|
||||||
|
tab === "operations"
|
||||||
? tab
|
? tab
|
||||||
: "details";
|
: "details";
|
||||||
const setActiveTabKey = (newTab: string | null) => {
|
const setActiveTabKey = (newTab: string | null) => {
|
||||||
@@ -152,6 +156,9 @@ export const Performer: React.FC = () => {
|
|||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title="Scenes">
|
||||||
<PerformerScenesPanel performer={performer} />
|
<PerformerScenesPanel performer={performer} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab eventKey="images" title="Images">
|
||||||
|
<PerformerImagesPanel performer={performer} />
|
||||||
|
</Tab>
|
||||||
<Tab eventKey="edit" title="Edit">
|
<Tab eventKey="edit" title="Edit">
|
||||||
<PerformerDetailsPanel
|
<PerformerDetailsPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
|
import { performerFilterHook } from "src/core/performers";
|
||||||
|
|
||||||
|
interface IPerformerImagesPanel {
|
||||||
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> = ({
|
||||||
|
performer,
|
||||||
|
}) => {
|
||||||
|
return <ImageList filterHook={performerFilterHook(performer)} />;
|
||||||
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { SceneList } from "src/components/Scenes/SceneList";
|
||||||
|
import { performerFilterHook } from "src/core/performers";
|
||||||
|
|
||||||
interface IPerformerDetailsProps {
|
interface IPerformerDetailsProps {
|
||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
@@ -11,37 +10,5 @@ interface IPerformerDetailsProps {
|
|||||||
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
|
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||||
performer,
|
performer,
|
||||||
}) => {
|
}) => {
|
||||||
function filterHook(filter: ListFilterModel) {
|
return <SceneList filterHook={performerFilterHook(performer)} />;
|
||||||
const performerValue = { id: performer.id!, label: performer.name! };
|
|
||||||
// if performers is already present, then we modify it, otherwise add
|
|
||||||
let performerCriterion = filter.criteria.find((c) => {
|
|
||||||
return c.type === "performers";
|
|
||||||
}) as PerformersCriterion;
|
|
||||||
|
|
||||||
if (
|
|
||||||
performerCriterion &&
|
|
||||||
(performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
|
||||||
performerCriterion.modifier === GQL.CriterionModifier.Includes)
|
|
||||||
) {
|
|
||||||
// add the performer if not present
|
|
||||||
if (
|
|
||||||
!performerCriterion.value.find((p) => {
|
|
||||||
return p.id === performer.id;
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
performerCriterion.value.push(performerValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
performerCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
|
||||||
} else {
|
|
||||||
// overwrite
|
|
||||||
performerCriterion = new PerformersCriterion();
|
|
||||||
performerCriterion.value = [performerValue];
|
|
||||||
filter.criteria.push(performerCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SceneList filterHook={filterHook} />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ import {
|
|||||||
useAllPerformersForFilter,
|
useAllPerformersForFilter,
|
||||||
useMarkerStrings,
|
useMarkerStrings,
|
||||||
useScrapePerformerList,
|
useScrapePerformerList,
|
||||||
useValidGalleriesForScene,
|
|
||||||
useTagCreate,
|
useTagCreate,
|
||||||
useStudioCreate,
|
useStudioCreate,
|
||||||
usePerformerCreate,
|
usePerformerCreate,
|
||||||
|
useFindGalleries,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { FilterMode } from "src/models/list-filter/types";
|
||||||
|
|
||||||
type ValidTypes =
|
type ValidTypes =
|
||||||
| GQL.SlimPerformerDataFragment
|
| GQL.SlimPerformerDataFragment
|
||||||
@@ -90,12 +92,24 @@ const getSelectedValues = (selectedItems: ValueType<Option>) =>
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
|
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
|
||||||
const { data, loading } = useValidGalleriesForScene(props.sceneId);
|
const [query, setQuery] = React.useState<string>("");
|
||||||
const galleries = data?.validGalleriesForScene ?? [];
|
const { data, loading } = useFindGalleries(getFilter());
|
||||||
const items = (galleries.length > 0
|
|
||||||
? [{ path: "None", id: "0" }, ...galleries]
|
const galleries = data?.findGalleries.galleries ?? [];
|
||||||
: []
|
const items = galleries.map((g) => ({
|
||||||
).map((g) => ({ label: g.title ?? "", value: g.id }));
|
label: g.title ?? g.path ?? "",
|
||||||
|
value: g.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
function getFilter() {
|
||||||
|
const ret = new ListFilterModel(FilterMode.Galleries);
|
||||||
|
ret.searchTerm = query;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInputChange = debounce((input: string) => {
|
||||||
|
setQuery(input);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
const onChange = (selectedItems: ValueType<Option>) => {
|
const onChange = (selectedItems: ValueType<Option>) => {
|
||||||
const selectedItem = getSelectedValues(selectedItems)[0];
|
const selectedItem = getSelectedValues(selectedItems)[0];
|
||||||
@@ -110,8 +124,8 @@ export const SceneGallerySelect: React.FC<ISceneGallerySelect> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectComponent
|
<SelectComponent
|
||||||
className="input-control"
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
onInputChange={onInputChange}
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
items={items}
|
items={items}
|
||||||
selectedOptions={selectedOptions}
|
selectedOptions={selectedOptions}
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ import { useStats } from "src/core/StashService";
|
|||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared";
|
import { LoadingIndicator } from "src/components/Shared";
|
||||||
import Changelog from "src/components/Changelog/Changelog";
|
import Changelog from "src/components/Changelog/Changelog";
|
||||||
|
import { TextUtils } from "src/utils";
|
||||||
|
|
||||||
export const Stats: React.FC = () => {
|
export const Stats: React.FC = () => {
|
||||||
const { data, error, loading } = useStats();
|
const { data, error, loading } = useStats();
|
||||||
|
|
||||||
|
if (error) return <span>{error.message}</span>;
|
||||||
if (loading || !data) return <LoadingIndicator />;
|
if (loading || !data) return <LoadingIndicator />;
|
||||||
|
|
||||||
if (error) return <span>error.message</span>;
|
const scenesSize = TextUtils.fileSize(data.stats.scenes_size);
|
||||||
|
const imagesSize = TextUtils.fileSize(data.stats.images_size);
|
||||||
const size = data.stats.scene_size_count.split(" ");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<div className="col col-sm-8 m-sm-auto row stats">
|
<div className="col col-sm-8 m-sm-auto row stats">
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
<p className="title">
|
<p className="title">
|
||||||
<FormattedNumber value={parseFloat(size[0])} />
|
<FormattedNumber value={Math.floor(scenesSize.size)} />
|
||||||
{` ${size[1]}`}
|
{` ${TextUtils.formatFileSizeUnit(scenesSize.unit)}`}
|
||||||
</p>
|
</p>
|
||||||
<p className="heading">
|
<p className="heading">
|
||||||
<FormattedMessage id="library-size" defaultMessage="Library size" />
|
<FormattedMessage id="scenes-size" defaultMessage="Scenes size" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
@@ -33,6 +34,25 @@ export const Stats: React.FC = () => {
|
|||||||
<FormattedMessage id="scenes" defaultMessage="Scenes" />
|
<FormattedMessage id="scenes" defaultMessage="Scenes" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="stats-element">
|
||||||
|
<p className="title">
|
||||||
|
<FormattedNumber value={Math.floor(imagesSize.size)} />
|
||||||
|
{` ${TextUtils.formatFileSizeUnit(imagesSize.unit)}`}
|
||||||
|
</p>
|
||||||
|
<p className="heading">
|
||||||
|
<FormattedMessage id="images-size" defaultMessage="Images size" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="stats-element">
|
||||||
|
<p className="title">
|
||||||
|
<FormattedNumber value={data.stats.image_count} />
|
||||||
|
</p>
|
||||||
|
<p className="heading">
|
||||||
|
<FormattedMessage id="images" defaultMessage="Images" />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col col-sm-8 m-sm-auto row stats">
|
||||||
<div className="stats-element">
|
<div className="stats-element">
|
||||||
<p className="title">
|
<p className="title">
|
||||||
<FormattedNumber value={data.stats.movie_count} />
|
<FormattedNumber value={data.stats.movie_count} />
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||||
|
import { StudioImagesPanel } from "./StudioImagesPanel";
|
||||||
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
import { StudioChildrenPanel } from "./StudioChildrenPanel";
|
||||||
|
|
||||||
interface IStudioParams {
|
interface IStudioParams {
|
||||||
@@ -195,7 +196,8 @@ export const Studio: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const activeTabKey = tab === "childstudios" ? tab : "scenes";
|
const activeTabKey =
|
||||||
|
tab === "childstudios" || tab === "images" ? tab : "scenes";
|
||||||
const setActiveTabKey = (newTab: string | null) => {
|
const setActiveTabKey = (newTab: string | null) => {
|
||||||
if (tab !== newTab) {
|
if (tab !== newTab) {
|
||||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||||
@@ -290,6 +292,9 @@ export const Studio: React.FC = () => {
|
|||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title="Scenes">
|
||||||
<StudioScenesPanel studio={studio} />
|
<StudioScenesPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab eventKey="images" title="Images">
|
||||||
|
<StudioImagesPanel studio={studio} />
|
||||||
|
</Tab>
|
||||||
<Tab eventKey="childstudios" title="Child Studios">
|
<Tab eventKey="childstudios" title="Child Studios">
|
||||||
<StudioChildrenPanel studio={studio} />
|
<StudioChildrenPanel studio={studio} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { studioFilterHook } from "src/core/studios";
|
||||||
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
|
|
||||||
|
interface IStudioImagesPanel {
|
||||||
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StudioImagesPanel: React.FC<IStudioImagesPanel> = ({ studio }) => {
|
||||||
|
return <ImageList filterHook={studioFilterHook(studio)} />;
|
||||||
|
};
|
||||||
@@ -1,45 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { SceneList } from "src/components/Scenes/SceneList";
|
||||||
|
import { studioFilterHook } from "src/core/studios";
|
||||||
|
|
||||||
interface IStudioScenesPanel {
|
interface IStudioScenesPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
|
export const StudioScenesPanel: React.FC<IStudioScenesPanel> = ({ studio }) => {
|
||||||
function filterHook(filter: ListFilterModel) {
|
return <SceneList filterHook={studioFilterHook(studio)} />;
|
||||||
const studioValue = { id: studio.id!, label: studio.name! };
|
|
||||||
// if studio is already present, then we modify it, otherwise add
|
|
||||||
let studioCriterion = filter.criteria.find((c) => {
|
|
||||||
return c.type === "studios";
|
|
||||||
}) as StudiosCriterion;
|
|
||||||
|
|
||||||
if (
|
|
||||||
studioCriterion &&
|
|
||||||
(studioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
|
||||||
studioCriterion.modifier === GQL.CriterionModifier.Includes)
|
|
||||||
) {
|
|
||||||
// add the studio if not present
|
|
||||||
if (
|
|
||||||
!studioCriterion.value.find((p) => {
|
|
||||||
return p.id === studio.id;
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
studioCriterion.value.push(studioValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
|
||||||
} else {
|
|
||||||
// overwrite
|
|
||||||
studioCriterion = new StudiosCriterion();
|
|
||||||
studioCriterion.value = [studioValue];
|
|
||||||
filter.criteria.push(studioCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SceneList filterHook={filterHook} />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { TagScenesPanel } from "./TagScenesPanel";
|
import { TagScenesPanel } from "./TagScenesPanel";
|
||||||
import { TagMarkersPanel } from "./TagMarkersPanel";
|
import { TagMarkersPanel } from "./TagMarkersPanel";
|
||||||
|
import { TagImagesPanel } from "./TagImagesPanel";
|
||||||
|
|
||||||
interface ITabParams {
|
interface ITabParams {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -49,7 +50,7 @@ export const Tag: React.FC = () => {
|
|||||||
const [createTag] = useTagCreate(getTagInput() as GQL.TagUpdateInput);
|
const [createTag] = useTagCreate(getTagInput() as GQL.TagUpdateInput);
|
||||||
const [deleteTag] = useTagDestroy(getTagInput() as GQL.TagUpdateInput);
|
const [deleteTag] = useTagDestroy(getTagInput() as GQL.TagUpdateInput);
|
||||||
|
|
||||||
const activeTabKey = tab === "markers" ? tab : "scenes";
|
const activeTabKey = tab === "markers" || tab === "images" ? tab : "scenes";
|
||||||
const setActiveTabKey = (newTab: string | null) => {
|
const setActiveTabKey = (newTab: string | null) => {
|
||||||
if (tab !== newTab) {
|
if (tab !== newTab) {
|
||||||
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
const tabParam = newTab === "scenes" ? "" : `/${newTab}`;
|
||||||
@@ -246,6 +247,9 @@ export const Tag: React.FC = () => {
|
|||||||
<Tab eventKey="scenes" title="Scenes">
|
<Tab eventKey="scenes" title="Scenes">
|
||||||
<TagScenesPanel tag={tag} />
|
<TagScenesPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab eventKey="images" title="Images">
|
||||||
|
<TagImagesPanel tag={tag} />
|
||||||
|
</Tab>
|
||||||
<Tab eventKey="markers" title="Markers">
|
<Tab eventKey="markers" title="Markers">
|
||||||
<TagMarkersPanel tag={tag} />
|
<TagMarkersPanel tag={tag} />
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
12
ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
Normal file
12
ui/v2.5/src/components/Tags/TagDetails/TagImagesPanel.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { tagFilterHook } from "src/core/tags";
|
||||||
|
import { ImageList } from "src/components/Images/ImageList";
|
||||||
|
|
||||||
|
interface ITagImagesPanel {
|
||||||
|
tag: GQL.TagDataFragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TagImagesPanel: React.FC<ITagImagesPanel> = ({ tag }) => {
|
||||||
|
return <ImageList filterHook={tagFilterHook(tag)} />;
|
||||||
|
};
|
||||||
@@ -1,45 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
|
||||||
import { SceneList } from "src/components/Scenes/SceneList";
|
import { SceneList } from "src/components/Scenes/SceneList";
|
||||||
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
import { tagFilterHook } from "src/core/tags";
|
||||||
|
|
||||||
interface ITagScenesPanel {
|
interface ITagScenesPanel {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ tag }) => {
|
export const TagScenesPanel: React.FC<ITagScenesPanel> = ({ tag }) => {
|
||||||
function filterHook(filter: ListFilterModel) {
|
return <SceneList filterHook={tagFilterHook(tag)} />;
|
||||||
const tagValue = { id: tag.id, label: tag.name };
|
|
||||||
// if tag is already present, then we modify it, otherwise add
|
|
||||||
let tagCriterion = filter.criteria.find((c) => {
|
|
||||||
return c.type === "tags";
|
|
||||||
}) as TagsCriterion;
|
|
||||||
|
|
||||||
if (
|
|
||||||
tagCriterion &&
|
|
||||||
(tagCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
|
||||||
tagCriterion.modifier === GQL.CriterionModifier.Includes)
|
|
||||||
) {
|
|
||||||
// add the tag if not present
|
|
||||||
if (
|
|
||||||
!tagCriterion.value.find((p) => {
|
|
||||||
return p.id === tag.id;
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
tagCriterion.value.push(tagValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
|
||||||
} else {
|
|
||||||
// overwrite
|
|
||||||
tagCriterion = new TagsCriterion("tags");
|
|
||||||
tagCriterion.value = [tagValue];
|
|
||||||
filter.criteria.push(tagCriterion);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SceneList filterHook={filterHook} />;
|
|
||||||
};
|
};
|
||||||
|
|||||||
39
ui/v2.5/src/core/performers.ts
Normal file
39
ui/v2.5/src/core/performers.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
|
||||||
|
export const performerFilterHook = (
|
||||||
|
performer: Partial<GQL.PerformerDataFragment>
|
||||||
|
) => {
|
||||||
|
return (filter: ListFilterModel) => {
|
||||||
|
const performerValue = { id: performer.id!, label: performer.name! };
|
||||||
|
// if performers is already present, then we modify it, otherwise add
|
||||||
|
let performerCriterion = filter.criteria.find((c) => {
|
||||||
|
return c.type === "performers";
|
||||||
|
}) as PerformersCriterion;
|
||||||
|
|
||||||
|
if (
|
||||||
|
performerCriterion &&
|
||||||
|
(performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
|
performerCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||||
|
) {
|
||||||
|
// add the performer if not present
|
||||||
|
if (
|
||||||
|
!performerCriterion.value.find((p) => {
|
||||||
|
return p.id === performer.id;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
performerCriterion.value.push(performerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
performerCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
|
} else {
|
||||||
|
// overwrite
|
||||||
|
performerCriterion = new PerformersCriterion();
|
||||||
|
performerCriterion.value = [performerValue];
|
||||||
|
filter.criteria.push(performerCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
|
};
|
||||||
37
ui/v2.5/src/core/studios.ts
Normal file
37
ui/v2.5/src/core/studios.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { StudiosCriterion } from "src/models/list-filter/criteria/studios";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
|
||||||
|
export const studioFilterHook = (studio: Partial<GQL.StudioDataFragment>) => {
|
||||||
|
return (filter: ListFilterModel) => {
|
||||||
|
const studioValue = { id: studio.id!, label: studio.name! };
|
||||||
|
// if studio is already present, then we modify it, otherwise add
|
||||||
|
let studioCriterion = filter.criteria.find((c) => {
|
||||||
|
return c.type === "studios";
|
||||||
|
}) as StudiosCriterion;
|
||||||
|
|
||||||
|
if (
|
||||||
|
studioCriterion &&
|
||||||
|
(studioCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
|
studioCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||||
|
) {
|
||||||
|
// add the studio if not present
|
||||||
|
if (
|
||||||
|
!studioCriterion.value.find((p) => {
|
||||||
|
return p.id === studio.id;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
studioCriterion.value.push(studioValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
studioCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
|
} else {
|
||||||
|
// overwrite
|
||||||
|
studioCriterion = new StudiosCriterion();
|
||||||
|
studioCriterion.value = [studioValue];
|
||||||
|
filter.criteria.push(studioCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
|
};
|
||||||
37
ui/v2.5/src/core/tags.ts
Normal file
37
ui/v2.5/src/core/tags.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { TagsCriterion } from "src/models/list-filter/criteria/tags";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
|
||||||
|
export const tagFilterHook = (tag: GQL.TagDataFragment) => {
|
||||||
|
return (filter: ListFilterModel) => {
|
||||||
|
const tagValue = { id: tag.id, label: tag.name };
|
||||||
|
// if tag is already present, then we modify it, otherwise add
|
||||||
|
let tagCriterion = filter.criteria.find((c) => {
|
||||||
|
return c.type === "tags";
|
||||||
|
}) as TagsCriterion;
|
||||||
|
|
||||||
|
if (
|
||||||
|
tagCriterion &&
|
||||||
|
(tagCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
|
tagCriterion.modifier === GQL.CriterionModifier.Includes)
|
||||||
|
) {
|
||||||
|
// add the tag if not present
|
||||||
|
if (
|
||||||
|
!tagCriterion.value.find((p) => {
|
||||||
|
return p.id === tag.id;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
tagCriterion.value.push(tagValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
tagCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
|
} else {
|
||||||
|
// overwrite
|
||||||
|
tagCriterion = new TagsCriterion("tags");
|
||||||
|
tagCriterion.value = [tagValue];
|
||||||
|
filter.criteria.push(tagCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ export type CriterionType =
|
|||||||
| "rating"
|
| "rating"
|
||||||
| "o_counter"
|
| "o_counter"
|
||||||
| "resolution"
|
| "resolution"
|
||||||
|
| "average_resolution"
|
||||||
| "duration"
|
| "duration"
|
||||||
| "favorite"
|
| "favorite"
|
||||||
| "hasMarkers"
|
| "hasMarkers"
|
||||||
@@ -59,6 +60,8 @@ export abstract class Criterion {
|
|||||||
return "O-Counter";
|
return "O-Counter";
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return "Resolution";
|
return "Resolution";
|
||||||
|
case "average_resolution":
|
||||||
|
return "Average Resolution";
|
||||||
case "duration":
|
case "duration":
|
||||||
return "Duration";
|
return "Duration";
|
||||||
case "favorite":
|
case "favorite":
|
||||||
|
|||||||
@@ -73,7 +73,16 @@ export class PerformerIsMissingCriterionOption implements ICriterionOption {
|
|||||||
|
|
||||||
export class GalleryIsMissingCriterion extends IsMissingCriterion {
|
export class GalleryIsMissingCriterion extends IsMissingCriterion {
|
||||||
public type: CriterionType = "galleryIsMissing";
|
public type: CriterionType = "galleryIsMissing";
|
||||||
public options: string[] = ["scene"];
|
public options: string[] = [
|
||||||
|
"title",
|
||||||
|
"details",
|
||||||
|
"url",
|
||||||
|
"date",
|
||||||
|
"studio",
|
||||||
|
"performers",
|
||||||
|
"tags",
|
||||||
|
"scene",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GalleryIsMissingCriterionOption implements ICriterionOption {
|
export class GalleryIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
|||||||
@@ -14,3 +14,13 @@ export class ResolutionCriterionOption implements ICriterionOption {
|
|||||||
public label: string = Criterion.getLabel("resolution");
|
public label: string = Criterion.getLabel("resolution");
|
||||||
public value: CriterionType = "resolution";
|
public value: CriterionType = "resolution";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AverageResolutionCriterion extends ResolutionCriterion {
|
||||||
|
public type: CriterionType = "average_resolution";
|
||||||
|
public parameterName: string = "average_resolution";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AverageResolutionCriterionOption extends ResolutionCriterionOption {
|
||||||
|
public label: string = Criterion.getLabel("average_resolution");
|
||||||
|
public value: CriterionType = "average_resolution";
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { NoneCriterion } from "./none";
|
import { NoneCriterion } from "./none";
|
||||||
import { PerformersCriterion } from "./performers";
|
import { PerformersCriterion } from "./performers";
|
||||||
import { RatingCriterion } from "./rating";
|
import { RatingCriterion } from "./rating";
|
||||||
import { ResolutionCriterion } from "./resolution";
|
import { AverageResolutionCriterion, ResolutionCriterion } from "./resolution";
|
||||||
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
import { StudiosCriterion, ParentStudiosCriterion } from "./studios";
|
||||||
import { TagsCriterion } from "./tags";
|
import { TagsCriterion } from "./tags";
|
||||||
import { GenderCriterion } from "./gender";
|
import { GenderCriterion } from "./gender";
|
||||||
@@ -43,6 +43,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new NumberCriterion(type, type);
|
return new NumberCriterion(type, type);
|
||||||
case "resolution":
|
case "resolution":
|
||||||
return new ResolutionCriterion();
|
return new ResolutionCriterion();
|
||||||
|
case "average_resolution":
|
||||||
|
return new AverageResolutionCriterion();
|
||||||
case "duration":
|
case "duration":
|
||||||
return new DurationCriterion(type, type);
|
return new DurationCriterion(type, type);
|
||||||
case "favorite":
|
case "favorite":
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import {
|
|||||||
} from "./criteria/performers";
|
} from "./criteria/performers";
|
||||||
import { RatingCriterion, RatingCriterionOption } from "./criteria/rating";
|
import { RatingCriterion, RatingCriterionOption } from "./criteria/rating";
|
||||||
import {
|
import {
|
||||||
|
AverageResolutionCriterion,
|
||||||
|
AverageResolutionCriterionOption,
|
||||||
ResolutionCriterion,
|
ResolutionCriterion,
|
||||||
ResolutionCriterionOption,
|
ResolutionCriterionOption,
|
||||||
} from "./criteria/resolution";
|
} from "./criteria/resolution";
|
||||||
@@ -154,6 +156,7 @@ export class ListFilterModel {
|
|||||||
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall];
|
||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
|
ListFilterModel.createCriterionOption("path"),
|
||||||
new RatingCriterionOption(),
|
new RatingCriterionOption(),
|
||||||
ListFilterModel.createCriterionOption("o_counter"),
|
ListFilterModel.createCriterionOption("o_counter"),
|
||||||
new ResolutionCriterionOption(),
|
new ResolutionCriterionOption(),
|
||||||
@@ -227,7 +230,12 @@ export class ListFilterModel {
|
|||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
ListFilterModel.createCriterionOption("path"),
|
ListFilterModel.createCriterionOption("path"),
|
||||||
|
new RatingCriterionOption(),
|
||||||
|
new AverageResolutionCriterionOption(),
|
||||||
new GalleryIsMissingCriterionOption(),
|
new GalleryIsMissingCriterionOption(),
|
||||||
|
new TagsCriterionOption(),
|
||||||
|
new PerformersCriterionOption(),
|
||||||
|
new StudiosCriterionOption(),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.SceneMarkers:
|
case FilterMode.SceneMarkers:
|
||||||
@@ -645,6 +653,14 @@ export class ListFilterModel {
|
|||||||
const result: ImageFilterType = {};
|
const result: ImageFilterType = {};
|
||||||
this.criteria.forEach((criterion) => {
|
this.criteria.forEach((criterion) => {
|
||||||
switch (criterion.type) {
|
switch (criterion.type) {
|
||||||
|
case "path": {
|
||||||
|
const pathCrit = criterion as MandatoryStringCriterion;
|
||||||
|
result.path = {
|
||||||
|
value: pathCrit.value,
|
||||||
|
modifier: pathCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "rating": {
|
case "rating": {
|
||||||
const ratingCrit = criterion as RatingCriterion;
|
const ratingCrit = criterion as RatingCriterion;
|
||||||
result.rating = {
|
result.rating = {
|
||||||
@@ -695,7 +711,7 @@ export class ListFilterModel {
|
|||||||
}
|
}
|
||||||
case "performers": {
|
case "performers": {
|
||||||
const perfCrit = criterion as PerformersCriterion;
|
const perfCrit = criterion as PerformersCriterion;
|
||||||
result.galleries = {
|
result.performers = {
|
||||||
value: perfCrit.value.map((perf) => perf.id),
|
value: perfCrit.value.map((perf) => perf.id),
|
||||||
modifier: perfCrit.modifier,
|
modifier: perfCrit.modifier,
|
||||||
};
|
};
|
||||||
@@ -776,9 +792,62 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "rating": {
|
||||||
|
const ratingCrit = criterion as RatingCriterion;
|
||||||
|
result.rating = {
|
||||||
|
value: ratingCrit.value,
|
||||||
|
modifier: ratingCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "average_resolution": {
|
||||||
|
switch ((criterion as AverageResolutionCriterion).value) {
|
||||||
|
case "240p":
|
||||||
|
result.average_resolution = ResolutionEnum.Low;
|
||||||
|
break;
|
||||||
|
case "480p":
|
||||||
|
result.average_resolution = ResolutionEnum.Standard;
|
||||||
|
break;
|
||||||
|
case "720p":
|
||||||
|
result.average_resolution = ResolutionEnum.StandardHd;
|
||||||
|
break;
|
||||||
|
case "1080p":
|
||||||
|
result.average_resolution = ResolutionEnum.FullHd;
|
||||||
|
break;
|
||||||
|
case "4k":
|
||||||
|
result.average_resolution = ResolutionEnum.FourK;
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "galleryIsMissing":
|
case "galleryIsMissing":
|
||||||
result.is_missing = (criterion as IsMissingCriterion).value;
|
result.is_missing = (criterion as IsMissingCriterion).value;
|
||||||
break;
|
break;
|
||||||
|
case "tags": {
|
||||||
|
const tagsCrit = criterion as TagsCriterion;
|
||||||
|
result.tags = {
|
||||||
|
value: tagsCrit.value.map((tag) => tag.id),
|
||||||
|
modifier: tagsCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "performers": {
|
||||||
|
const perfCrit = criterion as PerformersCriterion;
|
||||||
|
result.performers = {
|
||||||
|
value: perfCrit.value.map((perf) => perf.id),
|
||||||
|
modifier: perfCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "studios": {
|
||||||
|
const studCrit = criterion as StudiosCriterion;
|
||||||
|
result.studios = {
|
||||||
|
value: studCrit.value.map((studio) => studio.id),
|
||||||
|
modifier: studCrit.modifier,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const Units: Unit[] = [
|
|||||||
"terabyte",
|
"terabyte",
|
||||||
"petabyte",
|
"petabyte",
|
||||||
];
|
];
|
||||||
|
const shortUnits = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
|
||||||
const truncate = (
|
const truncate = (
|
||||||
value?: string,
|
value?: string,
|
||||||
@@ -32,7 +33,7 @@ const fileSize = (bytes: number = 0) => {
|
|||||||
|
|
||||||
let unit = 0;
|
let unit = 0;
|
||||||
let count = bytes;
|
let count = bytes;
|
||||||
while (count >= 1024) {
|
while (count >= 1024 && unit + 1 < Units.length) {
|
||||||
count /= 1024;
|
count /= 1024;
|
||||||
unit++;
|
unit++;
|
||||||
}
|
}
|
||||||
@@ -43,6 +44,11 @@ const fileSize = (bytes: number = 0) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatFileSizeUnit = (u: Unit) => {
|
||||||
|
const i = Units.indexOf(u);
|
||||||
|
return shortUnits[i];
|
||||||
|
};
|
||||||
|
|
||||||
const secondsToTimestamp = (seconds: number) => {
|
const secondsToTimestamp = (seconds: number) => {
|
||||||
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
let ret = new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ const formatDate = (intl: IntlShape, date?: string) => {
|
|||||||
const TextUtils = {
|
const TextUtils = {
|
||||||
truncate,
|
truncate,
|
||||||
fileSize,
|
fileSize,
|
||||||
|
formatFileSizeUnit,
|
||||||
secondsToTimestamp,
|
secondsToTimestamp,
|
||||||
fileNameFromPath,
|
fileNameFromPath,
|
||||||
age: getAge,
|
age: getAge,
|
||||||
|
|||||||
Reference in New Issue
Block a user