diff --git a/pkg/sqlite/migrations/32_postmigrate.go b/pkg/sqlite/migrations/32_postmigrate.go index 4dbd65df8..6a4cf3d00 100644 --- a/pkg/sqlite/migrations/32_postmigrate.go +++ b/pkg/sqlite/migrations/32_postmigrate.go @@ -74,7 +74,7 @@ func (m *schema32Migrator) migrateFolders(ctx context.Context) error { query += fmt.Sprintf("ORDER BY `folders`.`id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -94,12 +94,12 @@ func (m *schema32Migrator) migrateFolders(ctx context.Context) error { count++ parent := filepath.Dir(p) - parentID, zipFileID, err := m.createFolderHierarchy(parent) + parentID, zipFileID, err := m.createFolderHierarchy(tx, parent) if err != nil { return err } - _, err = m.db.Exec("UPDATE `folders` SET `parent_folder_id` = ?, `zip_file_id` = ? WHERE `id` = ?", parentID, zipFileID, id) + _, err = tx.Exec("UPDATE `folders` SET `parent_folder_id` = ?, `zip_file_id` = ? WHERE `id` = ?", parentID, zipFileID, id) if err != nil { return err } @@ -153,7 +153,7 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error { query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit) if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -178,12 +178,12 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error { parent := filepath.Dir(p) basename := filepath.Base(p) if parent != "." { - parentID, zipFileID, err := m.createFolderHierarchy(parent) + parentID, zipFileID, err := m.createFolderHierarchy(tx, parent) if err != nil { return err } - _, err = m.db.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id) + _, err = tx.Exec("UPDATE `files` SET `parent_folder_id` = ?, `zip_file_id` = ?, `basename` = ? WHERE `id` = ?", parentID, zipFileID, basename, id) if err != nil { return fmt.Errorf("migrating file %s: %w", p, err) } @@ -245,16 +245,18 @@ func (m *schema32Migrator) deletePlaceholderFolder(ctx context.Context) error { return fmt.Errorf("not deleting placeholder folder because it has %d folders", result.Count) } - _, err := m.db.Exec("DELETE FROM `folders` WHERE `id` = 1") - return err + return m.withTxn(ctx, func(tx *sqlx.Tx) error { + _, err := tx.Exec("DELETE FROM `folders` WHERE `id` = 1") + return err + }) } -func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64, error) { +func (m *schema32Migrator) createFolderHierarchy(tx *sqlx.Tx, p string) (*int, sql.NullInt64, error) { parent := filepath.Dir(p) if parent == p { // get or create this folder - return m.getOrCreateFolder(p, nil, sql.NullInt64{}) + return m.getOrCreateFolder(tx, p, nil, sql.NullInt64{}) } var ( @@ -269,23 +271,23 @@ func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64, parentID = &foundEntry.id zipFileID = foundEntry.zipID } else { - parentID, zipFileID, err = m.createFolderHierarchy(parent) + parentID, zipFileID, err = m.createFolderHierarchy(tx, parent) if err != nil { return nil, sql.NullInt64{}, err } } - return m.getOrCreateFolder(p, parentID, zipFileID) + return m.getOrCreateFolder(tx, p, parentID, zipFileID) } -func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFileID sql.NullInt64) (*int, sql.NullInt64, error) { +func (m *schema32Migrator) getOrCreateFolder(tx *sqlx.Tx, path string, parentID *int, zipFileID sql.NullInt64) (*int, sql.NullInt64, error) { foundEntry, ok := m.folderCache[path] if ok { return &foundEntry.id, foundEntry.zipID, nil } const query = "SELECT `id`, `zip_file_id` FROM `folders` WHERE `path` = ?" - rows, err := m.db.Query(query, path) + rows, err := tx.Query(query, path) if err != nil { return nil, sql.NullInt64{}, err } @@ -314,7 +316,7 @@ func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFile } now := time.Now() - result, err := m.db.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now) + result, err := tx.Exec(insertSQL, path, parentFolderID, zipFileID, time.Time{}, now, now) if err != nil { return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err) } diff --git a/pkg/sqlite/migrations/32_premigrate.go b/pkg/sqlite/migrations/32_premigrate.go index 12906f7d5..caba639bc 100644 --- a/pkg/sqlite/migrations/32_premigrate.go +++ b/pkg/sqlite/migrations/32_premigrate.go @@ -65,7 +65,7 @@ func (m *schema32PreMigrator) migrate(ctx context.Context) error { query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -100,7 +100,7 @@ func (m *schema32PreMigrator) migrate(ctx context.Context) error { logger.Infof("Correcting %q gallery to be zip-based.", p) - _, err = m.db.Exec("UPDATE `galleries` SET `zip` = '1' WHERE `id` = ?", id) + _, err = tx.Exec("UPDATE `galleries` SET `zip` = '1' WHERE `id` = ?", id) if err != nil { return err } diff --git a/pkg/sqlite/migrations/34_postmigrate.go b/pkg/sqlite/migrations/34_postmigrate.go index e167c9a97..769655cb6 100644 --- a/pkg/sqlite/migrations/34_postmigrate.go +++ b/pkg/sqlite/migrations/34_postmigrate.go @@ -88,7 +88,7 @@ func (m *schema34Migrator) migrateObjects(ctx context.Context, table string, col query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -126,7 +126,7 @@ func (m *schema34Migrator) migrateObjects(ctx context.Context, table string, col updateSQL := fmt.Sprintf("UPDATE `%s` SET %s WHERE `id` = ?", table, updateList) - _, err = m.db.Exec(updateSQL, args...) + _, err = tx.Exec(updateSQL, args...) if err != nil { return err } diff --git a/pkg/sqlite/migrations/42_postmigrate.go b/pkg/sqlite/migrations/42_postmigrate.go index afb0db9e7..42180ff0f 100644 --- a/pkg/sqlite/migrations/42_postmigrate.go +++ b/pkg/sqlite/migrations/42_postmigrate.go @@ -71,7 +71,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error { query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -92,7 +92,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error { gotSome = true count++ - if err := m.migratePerformerAliases(id, aliases); err != nil { + if err := m.migratePerformerAliases(tx, id, aliases); err != nil { return err } } @@ -114,7 +114,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error { return nil } -func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error { +func (m *schema42Migrator) migratePerformerAliases(tx *sqlx.Tx, id int, aliases string) error { // split aliases by , or / aliasList := strings.FieldsFunc(aliases, func(r rune) bool { return strings.ContainsRune(",/", r) @@ -126,7 +126,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error } // delete the existing row - if _, err := m.db.Exec("DELETE FROM `performer_aliases` WHERE `performer_id` = ?", id); err != nil { + if _, err := tx.Exec("DELETE FROM `performer_aliases` WHERE `performer_id` = ?", id); err != nil { return err } @@ -140,7 +140,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error // insert aliases into table for _, alias := range aliasList { - _, err := m.db.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias) + _, err := tx.Exec("INSERT INTO `performer_aliases` (`performer_id`, `alias`) VALUES (?, ?)", id, alias) if err != nil { return err } @@ -173,7 +173,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'` query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -194,7 +194,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'` lastID = id count++ - if err := m.massagePerformerName(id, name); err != nil { + if err := m.massagePerformerName(tx, id, name); err != nil { return err } } @@ -220,7 +220,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'` // the format "name (disambiguation)". var performerDisRE = regexp.MustCompile(`^((?:[^(\s]+\s)+)\(([^)]+)\)$`) -func (m *schema42Migrator) massagePerformerName(performerID int, name string) error { +func (m *schema42Migrator) massagePerformerName(tx *sqlx.Tx, performerID int, name string) error { r := performerDisRE.FindStringSubmatch(name) if len(r) != 3 { @@ -235,7 +235,7 @@ func (m *schema42Migrator) massagePerformerName(performerID int, name string) er logger.Infof("Separating %q into %q and disambiguation %q", name, newName, newDis) - _, err := m.db.Exec("UPDATE performers SET name = ?, disambiguation = ? WHERE id = ?", newName, newDis, performerID) + _, err := tx.Exec("UPDATE performers SET name = ?, disambiguation = ? WHERE id = ?", newName, newDis, performerID) if err != nil { return err } @@ -266,7 +266,7 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -286,7 +286,7 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS gotSome = true count++ - if err := m.migrateDuplicatePerformer(id, name); err != nil { + if err := m.migrateDuplicatePerformer(tx, id, name); err != nil { return err } } @@ -308,13 +308,13 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS return nil } -func (m *schema42Migrator) migrateDuplicatePerformer(performerID int, name string) error { +func (m *schema42Migrator) migrateDuplicatePerformer(tx *sqlx.Tx, performerID int, name string) error { // get the highest value of disambiguation for this performer name query := ` SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1` var disambiguation sql.NullString - if err := m.db.Get(&disambiguation, query, name); err != nil { + if err := tx.Get(&disambiguation, query, name); err != nil { return err } @@ -333,7 +333,7 @@ SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DES logger.Infof("Adding disambiguation '%d' for performer %q", newDisambiguation, name) - _, err := m.db.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID) + _, err := tx.Exec("UPDATE performers SET disambiguation = ? WHERE id = ?", strconv.Itoa(newDisambiguation), performerID) if err != nil { return err } diff --git a/pkg/sqlite/migrations/45_postmigrate.go b/pkg/sqlite/migrations/45_postmigrate.go index 3a2ee6702..9e2bb1f8f 100644 --- a/pkg/sqlite/migrations/45_postmigrate.go +++ b/pkg/sqlite/migrations/45_postmigrate.go @@ -161,7 +161,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra query += fmt.Sprintf(" LIMIT %d", limit) - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -191,7 +191,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra image := result[i+1].(*[]byte) if len(*image) > 0 { - if err := m.insertImage(*image, id, options.destTable, col.destCol); err != nil { + if err := m.insertImage(tx, *image, id, options.destTable, col.destCol); err != nil { return err } } @@ -202,7 +202,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra "joinTable": options.joinTable, "joinIDCol": options.joinIDCol, }) - if _, err := m.db.Exec(deleteSQL, id); err != nil { + if _, err := tx.Exec(deleteSQL, id); err != nil { return err } } @@ -224,11 +224,11 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra return nil } -func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, destCol string) error { +func (m *schema45Migrator) insertImage(tx *sqlx.Tx, data []byte, id int, destTable string, destCol string) error { // calculate checksum and insert into blobs table checksum := md5.FromBytes(data) - if _, err := m.db.Exec("INSERT INTO `blobs` (`checksum`, `blob`) VALUES (?, ?) ON CONFLICT DO NOTHING", checksum, data); err != nil { + if _, err := tx.Exec("INSERT INTO `blobs` (`checksum`, `blob`) VALUES (?, ?) ON CONFLICT DO NOTHING", checksum, data); err != nil { return err } @@ -237,7 +237,7 @@ func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, de "destTable": destTable, "destCol": destCol, }) - if _, err := m.db.Exec(updateSQL, checksum, id); err != nil { + if _, err := tx.Exec(updateSQL, checksum, id); err != nil { return err } diff --git a/pkg/sqlite/migrations/49_postmigrate.go b/pkg/sqlite/migrations/49_postmigrate.go index 67e128f2c..8ba900d16 100644 --- a/pkg/sqlite/migrations/49_postmigrate.go +++ b/pkg/sqlite/migrations/49_postmigrate.go @@ -112,7 +112,7 @@ type schema49Migrator struct { func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { - rows, err := m.db.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id") + rows, err := tx.Query("SELECT id, mode, find_filter FROM saved_filters ORDER BY id") if err != nil { return err } @@ -147,7 +147,7 @@ func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error { return fmt.Errorf("failed to get display options for saved filter %s : %w", findFilter, err) } - _, err = m.db.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id) + _, err = tx.Exec("UPDATE saved_filters SET find_filter = ?, object_filter = ?, ui_options = ? WHERE id = ?", newFindFilter, objectFilter, uiOptions, id) if err != nil { return fmt.Errorf("failed to update saved filter %d: %w", id, err) } diff --git a/pkg/sqlite/migrations/52_postmigrate.go b/pkg/sqlite/migrations/52_postmigrate.go index f173d8087..8235b4cf0 100644 --- a/pkg/sqlite/migrations/52_postmigrate.go +++ b/pkg/sqlite/migrations/52_postmigrate.go @@ -34,7 +34,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error { query := "SELECT `folders`.`id`, `folders`.`path`, `parent_folder`.`path` FROM `folders` " + "INNER JOIN `folders` AS `parent_folder` ON `parent_folder`.`id` = `folders`.`parent_folder_id`" - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -64,7 +64,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error { // ensure the correct path is unique var v int - isEmptyErr := m.db.Get(&v, "SELECT 1 FROM folders WHERE path = ?", correctPath) + isEmptyErr := tx.Get(&v, "SELECT 1 FROM folders WHERE path = ?", correctPath) if isEmptyErr != nil && !errors.Is(isEmptyErr, sql.ErrNoRows) { return fmt.Errorf("error checking if correct path %s is unique: %w", correctPath, isEmptyErr) } @@ -75,7 +75,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error { continue } - if _, err := m.db.Exec("UPDATE folders SET path = ? WHERE id = ?", correctPath, id); err != nil { + if _, err := tx.Exec("UPDATE folders SET path = ? WHERE id = ?", correctPath, id); err != nil { return fmt.Errorf("error updating folder path %s to %s: %w", folderPath, correctPath, err) } } diff --git a/pkg/sqlite/migrations/55_postmigrate.go b/pkg/sqlite/migrations/55_postmigrate.go index 1a3a5c566..5db79a531 100644 --- a/pkg/sqlite/migrations/55_postmigrate.go +++ b/pkg/sqlite/migrations/55_postmigrate.go @@ -31,7 +31,7 @@ func (m *schema55Migrator) migrate(ctx context.Context) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`" - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -53,7 +53,7 @@ func (m *schema55Migrator) migrate(ctx context.Context) error { } // convert the timestamp to the correct format - if _, err := m.db.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE view_date = ?", utcTimestamp, viewDate.Timestamp); err != nil { + if _, err := tx.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE view_date = ?", utcTimestamp, viewDate.Timestamp); err != nil { return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err) } } diff --git a/pkg/sqlite/migrations/64_postmigrate.go b/pkg/sqlite/migrations/64_postmigrate.go index 5b0f31a25..023f7db1d 100644 --- a/pkg/sqlite/migrations/64_postmigrate.go +++ b/pkg/sqlite/migrations/64_postmigrate.go @@ -35,7 +35,7 @@ func (m *schema64Migrator) migrate(ctx context.Context) error { if err := m.withTxn(ctx, func(tx *sqlx.Tx) error { query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`" - rows, err := m.db.Query(query) + rows, err := tx.Query(query) if err != nil { return err } @@ -64,7 +64,7 @@ func (m *schema64Migrator) migrate(ctx context.Context) error { // convert the timestamp to the correct format logger.Debugf("correcting view date %q to UTC date %q for scene %d", viewDate.Timestamp, viewDate.Timestamp.UTC(), id) - r, err := m.db.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE scene_id = ? AND (view_date = ? OR view_date = ?)", utcTimestamp, id, viewDate.Timestamp, viewDate) + r, err := tx.Exec("UPDATE scenes_view_dates SET view_date = ? WHERE scene_id = ? AND (view_date = ? OR view_date = ?)", utcTimestamp, id, viewDate.Timestamp, viewDate) if err != nil { return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err) } diff --git a/pkg/sqlite/scene.go b/pkg/sqlite/scene.go index c950be4d1..5df614b88 100644 --- a/pkg/sqlite/scene.go +++ b/pkg/sqlite/scene.go @@ -1128,9 +1128,12 @@ func (qb *SceneStore) setSceneSort(query *queryBuilder, findFilter *models.FindF direction := findFilter.GetDirection() switch sort { - case "movie_scene_number", "group_scene_number": + case "movie_scene_number": query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id") query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable) + case "group_scene_number": + query.join(groupsScenesTable, "scene_group", "scenes.id = scene_group.scene_id") + query.sortAndPagination += getSort("scene_index", direction, "scene_group") case "tag_count": query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction) case "performer_count": diff --git a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx index 71ff92e5e..8d96a8a8d 100644 --- a/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/HierarchicalLabelValueFilter.tsx @@ -4,6 +4,7 @@ import { defineMessages, MessageDescriptor, useIntl } from "react-intl"; import { FilterSelect, SelectObject } from "src/components/Shared/Select"; import { Criterion } from "src/models/list-filter/criteria/criterion"; import { IHierarchicalLabelValue } from "src/models/list-filter/types"; +import { NumberField } from "src/utils/form"; interface IHierarchicalLabelValueFilterProps { criterion: Criterion; @@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC< {criterion.value.depth !== 0 && ( - onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) diff --git a/ui/v2.5/src/components/List/Filters/NumberFilter.tsx b/ui/v2.5/src/components/List/Filters/NumberFilter.tsx index 7aa574a2e..21120f799 100644 --- a/ui/v2.5/src/components/List/Filters/NumberFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/NumberFilter.tsx @@ -4,6 +4,7 @@ import { useIntl } from "react-intl"; import { CriterionModifier } from "../../../core/generated-graphql"; import { INumberValue } from "../../../models/list-filter/types"; import { NumberCriterion } from "../../../models/list-filter/criteria/criterion"; +import { NumberField } from "src/utils/form"; interface IDurationFilterProps { criterion: NumberCriterion; @@ -36,9 +37,8 @@ export const NumberFilter: React.FC = ({ ) { equalsControl = ( - ) => onChanged(e, "value") } @@ -57,9 +57,8 @@ export const NumberFilter: React.FC = ({ ) { lowerControl = ( - ) => onChanged(e, "value") } @@ -78,9 +77,8 @@ export const NumberFilter: React.FC = ({ ) { upperControl = ( - ) => onChanged( e, diff --git a/ui/v2.5/src/components/List/Filters/PhashFilter.tsx b/ui/v2.5/src/components/List/Filters/PhashFilter.tsx index 68eda8002..a42a61287 100644 --- a/ui/v2.5/src/components/List/Filters/PhashFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/PhashFilter.tsx @@ -4,6 +4,7 @@ import { useIntl } from "react-intl"; import { IPhashDistanceValue } from "../../../models/list-filter/types"; import { Criterion } from "../../../models/list-filter/criteria/criterion"; import { CriterionModifier } from "src/core/generated-graphql"; +import { NumberField } from "src/utils/form"; interface IPhashFilterProps { criterion: Criterion; @@ -49,10 +50,9 @@ export const PhashFilter: React.FC = ({ {criterion.modifier !== CriterionModifier.IsNull && criterion.modifier !== CriterionModifier.NotNull && ( - diff --git a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx index 19d2b70bb..e6f8f9fcf 100644 --- a/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx +++ b/ui/v2.5/src/components/List/Filters/SelectableFilter.tsx @@ -25,6 +25,7 @@ import { keyboardClickHandler } from "src/utils/keyboard"; import { useDebounce } from "src/hooks/debounce"; import useFocus from "src/utils/focus"; import ScreenUtils from "src/utils/screen"; +import { NumberField } from "src/utils/form"; interface ISelectedItem { item: ILabeledId; @@ -361,9 +362,8 @@ export const HierarchicalObjectsFilter = < {criterion.value.depth !== 0 && ( - onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1) diff --git a/ui/v2.5/src/components/List/ListFilter.tsx b/ui/v2.5/src/components/List/ListFilter.tsx index 24ea02af1..109ac127a 100644 --- a/ui/v2.5/src/components/List/ListFilter.tsx +++ b/ui/v2.5/src/components/List/ListFilter.tsx @@ -35,6 +35,7 @@ import { FilterButton } from "./Filters/FilterButton"; import { useDebounce } from "src/hooks/debounce"; import { View } from "./views"; import { ClearableInput } from "../Shared/ClearableInput"; +import { useStopWheelScroll } from "src/utils/form"; export function useDebouncedSearchInput( filter: ListFilterModel, @@ -126,6 +127,8 @@ export const PageSizeSelector: React.FC<{ } }, [customPageSizeShowing, perPageFocus]); + useStopWheelScroll(perPageInput); + const pageSizeOptions = useMemo(() => { const ret = PAGE_SIZE_OPTIONS.map((o) => { return { @@ -190,6 +193,7 @@ export const PageSizeSelector: React.FC<{
+ {/* can't use NumberField because of the ref */} { const maxPagesToShow = 10; const min = Math.max(1, currentPage - maxPagesToShow / 2); @@ -98,6 +101,7 @@ const PageCount: React.FC<{ + {/* can't use NumberField because of the ref */} ; @@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC = (props) => { /> - ) => { updateFieldChanged( diff --git a/ui/v2.5/src/components/Settings/GeneratePreviewOptions.tsx b/ui/v2.5/src/components/Settings/GeneratePreviewOptions.tsx index bb4cba77d..c7987add6 100644 --- a/ui/v2.5/src/components/Settings/GeneratePreviewOptions.tsx +++ b/ui/v2.5/src/components/Settings/GeneratePreviewOptions.tsx @@ -2,6 +2,7 @@ import React from "react"; import { useIntl } from "react-intl"; import { Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; +import { NumberField } from "src/utils/form"; export type VideoPreviewSettingsInput = Pick< GQL.ConfigGeneralInput, @@ -44,9 +45,8 @@ export const VideoPreviewInput: React.FC = ({ id: "dialogs.scene_gen.preview_seg_count_head", })} - ) => @@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC = ({ id: "dialogs.scene_gen.preview_seg_duration_head", })} - ) => set({ diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index 073be4e11..b84232d1f 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -6,6 +6,7 @@ import { Icon } from "../Shared/Icon"; import { StringListInput } from "../Shared/StringListInput"; import { PatchComponent } from "src/patch"; import { useSettings, useSettingsOptional } from "./context"; +import { NumberField } from "src/utils/form"; interface ISetting { id?: string; @@ -484,9 +485,8 @@ export const NumberSetting: React.FC = PatchComponent( {...props} renderField={(value, setValue) => ( - ) => setValue(Number.parseInt(e.currentTarget.value || "0", 10)) diff --git a/ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx b/ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx index 14cd701d1..69195ff40 100644 --- a/ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx +++ b/ui/v2.5/src/components/Shared/Rating/RatingNumber.tsx @@ -3,6 +3,7 @@ import { Button } from "react-bootstrap"; import { Icon } from "../Icon"; import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons"; import { useFocusOnce } from "src/utils/focus"; +import { useStopWheelScroll } from "src/utils/form"; export interface IRatingNumberProps { value: number | null; @@ -26,6 +27,7 @@ export const RatingNumber: React.FC = ( const showTextField = !props.disabled && (editing || !props.clickToRate); const [ratingRef] = useFocusOnce(editing, true); + useStopWheelScroll(ratingRef); const effectiveValue = editing ? valueStage : props.value; diff --git a/ui/v2.5/src/docs/en/Changelog/v0270.md b/ui/v2.5/src/docs/en/Changelog/v0270.md index 6ac27fc28..b9e1c7b67 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0270.md +++ b/ui/v2.5/src/docs/en/Changelog/v0270.md @@ -13,6 +13,7 @@ * Added support for bulk-editing Tags. ([#4925](https://github.com/stashapp/stash/pull/4925)) * Added filter to Scrapers menu. ([#5041](https://github.com/stashapp/stash/pull/5041)) * Added ability to set the location of ssl certificate files. ([#4910](https://github.com/stashapp/stash/pull/4910)) +* Added option to rescan all files in the Scan task. ([#5254](https://github.com/stashapp/stash/pull/5254)) ### 🎨 Improvements * Added button to view sub-studio/sub-tag content on Studio/Tag details pages. ([#5080](https://github.com/stashapp/stash/pull/5080)) @@ -38,10 +39,13 @@ * Anonymise now truncates o- and view history data. ([#5166](https://github.com/stashapp/stash/pull/5166)) * Fixed issue where using mouse wheel on numeric input fields would scroll the window in addition to changing the value. ([#5199](https://github.com/stashapp/stash/pull/5199)) * Fixed issue where some o-dates could not be deleted. ([#4971](https://github.com/stashapp/stash/pull/4971)) +* Fixed handling of symlink zip files. ([#5249](https://github.com/stashapp/stash/pull/5249)) +* Fixed default database backup directory being set to the config file directory instead of the database directory. ([#5250](https://github.com/stashapp/stash/pull/5250)) * Added API key to DASH and HLS manifests. ([#5061](https://github.com/stashapp/stash/pull/5061)) * Query field no longer focused when selecting items in the filter list on touch devices. ([#5204](https://github.com/stashapp/stash/pull/5204)) * Fixed weird scrolling behaviour on Gallery detail page on smaller viewports ([#5205](https://github.com/stashapp/stash/pull/5205)) * Performer popover links now correctly link to the applicable scenes/image/gallery query page instead of always going to scenes. ([#5195](https://github.com/stashapp/stash/pull/5195)) +* Fixed scene player source selector appearing behind the player controls. ([#5229](https://github.com/stashapp/stash/pull/5229)) * Fixed red/green/blue slider values in the Scene Filter panel. ([#5221](https://github.com/stashapp/stash/pull/5221)) * Play button no longer appears on file-less Scenes. ([#5141](https://github.com/stashapp/stash/pull/5141)) * Fixed transgender icon colouring. ([#5090](https://github.com/stashapp/stash/pull/5090)) diff --git a/ui/v2.5/src/docs/en/Manual/Browsing.md b/ui/v2.5/src/docs/en/Manual/Browsing.md index a0c2896fa..d41388232 100644 --- a/ui/v2.5/src/docs/en/Manual/Browsing.md +++ b/ui/v2.5/src/docs/en/Manual/Browsing.md @@ -10,7 +10,7 @@ The text field allows you to search using keywords. Keyword searching matches on |------|-----------------| | Scene | Title, Details, Path, OSHash, Checksum, Marker titles | | Image | Title, Path, Checksum | -| Movie | Title | +| Group | Title | | Marker | Title, Scene title | | Gallery | Title, Path, Checksum | | Performer | Name, Aliases | diff --git a/ui/v2.5/src/docs/en/Manual/JSONSpec.md b/ui/v2.5/src/docs/en/Manual/JSONSpec.md index e7e22bfe8..0a53d09f2 100644 --- a/ui/v2.5/src/docs/en/Manual/JSONSpec.md +++ b/ui/v2.5/src/docs/en/Manual/JSONSpec.md @@ -8,7 +8,7 @@ The metadata given to Stash can be exported into the JSON format. This structure * `performers` * `scenes` * `studios` -* `movies` +* `groups` ## File naming @@ -22,7 +22,7 @@ When exported, files are named with different formats depending on the object ty | Performers | `.json` | | Scenes | `.<hash>.json` | | Studios | `<name>.json` | -| Movies | `<name>.json` | +| Groups | `<name>.json` | Note that the file naming is not significant when importing. All json files will be read from the subdirectories. diff --git a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md index 3ece6f90a..2c250cb65 100644 --- a/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md +++ b/ui/v2.5/src/docs/en/Manual/KeyboardShortcuts.md @@ -12,7 +12,7 @@ |-------------------|--------| | `g s` | Scenes | | `g i` | Images | -| `g v` | Movies | +| `g v` | Groups | | `g k` | Markers | | `g l` | Galleries | | `g p` | Performers | @@ -104,28 +104,28 @@ [//]: # "(| `l` | Focus Gallery selector |)" [//]: # "(| `u` | Focus Studio selector |)" [//]: # "(| `p` | Focus Performers selector |)" -[//]: # "(| `v` | Focus Movies selector |)" +[//]: # "(| `v` | Focus Groups selector |)" [//]: # "(| `t` | Focus Tags selector |)" -## Movies Page shortcuts +## Groups Page shortcuts | Keyboard sequence | Action | |-------------------|--------| -| `n` | New Movie | +| `n` | New Group | -## Movie Page shortcuts +## Group Page shortcuts | Keyboard sequence | Action | |-------------------|--------| -| `e` | Edit Movie | -| `s s` | Save Movie | -| `d d` | Delete Movie | +| `e` | Edit Group | +| `s s` | Save Group | +| `d d` | Delete Group | | `r {1-5}` | [Edit mode] Set rating (stars) | | `r 0` | [Edit mode] Unset rating (stars) | | `r {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) | | ``r ` `` | [Edit mode] Unset rating (decimal) | | `,` | Expand/Collapse Details | -| `Ctrl + v` | Paste Movie image | +| `Ctrl + v` | Paste Group image | [//]: # "Commented until implementation is dealt with" [//]: # "(| `u` | Focus Studio selector (in edit mode) |)" diff --git a/ui/v2.5/src/docs/en/Manual/Plugins.md b/ui/v2.5/src/docs/en/Manual/Plugins.md index 90a2782d7..23ef0bc81 100644 --- a/ui/v2.5/src/docs/en/Manual/Plugins.md +++ b/ui/v2.5/src/docs/en/Manual/Plugins.md @@ -248,7 +248,7 @@ The following object types are supported: * `SceneMarker` * `Image` * `Gallery` -* `Movie` +* `Group` * `Performer` * `Studio` * `Tag` @@ -296,7 +296,7 @@ For example, here is the `args` values for a Scene update operation: "studio_id":null, "gallery_ids":null, "performer_ids":null, - "movies":null, + "groups":null, "tag_ids":["21"], "cover_image":null, "stash_ids":null diff --git a/ui/v2.5/src/docs/en/Manual/Scraping.md b/ui/v2.5/src/docs/en/Manual/Scraping.md index 267f03b97..7f7b88a49 100644 --- a/ui/v2.5/src/docs/en/Manual/Scraping.md +++ b/ui/v2.5/src/docs/en/Manual/Scraping.md @@ -15,7 +15,7 @@ Stash supports scraping of metadata from various external sources. | | Fragment | Search | URL | |---|:---:|:---:|:---:| | gallery | ✔️ | | ✔️ | -| movie | | | ✔️ | +| group | | | ✔️ | | performer | | ✔️ | ✔️ | | scene | ✔️ | ✔️ | ✔️ | @@ -94,7 +94,7 @@ When used in combination with stash-box, the user can optionally submit scene fi | | Has Tagger | Source Selection | |---|:---:|:---:| | gallery | | | -| movie | | | +| group | | | | performer | ✔️ | | | scene | ✔️ | ✔️ | diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index 7d267d1b1..760dfef54 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -153,9 +153,9 @@ Returns `void`. - `Icon` - `ImageDetailPanel` - `ModalSetting` -- `MovieIDSelect` -- `MovieSelect` -- `MovieSelect.sort` +- `GroupIDSelect` +- `GroupSelect` +- `GroupSelect.sort` - `NumberSetting` - `PerformerDetailsPanel` - `PerformerDetailsPanel.DetailGroup` diff --git a/ui/v2.5/src/utils/form.tsx b/ui/v2.5/src/utils/form.tsx index 40fedc7bf..45b0aa86f 100644 --- a/ui/v2.5/src/utils/form.tsx +++ b/ui/v2.5/src/utils/form.tsx @@ -48,6 +48,7 @@ export function renderLabel(options: { // the mouse wheel will change the field value _and_ scroll the window. // This hook prevents the propagation that causes the window to scroll. export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) { + // removed the dependency array because the underlying ref value may change useEffect(() => { const { current } = ref; @@ -66,17 +67,18 @@ export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) { current.removeEventListener("wheel", stopWheelScroll); } }; - }, [ref]); + }); } -const InputField: React.FC< +// NumberField is a wrapper around Form.Control that prevents wheel events from scrolling the window. +export const NumberField: React.FC< InputHTMLAttributes<HTMLInputElement> & FormControlProps > = (props) => { const inputRef = useRef<HTMLInputElement>(null); useStopWheelScroll(inputRef); - return <Form.Control {...props} ref={inputRef} />; + return <Form.Control {...props} type="number" ref={inputRef} />; }; type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>; @@ -134,9 +136,18 @@ export function formikUtils<V extends FormikValues>( isInvalid={!!error} /> ); + } else if (type === "number") { + <NumberField + type={type} + className="text-input" + placeholder={placeholder} + {...formikProps} + value={value} + isInvalid={!!error} + />; } else { control = ( - <InputField + <Form.Control type={type} className="text-input" placeholder={placeholder}