Skip to content

Commit

Permalink
Gallery list improvement (stashapp#622)
Browse files Browse the repository at this point in the history
* Add grid view to galleries
* Show scene in gallery card
* Add is missing scene gallery filter
* Don't store galleries with no images
  • Loading branch information
WithoutPants authored Jun 21, 2020
1 parent fc1c80a commit bee5dda
Show file tree
Hide file tree
Showing 26 changed files with 296 additions and 35 deletions.
5 changes: 5 additions & 0 deletions graphql/documents/data/gallery.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ fragment GalleryData on Gallery {
name
path
}
scene {
id
title
path
}
}
4 changes: 2 additions & 2 deletions graphql/documents/queries/gallery.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query FindGalleries($filter: FindFilterType) {
findGalleries(filter: $filter) {
query FindGalleries($filter: FindFilterType, $gallery_filter: GalleryFilterType) {
findGalleries(gallery_filter: $gallery_filter, filter: $filter) {
count
galleries {
...GalleryData
Expand Down
2 changes: 1 addition & 1 deletion graphql/schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type Query {
findMovies(movie_filter: MovieFilterType, filter: FindFilterType): FindMoviesResultType!

findGallery(id: ID!): Gallery
findGalleries(filter: FindFilterType): FindGalleriesResultType!
findGalleries(gallery_filter: GalleryFilterType, filter: FindFilterType): FindGalleriesResultType!

findTag(id: ID!): Tag

Expand Down
5 changes: 5 additions & 0 deletions graphql/schema/types/filters.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ input StudioFilterType {
parents: MultiCriterionInput
}

input GalleryFilterType {
"""Filter to only include galleries missing this property"""
is_missing: String
}

enum CriterionModifier {
"""="""
EQUALS,
Expand Down
1 change: 1 addition & 0 deletions graphql/schema/types/gallery.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type Gallery {
checksum: String!
path: String!
title: String
scene: Scene

"""The files in the gallery"""
files: [GalleryFilesType!]! # Resolver
Expand Down
10 changes: 10 additions & 0 deletions pkg/api/resolver_model_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"context"

"github.com/stashapp/stash/pkg/models"
)

Expand All @@ -13,3 +14,12 @@ func (r *galleryResolver) Files(ctx context.Context, obj *models.Gallery) ([]*mo
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
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))
}
7 changes: 4 additions & 3 deletions pkg/api/resolver_query_find_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ package api

import (
"context"
"github.com/stashapp/stash/pkg/models"
"strconv"

"github.com/stashapp/stash/pkg/models"
)

func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gallery, error) {
Expand All @@ -12,9 +13,9 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gal
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()
galleries, total := qb.Query(filter)
galleries, total := qb.Query(galleryFilter, filter)
return &models.FindGalleriesResultType{
Count: total,
Galleries: galleries,
Expand Down
15 changes: 14 additions & 1 deletion pkg/manager/task_clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func (t *CleanTask) Start(wg *sync.WaitGroup) {
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)
}
}
Expand All @@ -46,6 +46,19 @@ func (t *CleanTask) shouldClean(path string) bool {
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) {
ctx := context.TODO()
qb := models.NewSceneQueryBuilder()
Expand Down
8 changes: 6 additions & 2 deletions pkg/manager/task_scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ func (t *ScanTask) scanGallery() {
_, err = qb.Update(*gallery, tx)
}
} else {
logger.Infof("%s doesn't exist. Creating new item...", t.FilePath)
currentTime := time.Now()

newGallery := models.Gallery{
Expand All @@ -73,7 +72,12 @@ func (t *ScanTask) scanGallery() {
CreatedAt: 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 {
Expand Down
21 changes: 16 additions & 5 deletions pkg/models/model_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import (
"archive/zip"
"bytes"
"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/jpeg"
"io/ioutil"
"path/filepath"
"sort"
"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 {
Expand All @@ -28,6 +29,16 @@ type Gallery struct {

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 {
var galleryFiles []*GalleryFilesType
filteredFiles, readCloser, err := g.listZipContents()
Expand Down
31 changes: 22 additions & 9 deletions pkg/models/querybuilder_gallery.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/stashapp/stash/pkg/database"
)

const galleryTable = "galleries"

type GalleryQueryBuilder struct{}

func NewGalleryQueryBuilder() GalleryQueryBuilder {
Expand Down Expand Up @@ -112,25 +114,36 @@ func (qb *GalleryQueryBuilder) All() ([]*Gallery, error) {
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 {
findFilter = &FindFilterType{}
}

var whereClauses []string
var havingClauses []string
var args []interface{}
body := selectDistinctIDs("galleries")
query := queryBuilder{
tableName: galleryTable,
}

query.body = selectDistinctIDs("galleries")

if q := findFilter.Q; q != nil && *q != "" {
searchColumns := []string{"galleries.path", "galleries.checksum"}
clause, thisArgs := getSearchBinding(searchColumns, *q, false)
whereClauses = append(whereClauses, clause)
args = append(args, thisArgs...)
query.addWhere(clause)
query.addArg(thisArgs...)
}

if isMissingFilter := galleryFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
switch *isMissingFilter {
case "scene":
query.addWhere("galleries.scene_id IS NULL")
}
}

sortAndPagination := qb.getGallerySort(findFilter) + getPagination(findFilter)
idsResult, countResult := executeFindQuery("galleries", body, args, sortAndPagination, whereClauses, havingClauses)
query.sortAndPagination = qb.getGallerySort(findFilter) + getPagination(findFilter)
idsResult, countResult := query.executeFind()

var galleries []*Gallery
for _, id := range idsResult {
Expand Down
52 changes: 52 additions & 0 deletions pkg/models/querybuilder_gallery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,58 @@ func TestGalleryFindBySceneID(t *testing.T) {
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 Count
// TODO All
Expand Down
2 changes: 1 addition & 1 deletion pkg/models/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const performersNameCase = 3
const performersNameNoCase = 2
const moviesNameCase = 2
const moviesNameNoCase = 1
const totalGalleries = 1
const totalGalleries = 2
const tagsNameNoCase = 2
const tagsNameCase = 5
const studiosNameCase = 4
Expand Down
3 changes: 3 additions & 0 deletions ui/v2.5/src/components/Changelog/versions/v030.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const markup = `
* Add support for parent/child studios.
### 🎨 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.
* Add split xpath post-processing action.
* Improved the layout of the scene page.
Expand Down
71 changes: 71 additions & 0 deletions ui/v2.5/src/components/Galleries/GalleryCard.tsx
Original file line number Diff line number Diff line change
@@ -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}&nbsp;
<FormattedPlural
value={gallery.files.length ?? 0}
one="image"
other="images"
/>
.
</span>
</div>
{maybeRenderPopoverButtonGroup()}
</Card>
);
};
Loading

0 comments on commit bee5dda

Please sign in to comment.