mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Merge branch 'stashapp:develop' into feature_selectbycodec
This commit is contained in:
commit
b8d11b5788
28 changed files with 119 additions and 92 deletions
|
|
@ -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 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" })}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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) |)"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 | ✔️ | ✔️ |
|
||||
|
||||
|
|
|
|||
|
|
@ -153,9 +153,9 @@ Returns `void`.
|
|||
- `Icon`
|
||||
- `ImageDetailPanel`
|
||||
- `ModalSetting`
|
||||
- `MovieIDSelect`
|
||||
- `MovieSelect`
|
||||
- `MovieSelect.sort`
|
||||
- `GroupIDSelect`
|
||||
- `GroupSelect`
|
||||
- `GroupSelect.sort`
|
||||
- `NumberSetting`
|
||||
- `PerformerDetailsPanel`
|
||||
- `PerformerDetailsPanel.DetailGroup`
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue