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)
|
query += fmt.Sprintf("ORDER BY `folders`.`id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -94,12 +94,12 @@ func (m *schema32Migrator) migrateFolders(ctx context.Context) error {
|
||||||
count++
|
count++
|
||||||
|
|
||||||
parent := filepath.Dir(p)
|
parent := filepath.Dir(p)
|
||||||
parentID, zipFileID, err := m.createFolderHierarchy(parent)
|
parentID, zipFileID, err := m.createFolderHierarchy(tx, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error {
|
||||||
query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit)
|
query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit)
|
||||||
|
|
||||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -178,12 +178,12 @@ func (m *schema32Migrator) migrateFiles(ctx context.Context) error {
|
||||||
parent := filepath.Dir(p)
|
parent := filepath.Dir(p)
|
||||||
basename := filepath.Base(p)
|
basename := filepath.Base(p)
|
||||||
if parent != "." {
|
if parent != "." {
|
||||||
parentID, zipFileID, err := m.createFolderHierarchy(parent)
|
parentID, zipFileID, err := m.createFolderHierarchy(tx, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("migrating file %s: %w", p, err)
|
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)
|
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 {
|
||||||
return err
|
_, 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)
|
parent := filepath.Dir(p)
|
||||||
|
|
||||||
if parent == p {
|
if parent == p {
|
||||||
// get or create this folder
|
// get or create this folder
|
||||||
return m.getOrCreateFolder(p, nil, sql.NullInt64{})
|
return m.getOrCreateFolder(tx, p, nil, sql.NullInt64{})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -269,23 +271,23 @@ func (m *schema32Migrator) createFolderHierarchy(p string) (*int, sql.NullInt64,
|
||||||
parentID = &foundEntry.id
|
parentID = &foundEntry.id
|
||||||
zipFileID = foundEntry.zipID
|
zipFileID = foundEntry.zipID
|
||||||
} else {
|
} else {
|
||||||
parentID, zipFileID, err = m.createFolderHierarchy(parent)
|
parentID, zipFileID, err = m.createFolderHierarchy(tx, parent)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, sql.NullInt64{}, err
|
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]
|
foundEntry, ok := m.folderCache[path]
|
||||||
if ok {
|
if ok {
|
||||||
return &foundEntry.id, foundEntry.zipID, nil
|
return &foundEntry.id, foundEntry.zipID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = "SELECT `id`, `zip_file_id` FROM `folders` WHERE `path` = ?"
|
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 {
|
if err != nil {
|
||||||
return nil, sql.NullInt64{}, err
|
return nil, sql.NullInt64{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +316,7 @@ func (m *schema32Migrator) getOrCreateFolder(path string, parentID *int, zipFile
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
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 {
|
if err != nil {
|
||||||
return nil, sql.NullInt64{}, fmt.Errorf("creating folder %s: %w", path, err)
|
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)
|
query += fmt.Sprintf("ORDER BY `id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +100,7 @@ func (m *schema32PreMigrator) migrate(ctx context.Context) error {
|
||||||
|
|
||||||
logger.Infof("Correcting %q gallery to be zip-based.", p)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
|
||||||
|
|
||||||
query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit)
|
query += fmt.Sprintf(" ORDER BY `performer_id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -92,7 +92,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
|
||||||
gotSome = true
|
gotSome = true
|
||||||
count++
|
count++
|
||||||
|
|
||||||
if err := m.migratePerformerAliases(id, aliases); err != nil {
|
if err := m.migratePerformerAliases(tx, id, aliases); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ func (m *schema42Migrator) migrate(ctx context.Context) error {
|
||||||
return nil
|
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 /
|
// split aliases by , or /
|
||||||
aliasList := strings.FieldsFunc(aliases, func(r rune) bool {
|
aliasList := strings.FieldsFunc(aliases, func(r rune) bool {
|
||||||
return strings.ContainsRune(",/", r)
|
return strings.ContainsRune(",/", r)
|
||||||
|
|
@ -126,7 +126,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete the existing row
|
// 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ func (m *schema42Migrator) migratePerformerAliases(id int, aliases string) error
|
||||||
|
|
||||||
// insert aliases into table
|
// insert aliases into table
|
||||||
for _, alias := range aliasList {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +173,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
|
||||||
|
|
||||||
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -194,7 +194,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
|
||||||
lastID = id
|
lastID = id
|
||||||
count++
|
count++
|
||||||
|
|
||||||
if err := m.massagePerformerName(id, name); err != nil {
|
if err := m.massagePerformerName(tx, id, name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -220,7 +220,7 @@ SELECT id, name FROM performers WHERE performers.name like '% (%)'`
|
||||||
// the format "name (disambiguation)".
|
// the format "name (disambiguation)".
|
||||||
var performerDisRE = regexp.MustCompile(`^((?:[^(\s]+\s)+)\(([^)]+)\)$`)
|
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)
|
r := performerDisRE.FindStringSubmatch(name)
|
||||||
if len(r) != 3 {
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
query += fmt.Sprintf(" ORDER BY `id` LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +286,7 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS
|
||||||
gotSome = true
|
gotSome = true
|
||||||
count++
|
count++
|
||||||
|
|
||||||
if err := m.migrateDuplicatePerformer(id, name); err != nil {
|
if err := m.migrateDuplicatePerformer(tx, id, name); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -308,13 +308,13 @@ SELECT id, name FROM performers WHERE performers.disambiguation IS NULL AND EXIS
|
||||||
return nil
|
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
|
// get the highest value of disambiguation for this performer name
|
||||||
query := `
|
query := `
|
||||||
SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1`
|
SELECT disambiguation FROM performers WHERE name = ? ORDER BY disambiguation DESC LIMIT 1`
|
||||||
|
|
||||||
var disambiguation sql.NullString
|
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
|
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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
|
||||||
|
|
||||||
query += fmt.Sprintf(" LIMIT %d", limit)
|
query += fmt.Sprintf(" LIMIT %d", limit)
|
||||||
|
|
||||||
rows, err := m.db.Query(query)
|
rows, err := tx.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -191,7 +191,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
|
||||||
image := result[i+1].(*[]byte)
|
image := result[i+1].(*[]byte)
|
||||||
|
|
||||||
if len(*image) > 0 {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +202,7 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
|
||||||
"joinTable": options.joinTable,
|
"joinTable": options.joinTable,
|
||||||
"joinIDCol": options.joinIDCol,
|
"joinIDCol": options.joinIDCol,
|
||||||
})
|
})
|
||||||
if _, err := m.db.Exec(deleteSQL, id); err != nil {
|
if _, err := tx.Exec(deleteSQL, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -224,11 +224,11 @@ func (m *schema45Migrator) migrateImagesTable(ctx context.Context, options migra
|
||||||
return nil
|
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
|
// calculate checksum and insert into blobs table
|
||||||
checksum := md5.FromBytes(data)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,7 +237,7 @@ func (m *schema45Migrator) insertImage(data []byte, id int, destTable string, de
|
||||||
"destTable": destTable,
|
"destTable": destTable,
|
||||||
"destCol": destCol,
|
"destCol": destCol,
|
||||||
})
|
})
|
||||||
if _, err := m.db.Exec(updateSQL, checksum, id); err != nil {
|
if _, err := tx.Exec(updateSQL, checksum, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,7 @@ type schema49Migrator struct {
|
||||||
|
|
||||||
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
|
func (m *schema49Migrator) migrateSavedFilters(ctx context.Context) error {
|
||||||
if err := m.withTxn(ctx, func(tx *sqlx.Tx) 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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to update saved filter %d: %w", id, err)
|
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` " +
|
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`"
|
"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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ func (m *schema52Migrator) migrate(ctx context.Context) error {
|
||||||
|
|
||||||
// ensure the correct path is unique
|
// ensure the correct path is unique
|
||||||
var v int
|
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) {
|
if isEmptyErr != nil && !errors.Is(isEmptyErr, sql.ErrNoRows) {
|
||||||
return fmt.Errorf("error checking if correct path %s is unique: %w", correctPath, isEmptyErr)
|
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
|
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)
|
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 {
|
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||||
query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`"
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +53,7 @@ func (m *schema55Migrator) migrate(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert the timestamp to the correct format
|
// 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)
|
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 {
|
if err := m.withTxn(ctx, func(tx *sqlx.Tx) error {
|
||||||
query := "SELECT DISTINCT `scene_id`, `view_date` FROM `scenes_view_dates`"
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +64,7 @@ func (m *schema64Migrator) migrate(ctx context.Context) error {
|
||||||
|
|
||||||
// convert the timestamp to the correct format
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error correcting view date %s to %s: %w", viewDate.Timestamp, viewDate, err)
|
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()
|
direction := findFilter.GetDirection()
|
||||||
switch sort {
|
switch sort {
|
||||||
case "movie_scene_number", "group_scene_number":
|
case "movie_scene_number":
|
||||||
query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id")
|
query.join(groupsScenesTable, "", "scenes.id = groups_scenes.scene_id")
|
||||||
query.sortAndPagination += getSort("scene_index", direction, groupsScenesTable)
|
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":
|
case "tag_count":
|
||||||
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
|
query.sortAndPagination += getCountSort(sceneTable, scenesTagsTable, sceneIDColumn, direction)
|
||||||
case "performer_count":
|
case "performer_count":
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { defineMessages, MessageDescriptor, useIntl } from "react-intl";
|
||||||
import { FilterSelect, SelectObject } from "src/components/Shared/Select";
|
import { FilterSelect, SelectObject } from "src/components/Shared/Select";
|
||||||
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
import { Criterion } from "src/models/list-filter/criteria/criterion";
|
||||||
import { IHierarchicalLabelValue } from "src/models/list-filter/types";
|
import { IHierarchicalLabelValue } from "src/models/list-filter/types";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
interface IHierarchicalLabelValueFilterProps {
|
interface IHierarchicalLabelValueFilterProps {
|
||||||
criterion: Criterion<IHierarchicalLabelValue>;
|
criterion: Criterion<IHierarchicalLabelValue>;
|
||||||
|
|
@ -104,9 +105,8 @@ export const HierarchicalLabelValueFilter: React.FC<
|
||||||
|
|
||||||
{criterion.value.depth !== 0 && (
|
{criterion.value.depth !== 0 && (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type="number"
|
|
||||||
placeholder={intl.formatMessage(messages.studio_depth)}
|
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)
|
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 { CriterionModifier } from "../../../core/generated-graphql";
|
||||||
import { INumberValue } from "../../../models/list-filter/types";
|
import { INumberValue } from "../../../models/list-filter/types";
|
||||||
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
|
import { NumberCriterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
interface IDurationFilterProps {
|
interface IDurationFilterProps {
|
||||||
criterion: NumberCriterion;
|
criterion: NumberCriterion;
|
||||||
|
|
@ -36,9 +37,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||||
) {
|
) {
|
||||||
equalsControl = (
|
equalsControl = (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type="number"
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
|
|
@ -57,9 +57,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||||
) {
|
) {
|
||||||
lowerControl = (
|
lowerControl = (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type="number"
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(e, "value")
|
onChanged(e, "value")
|
||||||
}
|
}
|
||||||
|
|
@ -78,9 +77,8 @@ export const NumberFilter: React.FC<IDurationFilterProps> = ({
|
||||||
) {
|
) {
|
||||||
upperControl = (
|
upperControl = (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type="number"
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChanged(
|
onChanged(
|
||||||
e,
|
e,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
||||||
import { IPhashDistanceValue } from "../../../models/list-filter/types";
|
import { IPhashDistanceValue } from "../../../models/list-filter/types";
|
||||||
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
import { Criterion } from "../../../models/list-filter/criteria/criterion";
|
||||||
import { CriterionModifier } from "src/core/generated-graphql";
|
import { CriterionModifier } from "src/core/generated-graphql";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
interface IPhashFilterProps {
|
interface IPhashFilterProps {
|
||||||
criterion: Criterion<IPhashDistanceValue>;
|
criterion: Criterion<IPhashDistanceValue>;
|
||||||
|
|
@ -49,10 +50,9 @@ export const PhashFilter: React.FC<IPhashFilterProps> = ({
|
||||||
{criterion.modifier !== CriterionModifier.IsNull &&
|
{criterion.modifier !== CriterionModifier.IsNull &&
|
||||||
criterion.modifier !== CriterionModifier.NotNull && (
|
criterion.modifier !== CriterionModifier.NotNull && (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
onChange={distanceChanged}
|
onChange={distanceChanged}
|
||||||
type="number"
|
|
||||||
value={value ? value.distance : ""}
|
value={value ? value.distance : ""}
|
||||||
placeholder={intl.formatMessage({ id: "distance" })}
|
placeholder={intl.formatMessage({ id: "distance" })}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { keyboardClickHandler } from "src/utils/keyboard";
|
||||||
import { useDebounce } from "src/hooks/debounce";
|
import { useDebounce } from "src/hooks/debounce";
|
||||||
import useFocus from "src/utils/focus";
|
import useFocus from "src/utils/focus";
|
||||||
import ScreenUtils from "src/utils/screen";
|
import ScreenUtils from "src/utils/screen";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
interface ISelectedItem {
|
interface ISelectedItem {
|
||||||
item: ILabeledId;
|
item: ILabeledId;
|
||||||
|
|
@ -361,9 +362,8 @@ export const HierarchicalObjectsFilter = <
|
||||||
|
|
||||||
{criterion.value.depth !== 0 && (
|
{criterion.value.depth !== 0 && (
|
||||||
<Form.Group>
|
<Form.Group>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="btn-secondary"
|
className="btn-secondary"
|
||||||
type="number"
|
|
||||||
placeholder={intl.formatMessage(messages.studio_depth)}
|
placeholder={intl.formatMessage(messages.studio_depth)}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
onDepthChanged(e.target.value ? parseInt(e.target.value, 10) : -1)
|
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 { useDebounce } from "src/hooks/debounce";
|
||||||
import { View } from "./views";
|
import { View } from "./views";
|
||||||
import { ClearableInput } from "../Shared/ClearableInput";
|
import { ClearableInput } from "../Shared/ClearableInput";
|
||||||
|
import { useStopWheelScroll } from "src/utils/form";
|
||||||
|
|
||||||
export function useDebouncedSearchInput(
|
export function useDebouncedSearchInput(
|
||||||
filter: ListFilterModel,
|
filter: ListFilterModel,
|
||||||
|
|
@ -126,6 +127,8 @@ export const PageSizeSelector: React.FC<{
|
||||||
}
|
}
|
||||||
}, [customPageSizeShowing, perPageFocus]);
|
}, [customPageSizeShowing, perPageFocus]);
|
||||||
|
|
||||||
|
useStopWheelScroll(perPageInput);
|
||||||
|
|
||||||
const pageSizeOptions = useMemo(() => {
|
const pageSizeOptions = useMemo(() => {
|
||||||
const ret = PAGE_SIZE_OPTIONS.map((o) => {
|
const ret = PAGE_SIZE_OPTIONS.map((o) => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -190,6 +193,7 @@ export const PageSizeSelector: React.FC<{
|
||||||
<Popover id="custom_pagesize_popover">
|
<Popover id="custom_pagesize_popover">
|
||||||
<Form inline>
|
<Form inline>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
{/* can't use NumberField because of the ref */}
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||||
import useFocus from "src/utils/focus";
|
import useFocus from "src/utils/focus";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faChevronDown } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useStopWheelScroll } from "src/utils/form";
|
||||||
|
|
||||||
const PageCount: React.FC<{
|
const PageCount: React.FC<{
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
|
|
@ -32,6 +33,8 @@ const PageCount: React.FC<{
|
||||||
}
|
}
|
||||||
}, [showSelectPage, pageFocus]);
|
}, [showSelectPage, pageFocus]);
|
||||||
|
|
||||||
|
useStopWheelScroll(pageInput);
|
||||||
|
|
||||||
const pageOptions = useMemo(() => {
|
const pageOptions = useMemo(() => {
|
||||||
const maxPagesToShow = 10;
|
const maxPagesToShow = 10;
|
||||||
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
const min = Math.max(1, currentPage - maxPagesToShow / 2);
|
||||||
|
|
@ -98,6 +101,7 @@ const PageCount: React.FC<{
|
||||||
<Popover id="select_page_popover">
|
<Popover id="select_page_popover">
|
||||||
<Form inline>
|
<Form inline>
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
{/* can't use NumberField because of the ref */}
|
||||||
<Form.Control
|
<Form.Control
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||||
import { Form, Row, Col } from "react-bootstrap";
|
import { Form, Row, Col } from "react-bootstrap";
|
||||||
import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
|
import { Group, GroupSelect } from "src/components/Groups/GroupSelect";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
export type GroupSceneIndexMap = Map<string, number | undefined>;
|
export type GroupSceneIndexMap = Map<string, number | undefined>;
|
||||||
|
|
||||||
|
|
@ -92,9 +93,8 @@ export const SceneGroupTable: React.FC<IProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="text-input"
|
className="text-input"
|
||||||
type="number"
|
|
||||||
value={m.scene_index ?? ""}
|
value={m.scene_index ?? ""}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
updateFieldChanged(
|
updateFieldChanged(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Form } from "react-bootstrap";
|
import { Form } from "react-bootstrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
export type VideoPreviewSettingsInput = Pick<
|
export type VideoPreviewSettingsInput = Pick<
|
||||||
GQL.ConfigGeneralInput,
|
GQL.ConfigGeneralInput,
|
||||||
|
|
@ -44,9 +45,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
|
||||||
id: "dialogs.scene_gen.preview_seg_count_head",
|
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||||
})}
|
})}
|
||||||
</h6>
|
</h6>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="text-input"
|
className="text-input"
|
||||||
type="number"
|
|
||||||
value={previewSegments?.toString() ?? 1}
|
value={previewSegments?.toString() ?? 1}
|
||||||
min={1}
|
min={1}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
|
@ -71,9 +71,8 @@ export const VideoPreviewInput: React.FC<IVideoPreviewInput> = ({
|
||||||
id: "dialogs.scene_gen.preview_seg_duration_head",
|
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||||
})}
|
})}
|
||||||
</h6>
|
</h6>
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="text-input"
|
className="text-input"
|
||||||
type="number"
|
|
||||||
value={previewSegmentDuration?.toString() ?? 0}
|
value={previewSegmentDuration?.toString() ?? 0}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
set({
|
set({
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Icon } from "../Shared/Icon";
|
||||||
import { StringListInput } from "../Shared/StringListInput";
|
import { StringListInput } from "../Shared/StringListInput";
|
||||||
import { PatchComponent } from "src/patch";
|
import { PatchComponent } from "src/patch";
|
||||||
import { useSettings, useSettingsOptional } from "./context";
|
import { useSettings, useSettingsOptional } from "./context";
|
||||||
|
import { NumberField } from "src/utils/form";
|
||||||
|
|
||||||
interface ISetting {
|
interface ISetting {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
@ -484,9 +485,8 @@ export const NumberSetting: React.FC<INumberSetting> = PatchComponent(
|
||||||
<ModalSetting<number>
|
<ModalSetting<number>
|
||||||
{...props}
|
{...props}
|
||||||
renderField={(value, setValue) => (
|
renderField={(value, setValue) => (
|
||||||
<Form.Control
|
<NumberField
|
||||||
className="text-input"
|
className="text-input"
|
||||||
type="number"
|
|
||||||
value={value ?? 0}
|
value={value ?? 0}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
setValue(Number.parseInt(e.currentTarget.value || "0", 10))
|
setValue(Number.parseInt(e.currentTarget.value || "0", 10))
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Button } from "react-bootstrap";
|
||||||
import { Icon } from "../Icon";
|
import { Icon } from "../Icon";
|
||||||
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
|
import { faPencil, faStar } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useFocusOnce } from "src/utils/focus";
|
import { useFocusOnce } from "src/utils/focus";
|
||||||
|
import { useStopWheelScroll } from "src/utils/form";
|
||||||
|
|
||||||
export interface IRatingNumberProps {
|
export interface IRatingNumberProps {
|
||||||
value: number | null;
|
value: number | null;
|
||||||
|
|
@ -26,6 +27,7 @@ export const RatingNumber: React.FC<IRatingNumberProps> = (
|
||||||
const showTextField = !props.disabled && (editing || !props.clickToRate);
|
const showTextField = !props.disabled && (editing || !props.clickToRate);
|
||||||
|
|
||||||
const [ratingRef] = useFocusOnce(editing, true);
|
const [ratingRef] = useFocusOnce(editing, true);
|
||||||
|
useStopWheelScroll(ratingRef);
|
||||||
|
|
||||||
const effectiveValue = editing ? valueStage : props.value;
|
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 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 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 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
|
### 🎨 Improvements
|
||||||
* Added button to view sub-studio/sub-tag content on Studio/Tag details pages. ([#5080](https://github.com/stashapp/stash/pull/5080))
|
* 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))
|
* 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 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 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))
|
* 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))
|
* 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))
|
* 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))
|
* 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))
|
* 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))
|
* 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))
|
* 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 |
|
| Scene | Title, Details, Path, OSHash, Checksum, Marker titles |
|
||||||
| Image | Title, Path, Checksum |
|
| Image | Title, Path, Checksum |
|
||||||
| Movie | Title |
|
| Group | Title |
|
||||||
| Marker | Title, Scene title |
|
| Marker | Title, Scene title |
|
||||||
| Gallery | Title, Path, Checksum |
|
| Gallery | Title, Path, Checksum |
|
||||||
| Performer | Name, Aliases |
|
| Performer | Name, Aliases |
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ The metadata given to Stash can be exported into the JSON format. This structure
|
||||||
* `performers`
|
* `performers`
|
||||||
* `scenes`
|
* `scenes`
|
||||||
* `studios`
|
* `studios`
|
||||||
* `movies`
|
* `groups`
|
||||||
|
|
||||||
## File naming
|
## File naming
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ When exported, files are named with different formats depending on the object ty
|
||||||
| Performers | `<name>.json` |
|
| Performers | `<name>.json` |
|
||||||
| Scenes | `<title or first file basename>.<hash>.json` |
|
| Scenes | `<title or first file basename>.<hash>.json` |
|
||||||
| Studios | `<name>.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.
|
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 s` | Scenes |
|
||||||
| `g i` | Images |
|
| `g i` | Images |
|
||||||
| `g v` | Movies |
|
| `g v` | Groups |
|
||||||
| `g k` | Markers |
|
| `g k` | Markers |
|
||||||
| `g l` | Galleries |
|
| `g l` | Galleries |
|
||||||
| `g p` | Performers |
|
| `g p` | Performers |
|
||||||
|
|
@ -104,28 +104,28 @@
|
||||||
[//]: # "(| `l` | Focus Gallery selector |)"
|
[//]: # "(| `l` | Focus Gallery selector |)"
|
||||||
[//]: # "(| `u` | Focus Studio selector |)"
|
[//]: # "(| `u` | Focus Studio selector |)"
|
||||||
[//]: # "(| `p` | Focus Performers selector |)"
|
[//]: # "(| `p` | Focus Performers selector |)"
|
||||||
[//]: # "(| `v` | Focus Movies selector |)"
|
[//]: # "(| `v` | Focus Groups selector |)"
|
||||||
[//]: # "(| `t` | Focus Tags selector |)"
|
[//]: # "(| `t` | Focus Tags selector |)"
|
||||||
|
|
||||||
## Movies Page shortcuts
|
## Groups Page shortcuts
|
||||||
|
|
||||||
| Keyboard sequence | Action |
|
| Keyboard sequence | Action |
|
||||||
|-------------------|--------|
|
|-------------------|--------|
|
||||||
| `n` | New Movie |
|
| `n` | New Group |
|
||||||
|
|
||||||
## Movie Page shortcuts
|
## Group Page shortcuts
|
||||||
|
|
||||||
| Keyboard sequence | Action |
|
| Keyboard sequence | Action |
|
||||||
|-------------------|--------|
|
|-------------------|--------|
|
||||||
| `e` | Edit Movie |
|
| `e` | Edit Group |
|
||||||
| `s s` | Save Movie |
|
| `s s` | Save Group |
|
||||||
| `d d` | Delete Movie |
|
| `d d` | Delete Group |
|
||||||
| `r {1-5}` | [Edit mode] Set rating (stars) |
|
| `r {1-5}` | [Edit mode] Set rating (stars) |
|
||||||
| `r 0` | [Edit mode] Unset 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 {0-9} {0-9}` | [Edit mode] Set rating (decimal - `r 0 0` for `10.0`) |
|
||||||
| ``r ` `` | [Edit mode] Unset rating (decimal) |
|
| ``r ` `` | [Edit mode] Unset rating (decimal) |
|
||||||
| `,` | Expand/Collapse Details |
|
| `,` | Expand/Collapse Details |
|
||||||
| `Ctrl + v` | Paste Movie image |
|
| `Ctrl + v` | Paste Group image |
|
||||||
|
|
||||||
[//]: # "Commented until implementation is dealt with"
|
[//]: # "Commented until implementation is dealt with"
|
||||||
[//]: # "(| `u` | Focus Studio selector (in edit mode) |)"
|
[//]: # "(| `u` | Focus Studio selector (in edit mode) |)"
|
||||||
|
|
|
||||||
|
|
@ -248,7 +248,7 @@ The following object types are supported:
|
||||||
* `SceneMarker`
|
* `SceneMarker`
|
||||||
* `Image`
|
* `Image`
|
||||||
* `Gallery`
|
* `Gallery`
|
||||||
* `Movie`
|
* `Group`
|
||||||
* `Performer`
|
* `Performer`
|
||||||
* `Studio`
|
* `Studio`
|
||||||
* `Tag`
|
* `Tag`
|
||||||
|
|
@ -296,7 +296,7 @@ For example, here is the `args` values for a Scene update operation:
|
||||||
"studio_id":null,
|
"studio_id":null,
|
||||||
"gallery_ids":null,
|
"gallery_ids":null,
|
||||||
"performer_ids":null,
|
"performer_ids":null,
|
||||||
"movies":null,
|
"groups":null,
|
||||||
"tag_ids":["21"],
|
"tag_ids":["21"],
|
||||||
"cover_image":null,
|
"cover_image":null,
|
||||||
"stash_ids":null
|
"stash_ids":null
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Stash supports scraping of metadata from various external sources.
|
||||||
| | Fragment | Search | URL |
|
| | Fragment | Search | URL |
|
||||||
|---|:---:|:---:|:---:|
|
|---|:---:|:---:|:---:|
|
||||||
| gallery | ✔️ | | ✔️ |
|
| gallery | ✔️ | | ✔️ |
|
||||||
| movie | | | ✔️ |
|
| group | | | ✔️ |
|
||||||
| performer | | ✔️ | ✔️ |
|
| performer | | ✔️ | ✔️ |
|
||||||
| scene | ✔️ | ✔️ | ✔️ |
|
| scene | ✔️ | ✔️ | ✔️ |
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ When used in combination with stash-box, the user can optionally submit scene fi
|
||||||
| | Has Tagger | Source Selection |
|
| | Has Tagger | Source Selection |
|
||||||
|---|:---:|:---:|
|
|---|:---:|:---:|
|
||||||
| gallery | | |
|
| gallery | | |
|
||||||
| movie | | |
|
| group | | |
|
||||||
| performer | ✔️ | |
|
| performer | ✔️ | |
|
||||||
| scene | ✔️ | ✔️ |
|
| scene | ✔️ | ✔️ |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -153,9 +153,9 @@ Returns `void`.
|
||||||
- `Icon`
|
- `Icon`
|
||||||
- `ImageDetailPanel`
|
- `ImageDetailPanel`
|
||||||
- `ModalSetting`
|
- `ModalSetting`
|
||||||
- `MovieIDSelect`
|
- `GroupIDSelect`
|
||||||
- `MovieSelect`
|
- `GroupSelect`
|
||||||
- `MovieSelect.sort`
|
- `GroupSelect.sort`
|
||||||
- `NumberSetting`
|
- `NumberSetting`
|
||||||
- `PerformerDetailsPanel`
|
- `PerformerDetailsPanel`
|
||||||
- `PerformerDetailsPanel.DetailGroup`
|
- `PerformerDetailsPanel.DetailGroup`
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export function renderLabel(options: {
|
||||||
// the mouse wheel will change the field value _and_ scroll the window.
|
// the mouse wheel will change the field value _and_ scroll the window.
|
||||||
// This hook prevents the propagation that causes the window to scroll.
|
// This hook prevents the propagation that causes the window to scroll.
|
||||||
export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
|
export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
|
||||||
|
// removed the dependency array because the underlying ref value may change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current } = ref;
|
const { current } = ref;
|
||||||
|
|
||||||
|
|
@ -66,17 +67,18 @@ export function useStopWheelScroll(ref: React.RefObject<HTMLElement>) {
|
||||||
current.removeEventListener("wheel", stopWheelScroll);
|
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
|
InputHTMLAttributes<HTMLInputElement> & FormControlProps
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
useStopWheelScroll(inputRef);
|
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>>;
|
type Formik<V extends FormikValues> = ReturnType<typeof useFormik<V>>;
|
||||||
|
|
@ -134,9 +136,18 @@ export function formikUtils<V extends FormikValues>(
|
||||||
isInvalid={!!error}
|
isInvalid={!!error}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (type === "number") {
|
||||||
|
<NumberField
|
||||||
|
type={type}
|
||||||
|
className="text-input"
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...formikProps}
|
||||||
|
value={value}
|
||||||
|
isInvalid={!!error}
|
||||||
|
/>;
|
||||||
} else {
|
} else {
|
||||||
control = (
|
control = (
|
||||||
<InputField
|
<Form.Control
|
||||||
type={type}
|
type={type}
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue