Merge branch 'stashapp:develop' into feature_selectbycodec

This commit is contained in:
y-e-r 2024-09-22 02:54:34 -04:00 committed by GitHub
commit b8d11b5788
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 119 additions and 92 deletions

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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":

View file

@ -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<IHierarchicalLabelValue>;
@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC<
{criterion.value.depth !== 0 && (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View file

@ -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<IDurationFilterProps> = ({
) {
equalsControl = (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value")
}
@ -57,9 +57,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) {
lowerControl = (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(e, "value")
}
@ -78,9 +77,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
) {
upperControl = (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChanged(
e,

View file

@ -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<IPhashDistanceValue>;
@ -49,10 +50,9 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
{criterion.modifier !== CriterionModifier.IsNull &&
criterion.modifier !== CriterionModifier.NotNull && (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
onChange={distanceChanged}
type="number"
value={value ? value.distance : ""}
placeholder={intl.formatMessage({ id: "distance" })}
/>

View file

@ -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 && (
<Form.Group>
<Form.Control
<NumberField
className="btn-secondary"
type="number"
placeholder={intl.formatMessage(messages.studio_depth)}
onChange={(e) =>
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)

View file

@ -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<{
<Popover id="custom_pagesize_popover">
<Form inline>
<InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control
type="number"
min={1}

View file

@ -12,6 +12,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import useFocus from "src/utils/focus";
import { Icon } from "../Shared/Icon";
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
import { useStopWheelScroll } from "src/utils/form";
const PageCount: React.FC<{
totalPages: number;
@ -32,6 +33,8 @@ const PageCount: React.FC<{
}
}, [showSelectPage, pageFocus]);
useStopWheelScroll(pageInput);
const pageOptions = useMemo(() => {
const maxPagesToShow = 10;
const min = Math.max(1, currentPage - maxPagesToShow / 2);
@ -98,6 +101,7 @@ const PageCount: React.FC<{
<Popover id="select_page_popover">
<Form inline>
<InputGroup>
{/* can't use NumberField because of the ref */}
<Form.Control
type="number"
min={1}

View file

@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
import { Form, Row, Col } from "react-bootstrap";
import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
import cx from "classnames";
import { NumberField } from "src/utils/form";
export type GroupSceneIndexMap = Map<string, number | undefined>;
@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
/>
</Col>
<Col xs={3}>
<Form.Control
<NumberField
className="text-input"
type="number"
value={m.scene_index ?? ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateFieldChanged(

View file

@ -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<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_count_head",
})}
</h6>
<Form.Control
<NumberField
className="text-input"
type="number"
value={previewSegments?.toString() ?? 1}
min={1}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
id: "dialogs.scene_gen.preview_seg_duration_head",
})}
</h6>
<Form.Control
<NumberField
className="text-input"
type="number"
value={previewSegmentDuration?.toString() ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
set({

View file

@ -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<INumberSetting> = PatchComponent(
<ModalSetting<number>
{...props}
renderField={(value, setValue) => (
<Form.Control
<NumberField
className="text-input"
type="number"
value={value ?? 0}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue(Number.parseInt(e.currentTarget.value || "0", 10))

View file

@ -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<IRatingNumberProps> = (
const showTextField = !props.disabled && (editing || !props.clickToRate);
const [ratingRef] = useFocusOnce(editing, true);
useStopWheelScroll(ratingRef);
const effectiveValue = editing ? valueStage : props.value;

View file

@ -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))

View file

@ -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 |

View file

@ -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 | `<name>.json` |
| Scenes | `<title or first file basename>.<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.

View file

@ -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) |)"

View file

@ -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

View file

@ -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 | ✔️ | ✔️ |

View file

@ -153,9 +153,9 @@ Returns `void`.
- `Icon`
- `ImageDetailPanel`
- `ModalSetting`
- `MovieIDSelect`
- `MovieSelect`
- `MovieSelect.sort`
- `GroupIDSelect`
- `GroupSelect`
- `GroupSelect.sort`
- `NumberSetting`
- `PerformerDetailsPanel`
- `PerformerDetailsPanel.DetailGroup`

View file

@ -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}