From 5c4eeef870fb3660545831fae4550b50ef0aebc5 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Fri, 5 Aug 2022 19:37:55 +0100
Subject: [PATCH 1/7] Introduce thumbnail filtering options.

---
 api/graphql/generated.go                      | 242 ++++++++++++++++--
 api/graphql/models/site_info.go               |   2 +
 api/graphql/resolvers/thumbnails.go           |  30 +++
 api/graphql/schema.graphql                    |   5 +
 api/scanner/media_encoding/encode_photo.go    |  20 +-
 .../processing_tasks/process_photo_task.go    |   2 +-
 .../processing_tasks/processing_functions.go  |   2 +-
 ui/src/Pages/SettingsPage/SettingsPage.tsx    |   2 +
 .../SettingsPage/ThumbnailPreferences.tsx     | 192 ++++++++++++++
 .../setThumbnailMethodMutation.ts             |  19 ++
 .../__generated__/thumbnailMethodQuery.ts     |  20 ++
 11 files changed, 508 insertions(+), 28 deletions(-)
 create mode 100644 api/graphql/resolvers/thumbnails.go
 create mode 100644 ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
 create mode 100644 ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
 create mode 100644 ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts

diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index e6d58d2..88b97b7 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -151,30 +151,31 @@ type ComplexityRoot struct {
 	}
 
 	Mutation struct {
-		AuthorizeUser               func(childComplexity int, username string, password string) int
-		ChangeUserPreferences       func(childComplexity int, language *string) int
-		CombineFaceGroups           func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int
-		CreateUser                  func(childComplexity int, username string, password *string, admin bool) int
-		DeleteShareToken            func(childComplexity int, token string) int
-		DeleteUser                  func(childComplexity int, id int) int
-		DetachImageFaces            func(childComplexity int, imageFaceIDs []int) int
-		FavoriteMedia               func(childComplexity int, mediaID int, favorite bool) int
-		InitialSetupWizard          func(childComplexity int, username string, password string, rootPath string) int
-		MoveImageFaces              func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
-		ProtectShareToken           func(childComplexity int, token string, password *string) int
-		RecognizeUnlabeledFaces     func(childComplexity int) int
-		ResetAlbumCover             func(childComplexity int, albumID int) int
-		ScanAll                     func(childComplexity int) int
-		ScanUser                    func(childComplexity int, userID int) int
-		SetAlbumCover               func(childComplexity int, coverID int) int
-		SetFaceGroupLabel           func(childComplexity int, faceGroupID int, label *string) int
-		SetPeriodicScanInterval     func(childComplexity int, interval int) int
-		SetScannerConcurrentWorkers func(childComplexity int, workers int) int
-		ShareAlbum                  func(childComplexity int, albumID int, expire *time.Time, password *string) int
-		ShareMedia                  func(childComplexity int, mediaID int, expire *time.Time, password *string) int
-		UpdateUser                  func(childComplexity int, id int, username *string, password *string, admin *bool) int
-		UserAddRootPath             func(childComplexity int, id int, rootPath string) int
-		UserRemoveRootAlbum         func(childComplexity int, userID int, albumID int) int
+		AuthorizeUser                func(childComplexity int, username string, password string) int
+		ChangeUserPreferences        func(childComplexity int, language *string) int
+		CombineFaceGroups            func(childComplexity int, destinationFaceGroupID int, sourceFaceGroupID int) int
+		CreateUser                   func(childComplexity int, username string, password *string, admin bool) int
+		DeleteShareToken             func(childComplexity int, token string) int
+		DeleteUser                   func(childComplexity int, id int) int
+		DetachImageFaces             func(childComplexity int, imageFaceIDs []int) int
+		FavoriteMedia                func(childComplexity int, mediaID int, favorite bool) int
+		InitialSetupWizard           func(childComplexity int, username string, password string, rootPath string) int
+		MoveImageFaces               func(childComplexity int, imageFaceIDs []int, destinationFaceGroupID int) int
+		ProtectShareToken            func(childComplexity int, token string, password *string) int
+		RecognizeUnlabeledFaces      func(childComplexity int) int
+		ResetAlbumCover              func(childComplexity int, albumID int) int
+		ScanAll                      func(childComplexity int) int
+		ScanUser                     func(childComplexity int, userID int) int
+		SetAlbumCover                func(childComplexity int, coverID int) int
+		SetFaceGroupLabel            func(childComplexity int, faceGroupID int, label *string) int
+		SetPeriodicScanInterval      func(childComplexity int, interval int) int
+		SetScannerConcurrentWorkers  func(childComplexity int, workers int) int
+		SetThumbnailDownsampleMethod func(childComplexity int, method int) int
+		ShareAlbum                   func(childComplexity int, albumID int, expire *time.Time, password *string) int
+		ShareMedia                   func(childComplexity int, mediaID int, expire *time.Time, password *string) int
+		UpdateUser                   func(childComplexity int, id int, username *string, password *string, admin *bool) int
+		UserAddRootPath              func(childComplexity int, id int, rootPath string) int
+		UserRemoveRootAlbum          func(childComplexity int, userID int, albumID int) int
 	}
 
 	Notification struct {
@@ -236,6 +237,7 @@ type ComplexityRoot struct {
 		FaceDetectionEnabled func(childComplexity int) int
 		InitialSetup         func(childComplexity int) int
 		PeriodicScanInterval func(childComplexity int) int
+		ThumbnailMethod      func(childComplexity int) int
 	}
 
 	Subscription struct {
@@ -326,6 +328,7 @@ type MutationResolver interface {
 	UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
 	SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
 	SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
+	SetThumbnailDownsampleMethod(ctx context.Context, method int) (int, error)
 	ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error)
 	ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error)
 	SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error)
@@ -1057,6 +1060,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Mutation.SetScannerConcurrentWorkers(childComplexity, args["workers"].(int)), true
 
+	case "Mutation.setThumbnailDownsampleMethod":
+		if e.complexity.Mutation.SetThumbnailDownsampleMethod == nil {
+			break
+		}
+
+		args, err := ec.field_Mutation_setThumbnailDownsampleMethod_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Mutation.SetThumbnailDownsampleMethod(childComplexity, args["method"].(int)), true
+
 	case "Mutation.shareAlbum":
 		if e.complexity.Mutation.ShareAlbum == nil {
 			break
@@ -1478,6 +1493,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.SiteInfo.PeriodicScanInterval(childComplexity), true
 
+	case "SiteInfo.thumbnailMethod":
+		if e.complexity.SiteInfo.ThumbnailMethod == nil {
+			break
+		}
+
+		return e.complexity.SiteInfo.ThumbnailMethod(childComplexity), true
+
 	case "Subscription.notification":
 		if e.complexity.Subscription.Notification == nil {
 			break
@@ -2156,6 +2178,21 @@ func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx
 	return args, nil
 }
 
+func (ec *executionContext) field_Mutation_setThumbnailDownsampleMethod_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 int
+	if tmp, ok := rawArgs["method"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("method"))
+		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["method"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Mutation_shareAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -7158,6 +7195,81 @@ func (ec *executionContext) fieldContext_Mutation_setScannerConcurrentWorkers(ct
 	return fc, nil
 }
 
+func (ec *executionContext) _Mutation_setThumbnailDownsampleMethod(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_Mutation_setThumbnailDownsampleMethod(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return ec.resolvers.Mutation().SetThumbnailDownsampleMethod(rctx, fc.Args["method"].(int))
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, nil, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(int); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int)
+	fc.Result = res
+	return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_setThumbnailDownsampleMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "Mutation",
+		Field:      field,
+		IsMethod:   true,
+		IsResolver: true,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	defer func() {
+		if r := recover(); r != nil {
+			err = ec.Recover(ctx, r)
+			ec.Error(ctx, err)
+		}
+	}()
+	ctx = graphql.WithFieldContext(ctx, fc)
+	if fc.Args, err = ec.field_Mutation_setThumbnailDownsampleMethod_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+		ec.Error(ctx, err)
+		return
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Mutation_changeUserPreferences(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	fc, err := ec.fieldContext_Mutation_changeUserPreferences(ctx, field)
 	if err != nil {
@@ -8240,6 +8352,8 @@ func (ec *executionContext) fieldContext_Query_siteInfo(ctx context.Context, fie
 				return ec.fieldContext_SiteInfo_periodicScanInterval(ctx, field)
 			case "concurrentWorkers":
 				return ec.fieldContext_SiteInfo_concurrentWorkers(ctx, field)
+			case "thumbnailMethod":
+				return ec.fieldContext_SiteInfo_thumbnailMethod(ctx, field)
 			}
 			return nil, fmt.Errorf("no field named %q was found under type SiteInfo", field.Name)
 		},
@@ -10584,6 +10698,70 @@ func (ec *executionContext) fieldContext_SiteInfo_concurrentWorkers(ctx context.
 	return fc, nil
 }
 
+func (ec *executionContext) _SiteInfo_thumbnailMethod(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) {
+	fc, err := ec.fieldContext_SiteInfo_thumbnailMethod(ctx, field)
+	if err != nil {
+		return graphql.Null
+	}
+	ctx = graphql.WithFieldContext(ctx, fc)
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		directive0 := func(rctx context.Context) (interface{}, error) {
+			ctx = rctx // use context from middleware stack in children
+			return obj.ThumbnailMethod, nil
+		}
+		directive1 := func(ctx context.Context) (interface{}, error) {
+			if ec.directives.IsAdmin == nil {
+				return nil, errors.New("directive isAdmin is not implemented")
+			}
+			return ec.directives.IsAdmin(ctx, obj, directive0)
+		}
+
+		tmp, err := directive1(rctx)
+		if err != nil {
+			return nil, graphql.ErrorOnPath(ctx, err)
+		}
+		if tmp == nil {
+			return nil, nil
+		}
+		if data, ok := tmp.(int); ok {
+			return data, nil
+		}
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp)
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(int)
+	fc.Result = res
+	return ec.marshalNInt2int(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SiteInfo_thumbnailMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+	fc = &graphql.FieldContext{
+		Object:     "SiteInfo",
+		Field:      field,
+		IsMethod:   false,
+		IsResolver: false,
+		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+			return nil, errors.New("field of type Int does not have child fields")
+		},
+	}
+	return fc, nil
+}
+
 func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func(ctx context.Context) graphql.Marshaler) {
 	fc, err := ec.fieldContext_Subscription_notification(ctx, field)
 	if err != nil {
@@ -14625,6 +14803,15 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
 				return ec._Mutation_setScannerConcurrentWorkers(ctx, field)
 			})
 
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		case "setThumbnailDownsampleMethod":
+
+			out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+				return ec._Mutation_setThumbnailDownsampleMethod(ctx, field)
+			})
+
 			if out.Values[i] == graphql.Null {
 				invalids++
 			}
@@ -15419,6 +15606,13 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
 
 			out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj)
 
+			if out.Values[i] == graphql.Null {
+				atomic.AddUint32(&invalids, 1)
+			}
+		case "thumbnailMethod":
+
+			out.Values[i] = ec._SiteInfo_thumbnailMethod(ctx, field, obj)
+
 			if out.Values[i] == graphql.Null {
 				atomic.AddUint32(&invalids, 1)
 			}
diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go
index d358adb..1ea1c50 100644
--- a/api/graphql/models/site_info.go
+++ b/api/graphql/models/site_info.go
@@ -10,6 +10,7 @@ type SiteInfo struct {
 	InitialSetup         bool `gorm:"not null"`
 	PeriodicScanInterval int  `gorm:"not null"`
 	ConcurrentWorkers    int  `gorm:"not null"`
+	ThumbnailMethod   	 int  `gorm:"not null"`
 }
 
 func (SiteInfo) TableName() string {
@@ -26,6 +27,7 @@ func DefaultSiteInfo(db *gorm.DB) SiteInfo {
 		InitialSetup:         true,
 		PeriodicScanInterval: 0,
 		ConcurrentWorkers:    defaultConcurrentWorkers,
+		ThumbnailMethod:			0,
 	}
 }
 
diff --git a/api/graphql/resolvers/thumbnails.go b/api/graphql/resolvers/thumbnails.go
new file mode 100644
index 0000000..8f5fb02
--- /dev/null
+++ b/api/graphql/resolvers/thumbnails.go
@@ -0,0 +1,30 @@
+package resolvers
+
+import (
+	"context"
+	
+	"github.com/photoview/photoview/api/graphql/models"
+	"github.com/pkg/errors"
+	"gorm.io/gorm"
+)
+
+func (r *mutationResolver) SetThumbnailDownsampleMethod(ctx context.Context, method int) (int, error) {
+	db := r.DB(ctx)
+
+	if method > 5 {
+		return 0, errors.New("The requested filter is unsupported, defaulting to nearest neighbor")
+	}
+
+	if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&models.SiteInfo{}).Update("thumbnail_method", method).Error; err != nil {
+		return 0, err
+	}
+
+	var siteInfo models.SiteInfo
+	if err := db.First(&siteInfo).Error; err != nil {
+		return 0, err
+	}
+
+	// scanner_queue.ChangeScannerConcurrentWorkers(siteInfo.ConcurrentWorkers)
+
+	return siteInfo.ThumbnailMethod, nil
+}
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index bccc552..91c7dfb 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -163,6 +163,9 @@ type Mutation {
   "Set max number of concurrent scanner jobs running at once"
   setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin
 
+  "Set the filter to be used when generating thumbnails"
+  setThumbnailDownsampleMethod(method: Int!): Int! @isAdmin
+
   "Change user preferences for the logged in user"
   changeUserPreferences(language: String): UserPreferences! @isAuthorized
 
@@ -257,6 +260,8 @@ type SiteInfo {
   periodicScanInterval: Int! @isAdmin
   "How many max concurrent scanner jobs that should run at once"
   concurrentWorkers: Int! @isAdmin
+  "The filter to use when generating thumbnails"
+  thumbnailMethod: Int! @isAdmin
 }
 
 type User {
diff --git a/api/scanner/media_encoding/encode_photo.go b/api/scanner/media_encoding/encode_photo.go
index fedc47c..b7221d9 100644
--- a/api/scanner/media_encoding/encode_photo.go
+++ b/api/scanner/media_encoding/encode_photo.go
@@ -17,9 +17,25 @@ import (
 	"gopkg.in/vansante/go-ffprobe.v2"
 
 	_ "github.com/strukturag/libheif/go/heif"
+
+	"gorm.io/gorm"
 )
 
-func EncodeThumbnail(inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) {
+var thumbFilter = map[int]imaging.ResampleFilter{
+	0:  imaging.NearestNeighbor,
+	1:  imaging.Box,
+	2:	imaging.Linear,
+	3:	imaging.MitchellNetravali,
+	4:	imaging.CatmullRom,
+	5:	imaging.Lanczos,
+}
+
+func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) {
+
+	var siteInfo models.SiteInfo
+	if err := db.First(&siteInfo).Error; err != nil {
+		return nil, err
+	}
 
 	inputImage, err := imaging.Open(inputPath, imaging.AutoOrientation(true))
 	if err != nil {
@@ -29,7 +45,7 @@ func EncodeThumbnail(inputPath string, outputPath string) (*media_utils.PhotoDim
 	dimensions := media_utils.PhotoDimensionsFromRect(inputImage.Bounds())
 	dimensions = dimensions.ThumbnailScale()
 
-	thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, imaging.NearestNeighbor)
+	thumbImage := imaging.Resize(inputImage, dimensions.Width, dimensions.Height, thumbFilter[siteInfo.ThumbnailMethod])
 	if err = encodeImageJPEG(thumbImage, outputPath, 60); err != nil {
 		return nil, err
 	}
diff --git a/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
index 042eac2..5ea94d3 100644
--- a/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
+++ b/api/scanner/scanner_tasks/processing_tasks/process_photo_task.go
@@ -128,7 +128,7 @@ func (t ProcessPhotoTask) ProcessMedia(ctx scanner_task.TaskContext, mediaData *
 			updatedURLs = append(updatedURLs, thumbURL)
 			fmt.Printf("Thumbnail photo found in database but not in cache, re-encoding photo to cache: %s\n", thumbURL.MediaName)
 
-			_, err := media_encoding.EncodeThumbnail(baseImagePath, thumbPath)
+			_, err := media_encoding.EncodeThumbnail(ctx.GetDB(), baseImagePath, thumbPath)
 			if err != nil {
 				return []*models.MediaURL{}, errors.Wrap(err, "could not create thumbnail cached image")
 			}
diff --git a/api/scanner/scanner_tasks/processing_tasks/processing_functions.go b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
index 2f7b040..6e08ad5 100644
--- a/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
+++ b/api/scanner/scanner_tasks/processing_tasks/processing_functions.go
@@ -59,7 +59,7 @@ func generateSaveHighResJPEG(tx *gorm.DB, media *models.Media, imageData *media_
 func generateSaveThumbnailJPEG(tx *gorm.DB, media *models.Media, thumbnail_name string, photoCachePath string, baseImagePath string, mediaURL *models.MediaURL) (*models.MediaURL, error) {
 	thumbOutputPath := path.Join(photoCachePath, thumbnail_name)
 
-	thumbSize, err := media_encoding.EncodeThumbnail(baseImagePath, thumbOutputPath)
+	thumbSize, err := media_encoding.EncodeThumbnail(tx, baseImagePath, thumbOutputPath)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not create thumbnail cached image")
 	}
diff --git a/ui/src/Pages/SettingsPage/SettingsPage.tsx b/ui/src/Pages/SettingsPage/SettingsPage.tsx
index 9da1757..4530198 100644
--- a/ui/src/Pages/SettingsPage/SettingsPage.tsx
+++ b/ui/src/Pages/SettingsPage/SettingsPage.tsx
@@ -5,6 +5,7 @@ import { useIsAdmin } from '../../components/routes/AuthorizedRoute'
 import Layout from '../../components/layout/Layout'
 import ScannerSection from './ScannerSection'
 import UserPreferences from './UserPreferences'
+import ThumbnailPreferences from './ThumbnailPreferences'
 import UsersTable from './Users/UsersTable'
 import VersionInfo from './VersionInfo'
 import classNames from 'classnames'
@@ -46,6 +47,7 @@ const SettingsPage = () => {
         <>
           <ScannerSection />
           <UsersTable />
+          <ThumbnailPreferences />
         </>
       )}
       <VersionInfo />
diff --git a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
new file mode 100644
index 0000000..333e27e
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
@@ -0,0 +1,192 @@
+import { gql } from '@apollo/client'
+import React, { useRef, useState } from 'react'
+import { useMutation, useQuery } from '@apollo/client'
+import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
+import { useTranslation } from 'react-i18next'
+import { thumbnailMethodQuery } from './__generated__/thumbnailMethodQuery'
+import {
+  setThumbnailMethodMutation,
+  setThumbnailMethodMutationVariables,
+} from './__generated__/setThumbnailMethodMutation'
+import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
+import Loader from '../../primitives/Loader'
+
+export const THUMBNAIL_METHOD_QUERY = gql`
+  query thumbnailMethodQuery {
+    siteInfo {
+      thumbnailMethod
+    }
+  }
+`
+
+export const SET_THUMBNAIL_METHOD_MUTATION = gql`
+  mutation setThumbnailMethodMutation($method: Int!) {
+    setThumbnailDownsampleMethod(method: $method)
+  }
+`
+
+const ThumbnailPreferences = () => {
+  const { t } = useTranslation()
+
+  const downsampleMethodServerValue = useRef<null | number>(null)
+  const [downsampleMethod, setDownsampleMethod] = useState(0)
+
+  const downsampleMethodQuery = useQuery<thumbnailMethodQuery>(
+    THUMBNAIL_METHOD_QUERY,
+    {
+      onCompleted(data) {
+        setDownsampleMethod(data.siteInfo.thumbnailMethod)
+        downsampleMethodServerValue.current = data.siteInfo.thumbnailMethod
+      },
+    }
+  )
+
+  const [setDownsampleMutation, downsampleMutationData] = useMutation<
+    setThumbnailMethodMutation,
+    setThumbnailMethodMutationVariables
+  >(SET_THUMBNAIL_METHOD_MUTATION)
+
+  const updateDownsampleMethod = (downsampleMethod: number) => {
+    if (downsampleMethodServerValue.current != downsampleMethod) {
+      downsampleMethodServerValue.current = downsampleMethod
+      setDownsampleMutation({
+        variables: {
+          method: downsampleMethod,
+        },
+      })
+    }
+  }
+
+  const methodItems: DropdownItem[] = [
+    {
+      label: t(
+        'settings.thumbnails.downsample_method.nearest_neighbor',
+        'Nearest Neighbor'
+      ),
+      value: 0,
+    },
+    {
+      label: t('settings.thumbnails.downsample_method.box', 'Box'),
+      value: 1,
+    },
+    {
+      label: t('settings.thumbnails.downsample_method.linear', 'Linear'),
+      value: 2,
+    },
+    {
+      label: t(
+        'settings.thumbnails.downsample_method.mitchell_netravali',
+        'Mitchell-Netravali'
+      ),
+      value: 3,
+    },
+    {
+      label: t(
+        'settings.thumbnails.downsample_method.catmull_rom',
+        'Catmull-Rom'
+      ),
+      value: 4,
+    },
+    {
+      label: t('settings.thumbnails.downsample_method.Lanczos', 'Lanczos'),
+      value: 5,
+    },
+  ]
+
+  // const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
+  // const [thumbnailMethod, setThumbnailMethod] = useState({
+  //   value: 0,
+  //   unit: TimeUnit.Second,
+  // })
+  //
+  // const scanIntervalServerValue = useRef<number | null>(null)
+  //
+  // const scanIntervalQuery = useQuery<scanIntervalQuery>(SCAN_INTERVAL_QUERY, {
+  //   onCompleted(data) {
+  //     const queryScanInterval = data.siteInfo.periodicScanInterval
+  //
+  //     if (queryScanInterval == 0) {
+  //       setScanInterval({
+  //         unit: TimeUnit.Second,
+  //         value: 0,
+  //       })
+  //     } else {
+  //       setScanInterval(
+  //         convertToAppropriateUnit({
+  //           unit: TimeUnit.Second,
+  //           value: queryScanInterval,
+  //         })
+  //       )
+  //     }
+  //
+  //     setEnablePeriodicScanner(queryScanInterval > 0)
+  //   },
+  // })
+  //
+  // const [setScanIntervalMutation, { loading: scanIntervalMutationLoading }] =
+  //   useMutation<
+  //     changeScanIntervalMutation,
+  //     changeScanIntervalMutationVariables
+  //   >(SCAN_INTERVAL_MUTATION)
+  //
+  // const onScanIntervalCheckboxChange = (checked: boolean) => {
+  //   setEnablePeriodicScanner(checked)
+  //
+  //   onScanIntervalUpdate(
+  //     checked ? scanInterval : { value: 0, unit: TimeUnit.Second }
+  //   )
+  // }
+  //
+  // const onScanIntervalUpdate = (scanInterval: TimeValue) => {
+  //   const seconds = convertToSeconds(scanInterval)
+  //
+  //   if (scanIntervalServerValue.current != seconds) {
+  //     setScanIntervalMutation({
+  //       variables: {
+  //         interval: seconds,
+  //       },
+  //     })
+  //     scanIntervalServerValue.current = seconds
+  //   }
+  // }
+
+  return (
+    <>
+      <div className="mt-4">
+        <label htmlFor="thumbnail_method_field">
+          <InputLabelTitle>
+            {t('settings.thumbnails.field.label', 'Downsampling method')}
+          </InputLabelTitle>
+          <InputLabelDescription>
+            {t(
+              'settings.thumbnails.field.description',
+              'The filter to use when generating thumbnails'
+            )}
+          </InputLabelDescription>
+        </label>
+        <div className="flex gap-2">
+          <Dropdown
+            aria-label="Method"
+            items={methodItems}
+            selected={downsampleMethod}
+            setSelected={value => {
+              setDownsampleMethod(value)
+              updateDownsampleMethod(value)
+            }}
+          />
+        </div>
+      </div>
+      <Loader
+        active={downsampleMethodQuery.loading || downsampleMutationData.loading}
+        size="small"
+        style={{ marginLeft: 16 }}
+      />
+    </>
+  )
+}
+
+export default ThumbnailPreferences
+
+// <h3 className="font-semibold text-lg mt-4 mb-2">
+//   {t('settings.thumbnails.title', 'Thumbnail preferences')}
+// </h3>
diff --git a/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
new file mode 100644
index 0000000..15611ca
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
@@ -0,0 +1,19 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL mutation operation: setThumbnailMethodMutation
+// ====================================================
+
+export interface setThumbnailMethodMutation {
+  /**
+   * Set the filter to be used when generating thumbnails
+   */
+  setThumbnailDownsampleMethod: number
+}
+
+export interface setThumbnailMethodMutationVariables {
+  method: number
+}
diff --git a/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
new file mode 100644
index 0000000..ef0690a
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
@@ -0,0 +1,20 @@
+/* tslint:disable */
+/* eslint-disable */
+// @generated
+// This file was automatically generated and should not be edited.
+
+// ====================================================
+// GraphQL query operation: thumbnailMethodQuery
+// ====================================================
+
+export interface thumbnailMethodQuery_siteInfo {
+  __typename: 'SiteInfo'
+  /**
+   * The filter to use when generating thumbnails
+   */
+  thumbnailMethod: number
+}
+
+export interface thumbnailMethodQuery {
+  siteInfo: thumbnailMethodQuery_siteInfo
+}

From ccf4e30639e9be6cf2c22e053a3f917334129f33 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Fri, 5 Aug 2022 19:39:06 +0100
Subject: [PATCH 2/7] Including ui test

---
 .../thumbnailPreferences.test.tsx             | 51 +++++++++++++++++++
 1 file changed, 51 insertions(+)
 create mode 100644 ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx

diff --git a/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx b/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
new file mode 100644
index 0000000..cd5af58
--- /dev/null
+++ b/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
@@ -0,0 +1,51 @@
+import React from 'react'
+import { MockedProvider } from '@apollo/client/testing'
+
+import { render, screen } from '@testing-library/react'
+
+import {
+  THUMBNAIL_METHOD_QUERY,
+  SET_THUMBNAIL_METHOD_MUTATION,
+  ThumbnailPreferences,
+} from './ThumbnailPreferences'
+
+test('load ThumbnailPreferences', () => {
+  const graphqlMocks = [
+    {
+      request: {
+        query: THUMBNAIL_METHOD_QUERY,
+      },
+      result: {
+        data: {
+          siteInfo: { method: 0 },
+        },
+      },
+    },
+    {
+      request: {
+        query: SET_THUMBNAIL_METHOD_MUTATION,
+        variables: {
+          method: '5',
+        },
+      },
+      result: {
+        data: {},
+      },
+    },
+  ]
+  render(
+    <MockedProvider
+      mocks={graphqlMocks}
+      addTypename={false}
+      defaultOptions={{
+        // disable cache, required to make fragments work
+        watchQuery: { fetchPolicy: 'no-cache' },
+        query: { fetchPolicy: 'no-cache' },
+      }}
+    >
+      <ThumbnailPreferences />
+    </MockedProvider>
+  )
+
+  expect(screen.getByText('Downsampling method')).toBeInTheDocument()
+})

From 1c784fa5e98939146532907f6f369bfd1d746fe4 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Fri, 5 Aug 2022 19:40:26 +0100
Subject: [PATCH 3/7] Simple api test

---
 api/graphql/models/site_info_test.go | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go
index 0881939..1347286 100644
--- a/api/graphql/models/site_info_test.go
+++ b/api/graphql/models/site_info_test.go
@@ -22,6 +22,7 @@ func TestSiteInfo(t *testing.T) {
 	site_info.InitialSetup = false
 	site_info.PeriodicScanInterval = 360
 	site_info.ConcurrentWorkers = 10
+	site_info.ThumbnailMethod = 5
 
 	if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) {
 		return
@@ -36,6 +37,7 @@ func TestSiteInfo(t *testing.T) {
 		InitialSetup:         false,
 		PeriodicScanInterval: 360,
 		ConcurrentWorkers:    10,
+		ThumbnailMethod:    	5,
 	}, *site_info)
 
 }

From 31025f7616e76590bd76a07a6b042bbefa45e2b8 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Fri, 5 Aug 2022 20:35:00 +0100
Subject: [PATCH 4/7] Updated ui layout.

---
 .../SettingsPage/ThumbnailPreferences.tsx     | 138 +++++-------------
 1 file changed, 40 insertions(+), 98 deletions(-)

diff --git a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
index 333e27e..8f46832 100644
--- a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
@@ -1,7 +1,11 @@
 import { gql } from '@apollo/client'
 import React, { useRef, useState } from 'react'
 import { useMutation, useQuery } from '@apollo/client'
-import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
+import {
+  SectionTitle,
+  InputLabelDescription,
+  InputLabelTitle,
+} from './SettingsPage'
 import { useTranslation } from 'react-i18next'
 import { thumbnailMethodQuery } from './__generated__/thumbnailMethodQuery'
 import {
@@ -60,133 +64,71 @@ const ThumbnailPreferences = () => {
   const methodItems: DropdownItem[] = [
     {
       label: t(
-        'settings.thumbnails.downsample_method.nearest_neighbor',
-        'Nearest Neighbor'
+        'settings.thumbnails.method.filter.nearest_neighbor',
+        'Nearest Neighbor (default)'
       ),
       value: 0,
     },
     {
-      label: t('settings.thumbnails.downsample_method.box', 'Box'),
+      label: t('settings.thumbnails.method.filter.box', 'Box'),
       value: 1,
     },
     {
-      label: t('settings.thumbnails.downsample_method.linear', 'Linear'),
+      label: t('settings.thumbnails.method.filter.linear', 'Linear'),
       value: 2,
     },
     {
       label: t(
-        'settings.thumbnails.downsample_method.mitchell_netravali',
+        'settings.thumbnails.method.filter.mitchell_netravali',
         'Mitchell-Netravali'
       ),
       value: 3,
     },
     {
-      label: t(
-        'settings.thumbnails.downsample_method.catmull_rom',
-        'Catmull-Rom'
-      ),
+      label: t('settings.thumbnails.method.filter.catmull_rom', 'Catmull-Rom'),
       value: 4,
     },
     {
-      label: t('settings.thumbnails.downsample_method.Lanczos', 'Lanczos'),
+      label: t(
+        'settings.thumbnails.method.filter.Lanczos',
+        'Lanczos (highest quality)'
+      ),
       value: 5,
     },
   ]
 
-  // const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
-  // const [thumbnailMethod, setThumbnailMethod] = useState({
-  //   value: 0,
-  //   unit: TimeUnit.Second,
-  // })
-  //
-  // const scanIntervalServerValue = useRef<number | null>(null)
-  //
-  // const scanIntervalQuery = useQuery<scanIntervalQuery>(SCAN_INTERVAL_QUERY, {
-  //   onCompleted(data) {
-  //     const queryScanInterval = data.siteInfo.periodicScanInterval
-  //
-  //     if (queryScanInterval == 0) {
-  //       setScanInterval({
-  //         unit: TimeUnit.Second,
-  //         value: 0,
-  //       })
-  //     } else {
-  //       setScanInterval(
-  //         convertToAppropriateUnit({
-  //           unit: TimeUnit.Second,
-  //           value: queryScanInterval,
-  //         })
-  //       )
-  //     }
-  //
-  //     setEnablePeriodicScanner(queryScanInterval > 0)
-  //   },
-  // })
-  //
-  // const [setScanIntervalMutation, { loading: scanIntervalMutationLoading }] =
-  //   useMutation<
-  //     changeScanIntervalMutation,
-  //     changeScanIntervalMutationVariables
-  //   >(SCAN_INTERVAL_MUTATION)
-  //
-  // const onScanIntervalCheckboxChange = (checked: boolean) => {
-  //   setEnablePeriodicScanner(checked)
-  //
-  //   onScanIntervalUpdate(
-  //     checked ? scanInterval : { value: 0, unit: TimeUnit.Second }
-  //   )
-  // }
-  //
-  // const onScanIntervalUpdate = (scanInterval: TimeValue) => {
-  //   const seconds = convertToSeconds(scanInterval)
-  //
-  //   if (scanIntervalServerValue.current != seconds) {
-  //     setScanIntervalMutation({
-  //       variables: {
-  //         interval: seconds,
-  //       },
-  //     })
-  //     scanIntervalServerValue.current = seconds
-  //   }
-  // }
-
   return (
-    <>
-      <div className="mt-4">
-        <label htmlFor="thumbnail_method_field">
-          <InputLabelTitle>
-            {t('settings.thumbnails.field.label', 'Downsampling method')}
-          </InputLabelTitle>
-          <InputLabelDescription>
-            {t(
-              'settings.thumbnails.field.description',
-              'The filter to use when generating thumbnails'
-            )}
-          </InputLabelDescription>
-        </label>
-        <div className="flex gap-2">
-          <Dropdown
-            aria-label="Method"
-            items={methodItems}
-            selected={downsampleMethod}
-            setSelected={value => {
-              setDownsampleMethod(value)
-              updateDownsampleMethod(value)
-            }}
-          />
-        </div>
-      </div>
+    <div>
+      <SectionTitle>
+        {t('settings.thumbnails.title', 'Thumbnail preferences')}
+      </SectionTitle>
+      <label htmlFor="thumbnail_method_field">
+        <InputLabelTitle>
+          {t('settings.thumbnails.method.label', 'Downsampling method')}
+        </InputLabelTitle>
+        <InputLabelDescription>
+          {t(
+            'settings.thumbnails.method.description',
+            'The filter to use when generating thumbnails'
+          )}
+        </InputLabelDescription>
+      </label>
+      <Dropdown
+        aria-label="Method"
+        items={methodItems}
+        selected={downsampleMethod}
+        setSelected={value => {
+          setDownsampleMethod(value)
+          updateDownsampleMethod(value)
+        }}
+      />
       <Loader
         active={downsampleMethodQuery.loading || downsampleMutationData.loading}
         size="small"
         style={{ marginLeft: 16 }}
       />
-    </>
+    </div>
   )
 }
 
 export default ThumbnailPreferences
-
-// <h3 className="font-semibold text-lg mt-4 mb-2">
-//   {t('settings.thumbnails.title', 'Thumbnail preferences')}
-// </h3>

From 866a3b4c1d8ed616af9c1e3a90cad60e38194c76 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Fri, 5 Aug 2022 20:56:38 +0100
Subject: [PATCH 5/7] Fixed ui test (default export).

---
 ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx b/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
index cd5af58..d725b2c 100644
--- a/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
+++ b/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
@@ -3,10 +3,9 @@ import { MockedProvider } from '@apollo/client/testing'
 
 import { render, screen } from '@testing-library/react'
 
-import {
+import ThumbnailPreferences, {
   THUMBNAIL_METHOD_QUERY,
   SET_THUMBNAIL_METHOD_MUTATION,
-  ThumbnailPreferences,
 } from './ThumbnailPreferences'
 
 test('load ThumbnailPreferences', () => {

From 3e780dbe48596759461d0ff9efc3c6c11663c049 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Tue, 9 Aug 2022 06:09:55 +0100
Subject: [PATCH 6/7] Changed downsampling method to custom type.

---
 api/graphql/generated.go                      | 42 ++++++++++------
 api/graphql/models/generated.go               | 50 +++++++++++++++++++
 api/graphql/models/site_info.go               |  4 +-
 api/graphql/models/site_info_test.go          |  4 +-
 api/graphql/resolvers/thumbnails.go           | 26 ++++++----
 api/graphql/schema.graphql                    | 14 +++++-
 api/scanner/media_encoding/encode_photo.go    | 14 +++---
 ...test.tsx => ThumbnailPreferences.test.tsx} |  6 ++-
 .../SettingsPage/ThumbnailPreferences.tsx     | 15 +++---
 .../setThumbnailMethodMutation.ts             |  6 ++-
 .../__generated__/thumbnailMethodQuery.ts     |  4 +-
 ui/src/__generated__/globalTypes.ts           | 12 +++++
 12 files changed, 146 insertions(+), 51 deletions(-)
 rename ui/src/Pages/SettingsPage/{thumbnailPreferences.test.tsx => ThumbnailPreferences.test.tsx} (85%)

diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 88b97b7..8bef0a7 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -170,7 +170,7 @@ type ComplexityRoot struct {
 		SetFaceGroupLabel            func(childComplexity int, faceGroupID int, label *string) int
 		SetPeriodicScanInterval      func(childComplexity int, interval int) int
 		SetScannerConcurrentWorkers  func(childComplexity int, workers int) int
-		SetThumbnailDownsampleMethod func(childComplexity int, method int) int
+		SetThumbnailDownsampleMethod func(childComplexity int, method models.ThumbnailFilter) int
 		ShareAlbum                   func(childComplexity int, albumID int, expire *time.Time, password *string) int
 		ShareMedia                   func(childComplexity int, mediaID int, expire *time.Time, password *string) int
 		UpdateUser                   func(childComplexity int, id int, username *string, password *string, admin *bool) int
@@ -328,7 +328,7 @@ type MutationResolver interface {
 	UserRemoveRootAlbum(ctx context.Context, userID int, albumID int) (*models.Album, error)
 	SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
 	SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
-	SetThumbnailDownsampleMethod(ctx context.Context, method int) (int, error)
+	SetThumbnailDownsampleMethod(ctx context.Context, method models.ThumbnailFilter) (models.ThumbnailFilter, error)
 	ChangeUserPreferences(ctx context.Context, language *string) (*models.UserPreferences, error)
 	ResetAlbumCover(ctx context.Context, albumID int) (*models.Album, error)
 	SetAlbumCover(ctx context.Context, coverID int) (*models.Album, error)
@@ -1070,7 +1070,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 			return 0, false
 		}
 
-		return e.complexity.Mutation.SetThumbnailDownsampleMethod(childComplexity, args["method"].(int)), true
+		return e.complexity.Mutation.SetThumbnailDownsampleMethod(childComplexity, args["method"].(models.ThumbnailFilter)), true
 
 	case "Mutation.shareAlbum":
 		if e.complexity.Mutation.ShareAlbum == nil {
@@ -2181,10 +2181,10 @@ func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx
 func (ec *executionContext) field_Mutation_setThumbnailDownsampleMethod_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
-	var arg0 int
+	var arg0 models.ThumbnailFilter
 	if tmp, ok := rawArgs["method"]; ok {
 		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("method"))
-		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
+		arg0, err = ec.unmarshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, tmp)
 		if err != nil {
 			return nil, err
 		}
@@ -7210,7 +7210,7 @@ func (ec *executionContext) _Mutation_setThumbnailDownsampleMethod(ctx context.C
 	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
 		directive0 := func(rctx context.Context) (interface{}, error) {
 			ctx = rctx // use context from middleware stack in children
-			return ec.resolvers.Mutation().SetThumbnailDownsampleMethod(rctx, fc.Args["method"].(int))
+			return ec.resolvers.Mutation().SetThumbnailDownsampleMethod(rctx, fc.Args["method"].(models.ThumbnailFilter))
 		}
 		directive1 := func(ctx context.Context) (interface{}, error) {
 			if ec.directives.IsAdmin == nil {
@@ -7226,10 +7226,10 @@ func (ec *executionContext) _Mutation_setThumbnailDownsampleMethod(ctx context.C
 		if tmp == nil {
 			return nil, nil
 		}
-		if data, ok := tmp.(int); ok {
+		if data, ok := tmp.(models.ThumbnailFilter); ok {
 			return data, nil
 		}
-		return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp)
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be github.com/photoview/photoview/api/graphql/models.ThumbnailFilter`, tmp)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -7241,9 +7241,9 @@ func (ec *executionContext) _Mutation_setThumbnailDownsampleMethod(ctx context.C
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int)
+	res := resTmp.(models.ThumbnailFilter)
 	fc.Result = res
-	return ec.marshalNInt2int(ctx, field.Selections, res)
+	return ec.marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_Mutation_setThumbnailDownsampleMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -7253,7 +7253,7 @@ func (ec *executionContext) fieldContext_Mutation_setThumbnailDownsampleMethod(c
 		IsMethod:   true,
 		IsResolver: true,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type ThumbnailFilter does not have child fields")
 		},
 	}
 	defer func() {
@@ -10729,10 +10729,10 @@ func (ec *executionContext) _SiteInfo_thumbnailMethod(ctx context.Context, field
 		if tmp == nil {
 			return nil, nil
 		}
-		if data, ok := tmp.(int); ok {
+		if data, ok := tmp.(models.ThumbnailFilter); ok {
 			return data, nil
 		}
-		return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp)
+		return nil, fmt.Errorf(`unexpected type %T from directive, should be github.com/photoview/photoview/api/graphql/models.ThumbnailFilter`, tmp)
 	})
 	if err != nil {
 		ec.Error(ctx, err)
@@ -10744,9 +10744,9 @@ func (ec *executionContext) _SiteInfo_thumbnailMethod(ctx context.Context, field
 		}
 		return graphql.Null
 	}
-	res := resTmp.(int)
+	res := resTmp.(models.ThumbnailFilter)
 	fc.Result = res
-	return ec.marshalNInt2int(ctx, field.Selections, res)
+	return ec.marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx, field.Selections, res)
 }
 
 func (ec *executionContext) fieldContext_SiteInfo_thumbnailMethod(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
@@ -10756,7 +10756,7 @@ func (ec *executionContext) fieldContext_SiteInfo_thumbnailMethod(ctx context.Co
 		IsMethod:   false,
 		IsResolver: false,
 		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
-			return nil, errors.New("field of type Int does not have child fields")
+			return nil, errors.New("field of type ThumbnailFilter does not have child fields")
 		},
 	}
 	return fc, nil
@@ -16802,6 +16802,16 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
 	return res
 }
 
+func (ec *executionContext) unmarshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx context.Context, v interface{}) (models.ThumbnailFilter, error) {
+	var res models.ThumbnailFilter
+	err := res.UnmarshalGQL(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNThumbnailFilter2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐThumbnailFilter(ctx context.Context, sel ast.SelectionSet, v models.ThumbnailFilter) graphql.Marshaler {
+	return v
+}
+
 func (ec *executionContext) unmarshalNTime2timeᚐTime(ctx context.Context, v interface{}) (time.Time, error) {
 	res, err := graphql.UnmarshalTime(v)
 	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/api/graphql/models/generated.go b/api/graphql/models/generated.go
index abd8589..23d4744 100644
--- a/api/graphql/models/generated.go
+++ b/api/graphql/models/generated.go
@@ -251,3 +251,53 @@ func (e *OrderDirection) UnmarshalGQL(v interface{}) error {
 func (e OrderDirection) MarshalGQL(w io.Writer) {
 	fmt.Fprint(w, strconv.Quote(e.String()))
 }
+
+// Supported downsampling filters for thumbnail generation
+type ThumbnailFilter string
+
+const (
+	ThumbnailFilterNearestNeighbor   ThumbnailFilter = "NearestNeighbor"
+	ThumbnailFilterBox               ThumbnailFilter = "Box"
+	ThumbnailFilterLinear            ThumbnailFilter = "Linear"
+	ThumbnailFilterMitchellNetravali ThumbnailFilter = "MitchellNetravali"
+	ThumbnailFilterCatmullRom        ThumbnailFilter = "CatmullRom"
+	ThumbnailFilterLanczos           ThumbnailFilter = "Lanczos"
+)
+
+var AllThumbnailFilter = []ThumbnailFilter{
+	ThumbnailFilterNearestNeighbor,
+	ThumbnailFilterBox,
+	ThumbnailFilterLinear,
+	ThumbnailFilterMitchellNetravali,
+	ThumbnailFilterCatmullRom,
+	ThumbnailFilterLanczos,
+}
+
+func (e ThumbnailFilter) IsValid() bool {
+	switch e {
+	case ThumbnailFilterNearestNeighbor, ThumbnailFilterBox, ThumbnailFilterLinear, ThumbnailFilterMitchellNetravali, ThumbnailFilterCatmullRom, ThumbnailFilterLanczos:
+		return true
+	}
+	return false
+}
+
+func (e ThumbnailFilter) String() string {
+	return string(e)
+}
+
+func (e *ThumbnailFilter) UnmarshalGQL(v interface{}) error {
+	str, ok := v.(string)
+	if !ok {
+		return fmt.Errorf("enums must be strings")
+	}
+
+	*e = ThumbnailFilter(str)
+	if !e.IsValid() {
+		return fmt.Errorf("%s is not a valid ThumbnailFilter", str)
+	}
+	return nil
+}
+
+func (e ThumbnailFilter) MarshalGQL(w io.Writer) {
+	fmt.Fprint(w, strconv.Quote(e.String()))
+}
diff --git a/api/graphql/models/site_info.go b/api/graphql/models/site_info.go
index 1ea1c50..57b966f 100644
--- a/api/graphql/models/site_info.go
+++ b/api/graphql/models/site_info.go
@@ -10,7 +10,7 @@ type SiteInfo struct {
 	InitialSetup         bool `gorm:"not null"`
 	PeriodicScanInterval int  `gorm:"not null"`
 	ConcurrentWorkers    int  `gorm:"not null"`
-	ThumbnailMethod   	 int  `gorm:"not null"`
+	ThumbnailMethod   	 ThumbnailFilter  `gorm:"not null"`
 }
 
 func (SiteInfo) TableName() string {
@@ -27,7 +27,7 @@ func DefaultSiteInfo(db *gorm.DB) SiteInfo {
 		InitialSetup:         true,
 		PeriodicScanInterval: 0,
 		ConcurrentWorkers:    defaultConcurrentWorkers,
-		ThumbnailMethod:			0,
+		ThumbnailMethod:			ThumbnailFilterNearestNeighbor,
 	}
 }
 
diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go
index 1347286..fb299be 100644
--- a/api/graphql/models/site_info_test.go
+++ b/api/graphql/models/site_info_test.go
@@ -22,7 +22,7 @@ func TestSiteInfo(t *testing.T) {
 	site_info.InitialSetup = false
 	site_info.PeriodicScanInterval = 360
 	site_info.ConcurrentWorkers = 10
-	site_info.ThumbnailMethod = 5
+	site_info.ThumbnailMethod = ThumbnailFilterLanczos
 
 	if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) {
 		return
@@ -37,7 +37,7 @@ func TestSiteInfo(t *testing.T) {
 		InitialSetup:         false,
 		PeriodicScanInterval: 360,
 		ConcurrentWorkers:    10,
-		ThumbnailMethod:    	5,
+		ThumbnailMethod:    	ThumbnailFilterLanczos,
 	}, *site_info)
 
 }
diff --git a/api/graphql/resolvers/thumbnails.go b/api/graphql/resolvers/thumbnails.go
index 8f5fb02..6305144 100644
--- a/api/graphql/resolvers/thumbnails.go
+++ b/api/graphql/resolvers/thumbnails.go
@@ -2,29 +2,35 @@ package resolvers
 
 import (
 	"context"
-	
+
 	"github.com/photoview/photoview/api/graphql/models"
-	"github.com/pkg/errors"
+	// "github.com/pkg/errors"
 	"gorm.io/gorm"
 )
 
-func (r *mutationResolver) SetThumbnailDownsampleMethod(ctx context.Context, method int) (int, error) {
+func (r *mutationResolver) SetThumbnailDownsampleMethod(ctx context.Context, method models.ThumbnailFilter) (models.ThumbnailFilter, error) {
 	db := r.DB(ctx)
 
-	if method > 5 {
-		return 0, errors.New("The requested filter is unsupported, defaulting to nearest neighbor")
-	}
+	// if method > 5 {
+	// 	return 0, errors.New("The requested filter is unsupported, defaulting to nearest neighbor")
+	// }
 
 	if err := db.Session(&gorm.Session{AllowGlobalUpdate: true}).Model(&models.SiteInfo{}).Update("thumbnail_method", method).Error; err != nil {
-		return 0, err
+		return models.ThumbnailFilterNearestNeighbor, err
 	}
 
 	var siteInfo models.SiteInfo
 	if err := db.First(&siteInfo).Error; err != nil {
-		return 0, err
+		return models.ThumbnailFilterNearestNeighbor, err
 	}
 
-	// scanner_queue.ChangeScannerConcurrentWorkers(siteInfo.ConcurrentWorkers)
-
 	return siteInfo.ThumbnailMethod, nil
+
+	// var langTrans *models.LanguageTranslation = nil
+	// if language != nil {
+	// 	lng := models.LanguageTranslation(*language)
+	// 	langTrans = &lng
+	// }
+
+
 }
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 91c7dfb..24fd737 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -164,7 +164,7 @@ type Mutation {
   setScannerConcurrentWorkers(workers: Int!): Int! @isAdmin
 
   "Set the filter to be used when generating thumbnails"
-  setThumbnailDownsampleMethod(method: Int!): Int! @isAdmin
+  setThumbnailDownsampleMethod(method: ThumbnailFilter!): ThumbnailFilter! @isAdmin
 
   "Change user preferences for the logged in user"
   changeUserPreferences(language: String): UserPreferences! @isAuthorized
@@ -250,6 +250,16 @@ type ShareToken {
   media: Media
 }
 
+"Supported downsampling filters for thumbnail generation"
+enum ThumbnailFilter {
+  NearestNeighbor,
+  Box,
+	Linear,
+	MitchellNetravali,
+	CatmullRom,
+	Lanczos,
+}
+
 "General information about the site"
 type SiteInfo {
   "Whether or not the initial setup wizard should be shown"
@@ -261,7 +271,7 @@ type SiteInfo {
   "How many max concurrent scanner jobs that should run at once"
   concurrentWorkers: Int! @isAdmin
   "The filter to use when generating thumbnails"
-  thumbnailMethod: Int! @isAdmin
+  thumbnailMethod: ThumbnailFilter! @isAdmin
 }
 
 type User {
diff --git a/api/scanner/media_encoding/encode_photo.go b/api/scanner/media_encoding/encode_photo.go
index b7221d9..adf4d8e 100644
--- a/api/scanner/media_encoding/encode_photo.go
+++ b/api/scanner/media_encoding/encode_photo.go
@@ -21,13 +21,13 @@ import (
 	"gorm.io/gorm"
 )
 
-var thumbFilter = map[int]imaging.ResampleFilter{
-	0:  imaging.NearestNeighbor,
-	1:  imaging.Box,
-	2:	imaging.Linear,
-	3:	imaging.MitchellNetravali,
-	4:	imaging.CatmullRom,
-	5:	imaging.Lanczos,
+var thumbFilter = map[models.ThumbnailFilter]imaging.ResampleFilter{
+	models.ThumbnailFilterNearestNeighbor:  imaging.NearestNeighbor,
+	models.ThumbnailFilterBox:  imaging.Box,
+	models.ThumbnailFilterLinear:	imaging.Linear,
+	models.ThumbnailFilterMitchellNetravali:	imaging.MitchellNetravali,
+	models.ThumbnailFilterCatmullRom:	imaging.CatmullRom,
+	models.ThumbnailFilterLanczos:	imaging.Lanczos,
 }
 
 func EncodeThumbnail(db *gorm.DB, inputPath string, outputPath string) (*media_utils.PhotoDimensions, error) {
diff --git a/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx
similarity index 85%
rename from ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
rename to ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx
index d725b2c..879ba16 100644
--- a/ui/src/Pages/SettingsPage/thumbnailPreferences.test.tsx
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.test.tsx
@@ -3,6 +3,8 @@ import { MockedProvider } from '@apollo/client/testing'
 
 import { render, screen } from '@testing-library/react'
 
+import { ThumbnailFilter } from '../../__generated__/globalTypes'
+
 import ThumbnailPreferences, {
   THUMBNAIL_METHOD_QUERY,
   SET_THUMBNAIL_METHOD_MUTATION,
@@ -16,7 +18,7 @@ test('load ThumbnailPreferences', () => {
       },
       result: {
         data: {
-          siteInfo: { method: 0 },
+          siteInfo: { method: ThumbnailFilter.NearestNeighbor },
         },
       },
     },
@@ -24,7 +26,7 @@ test('load ThumbnailPreferences', () => {
       request: {
         query: SET_THUMBNAIL_METHOD_MUTATION,
         variables: {
-          method: '5',
+          method: ThumbnailFilter.Lanczos,
         },
       },
       result: {
diff --git a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
index 8f46832..520a794 100644
--- a/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
+++ b/ui/src/Pages/SettingsPage/ThumbnailPreferences.tsx
@@ -7,6 +7,7 @@ import {
   InputLabelTitle,
 } from './SettingsPage'
 import { useTranslation } from 'react-i18next'
+import { ThumbnailFilter } from '../../__generated__/globalTypes'
 import { thumbnailMethodQuery } from './__generated__/thumbnailMethodQuery'
 import {
   setThumbnailMethodMutation,
@@ -24,7 +25,7 @@ export const THUMBNAIL_METHOD_QUERY = gql`
 `
 
 export const SET_THUMBNAIL_METHOD_MUTATION = gql`
-  mutation setThumbnailMethodMutation($method: Int!) {
+  mutation setThumbnailMethodMutation($method: ThumbnailFilter!) {
     setThumbnailDownsampleMethod(method: $method)
   }
 `
@@ -67,33 +68,33 @@ const ThumbnailPreferences = () => {
         'settings.thumbnails.method.filter.nearest_neighbor',
         'Nearest Neighbor (default)'
       ),
-      value: 0,
+      value: ThumbnailFilter.NearestNeighbor,
     },
     {
       label: t('settings.thumbnails.method.filter.box', 'Box'),
-      value: 1,
+      value: ThumbnailFilter.Box,
     },
     {
       label: t('settings.thumbnails.method.filter.linear', 'Linear'),
-      value: 2,
+      value: ThumbnailFilter.Linear,
     },
     {
       label: t(
         'settings.thumbnails.method.filter.mitchell_netravali',
         'Mitchell-Netravali'
       ),
-      value: 3,
+      value: ThumbnailFilter.MitchellNetravali,
     },
     {
       label: t('settings.thumbnails.method.filter.catmull_rom', 'Catmull-Rom'),
-      value: 4,
+      value: ThumbnailFilter.CatmullRom,
     },
     {
       label: t(
         'settings.thumbnails.method.filter.Lanczos',
         'Lanczos (highest quality)'
       ),
-      value: 5,
+      value: ThumbnailFilter.Lanczos,
     },
   ]
 
diff --git a/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
index 15611ca..bd7f061 100644
--- a/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
+++ b/ui/src/Pages/SettingsPage/__generated__/setThumbnailMethodMutation.ts
@@ -3,6 +3,8 @@
 // @generated
 // This file was automatically generated and should not be edited.
 
+import { ThumbnailFilter } from './../../../__generated__/globalTypes'
+
 // ====================================================
 // GraphQL mutation operation: setThumbnailMethodMutation
 // ====================================================
@@ -11,9 +13,9 @@ export interface setThumbnailMethodMutation {
   /**
    * Set the filter to be used when generating thumbnails
    */
-  setThumbnailDownsampleMethod: number
+  setThumbnailDownsampleMethod: ThumbnailFilter
 }
 
 export interface setThumbnailMethodMutationVariables {
-  method: number
+  method: ThumbnailFilter
 }
diff --git a/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
index ef0690a..7c9cb00 100644
--- a/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
+++ b/ui/src/Pages/SettingsPage/__generated__/thumbnailMethodQuery.ts
@@ -3,6 +3,8 @@
 // @generated
 // This file was automatically generated and should not be edited.
 
+import { ThumbnailFilter } from './../../../__generated__/globalTypes'
+
 // ====================================================
 // GraphQL query operation: thumbnailMethodQuery
 // ====================================================
@@ -12,7 +14,7 @@ export interface thumbnailMethodQuery_siteInfo {
   /**
    * The filter to use when generating thumbnails
    */
-  thumbnailMethod: number
+  thumbnailMethod: ThumbnailFilter
 }
 
 export interface thumbnailMethodQuery {
diff --git a/ui/src/__generated__/globalTypes.ts b/ui/src/__generated__/globalTypes.ts
index 8fda05f..89eae65 100644
--- a/ui/src/__generated__/globalTypes.ts
+++ b/ui/src/__generated__/globalTypes.ts
@@ -48,6 +48,18 @@ export enum OrderDirection {
   DESC = 'DESC',
 }
 
+/**
+ * Supported downsampling filters for thumbnail generation
+ */
+export enum ThumbnailFilter {
+  Box = 'Box',
+  CatmullRom = 'CatmullRom',
+  Lanczos = 'Lanczos',
+  Linear = 'Linear',
+  MitchellNetravali = 'MitchellNetravali',
+  NearestNeighbor = 'NearestNeighbor',
+}
+
 //==============================================================
 // END Enums and Input Objects
 //==============================================================

From ca889d02bd81f99e4703d0042ca58c7d1a5f2c16 Mon Sep 17 00:00:00 2001
From: PJ-Watson <59647390+PJ-Watson@users.noreply.github.com>
Date: Tue, 9 Aug 2022 06:43:25 +0100
Subject: [PATCH 7/7] Fixed site_info_test - missing model import.

---
 api/graphql/models/site_info_test.go | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/api/graphql/models/site_info_test.go b/api/graphql/models/site_info_test.go
index fb299be..d967e87 100644
--- a/api/graphql/models/site_info_test.go
+++ b/api/graphql/models/site_info_test.go
@@ -22,7 +22,7 @@ func TestSiteInfo(t *testing.T) {
 	site_info.InitialSetup = false
 	site_info.PeriodicScanInterval = 360
 	site_info.ConcurrentWorkers = 10
-	site_info.ThumbnailMethod = ThumbnailFilterLanczos
+	site_info.ThumbnailMethod = models.ThumbnailFilterLanczos
 
 	if !assert.NoError(t, db.Session(&gorm.Session{AllowGlobalUpdate: true}).Save(&site_info).Error) {
 		return
@@ -37,7 +37,7 @@ func TestSiteInfo(t *testing.T) {
 		InitialSetup:         false,
 		PeriodicScanInterval: 360,
 		ConcurrentWorkers:    10,
-		ThumbnailMethod:    	ThumbnailFilterLanczos,
+		ThumbnailMethod:    	models.ThumbnailFilterLanczos,
 	}, *site_info)
 
 }
