mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Gallery list improvement (#622)
* Add grid view to galleries * Show scene in gallery card * Add is missing scene gallery filter * Don't store galleries with no images
This commit is contained in:
@@ -8,4 +8,9 @@ fragment GalleryData on Gallery {
|
|||||||
name
|
name
|
||||||
path
|
path
|
||||||
}
|
}
|
||||||
|
scene {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
path
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
query FindGalleries($filter: FindFilterType) {
|
query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType) {
|
||||||
findGalleries(filter: $filter) {
|
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
|
||||||
count
|
count
|
||||||
galleries {
|
galleries {
|
||||||
...GalleryData
|
...GalleryData
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type Query {
|
|||||||
findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType!
|
findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType!
|
||||||
|
|
||||||
findGallery(id: ID!): Gallery
|
findGallery(id: ID!): Gallery
|
||||||
findGalleries(filter: FindFilterType): FindGalleriesResultType!
|
findGalleries(gallery_filter: GalleryFilterType, filter: FindFilterType): FindGalleriesResultType!
|
||||||
|
|
||||||
findTag(id: ID!): Tag
|
findTag(id: ID!): Tag
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ input StudioFilterType {
|
|||||||
parents: MultiCriterionInput
|
parents: MultiCriterionInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input GalleryFilterType {
|
||||||
|
"""Filter to only include galleries missing this property"""
|
||||||
|
is_missing: String
|
||||||
|
}
|
||||||
|
|
||||||
enum CriterionModifier {
|
enum CriterionModifier {
|
||||||
"""="""
|
"""="""
|
||||||
EQUALS,
|
EQUALS,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ type Gallery {
|
|||||||
checksum: String!
|
checksum: String!
|
||||||
path: String!
|
path: String!
|
||||||
title: String
|
title: String
|
||||||
|
scene: Scene
|
||||||
|
|
||||||
"""The files in the gallery"""
|
"""The files in the gallery"""
|
||||||
files: [GalleryFilesType!]! # Resolver
|
files: [GalleryFilesType!]! # Resolver
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,3 +14,12 @@ func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*mo
|
|||||||
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
|
||||||
return obj.GetFiles(baseURL), nil
|
return obj.GetFiles(baseURL), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *galleryResolver) Scene(ctx context.Context, obj *models.Gallery) (*models.Scene, error) {
|
||||||
|
if !obj.SceneID.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
return qb.Find(int(obj.SceneID.Int64))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/stashapp/stash/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
|
func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
|
||||||
@@ -12,9 +13,9 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gal
|
|||||||
return qb.Find(idInt)
|
return qb.Find(idInt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindGalleries(ctx context.Context, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
|
func (r *queryResolver) FindGalleries(ctx context.Context, galleryFilter *models.GalleryFilterType, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
|
||||||
qb := models.NewGalleryQueryBuilder()
|
qb := models.NewGalleryQueryBuilder()
|
||||||
galleries, total := qb.Query(filter)
|
galleries, total := qb.Query(galleryFilter, filter)
|
||||||
return &models.FindGalleriesResultType{
|
return &models.FindGalleriesResultType{
|
||||||
Count: total,
|
Count: total,
|
||||||
Galleries: galleries,
|
Galleries: galleries,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func (t *CleanTask) Start(wg *sync.WaitGroup) {
|
|||||||
t.deleteScene(t.Scene.ID)
|
t.deleteScene(t.Scene.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.Gallery != nil && t.shouldClean(t.Gallery.Path) {
|
if t.Gallery != nil && t.shouldCleanGallery(t.Gallery) {
|
||||||
t.deleteGallery(t.Gallery.ID)
|
t.deleteGallery(t.Gallery.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,6 +46,19 @@ func (t *CleanTask) shouldClean(path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *CleanTask) shouldCleanGallery(g *models.Gallery) bool {
|
||||||
|
if t.shouldClean(g.Path) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Gallery.CountFiles() == 0 {
|
||||||
|
logger.Infof("Gallery has 0 images. Cleaning: \"%s\"", g.Path)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (t *CleanTask) deleteScene(sceneID int) {
|
func (t *CleanTask) deleteScene(sceneID int) {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
qb := models.NewSceneQueryBuilder()
|
qb := models.NewSceneQueryBuilder()
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ func (t *ScanTask) scanGallery() {
|
|||||||
_, err = qb.Update(*gallery, tx)
|
_, err = qb.Update(*gallery, tx)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
|
|
||||||
newGallery := models.Gallery{
|
newGallery := models.Gallery{
|
||||||
@@ -73,7 +72,12 @@ func (t *ScanTask) scanGallery() {
|
|||||||
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
CreatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||||
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
|
||||||
}
|
}
|
||||||
_, err = qb.Create(newGallery, tx)
|
|
||||||
|
// don't create gallery if it has no images
|
||||||
|
if newGallery.CountFiles() > 0 {
|
||||||
|
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
|
||||||
|
_, err = qb.Create(newGallery, tx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import (
|
|||||||
"archive/zip"
|
"archive/zip"
|
||||||
"bytes"
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"github.com/disintegration/imaging"
|
|
||||||
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
|
||||||
"github.com/stashapp/stash/pkg/logger"
|
|
||||||
"github.com/stashapp/stash/pkg/utils"
|
|
||||||
_ "golang.org/x/image/webp"
|
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
|
"github.com/stashapp/stash/pkg/api/urlbuilders"
|
||||||
|
"github.com/stashapp/stash/pkg/logger"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Gallery struct {
|
type Gallery struct {
|
||||||
@@ -28,6 +29,16 @@ type Gallery struct {
|
|||||||
|
|
||||||
const DefaultGthumbWidth int = 200
|
const DefaultGthumbWidth int = 200
|
||||||
|
|
||||||
|
func (g *Gallery) CountFiles() int {
|
||||||
|
filteredFiles, readCloser, err := g.listZipContents()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
defer readCloser.Close()
|
||||||
|
|
||||||
|
return len(filteredFiles)
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Gallery) GetFiles(baseURL string) []*GalleryFilesType {
|
func (g *Gallery) GetFiles(baseURL string) []*GalleryFilesType {
|
||||||
var galleryFiles []*GalleryFilesType
|
var galleryFiles []*GalleryFilesType
|
||||||
filteredFiles, readCloser, err := g.listZipContents()
|
filteredFiles, readCloser, err := g.listZipContents()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/database"
|
"github.com/stashapp/stash/pkg/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const galleryTable = "galleries"
|
||||||
|
|
||||||
type GalleryQueryBuilder struct{}
|
type GalleryQueryBuilder struct{}
|
||||||
|
|
||||||
func NewGalleryQueryBuilder() GalleryQueryBuilder {
|
func NewGalleryQueryBuilder() GalleryQueryBuilder {
|
||||||
@@ -112,25 +114,36 @@ func (qb *GalleryQueryBuilder) All() ([]*Gallery, error) {
|
|||||||
return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil, nil)
|
return qb.queryGalleries(selectAll("galleries")+qb.getGallerySort(nil), nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (qb *GalleryQueryBuilder) Query(findFilter *FindFilterType) ([]*Gallery, int) {
|
func (qb *GalleryQueryBuilder) Query(galleryFilter *GalleryFilterType, findFilter *FindFilterType) ([]*Gallery, int) {
|
||||||
|
if galleryFilter == nil {
|
||||||
|
galleryFilter = &GalleryFilterType{}
|
||||||
|
}
|
||||||
if findFilter == nil {
|
if findFilter == nil {
|
||||||
findFilter = &FindFilterType{}
|
findFilter = &FindFilterType{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var whereClauses []string
|
query := queryBuilder{
|
||||||
var havingClauses []string
|
tableName: galleryTable,
|
||||||
var args []interface{}
|
}
|
||||||
body := selectDistinctIDs("galleries")
|
|
||||||
|
query.body = selectDistinctIDs("galleries")
|
||||||
|
|
||||||
if q := findFilter.Q; q != nil && *q != "" {
|
if q := findFilter.Q; q != nil && *q != "" {
|
||||||
searchColumns := []string{"galleries.path", "galleries.checksum"}
|
searchColumns := []string{"galleries.path", "galleries.checksum"}
|
||||||
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
|
||||||
whereClauses = append(whereClauses, clause)
|
query.addWhere(clause)
|
||||||
args = append(args, thisArgs...)
|
query.addArg(thisArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
sortAndPagination := qb.getGallerySort(findFilter) + getPagination(findFilter)
|
if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||||
idsResult, countResult := executeFindQuery("galleries", body, args, sortAndPagination, whereClauses, havingClauses)
|
switch *isMissingFilter {
|
||||||
|
case "scene":
|
||||||
|
query.addWhere("galleries.scene_id IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
|
||||||
|
idsResult, countResult := query.executeFind()
|
||||||
|
|
||||||
var galleries []*Gallery
|
var galleries []*Gallery
|
||||||
for _, id := range idsResult {
|
for _, id := range idsResult {
|
||||||
|
|||||||
@@ -98,6 +98,58 @@ func TestGalleryFindBySceneID(t *testing.T) {
|
|||||||
assert.Nil(t, gallery)
|
assert.Nil(t, gallery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryQ(t *testing.T) {
|
||||||
|
const galleryIdx = 0
|
||||||
|
|
||||||
|
q := getGalleryStringValue(galleryIdx, pathField)
|
||||||
|
|
||||||
|
sqb := models.NewGalleryQueryBuilder()
|
||||||
|
|
||||||
|
galleryQueryQ(t, sqb, q, galleryIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryQueryQ(t *testing.T, qb models.GalleryQueryBuilder, q string, expectedGalleryIdx int) {
|
||||||
|
filter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
galleries, _ := qb.Query(nil, &filter)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, 1)
|
||||||
|
gallery := galleries[0]
|
||||||
|
assert.Equal(t, galleryIDs[expectedGalleryIdx], gallery.ID)
|
||||||
|
|
||||||
|
// no Q should return all results
|
||||||
|
filter.Q = nil
|
||||||
|
galleries, _ = qb.Query(nil, &filter)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, totalGalleries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGalleryQueryIsMissingScene(t *testing.T) {
|
||||||
|
qb := models.NewGalleryQueryBuilder()
|
||||||
|
isMissing := "scene"
|
||||||
|
galleryFilter := models.GalleryFilterType{
|
||||||
|
IsMissing: &isMissing,
|
||||||
|
}
|
||||||
|
|
||||||
|
q := getGalleryStringValue(galleryIdxWithScene, titleField)
|
||||||
|
findFilter := models.FindFilterType{
|
||||||
|
Q: &q,
|
||||||
|
}
|
||||||
|
|
||||||
|
galleries, _ := qb.Query(&galleryFilter, &findFilter)
|
||||||
|
|
||||||
|
assert.Len(t, galleries, 0)
|
||||||
|
|
||||||
|
findFilter.Q = nil
|
||||||
|
galleries, _ = qb.Query(&galleryFilter, &findFilter)
|
||||||
|
|
||||||
|
// ensure non of the ids equal the one with gallery
|
||||||
|
for _, gallery := range galleries {
|
||||||
|
assert.NotEqual(t, galleryIDs[galleryIdxWithScene], gallery.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO ValidGalleriesForScenePath
|
// TODO ValidGalleriesForScenePath
|
||||||
// TODO Count
|
// TODO Count
|
||||||
// TODO All
|
// TODO All
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const performersNameCase = 3
|
|||||||
const performersNameNoCase = 2
|
const performersNameNoCase = 2
|
||||||
const moviesNameCase = 2
|
const moviesNameCase = 2
|
||||||
const moviesNameNoCase = 1
|
const moviesNameNoCase = 1
|
||||||
const totalGalleries = 1
|
const totalGalleries = 2
|
||||||
const tagsNameNoCase = 2
|
const tagsNameNoCase = 2
|
||||||
const tagsNameCase = 5
|
const tagsNameCase = 5
|
||||||
const studiosNameCase = 4
|
const studiosNameCase = 4
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ const markup = `
|
|||||||
* Add support for parent/child studios.
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Add gallery grid view.
|
||||||
|
* Add is-missing scene filter for gallery query.
|
||||||
|
* Don't import galleries with no images, and delete galleries with no images during clean.
|
||||||
* Show pagination at top as well as bottom of the page.
|
* Show pagination at top as well as bottom of the page.
|
||||||
* Add split xpath post-processing action.
|
* Add split xpath post-processing action.
|
||||||
* Improved the layout of the scene page.
|
* Improved the layout of the scene page.
|
||||||
|
|||||||
71
ui/v2.5/src/components/Galleries/GalleryCard.tsx
Normal file
71
ui/v2.5/src/components/Galleries/GalleryCard.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { Card, Button, ButtonGroup } from "react-bootstrap";
|
||||||
|
import React from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { FormattedPlural } from "react-intl";
|
||||||
|
import { HoverPopover, Icon, TagLink } from "../Shared";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
gallery: GQL.GalleryDataFragment;
|
||||||
|
zoomIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GalleryCard: React.FC<IProps> = ({ gallery, zoomIndex }) => {
|
||||||
|
function maybeRenderScenePopoverButton() {
|
||||||
|
if (!gallery.scene) return;
|
||||||
|
|
||||||
|
const popoverContent = (
|
||||||
|
<TagLink key={gallery.scene.id} scene={gallery.scene} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HoverPopover placement="bottom" content={popoverContent}>
|
||||||
|
<Link to={`/scenes/${gallery.scene.id}`}>
|
||||||
|
<Button className="minimal">
|
||||||
|
<Icon icon="play-circle" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</HoverPopover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderPopoverButtonGroup() {
|
||||||
|
if (gallery.scene) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<hr />
|
||||||
|
<ButtonGroup className="card-popovers">
|
||||||
|
{maybeRenderScenePopoverButton()}
|
||||||
|
</ButtonGroup>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`gallery-card zoom-${zoomIndex}`}>
|
||||||
|
<Link to={`/galleries/${gallery.id}`} className="gallery-card-header">
|
||||||
|
{gallery.files.length > 0 ? (
|
||||||
|
<img
|
||||||
|
className="gallery-card-image"
|
||||||
|
alt={gallery.path}
|
||||||
|
src={`${gallery.files[0].path}?thumb=true`}
|
||||||
|
/>
|
||||||
|
) : undefined}
|
||||||
|
</Link>
|
||||||
|
<div className="card-section">
|
||||||
|
<h5 className="card-section-title">{gallery.path}</h5>
|
||||||
|
<span>
|
||||||
|
{gallery.files.length}
|
||||||
|
<FormattedPlural
|
||||||
|
value={gallery.files.length ?? 0}
|
||||||
|
one="image"
|
||||||
|
other="images"
|
||||||
|
/>
|
||||||
|
.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{maybeRenderPopoverButtonGroup()}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,21 +5,35 @@ import { FindGalleriesQueryResult } from "src/core/generated-graphql";
|
|||||||
import { useGalleriesList } from "src/hooks";
|
import { useGalleriesList } from "src/hooks";
|
||||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
|
import { GalleryCard } from "./GalleryCard";
|
||||||
|
|
||||||
export const GalleryList: React.FC = () => {
|
export const GalleryList: React.FC = () => {
|
||||||
const listData = useGalleriesList({
|
const listData = useGalleriesList({
|
||||||
|
zoomable: true,
|
||||||
renderContent,
|
renderContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderContent(
|
function renderContent(
|
||||||
result: FindGalleriesQueryResult,
|
result: FindGalleriesQueryResult,
|
||||||
filter: ListFilterModel
|
filter: ListFilterModel,
|
||||||
|
selectedIds: Set<string>,
|
||||||
|
zoomIndex: number
|
||||||
) {
|
) {
|
||||||
if (!result.data || !result.data.findGalleries) {
|
if (!result.data || !result.data.findGalleries) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.Grid) {
|
if (filter.displayMode === DisplayMode.Grid) {
|
||||||
return <h1>TODO</h1>;
|
return (
|
||||||
|
<div className="row justify-content-center">
|
||||||
|
{result.data.findGalleries.galleries.map((gallery) => (
|
||||||
|
<GalleryCard
|
||||||
|
key={gallery.id}
|
||||||
|
gallery={gallery}
|
||||||
|
zoomIndex={zoomIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (filter.displayMode === DisplayMode.List) {
|
if (filter.displayMode === DisplayMode.List) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,3 +5,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* stylelint-enable selector-class-pattern */
|
/* stylelint-enable selector-class-pattern */
|
||||||
|
|
||||||
|
.gallery-card {
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&-image {
|
||||||
|
object-fit: contain;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<hr />
|
<hr />
|
||||||
<ButtonGroup className="scene-popovers">
|
<ButtonGroup className="card-popovers">
|
||||||
{maybeRenderTagPopoverButton()}
|
{maybeRenderTagPopoverButton()}
|
||||||
{maybeRenderPerformerPopoverButton()}
|
{maybeRenderPerformerPopoverButton()}
|
||||||
{maybeRenderMoviePopoverButton()}
|
{maybeRenderMoviePopoverButton()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.scene-popovers {
|
.card-popovers {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SceneMarkerDataFragment,
|
SceneMarkerDataFragment,
|
||||||
TagDataFragment,
|
TagDataFragment,
|
||||||
MovieDataFragment,
|
MovieDataFragment,
|
||||||
|
SceneDataFragment,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { NavUtils, TextUtils } from "src/utils";
|
import { NavUtils, TextUtils } from "src/utils";
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ interface IProps {
|
|||||||
performer?: Partial<PerformerDataFragment>;
|
performer?: Partial<PerformerDataFragment>;
|
||||||
marker?: Partial<SceneMarkerDataFragment>;
|
marker?: Partial<SceneMarkerDataFragment>;
|
||||||
movie?: Partial<MovieDataFragment>;
|
movie?: Partial<MovieDataFragment>;
|
||||||
|
scene?: Partial<SceneDataFragment>;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +36,11 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
|
|||||||
title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(
|
title = `${props.marker.title} - ${TextUtils.secondsToTimestamp(
|
||||||
props.marker.seconds || 0
|
props.marker.seconds || 0
|
||||||
)}`;
|
)}`;
|
||||||
|
} else if (props.scene) {
|
||||||
|
link = `/scenes/${props.scene.id}`;
|
||||||
|
title = props.scene.title
|
||||||
|
? props.scene.title
|
||||||
|
: TextUtils.fileNameFromPath(props.scene.path ?? "");
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Badge className={`tag-item ${props.className}`} variant="secondary">
|
<Badge className={`tag-item ${props.className}`} variant="secondary">
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export const useFindGalleries = (filter: ListFilterModel) =>
|
|||||||
GQL.useFindGalleriesQuery({
|
GQL.useFindGalleriesQuery({
|
||||||
variables: {
|
variables: {
|
||||||
filter: filter.makeFindFilter(),
|
filter: filter.makeFindFilter(),
|
||||||
|
gallery_filter: filter.makeGalleryFilter(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ textarea.text-input {
|
|||||||
.zoom-0 {
|
.zoom-0 {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
|
||||||
.scene-card-video {
|
.scene-card-video,
|
||||||
|
.gallery-card-image {
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +111,8 @@ textarea.text-input {
|
|||||||
.zoom-1 {
|
.zoom-1 {
|
||||||
width: 320px;
|
width: 320px;
|
||||||
|
|
||||||
.scene-card-video {
|
.scene-card-video,
|
||||||
|
.gallery-card-image {
|
||||||
max-height: 240px;
|
max-height: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,8 @@ textarea.text-input {
|
|||||||
.zoom-2 {
|
.zoom-2 {
|
||||||
width: 480px;
|
width: 480px;
|
||||||
|
|
||||||
.scene-card-video {
|
.scene-card-video,
|
||||||
|
.gallery-card-image {
|
||||||
max-height: 360px;
|
max-height: 360px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +137,8 @@ textarea.text-input {
|
|||||||
.zoom-3 {
|
.zoom-3 {
|
||||||
width: 640px;
|
width: 640px;
|
||||||
|
|
||||||
.scene-card-video {
|
.scene-card-video,
|
||||||
|
.gallery-card-image {
|
||||||
max-height: 480px;
|
max-height: 480px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +148,8 @@ textarea.text-input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-card-video {
|
.scene-card-video,
|
||||||
|
.gallery-card-image {
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type CriterionType =
|
|||||||
| "hasMarkers"
|
| "hasMarkers"
|
||||||
| "sceneIsMissing"
|
| "sceneIsMissing"
|
||||||
| "performerIsMissing"
|
| "performerIsMissing"
|
||||||
|
| "galleryIsMissing"
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "sceneTags"
|
||||||
| "performers"
|
| "performers"
|
||||||
@@ -58,6 +59,8 @@ export abstract class Criterion {
|
|||||||
return "Is Missing";
|
return "Is Missing";
|
||||||
case "performerIsMissing":
|
case "performerIsMissing":
|
||||||
return "Is Missing";
|
return "Is Missing";
|
||||||
|
case "galleryIsMissing":
|
||||||
|
return "Is Missing";
|
||||||
case "tags":
|
case "tags":
|
||||||
return "Tags";
|
return "Tags";
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
|
|||||||
@@ -53,3 +53,13 @@ export class PerformerIsMissingCriterionOption implements ICriterionOption {
|
|||||||
public label: string = Criterion.getLabel("performerIsMissing");
|
public label: string = Criterion.getLabel("performerIsMissing");
|
||||||
public value: CriterionType = "performerIsMissing";
|
public value: CriterionType = "performerIsMissing";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class GalleryIsMissingCriterion extends IsMissingCriterion {
|
||||||
|
public type: CriterionType = "galleryIsMissing";
|
||||||
|
public options: string[] = ["scene"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GalleryIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
public label: string = Criterion.getLabel("galleryIsMissing");
|
||||||
|
public value: CriterionType = "galleryIsMissing";
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { HasMarkersCriterion } from "./has-markers";
|
|||||||
import {
|
import {
|
||||||
PerformerIsMissingCriterion,
|
PerformerIsMissingCriterion,
|
||||||
SceneIsMissingCriterion,
|
SceneIsMissingCriterion,
|
||||||
|
GalleryIsMissingCriterion,
|
||||||
} from "./is-missing";
|
} from "./is-missing";
|
||||||
import { NoneCriterion } from "./none";
|
import { NoneCriterion } from "./none";
|
||||||
import { PerformersCriterion } from "./performers";
|
import { PerformersCriterion } from "./performers";
|
||||||
@@ -42,6 +43,8 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new SceneIsMissingCriterion();
|
return new SceneIsMissingCriterion();
|
||||||
case "performerIsMissing":
|
case "performerIsMissing":
|
||||||
return new PerformerIsMissingCriterion();
|
return new PerformerIsMissingCriterion();
|
||||||
|
case "galleryIsMissing":
|
||||||
|
return new GalleryIsMissingCriterion();
|
||||||
case "tags":
|
case "tags":
|
||||||
return new TagsCriterion("tags");
|
return new TagsCriterion("tags");
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SortDirectionEnum,
|
SortDirectionEnum,
|
||||||
MovieFilterType,
|
MovieFilterType,
|
||||||
StudioFilterType,
|
StudioFilterType,
|
||||||
|
GalleryFilterType,
|
||||||
} from "src/core/generated-graphql";
|
} from "src/core/generated-graphql";
|
||||||
import { stringToGender } from "src/core/StashService";
|
import { stringToGender } from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
@@ -31,6 +32,7 @@ import {
|
|||||||
IsMissingCriterion,
|
IsMissingCriterion,
|
||||||
PerformerIsMissingCriterionOption,
|
PerformerIsMissingCriterionOption,
|
||||||
SceneIsMissingCriterionOption,
|
SceneIsMissingCriterionOption,
|
||||||
|
GalleryIsMissingCriterionOption,
|
||||||
} from "./criteria/is-missing";
|
} from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
import { NoneCriterionOption } from "./criteria/none";
|
||||||
import {
|
import {
|
||||||
@@ -182,8 +184,11 @@ export class ListFilterModel {
|
|||||||
case FilterMode.Galleries:
|
case FilterMode.Galleries:
|
||||||
this.sortBy = "path";
|
this.sortBy = "path";
|
||||||
this.sortByOptions = ["path"];
|
this.sortByOptions = ["path"];
|
||||||
this.displayModeOptions = [DisplayMode.List];
|
this.displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||||
this.criterionOptions = [new NoneCriterionOption()];
|
this.criterionOptions = [
|
||||||
|
new NoneCriterionOption(),
|
||||||
|
new GalleryIsMissingCriterionOption(),
|
||||||
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.SceneMarkers:
|
case FilterMode.SceneMarkers:
|
||||||
this.sortBy = "title";
|
this.sortBy = "title";
|
||||||
@@ -601,6 +606,21 @@ export class ListFilterModel {
|
|||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public makeGalleryFilter(): GalleryFilterType {
|
||||||
|
const result: GalleryFilterType = {};
|
||||||
|
this.criteria.forEach((criterion) => {
|
||||||
|
switch (criterion.type) {
|
||||||
|
case "galleryIsMissing":
|
||||||
|
result.is_missing = (criterion as IsMissingCriterion).value;
|
||||||
|
break;
|
||||||
|
// no default
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user