mirror of
https://github.com/stashapp/stash.git
synced 2025-12-10 00:08:04 -06:00
Update dependencies (#3123)
* Update localforage, remove query-string * Update fontawesome and flag-icons * Update formatjs * Update axios and videojs * Update apollo client and graphql * Update bootstrap and react * Update polyfills * Update vite * Update ESLint * Update stylelint * Update configs * Rebuild yarn.lock
This commit is contained in:
parent
0c9eeef143
commit
a1851b3713
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,7 +17,6 @@
|
||||
|
||||
# GraphQL generated output
|
||||
internal/api/generated_*.go
|
||||
ui/v2.5/src/core/generated-*.tsx
|
||||
|
||||
####
|
||||
# Jetbrains
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
BROWSER=none
|
||||
PORT=3000
|
||||
ESLINT_NO_DEV_ERRORS=true
|
||||
|
||||
@ -9,61 +9,69 @@
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"jsx-a11y"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "jsx-a11y"],
|
||||
"extends": [
|
||||
"airbnb-typescript",
|
||||
"airbnb/hooks",
|
||||
"plugin:react/recommended",
|
||||
"plugin:import/recommended",
|
||||
"prettier",
|
||||
"prettier/prettier"
|
||||
"airbnb-typescript",
|
||||
"plugin:import/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react/jsx-runtime",
|
||||
"airbnb/hooks",
|
||||
"prettier"
|
||||
],
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": 2,
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"@typescript-eslint/no-explicit-any": 2,
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"custom": {
|
||||
"regex": "^I[A-Z]",
|
||||
"match": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"import/named": "off",
|
||||
"import/namespace": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/style-prop-object": ["error", {
|
||||
}
|
||||
],
|
||||
"lines-between-class-members": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"import/extensions": [
|
||||
"error",
|
||||
"ignorePackages",
|
||||
{
|
||||
"js": "never",
|
||||
"jsx": "never",
|
||||
"ts": "never",
|
||||
"tsx": "never"
|
||||
}
|
||||
],
|
||||
"import/named": "off",
|
||||
"import/namespace": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"react/display-name": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/style-prop-object": [
|
||||
"error",
|
||||
{
|
||||
"allow": ["FormattedNumber"]
|
||||
}],
|
||||
"spaced-comment": ["error", "always", {
|
||||
"markers": ["/"]
|
||||
}],
|
||||
"prefer-destructuring": ["error", {"object": true, "array": false}],
|
||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }],
|
||||
"no-nested-ternary": "off"
|
||||
}
|
||||
],
|
||||
"spaced-comment": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"markers": ["/"]
|
||||
}
|
||||
],
|
||||
"prefer-destructuring": ["error", { "object": true, "array": false }],
|
||||
"@typescript-eslint/no-use-before-define": [
|
||||
"error",
|
||||
{ "functions": false, "classes": true }
|
||||
],
|
||||
"no-nested-ternary": "off"
|
||||
}
|
||||
}
|
||||
|
||||
5
ui/v2.5/.gitignore
vendored
5
ui/v2.5/.gitignore
vendored
@ -1,4 +1,5 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
# generated
|
||||
src/core/generated-*.tsx
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
@ -12,6 +13,7 @@
|
||||
/build
|
||||
|
||||
# misc
|
||||
.gitignore
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
@ -23,3 +25,4 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintcache
|
||||
.stylelintcache
|
||||
|
||||
18
ui/v2.5/.prettierignore
Normal file
18
ui/v2.5/.prettierignore
Normal file
@ -0,0 +1,18 @@
|
||||
*.md
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# locales
|
||||
src/locales/**/*.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# generated
|
||||
src/core/generated-graphql.tsx
|
||||
@ -1,14 +1,16 @@
|
||||
{
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
"plugins": ["stylelint-order"],
|
||||
"extends": "stylelint-config-prettier",
|
||||
"customSyntax": "postcss-scss",
|
||||
"rules": {
|
||||
"indentation": null,
|
||||
"at-rule-empty-line-before": [ "always", {
|
||||
except: ["after-same-name", "first-nested" ],
|
||||
ignore: ["after-comment"],
|
||||
} ],
|
||||
"at-rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["after-same-name", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"selector-no-vendor-prefix": true,
|
||||
"block-closing-brace-newline-after": "always",
|
||||
@ -21,10 +23,13 @@
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-empty-line-before": [ "always", {
|
||||
except: ["first-nested"],
|
||||
ignore: ["stylelint-commands"],
|
||||
} ],
|
||||
"comment-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": ["first-nested"],
|
||||
"ignore": ["stylelint-commands"]
|
||||
}
|
||||
],
|
||||
"comment-whitespace-inside": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
@ -63,15 +68,15 @@
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-max-precision": 3,
|
||||
"number-no-trailing-zeros": true,
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
"declarations"
|
||||
],
|
||||
"order/order": ["custom-properties", "declarations"],
|
||||
"order/properties-alphabetical-order": true,
|
||||
"rule-empty-line-before": ["always-multi-line", {
|
||||
except: ["after-single-line-comment", "first-nested" ],
|
||||
ignore: ["after-comment"],
|
||||
}],
|
||||
"rule-empty-line-before": [
|
||||
"always-multi-line",
|
||||
{
|
||||
"except": ["after-single-line-comment", "first-nested"],
|
||||
"ignore": ["after-comment"]
|
||||
}
|
||||
],
|
||||
"selector-max-id": 1,
|
||||
"selector-max-type": 2,
|
||||
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
|
||||
@ -87,5 +92,5 @@
|
||||
"time-min-milliseconds": 100,
|
||||
"value-list-comma-space-after": "always-single-line",
|
||||
"value-list-comma-space-before": "never"
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
18
ui/v2.5/.vscode/launch.json
vendored
18
ui/v2.5/.vscode/launch.json
vendored
@ -1,18 +0,0 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Chrome",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/src",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///src/*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
26
ui/v2.5/.vscode/settings.json
vendored
26
ui/v2.5/.vscode/settings.json
vendored
@ -1,26 +0,0 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.tabSize": 2,
|
||||
"editor.renderWhitespace": "boundary",
|
||||
"editor.wordWrap": "bounded",
|
||||
"javascript.preferences.importModuleSpecifier": "relative",
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"editor.wordWrapColumn": 120,
|
||||
"editor.rulers": [
|
||||
120
|
||||
],
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en-GB",
|
||||
"spellright.language": [
|
||||
"en"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"latex",
|
||||
"plaintext",
|
||||
"typescriptreact"
|
||||
]
|
||||
}
|
||||
@ -4,11 +4,9 @@ documents: "../../graphql/documents/**/*.graphql"
|
||||
generates:
|
||||
src/core/generated-graphql.tsx:
|
||||
plugins:
|
||||
- add:
|
||||
content: "/* eslint-disable */"
|
||||
- time
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typescript-react-apollo
|
||||
config:
|
||||
withRefetchFn: true
|
||||
withRefetchFn: true
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base href="/">
|
||||
<base href="/" />
|
||||
<meta charset="utf-8" />
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
|
||||
@ -10,27 +10,15 @@
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<meta name="theme-color" content="%COLOR%" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
|
||||
<title>Stash</title>
|
||||
<script>window.STASH_BASE_URL = "/%BASE_URL%/"</script>
|
||||
<script>
|
||||
window.STASH_BASE_URL = "/%BASE_URL%/";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
<script type="module" src="/src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -3,125 +3,117 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "./",
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"build-ci": "yarn validate && yarn build",
|
||||
"validate": "yarn lint && tsc --noEmit && yarn format-check",
|
||||
"lint": "yarn lint:css && yarn lint:js",
|
||||
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
|
||||
"lint:css": "stylelint \"src/**/*.scss\"",
|
||||
"format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
||||
"format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
|
||||
"build-ci": "yarn run validate && yarn run build",
|
||||
"validate": "yarn run lint && yarn run check && yarn run format-check",
|
||||
"lint": "yarn run lint:css && yarn run lint:js",
|
||||
"lint:css": "stylelint --cache \"src/**/*.scss\"",
|
||||
"lint:js": "eslint --cache src/",
|
||||
"check": "tsc --noEmit",
|
||||
"format": "prettier --write .",
|
||||
"format-check": "prettier --check .",
|
||||
"gqlgen": "gql-gen --config codegen.yml",
|
||||
"extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'"
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not ie <= 11",
|
||||
"not op_mini all"
|
||||
">0.5% and supports es6-module-dynamic-import"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.3.7",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.5.3",
|
||||
"@formatjs/intl-locale": "^2.4.14",
|
||||
"@formatjs/intl-numberformat": "^6.1.3",
|
||||
"@formatjs/intl-pluralrules": "^4.0.6",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.34",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.15.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.2",
|
||||
"@fortawesome/react-fontawesome": "^0.1.14",
|
||||
"@types/react-select": "^4.0.8",
|
||||
"ansi-regex": "^5.0.1",
|
||||
"apollo-upload-client": "^14.1.3",
|
||||
"axios": "^1.1.3",
|
||||
"@ant-design/react-slick": "^1.0.0",
|
||||
"@apollo/client": "^3.7.4",
|
||||
"@formatjs/intl-getcanonicallocales": "^2.0.5",
|
||||
"@formatjs/intl-locale": "^3.0.11",
|
||||
"@formatjs/intl-numberformat": "^8.3.3",
|
||||
"@formatjs/intl-pluralrules": "^5.1.8",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"axios": "^1.2.5",
|
||||
"base64-blob": "^1.4.1",
|
||||
"bootstrap": "^4.6.0",
|
||||
"classnames": "^2.2.6",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"bootstrap": "^4.6.2",
|
||||
"classnames": "^2.3.2",
|
||||
"flag-icons": "^6.6.6",
|
||||
"flexbin": "^0.2.0",
|
||||
"formik": "^2.2.6",
|
||||
"graphql": "^15.4.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"i18n-iso-countries": "^6.4.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"localforage": "^1.9.0",
|
||||
"formik": "^2.2.9",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.11.2",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"intersection-observer": "^0.12.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mousetrap": "^1.6.5",
|
||||
"mousetrap-pause": "^1.0.0",
|
||||
"normalize-url": "^4.5.1",
|
||||
"postcss": "^8.2.10",
|
||||
"query-string": "6.13.8",
|
||||
"react": "17.0.2",
|
||||
"react-bootstrap": "1.4.3",
|
||||
"react-dom": "17.0.2",
|
||||
"react": "^17.0.2",
|
||||
"react-bootstrap": "^1.6.6",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "^5.10.16",
|
||||
"react-markdown": "^7.1.0",
|
||||
"react-intl": "^6.2.6",
|
||||
"react-markdown": "^8.0.5",
|
||||
"react-router-bootstrap": "^0.25.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-router-hash-link": "^2.3.1",
|
||||
"react-select": "^4.0.2",
|
||||
"react-slick": "^0.29.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-select": "^5.6.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.32.5",
|
||||
"slick-carousel": "^1.8.1",
|
||||
"string.prototype.replaceall": "^1.0.4",
|
||||
"subscriptions-transport-ws": "^0.9.18",
|
||||
"string.prototype.replaceall": "^1.0.7",
|
||||
"thehandy": "^1.0.3",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"video.js": "^7.20.3",
|
||||
"video.js": "^7.21.1",
|
||||
"videojs-mobile-ui": "^0.8.0",
|
||||
"videojs-seek-buttons": "^3.0.1",
|
||||
"videojs-vtt.js": "^0.15.4",
|
||||
"vite": "^2.9.13",
|
||||
"vite-plugin-compression": "^0.3.5",
|
||||
"vite-tsconfig-paths": "^3.3.17",
|
||||
"ws": "^7.4.6",
|
||||
"yup": "^0.32.9"
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^2.0.2",
|
||||
"@graphql-codegen/cli": "^1.20.0",
|
||||
"@graphql-codegen/time": "^2.0.2",
|
||||
"@graphql-codegen/typescript": "^1.20.00",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.13",
|
||||
"@graphql-codegen/typescript-react-apollo": "^2.2.1",
|
||||
"@types/apollo-upload-client": "^14.1.0",
|
||||
"@types/classnames": "^2.2.11",
|
||||
"@types/fslightbox-react": "^1.4.0",
|
||||
"@babel/core": "^7.20.5",
|
||||
"@graphql-codegen/cli": "^2.16.4",
|
||||
"@graphql-codegen/time": "^3.2.3",
|
||||
"@graphql-codegen/typescript": "^2.8.7",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.12",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
"@types/mousetrap": "^1.6.5",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/react": "17.0.31",
|
||||
"@types/react-dom": "^17.0.10",
|
||||
"@types/react-helmet": "^6.1.3",
|
||||
"@types/mousetrap": "^1.6.11",
|
||||
"@types/node": "^14.18.36",
|
||||
"@types/react": "^17.0.53",
|
||||
"@types/react-dom": "^17.0.18",
|
||||
"@types/react-helmet": "^6.1.6",
|
||||
"@types/react-router-bootstrap": "^0.24.5",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/react-router-hash-link": "^1.2.1",
|
||||
"@types/react-slick": "^0.23.8",
|
||||
"@types/video.js": "^7.3.49",
|
||||
"@types/react-router-hash-link": "^2.4.5",
|
||||
"@types/video.js": "^7.3.50",
|
||||
"@types/videojs-mobile-ui": "^0.5.0",
|
||||
"@types/videojs-seek-buttons": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-typescript": "^14.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.26.1",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"@vitejs/plugin-react": "^3.0.1",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^17.0.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-react": "^7.32.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"postcss-safe-parser": "^5.0.2",
|
||||
"prettier": "2.2.1",
|
||||
"stylelint": "^13.9.0",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-order": "^4.1.0",
|
||||
"typescript": "~4.4.4"
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-scss": "^4.0.6",
|
||||
"prettier": "^2.8.3",
|
||||
"sass": "^1.57.1",
|
||||
"stylelint": "^14.16.1",
|
||||
"stylelint-config-prettier": "^9.0.4",
|
||||
"stylelint-order": "^6.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~4.8.4",
|
||||
"vite": "^4.0.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-tsconfig-paths": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
24
ui/v2.5/src/@types/mousetrap-pause.d.ts
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "mousetrap-pause" {
|
||||
import { MousetrapStatic } from "mousetrap";
|
||||
|
||||
function MousetrapPause(mousetrap: MousetrapStatic): MousetrapStatic;
|
||||
|
||||
export default MousetrapPause;
|
||||
|
||||
module "mousetrap" {
|
||||
interface MousetrapStatic {
|
||||
pause(): void;
|
||||
unpause(): void;
|
||||
pauseCombo(combo: string): void;
|
||||
unpauseCombo(combo: string): void;
|
||||
}
|
||||
interface MousetrapInstance {
|
||||
pause(): void;
|
||||
unpause(): void;
|
||||
pauseCombo(combo: string): void;
|
||||
unpauseCombo(combo: string): void;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
19
ui/v2.5/src/@types/string.prototype.replaceall.d.ts
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
declare module "string.prototype.replaceall" {
|
||||
function replaceAll(
|
||||
searchValue: string | RegExp,
|
||||
replaceValue: string
|
||||
): string;
|
||||
function replaceAll(
|
||||
searchValue: string | RegExp,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
replacer: (substring: string, ...args: any[]) => string
|
||||
): string;
|
||||
|
||||
namespace replaceAll {
|
||||
function getPolyfill(): typeof replaceAll;
|
||||
function implementation(): typeof replaceAll;
|
||||
function shim(): void;
|
||||
}
|
||||
|
||||
export default replaceAll;
|
||||
}
|
||||
210
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
210
ui/v2.5/src/@types/videojs-vtt.d.ts
vendored
@ -1,111 +1,111 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "videojs-vtt.js" {
|
||||
namespace vttjs {
|
||||
/**
|
||||
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
|
||||
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
|
||||
*
|
||||
* There are two error codes that can be reported back currently:
|
||||
* * 0 BadSignature
|
||||
* * 1 BadTimeStamp
|
||||
*
|
||||
* Note: Exceptions other then ParsingError will be thrown and not reported.
|
||||
*/
|
||||
class ParsingError extends Error {
|
||||
readonly name: string;
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
namespace WebVTT {
|
||||
/**
|
||||
* A parser for the WebVTT spec in JavaScript.
|
||||
*/
|
||||
class Parser {
|
||||
/**
|
||||
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
|
||||
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
|
||||
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
|
||||
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
|
||||
*
|
||||
* @param window the window object to use
|
||||
* @param vttjs the vtt.js module
|
||||
* @param decoder the decoder to decode `parse()` data with
|
||||
*/
|
||||
constructor(window: Window);
|
||||
constructor(window: Window, decoder: TextDecoder);
|
||||
constructor(window: Window, vttjs: vttjs, decoder: TextDecoder);
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
|
||||
*/
|
||||
onregion?: (cue: VTTRegion) => void;
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
|
||||
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
|
||||
*/
|
||||
oncue?: (cue: VTTCue) => void;
|
||||
|
||||
/**
|
||||
* Is invoked in response to `flush()` and after the content was parsed completely.
|
||||
*/
|
||||
onflush?: () => void;
|
||||
|
||||
/**
|
||||
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
|
||||
* Is passed a `ParsingError` object.
|
||||
*/
|
||||
onparsingerror?: (e: ParsingError) => void;
|
||||
|
||||
/**
|
||||
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
|
||||
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
|
||||
*
|
||||
* @param data data to be parsed
|
||||
*/
|
||||
parse(data: string): this;
|
||||
|
||||
/**
|
||||
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
|
||||
* Will also trigger `onflush`.
|
||||
*/
|
||||
flush(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to allow strings to be decoded instead of the default binary utf8 data.
|
||||
*/
|
||||
function StringDecoder(): TextDecoder;
|
||||
|
||||
/**
|
||||
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
|
||||
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
|
||||
*
|
||||
* @param window window object to use
|
||||
* @param cuetext cue text to parse
|
||||
*/
|
||||
function convertCueToDOMTree(
|
||||
window: Window,
|
||||
cuetext: string
|
||||
): HTMLDivElement | null;
|
||||
|
||||
/**
|
||||
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
|
||||
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
|
||||
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
|
||||
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
|
||||
*
|
||||
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
|
||||
*/
|
||||
function processCues(
|
||||
window: Window,
|
||||
cues: VTTCue[],
|
||||
overlay: Element
|
||||
): void;
|
||||
}
|
||||
/**
|
||||
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
|
||||
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
|
||||
*
|
||||
* There are two error codes that can be reported back currently:
|
||||
* * 0 BadSignature
|
||||
* * 1 BadTimeStamp
|
||||
*
|
||||
* Note: Exceptions other then ParsingError will be thrown and not reported.
|
||||
*/
|
||||
class ParsingError extends Error {
|
||||
readonly name: string;
|
||||
readonly code: number;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export = vttjs;
|
||||
export namespace WebVTT {
|
||||
/**
|
||||
* A parser for the WebVTT spec in JavaScript.
|
||||
*/
|
||||
class Parser {
|
||||
/**
|
||||
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
|
||||
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
|
||||
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
|
||||
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
|
||||
*
|
||||
* @param window the window object to use
|
||||
* @param vttjs the vtt.js module
|
||||
* @param decoder the decoder to decode `parse()` data with
|
||||
*/
|
||||
constructor(window: Window);
|
||||
constructor(window: Window, decoder: TextDecoder);
|
||||
constructor(
|
||||
window: Window,
|
||||
vttjs: typeof import("videojs-vtt.js"),
|
||||
decoder: TextDecoder
|
||||
);
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
|
||||
*/
|
||||
onregion?: (cue: VTTRegion) => void;
|
||||
|
||||
/**
|
||||
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
|
||||
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
|
||||
*/
|
||||
oncue?: (cue: VTTCue) => void;
|
||||
|
||||
/**
|
||||
* Is invoked in response to `flush()` and after the content was parsed completely.
|
||||
*/
|
||||
onflush?: () => void;
|
||||
|
||||
/**
|
||||
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
|
||||
* Is passed a `ParsingError` object.
|
||||
*/
|
||||
onparsingerror?: (e: ParsingError) => void;
|
||||
|
||||
/**
|
||||
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
|
||||
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
|
||||
*
|
||||
* @param data data to be parsed
|
||||
*/
|
||||
parse(data: string): this;
|
||||
|
||||
/**
|
||||
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
|
||||
* Will also trigger `onflush`.
|
||||
*/
|
||||
flush(): this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to allow strings to be decoded instead of the default binary utf8 data.
|
||||
*/
|
||||
function StringDecoder(): TextDecoder;
|
||||
|
||||
/**
|
||||
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
|
||||
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
|
||||
*
|
||||
* @param window window object to use
|
||||
* @param cuetext cue text to parse
|
||||
*/
|
||||
function convertCueToDOMTree(
|
||||
window: Window,
|
||||
cuetext: string
|
||||
): HTMLDivElement | null;
|
||||
|
||||
/**
|
||||
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
|
||||
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
|
||||
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
|
||||
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
|
||||
*
|
||||
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
|
||||
*/
|
||||
function processCues(
|
||||
window: Window,
|
||||
cues: VTTCue[],
|
||||
overlay: Element
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,10 +91,12 @@ export const App: React.FC = () => {
|
||||
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
||||
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
||||
const chosenMessages = (await locales[messageLanguage]()).default;
|
||||
const res = await fetch(getPlatformURL() + "customlocales");
|
||||
let customMessages = {};
|
||||
try {
|
||||
customMessages = res.ok ? await res.json() : {};
|
||||
const res = await fetch(getPlatformURL() + "customlocales");
|
||||
if (res.ok) {
|
||||
customMessages = await res.json();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
@ -26,9 +26,6 @@ import V0180 from "src/docs/en/Changelog/v0180.md";
|
||||
import V0190 from "src/docs/en/Changelog/v0190.md";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
|
||||
// to avoid use of explicit any
|
||||
type Module = typeof V010;
|
||||
|
||||
const Changelog: React.FC = () => {
|
||||
const [{ data, loading }, setOpenState] = useChangelogStorage();
|
||||
|
||||
@ -55,7 +52,7 @@ const Changelog: React.FC = () => {
|
||||
interface IStashRelease {
|
||||
version: string;
|
||||
date?: string;
|
||||
page: Module;
|
||||
page: string;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC<IFieldOptionsList> = ({
|
||||
allowSetDefault = true,
|
||||
defaultOptions,
|
||||
}) => {
|
||||
const [localFieldOptions, setLocalFieldOptions] = useState<
|
||||
GQL.IdentifyFieldOptions[]
|
||||
>();
|
||||
const [localFieldOptions, setLocalFieldOptions] =
|
||||
useState<GQL.IdentifyFieldOptions[]>();
|
||||
const [editField, setEditField] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -202,9 +202,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
|
||||
if (s.options) {
|
||||
const sourceOptions = withoutTypename(s.options);
|
||||
sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map(
|
||||
withoutTypename
|
||||
);
|
||||
sourceOptions.fieldOptions =
|
||||
sourceOptions.fieldOptions?.map(withoutTypename);
|
||||
ret.options = sourceOptions;
|
||||
}
|
||||
|
||||
@ -215,9 +214,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
|
||||
setSources(mappedSources);
|
||||
if (identifyDefaults.options) {
|
||||
const defaultOptions = withoutTypename(identifyDefaults.options);
|
||||
defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map(
|
||||
withoutTypename
|
||||
);
|
||||
defaultOptions.fieldOptions =
|
||||
defaultOptions.fieldOptions?.map(withoutTypename);
|
||||
setOptions(defaultOptions);
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -20,7 +20,7 @@ export const sceneFields = [
|
||||
"tags",
|
||||
"stash_ids",
|
||||
] as const;
|
||||
export type SceneField = typeof sceneFields[number];
|
||||
export type SceneField = (typeof sceneFields)[number];
|
||||
|
||||
export const multiValueSceneFields: SceneField[] = [
|
||||
"studio",
|
||||
|
||||
@ -4,10 +4,9 @@ import { Modal } from "src/components/Shared";
|
||||
import { faCogs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useIntl } from "react-intl";
|
||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||
import { Module } from "src/docs/en/ReleaseNotes";
|
||||
|
||||
interface IReleaseNotesDialog {
|
||||
notes: Module[];
|
||||
notes: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +31,8 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
setPerformerMode,
|
||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerMode, setPerformerMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
|
||||
@ -8,7 +8,7 @@ import Gallery from "./GalleryDetails/Gallery";
|
||||
import GalleryCreate from "./GalleryDetails/GalleryCreate";
|
||||
import { GalleryList } from "./GalleryList";
|
||||
|
||||
const Galleries = () => {
|
||||
const Galleries: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const title_template = `${intl.formatMessage({
|
||||
|
||||
@ -214,7 +214,6 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
||||
<Tab.Pane eventKey="gallery-edit-panel">
|
||||
<GalleryEditPanel
|
||||
isVisible={activeTabKey === "gallery-edit-panel"}
|
||||
isNew={false}
|
||||
gallery={gallery}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||
|
||||
const GalleryCreate: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const gallery = {
|
||||
title: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row new-view">
|
||||
@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => {
|
||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||
/>
|
||||
</h2>
|
||||
<GalleryEditPanel
|
||||
isNew
|
||||
gallery={{ title: nameQuery ?? "" }}
|
||||
isVisible
|
||||
onDelete={() => {}}
|
||||
/>
|
||||
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -40,23 +40,16 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
isVisible: boolean;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
interface INewProps {
|
||||
isNew: true;
|
||||
gallery?: Partial<GQL.GalleryDataFragment>;
|
||||
}
|
||||
|
||||
interface IExistingProps {
|
||||
isNew: false;
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
}
|
||||
|
||||
export const GalleryEditPanel: React.FC<
|
||||
IProps & (INewProps | IExistingProps)
|
||||
> = ({ gallery, isNew, isVisible, onDelete }) => {
|
||||
export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
gallery,
|
||||
isVisible,
|
||||
onDelete,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
@ -67,15 +60,14 @@ export const GalleryEditPanel: React.FC<
|
||||
}))
|
||||
);
|
||||
|
||||
const isNew = gallery.id === undefined;
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const Scrapers = useListGalleryScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [
|
||||
scrapedGallery,
|
||||
setScrapedGallery,
|
||||
] = useState<GQL.ScrapedGallery | null>();
|
||||
const [scrapedGallery, setScrapedGallery] =
|
||||
useState<GQL.ScrapedGallery | null>();
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindGalleries } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { GalleryCard } from "./GalleryCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const GalleryRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindGalleries(props.filter);
|
||||
const cardCount = result.data?.findGalleries.count;
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "sass:math";
|
||||
|
||||
.gallery-image {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
@ -138,14 +140,14 @@ $galleryTabWidth: 450px;
|
||||
}
|
||||
|
||||
@mixin galleryWidth($width) {
|
||||
height: ($width / 3) * 2;
|
||||
height: math.div($width, 3) * 2;
|
||||
|
||||
&-landscape {
|
||||
width: $width;
|
||||
}
|
||||
|
||||
&-portrait {
|
||||
width: $width / 2;
|
||||
width: math.div($width, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -173,11 +173,9 @@ export const Manual: React.FC<IManualProps> = ({
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) {
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
const href = (event.target as HTMLAnchorElement).getAttribute("href");
|
||||
const href = event.target.getAttribute("href");
|
||||
if (href && href.startsWith("/help")) {
|
||||
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
|
||||
"/help/".length
|
||||
);
|
||||
const newKey = event.target.pathname.substring("/help/".length);
|
||||
setActiveTab(newKey);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@ -31,10 +31,8 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
setPerformerMode,
|
||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerMode, setPerformerMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
|
||||
@ -239,7 +239,9 @@ export const Image: React.FC = () => {
|
||||
Mousetrap.bind("a", () => setActiveTabKey("image-details-panel"));
|
||||
Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel"));
|
||||
Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel"));
|
||||
Mousetrap.bind("o", () => onIncrementClick());
|
||||
Mousetrap.bind("o", () => {
|
||||
onIncrementClick();
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("a");
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindImages } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
import { RecommendationRow } from "../FrontPage/RecommendationRow";
|
||||
@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const ImageRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const ImageRecommendationRow: React.FC<IProps> = (props: IProps) => {
|
||||
const result = useFindImages(props.filter);
|
||||
const cardCount = result.data?.findImages.count;
|
||||
|
||||
|
||||
@ -10,10 +10,9 @@ interface IHierarchicalLabelValueFilterProps {
|
||||
onValueChanged: (value: IHierarchicalLabelValue) => void;
|
||||
}
|
||||
|
||||
export const HierarchicalLabelValueFilter: React.FC<IHierarchicalLabelValueFilterProps> = ({
|
||||
criterion,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
export const HierarchicalLabelValueFilter: React.FC<
|
||||
IHierarchicalLabelValueFilterProps
|
||||
> = ({ criterion, onValueChanged }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (
|
||||
|
||||
@ -18,7 +18,7 @@ import { ManualStateContext } from "./Help/context";
|
||||
import { SettingsButton } from "./SettingsButton";
|
||||
import {
|
||||
faBars,
|
||||
faChartBar,
|
||||
faChartColumn,
|
||||
faFilm,
|
||||
faHeart,
|
||||
faImage,
|
||||
@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => {
|
||||
|
||||
const pathname = location.pathname.replace(/\/$/, "");
|
||||
let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;
|
||||
if (newPath != null) {
|
||||
if (newPath !== null) {
|
||||
let queryParam = new URLSearchParams(location.search).get("q");
|
||||
if (queryParam != null) {
|
||||
newPath += "?name=" + encodeURIComponent(queryParam);
|
||||
if (queryParam) {
|
||||
newPath += "?q=" + encodeURIComponent(queryParam);
|
||||
}
|
||||
}
|
||||
|
||||
@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => {
|
||||
className="minimal d-flex align-items-center h-100"
|
||||
title={intl.formatMessage({ id: "statistics" })}
|
||||
>
|
||||
<Icon icon={faChartBar} />
|
||||
<Icon icon={faChartColumn} />
|
||||
</Button>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
@ -20,7 +20,7 @@ interface IProps {
|
||||
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
|
||||
export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
||||
function maybeRenderSceneNumber() {
|
||||
if (!props.sceneIndex) return;
|
||||
|
||||
|
||||
@ -51,7 +51,9 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("e");
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useMovieCreate } from "src/core/StashService";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { MovieEditPanel } from "./MovieEditPanel";
|
||||
|
||||
const MovieCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const Toast = useToast();
|
||||
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const movie = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
// Editing movie state
|
||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||
undefined
|
||||
);
|
||||
const [frontImage, setFrontImage] = useState<string | null>();
|
||||
const [backImage, setBackImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
|
||||
const [createMovie] = useMovieCreate();
|
||||
@ -84,6 +86,7 @@ const MovieCreate: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<MovieEditPanel
|
||||
movie={movie}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => history.push("/movies")}
|
||||
onDelete={() => {}}
|
||||
|
||||
@ -25,7 +25,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IMovieEditPanel {
|
||||
movie?: Partial<GQL.MovieDataFragment>;
|
||||
movie: Partial<GQL.MovieDataFragment>;
|
||||
onSubmit: (
|
||||
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
||||
) => void;
|
||||
@ -49,19 +49,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
const Toast = useToast();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const isNew = movie === undefined;
|
||||
const isNew = movie.id === undefined;
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||
|
||||
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [imageClipboard, setImageClipboard] = useState<string>();
|
||||
|
||||
const Scrapers = useListMovieScrapers();
|
||||
const [scrapedMovie, setScrapedMovie] = useState<
|
||||
GQL.ScrapedMovie | undefined
|
||||
>();
|
||||
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
|
||||
|
||||
const schema = yup.object({
|
||||
name: yup.string().required(),
|
||||
@ -113,10 +109,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||
setBackImage(formik.values.back_image);
|
||||
}, [formik.values.back_image, setBackImage]);
|
||||
|
||||
useEffect(() => onImageEncoding(encodingImage), [
|
||||
onImageEncoding,
|
||||
encodingImage,
|
||||
]);
|
||||
useEffect(
|
||||
() => onImageEncoding(encodingImage),
|
||||
[onImageEncoding, encodingImage]
|
||||
);
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating100", v);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useFindMovies } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { MovieCard } from "./MovieCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
|
||||
export const PageNotFound: FunctionComponent = () => {
|
||||
export const PageNotFound: React.FC = () => {
|
||||
return <h1>Page not found.</h1>;
|
||||
};
|
||||
|
||||
@ -60,10 +60,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
const [existingTagIds, setExistingTagIds] = useState<string[]>();
|
||||
const [
|
||||
aggregateState,
|
||||
setAggregateState,
|
||||
] = useState<GQL.BulkPerformerUpdateInput>({});
|
||||
const [aggregateState, setAggregateState] =
|
||||
useState<GQL.BulkPerformerUpdateInput>({});
|
||||
// weight needs conversion to/from number
|
||||
const [weight, setWeight] = useState<string | undefined>();
|
||||
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(
|
||||
|
||||
@ -32,12 +32,8 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
import { PerformerSubmitButton } from "./PerformerSubmitButton";
|
||||
import GenderIcon from "../GenderIcon";
|
||||
import {
|
||||
faCamera,
|
||||
faDove,
|
||||
faHeart,
|
||||
faLink,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
|
||||
import { IUIConfig } from "src/core/config";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
|
||||
@ -247,7 +243,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
isNew={false}
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
onCancelEditing={() => {
|
||||
@ -351,7 +346,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faDove} />
|
||||
<Icon icon={faTwitter} />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
@ -366,7 +361,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon icon={faCamera} />
|
||||
<Icon icon={faInstagram} />
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
@ -405,7 +400,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
<h2>
|
||||
<GenderIcon
|
||||
gender={performer.gender}
|
||||
className="gender-icon mr-2 flag-icon"
|
||||
className="gender-icon mr-2 fi"
|
||||
/>
|
||||
<CountryFlag country={performer.country} className="mr-2" />
|
||||
<span className="performer-name">{performer.name}</span>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
||||
@ -8,13 +8,11 @@ const PerformerCreate: React.FC = () => {
|
||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const performer = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const activeImage = imagePreview ?? "";
|
||||
const intl = useIntl();
|
||||
@ -50,9 +48,8 @@ const PerformerCreate: React.FC = () => {
|
||||
/>
|
||||
</h2>
|
||||
<PerformerEditPanel
|
||||
performer={{ name: nameQuery ?? "" }}
|
||||
performer={performer}
|
||||
isVisible
|
||||
isNew
|
||||
onImageChange={onImageChange}
|
||||
onImageEncoding={onImageEncoding}
|
||||
/>
|
||||
|
||||
@ -50,7 +50,6 @@ const isScraper = (
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: Partial<GQL.PerformerDataFragment>;
|
||||
isNew?: boolean;
|
||||
isVisible: boolean;
|
||||
onImageChange?: (image?: string | null) => void;
|
||||
onImageEncoding?: (loading?: boolean) => void;
|
||||
@ -59,7 +58,6 @@ interface IPerformerDetails {
|
||||
|
||||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
isNew,
|
||||
isVisible,
|
||||
onImageChange,
|
||||
onImageEncoding,
|
||||
@ -68,8 +66,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
const isNew = performer.id === undefined;
|
||||
|
||||
// Editing state
|
||||
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox | undefined>();
|
||||
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox>();
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
|
||||
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
|
||||
|
||||
@ -447,10 +447,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
return () => onImageChange?.();
|
||||
}, [formik.values.image, onImageChange]);
|
||||
|
||||
useEffect(() => onImageEncoding?.(imageEncoding), [
|
||||
onImageEncoding,
|
||||
imageEncoding,
|
||||
]);
|
||||
useEffect(
|
||||
() => onImageEncoding?.(imageEncoding),
|
||||
[onImageEncoding, imageEncoding]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newQueryableScrapers = (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindPerformers } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { PerformerCard } from "./PerformerCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const PerformerRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const PerformerRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindPerformers(props.filter);
|
||||
const cardCount = result.data?.findPerformers.count;
|
||||
|
||||
|
||||
@ -76,7 +76,7 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flag-icon {
|
||||
.fi {
|
||||
bottom: 1rem;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
|
||||
height: 2rem;
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
} from "react-bootstrap";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import querystring from "query-string";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
@ -47,20 +46,13 @@ const CLASSNAME = "duplicate-checker";
|
||||
export const SceneDuplicateChecker: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const { page, size, distance } = querystring.parse(history.location.search);
|
||||
const currentPage = Number.parseInt(
|
||||
Array.isArray(page) ? page[0] : page ?? "1",
|
||||
10
|
||||
);
|
||||
const pageSize = Number.parseInt(
|
||||
Array.isArray(size) ? size[0] : size ?? "20",
|
||||
10
|
||||
);
|
||||
|
||||
const query = new URLSearchParams(history.location.search);
|
||||
const currentPage = Number.parseInt(query.get("page") ?? "1", 10);
|
||||
const pageSize = Number.parseInt(query.get("size") ?? "20", 10);
|
||||
const hashDistance = Number.parseInt(query.get("distance") ?? "0", 10);
|
||||
|
||||
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
||||
const hashDistance = Number.parseInt(
|
||||
Array.isArray(distance) ? distance[0] : distance ?? "0",
|
||||
10
|
||||
);
|
||||
const [isMultiDelete, setIsMultiDelete] = useState(false);
|
||||
const [deletingScenes, setDeletingScenes] = useState(false);
|
||||
const [editingScenes, setEditingScenes] = useState(false);
|
||||
@ -90,9 +82,8 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
GQL.SlimSceneDataFragment[] | null
|
||||
>(null);
|
||||
|
||||
const [mergeScenes, setMergeScenes] = useState<
|
||||
{ id: string; title: string }[] | undefined
|
||||
>(undefined);
|
||||
const [mergeScenes, setMergeScenes] =
|
||||
useState<{ id: string; title: string }[]>();
|
||||
|
||||
if (loading) return <LoadingIndicator />;
|
||||
if (!data) return <ErrorMessage error="Error searching for duplicates." />;
|
||||
@ -107,12 +98,16 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
).length;
|
||||
|
||||
const setQuery = (q: Record<string, string | number | undefined>) => {
|
||||
history.push({
|
||||
search: querystring.stringify({
|
||||
...querystring.parse(history.location.search),
|
||||
...q,
|
||||
}),
|
||||
});
|
||||
const newQuery = new URLSearchParams(query);
|
||||
for (const key of Object.keys(q)) {
|
||||
const value = q[key];
|
||||
if (value !== undefined) {
|
||||
newQuery.set(key, String(value));
|
||||
} else {
|
||||
newQuery.delete(key);
|
||||
}
|
||||
}
|
||||
history.push({ search: newQuery.toString() });
|
||||
};
|
||||
|
||||
function onDeleteDialogClosed(deleted: boolean) {
|
||||
@ -504,7 +499,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
||||
page: undefined,
|
||||
})
|
||||
}
|
||||
defaultValue={distance ?? 0}
|
||||
defaultValue={hashDistance}
|
||||
className="input-control ml-4"
|
||||
>
|
||||
<option value={0}>
|
||||
|
||||
@ -40,9 +40,8 @@ export const SceneFilenameParser: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
|
||||
const [parserInput, setParserInput] = useState<IParserInput>(
|
||||
initialParserInput
|
||||
);
|
||||
const [parserInput, setParserInput] =
|
||||
useState<IParserInput>(initialParserInput);
|
||||
const prevParserInputRef = useRef<IParserInput>();
|
||||
const prevParserInput = prevParserInputRef.current;
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ interface IShowFieldsProps {
|
||||
onShowFieldsChanged: (fields: Map<string, boolean>) => void;
|
||||
}
|
||||
|
||||
export const ShowFields = (props: IShowFieldsProps) => {
|
||||
export const ShowFields: React.FC<IShowFieldsProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
|
||||
@ -10,12 +10,10 @@ class TrackActivityPlugin extends videojs.getPlugin("plugin") {
|
||||
incrementPlayCount: () => Promise<void> = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
saveActivity: (
|
||||
resumeTime: number,
|
||||
playDuration: number
|
||||
) => Promise<void> = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
saveActivity: (resumeTime: number, playDuration: number) => Promise<void> =
|
||||
() => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
private enabled = false;
|
||||
private playCountIncremented = false;
|
||||
|
||||
@ -32,10 +32,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
|
||||
const Toast = useToast();
|
||||
const [rating100, setRating] = useState<number>();
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [
|
||||
performerMode,
|
||||
setPerformerMode,
|
||||
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerMode, setPerformerMode] =
|
||||
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
|
||||
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(
|
||||
|
||||
@ -99,9 +99,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
);
|
||||
|
||||
// studio image is missing if it uses the default
|
||||
const missingStudioImage = props.scene.studio?.image_path?.endsWith(
|
||||
"?default=true"
|
||||
);
|
||||
const missingStudioImage =
|
||||
props.scene.studio?.image_path?.endsWith("?default=true");
|
||||
const showStudioAsText =
|
||||
missingStudioImage || (configuration?.interface.showStudioAsText ?? false);
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
|
||||
import queryString from "query-string";
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
@ -144,7 +143,9 @@ const ScenePage: React.FC<IProps> = ({
|
||||
Mousetrap.bind("e", () => setActiveTabKey("scene-edit-panel"));
|
||||
Mousetrap.bind("k", () => setActiveTabKey("scene-markers-panel"));
|
||||
Mousetrap.bind("i", () => setActiveTabKey("scene-file-info-panel"));
|
||||
Mousetrap.bind("o", () => onIncrementClick());
|
||||
Mousetrap.bind("o", () => {
|
||||
onIncrementClick();
|
||||
});
|
||||
Mousetrap.bind("p n", () => onQueueNext());
|
||||
Mousetrap.bind("p p", () => onQueuePrevious());
|
||||
Mousetrap.bind("p r", () => onQueueRandom());
|
||||
@ -521,7 +522,7 @@ const SceneLoader: React.FC = () => {
|
||||
const { data, loading, error } = useFindScene(id ?? "");
|
||||
|
||||
const queryParams = useMemo(
|
||||
() => queryString.parse(location.search, { decode: false }),
|
||||
() => new URLSearchParams(location.search),
|
||||
[location.search]
|
||||
);
|
||||
const sceneQueue = useMemo(
|
||||
@ -529,13 +530,13 @@ const SceneLoader: React.FC = () => {
|
||||
[queryParams]
|
||||
);
|
||||
const queryContinue = useMemo(() => {
|
||||
let cont = queryParams.continue;
|
||||
if (cont !== undefined) {
|
||||
let cont = queryParams.get("continue");
|
||||
if (cont) {
|
||||
return cont === "true";
|
||||
} else {
|
||||
return !!configuration?.interface.continuePlaylistDefault;
|
||||
}
|
||||
}, [configuration?.interface.continuePlaylistDefault, queryParams.continue]);
|
||||
}, [configuration?.interface.continuePlaylistDefault, queryParams]);
|
||||
|
||||
const [queueScenes, setQueueScenes] = useState<QueuedScene[]>([]);
|
||||
|
||||
@ -547,14 +548,13 @@ const SceneLoader: React.FC = () => {
|
||||
|
||||
const _setTimestamp = useRef<(value: number) => void>();
|
||||
const initialTimestamp = useMemo(() => {
|
||||
const t = Array.isArray(queryParams.t) ? queryParams.t[0] : queryParams.t;
|
||||
return Number.parseInt(t ?? "0", 10);
|
||||
return Number.parseInt(queryParams.get("t") ?? "0", 10);
|
||||
}, [queryParams]);
|
||||
|
||||
const [queueTotal, setQueueTotal] = useState(0);
|
||||
const [queueStart, setQueueStart] = useState(1);
|
||||
|
||||
const autoplay = queryParams.autoplay === "true";
|
||||
const autoplay = queryParams.get("autoplay") === "true";
|
||||
const currentQueueIndex = queueScenes
|
||||
? queueScenes.findIndex((s) => s.id === id)
|
||||
: -1;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { SceneEditPanel } from "./SceneEditPanel";
|
||||
import queryString from "query-string";
|
||||
import { useFindScene } from "src/core/StashService";
|
||||
import { ImageUtils } from "src/utils";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
@ -9,13 +9,13 @@ import { LoadingIndicator } from "src/components/Shared";
|
||||
const SceneCreate: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
// create scene from provided scene id if applicable
|
||||
const queryParams = queryString.parse(location.search);
|
||||
const location = useLocation();
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
|
||||
const fromSceneID = (queryParams?.from_scene_id ?? "") as string;
|
||||
const { data, loading } = useFindScene(fromSceneID ?? "");
|
||||
// create scene from provided scene id if applicable
|
||||
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "");
|
||||
const [loadingCoverImage, setLoadingCoverImage] = useState(false);
|
||||
const [coverImage, setCoverImage] = useState<string | undefined>(undefined);
|
||||
const [coverImage, setCoverImage] = useState<string>();
|
||||
|
||||
const scene = useMemo(() => {
|
||||
if (data?.findScene) {
|
||||
@ -26,8 +26,10 @@ const SceneCreate: React.FC = () => {
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}, [data?.findScene]);
|
||||
return {
|
||||
title: query.get("q") ?? undefined,
|
||||
};
|
||||
}, [data?.findScene, query]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchCoverImage() {
|
||||
@ -62,6 +64,7 @@ const SceneCreate: React.FC = () => {
|
||||
</h2>
|
||||
<SceneEditPanel
|
||||
scene={scene}
|
||||
fileID={query.get("file_id") ?? undefined}
|
||||
initialCoverImage={coverImage}
|
||||
isVisible
|
||||
isNew
|
||||
|
||||
@ -36,7 +36,6 @@ import { ImageUtils, FormUtils, getStashIDs } from "src/utils";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt, useHistory } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
@ -54,6 +53,7 @@ const SceneQueryModal = lazy(() => import("./SceneQueryModal"));
|
||||
|
||||
interface IProps {
|
||||
scene: Partial<GQL.SceneDataFragment>;
|
||||
fileID?: string;
|
||||
initialCoverImage?: string;
|
||||
isNew?: boolean;
|
||||
isVisible: boolean;
|
||||
@ -62,6 +62,7 @@ interface IProps {
|
||||
|
||||
export const SceneEditPanel: React.FC<IProps> = ({
|
||||
scene,
|
||||
fileID,
|
||||
initialCoverImage,
|
||||
isNew = false,
|
||||
isVisible,
|
||||
@ -71,10 +72,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
|
||||
const fileID = (queryParams?.file_id ?? "") as string;
|
||||
|
||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||
[]
|
||||
);
|
||||
@ -83,17 +80,13 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
|
||||
const [scraper, setScraper] = useState<GQL.ScraperSourceInput | undefined>();
|
||||
const [
|
||||
isScraperQueryModalOpen,
|
||||
setIsScraperQueryModalOpen,
|
||||
] = useState<boolean>(false);
|
||||
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
|
||||
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||
const [endpoint, setEndpoint] = useState<string | undefined>();
|
||||
const [endpoint, setEndpoint] = useState<string>();
|
||||
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
setCoverImagePreview(
|
||||
@ -286,7 +279,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
const createValues = getCreateValues(input);
|
||||
const result = await mutateCreateScene({
|
||||
...createValues,
|
||||
file_ids: fileID ? [fileID as string] : undefined,
|
||||
file_ids: fileID ? [fileID] : undefined,
|
||||
});
|
||||
if (result.data?.sceneCreate?.id) {
|
||||
history.push(`/scenes/${result.data?.sceneCreate.id}`);
|
||||
@ -901,9 +894,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
</Form.Label>
|
||||
<ul className="pl-0">
|
||||
{formik.values.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(
|
||||
/https?:\/\/.*?\//
|
||||
)?.[0];
|
||||
const base =
|
||||
stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
|
||||
@ -178,12 +178,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
const Toast = useToast();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [deletingFile, setDeletingFile] = useState<
|
||||
GQL.VideoFileDataFragment | undefined
|
||||
>();
|
||||
const [reassigningFile, setReassigningFile] = useState<
|
||||
GQL.VideoFileDataFragment | undefined
|
||||
>();
|
||||
const [deletingFile, setDeletingFile] = useState<GQL.VideoFileDataFragment>();
|
||||
const [reassigningFile, setReassigningFile] =
|
||||
useState<GQL.VideoFileDataFragment>();
|
||||
|
||||
function renderStashIDs() {
|
||||
if (!props.scene.stash_ids.length) {
|
||||
|
||||
@ -22,10 +22,8 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
|
||||
},
|
||||
});
|
||||
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
|
||||
const [
|
||||
editingMarker,
|
||||
setEditingMarker,
|
||||
] = useState<GQL.SceneMarkerDataFragment>();
|
||||
const [editingMarker, setEditingMarker] =
|
||||
useState<GQL.SceneMarkerDataFragment>();
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { MovieCard } from "src/components/Movies/MovieCard";
|
||||
|
||||
@ -6,7 +6,7 @@ interface ISceneMoviePanelProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
}
|
||||
|
||||
export const SceneMoviePanel: FunctionComponent<ISceneMoviePanelProps> = (
|
||||
export const SceneMoviePanel: React.FC<ISceneMoviePanelProps> = (
|
||||
props: ISceneMoviePanelProps
|
||||
) => {
|
||||
const cards = props.scene.movies.map((sceneMovie) => (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useAllMoviesForFilter } from "src/core/StashService";
|
||||
@ -11,9 +11,7 @@ export interface IProps {
|
||||
onUpdate: (value: GQL.SceneMovieInput[]) => void;
|
||||
}
|
||||
|
||||
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const SceneMovieTable: React.FC<IProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { data } = useAllMoviesForFilter();
|
||||
|
||||
|
||||
@ -97,14 +97,8 @@ interface IScrapedObjectsRow<T> {
|
||||
export const ScrapedObjectsRow = <T extends IHasName>(
|
||||
props: IScrapedObjectsRow<T>
|
||||
) => {
|
||||
const {
|
||||
title,
|
||||
result,
|
||||
onChange,
|
||||
newObjects,
|
||||
onCreateNew,
|
||||
renderObjects,
|
||||
} = props;
|
||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
|
||||
@ -43,9 +43,8 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
const history = useHistory();
|
||||
const config = React.useContext(ConfigurationContext);
|
||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
||||
const [mergeScenes, setMergeScenes] = useState<
|
||||
{ id: string; title: string }[] | undefined
|
||||
>(undefined);
|
||||
const [mergeScenes, setMergeScenes] =
|
||||
useState<{ id: string; title: string }[]>();
|
||||
const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false);
|
||||
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
|
||||
const [isExportAll, setIsExportAll] = useState(false);
|
||||
|
||||
@ -36,7 +36,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
|
||||
const renderMovies = (scene: GQL.SlimSceneDataFragment) =>
|
||||
scene.movies.map((sceneMovie) =>
|
||||
!sceneMovie.movie ? undefined : (
|
||||
<Link to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}>
|
||||
<Link
|
||||
key={sceneMovie.movie.id}
|
||||
to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}
|
||||
>
|
||||
<h6>{sceneMovie.movie.name}</h6>
|
||||
</Link>
|
||||
)
|
||||
|
||||
@ -133,7 +133,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
||||
setLoading(true);
|
||||
|
||||
const destData = await ImageUtils.imageToDataURL(dest.paths.screenshot);
|
||||
const srcData = await ImageUtils.imageToDataURL(src.paths!.screenshot!);
|
||||
const srcData = await ImageUtils.imageToDataURL(src.paths.screenshot!);
|
||||
|
||||
// keep destination image by default
|
||||
const useNewValue = false;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useFindScenes } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { SceneCard } from "./SceneCard";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
@ -14,9 +14,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const SceneRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const SceneRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindScenes(props.filter);
|
||||
const cardCount = result.data?.findScenes.count;
|
||||
|
||||
|
||||
@ -463,7 +463,7 @@ input[type="range"].blue-slider {
|
||||
}
|
||||
|
||||
@media (min-width: 1200px), (max-width: 575px) {
|
||||
.performer-card .flag-icon {
|
||||
.performer-card .fi {
|
||||
height: 1.33rem;
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useState } from "react";
|
||||
import React, { PropsWithChildren, useState } from "react";
|
||||
import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { PropsWithChildren } from "react-router/node_modules/@types/react";
|
||||
import { Icon } from "../Shared";
|
||||
import { StringListInput } from "../Shared/StringListInput";
|
||||
|
||||
@ -154,7 +153,7 @@ export const BooleanSetting: React.FC<IBooleanSetting> = (props) => {
|
||||
};
|
||||
|
||||
interface ISelectSetting extends ISetting {
|
||||
value?: string | number | string[] | undefined;
|
||||
value?: string | number | string[];
|
||||
onChange: (v: string) => void;
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Card } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { PropsWithChildren } from "react-router/node_modules/@types/react";
|
||||
|
||||
interface ISettingGroup {
|
||||
id?: string;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import queryString from "query-string";
|
||||
import { Tab, Nav, Row, Col } from "react-bootstrap";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
@ -23,7 +22,7 @@ export const Settings: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const defaultTab = queryString.parse(location.search).tab ?? "tasks";
|
||||
const defaultTab = new URLSearchParams(location.search).get("tab") ?? "tasks";
|
||||
|
||||
const onSelect = (val: string) => history.push(`?tab=${val}`);
|
||||
|
||||
|
||||
@ -9,14 +9,8 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export const SettingsLibraryPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
general,
|
||||
loading,
|
||||
error,
|
||||
saveGeneral,
|
||||
defaults,
|
||||
saveDefaults,
|
||||
} = React.useContext(SettingStateContext);
|
||||
const { general, loading, error, saveGeneral, defaults, saveDefaults } =
|
||||
React.useContext(SettingStateContext);
|
||||
|
||||
function commaDelimitedToList(value: string | undefined) {
|
||||
if (value) {
|
||||
|
||||
@ -75,31 +75,17 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
|
||||
export const SettingsScrapingPanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const {
|
||||
data: performerScrapers,
|
||||
loading: loadingPerformers,
|
||||
} = useListPerformerScrapers();
|
||||
const {
|
||||
data: sceneScrapers,
|
||||
loading: loadingScenes,
|
||||
} = useListSceneScrapers();
|
||||
const {
|
||||
data: galleryScrapers,
|
||||
loading: loadingGalleries,
|
||||
} = useListGalleryScrapers();
|
||||
const {
|
||||
data: movieScrapers,
|
||||
loading: loadingMovies,
|
||||
} = useListMovieScrapers();
|
||||
const { data: performerScrapers, loading: loadingPerformers } =
|
||||
useListPerformerScrapers();
|
||||
const { data: sceneScrapers, loading: loadingScenes } =
|
||||
useListSceneScrapers();
|
||||
const { data: galleryScrapers, loading: loadingGalleries } =
|
||||
useListGalleryScrapers();
|
||||
const { data: movieScrapers, loading: loadingMovies } =
|
||||
useListMovieScrapers();
|
||||
|
||||
const {
|
||||
general,
|
||||
scraping,
|
||||
loading,
|
||||
error,
|
||||
saveGeneral,
|
||||
saveScraping,
|
||||
} = React.useContext(SettingStateContext);
|
||||
const { general, scraping, loading, error, saveGeneral, saveScraping } =
|
||||
React.useContext(SettingStateContext);
|
||||
|
||||
async function onReloadScrapers() {
|
||||
await mutateReloadScrapers().catch((e) => Toast.error(e));
|
||||
|
||||
@ -71,14 +71,8 @@ export const SettingsSecurityPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const {
|
||||
general,
|
||||
apiKey,
|
||||
loading,
|
||||
error,
|
||||
saveGeneral,
|
||||
refetch,
|
||||
} = React.useContext(SettingStateContext);
|
||||
const { general, apiKey, loading, error, saveGeneral, refetch } =
|
||||
React.useContext(SettingStateContext);
|
||||
|
||||
const [generateAPIKey] = useGenerateAPIKey();
|
||||
|
||||
|
||||
@ -23,9 +23,12 @@ export const SettingsServicesPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { dlna, loading: configLoading, error, saveDLNA } = React.useContext(
|
||||
SettingStateContext
|
||||
);
|
||||
const {
|
||||
dlna,
|
||||
loading: configLoading,
|
||||
error,
|
||||
saveDLNA,
|
||||
} = React.useContext(SettingStateContext);
|
||||
|
||||
// undefined to hide dialog, true for enable, false for disable
|
||||
const [enableDisable, setEnableDisable] = useState<boolean | undefined>(
|
||||
|
||||
@ -17,9 +17,8 @@ import {
|
||||
} from "./GeneratePreviewOptions";
|
||||
|
||||
export const SettingsConfigurationPanel: React.FC = () => {
|
||||
const { general, loading, error, saveGeneral } = React.useContext(
|
||||
SettingStateContext
|
||||
);
|
||||
const { general, loading, error, saveGeneral } =
|
||||
React.useContext(SettingStateContext);
|
||||
|
||||
const transcodeQualities = [
|
||||
GQL.StreamingResolutionEnum.Low,
|
||||
|
||||
@ -17,12 +17,9 @@ interface IDirectorySelectionDialogProps {
|
||||
onClose: (paths?: string[]) => void;
|
||||
}
|
||||
|
||||
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = ({
|
||||
animation,
|
||||
allowEmpty = false,
|
||||
initialPaths = [],
|
||||
onClose,
|
||||
}) => {
|
||||
export const DirectorySelectionDialog: React.FC<
|
||||
IDirectorySelectionDialogProps
|
||||
> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
|
||||
@ -81,14 +81,12 @@ export const LibraryTasks: React.FC = () => {
|
||||
});
|
||||
|
||||
const [scanOptions, setScanOptions] = useState<GQL.ScanMetadataInput>({});
|
||||
const [
|
||||
autoTagOptions,
|
||||
setAutoTagOptions,
|
||||
] = useState<GQL.AutoTagMetadataInput>({
|
||||
performers: ["*"],
|
||||
studios: ["*"],
|
||||
tags: ["*"],
|
||||
});
|
||||
const [autoTagOptions, setAutoTagOptions] =
|
||||
useState<GQL.AutoTagMetadataInput>({
|
||||
performers: ["*"],
|
||||
studios: ["*"],
|
||||
tags: ["*"],
|
||||
});
|
||||
|
||||
function getDefaultGenerateOptions(): GQL.GenerateMetadataInput {
|
||||
return {
|
||||
@ -104,10 +102,8 @@ export const LibraryTasks: React.FC = () => {
|
||||
};
|
||||
}
|
||||
|
||||
const [
|
||||
generateOptions,
|
||||
setGenerateOptions,
|
||||
] = useState<GQL.GenerateMetadataInput>(getDefaultGenerateOptions());
|
||||
const [generateOptions, setGenerateOptions] =
|
||||
useState<GQL.GenerateMetadataInput>(getDefaultGenerateOptions());
|
||||
|
||||
type DialogOpenState = typeof dialogOpen;
|
||||
|
||||
|
||||
@ -22,9 +22,8 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export const Setup: React.FC = () => {
|
||||
const { configuration, loading: configLoading } = useContext(
|
||||
ConfigurationContext
|
||||
);
|
||||
const { configuration, loading: configLoading } =
|
||||
useContext(ConfigurationContext);
|
||||
|
||||
const [step, setStep] = useState(0);
|
||||
const [configLocation, setConfigLocation] = useState("");
|
||||
|
||||
@ -19,9 +19,7 @@ const CountryFlag: React.FC<ICountryFlag> = ({
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`${
|
||||
className ?? ""
|
||||
} flag-icon flag-icon-${isoCountry.toLowerCase()}`}
|
||||
className={`${className ?? ""} fi fi-${isoCountry.toLowerCase()}`}
|
||||
title={country}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { getCountries } from "src/utils";
|
||||
import CountryLabel from "./CountryLabel";
|
||||
|
||||
interface IProps {
|
||||
value?: string | undefined;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
|
||||
@ -20,9 +20,8 @@ export const FolderSelect: React.FC<IProps> = ({
|
||||
defaultDirectories,
|
||||
appendButton,
|
||||
}) => {
|
||||
const [debouncedDirectory, setDebouncedDirectory] = useState(
|
||||
currentDirectory
|
||||
);
|
||||
const [debouncedDirectory, setDebouncedDirectory] =
|
||||
useState(currentDirectory);
|
||||
const { data, error, loading } = useDirectory(debouncedDirectory);
|
||||
const intl = useIntl();
|
||||
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import gfm from "remark-gfm";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
interface IPageProps {
|
||||
// page is a markdown module
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
page: any;
|
||||
page: string;
|
||||
}
|
||||
|
||||
export const MarkdownPage: React.FC<IPageProps> = ({ page }) => {
|
||||
@ -20,7 +19,7 @@ export const MarkdownPage: React.FC<IPageProps> = ({ page }) => {
|
||||
}, [page, markdown]);
|
||||
|
||||
return (
|
||||
<ReactMarkdown className="markdown" plugins={[gfm]}>
|
||||
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import * as React from "react";
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@ -21,9 +21,7 @@ interface IMultiSetProps {
|
||||
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
|
||||
}
|
||||
|
||||
const MultiSet: React.FunctionComponent<IMultiSetProps> = (
|
||||
props: IMultiSetProps
|
||||
) => {
|
||||
const MultiSet: React.FC<IMultiSetProps> = (props) => {
|
||||
const intl = useIntl();
|
||||
const modes = [
|
||||
GQL.BulkUpdateIdMode.Set,
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Select, {
|
||||
ValueType,
|
||||
Styles,
|
||||
OnChangeValue,
|
||||
StylesConfig,
|
||||
OptionProps,
|
||||
components as reactSelectComponents,
|
||||
GroupedOptionsType,
|
||||
OptionsType,
|
||||
MenuListComponentProps,
|
||||
GroupTypeBase,
|
||||
Options,
|
||||
MenuListProps,
|
||||
GroupBase,
|
||||
OptionsOrGroups,
|
||||
} from "react-select";
|
||||
import CreatableSelect from "react-select/creatable";
|
||||
import debounce from "lodash-es/debounce";
|
||||
@ -24,7 +24,7 @@ import {
|
||||
usePerformerCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { SelectComponents } from "react-select/src/components";
|
||||
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { objectTitle } from "src/core/files";
|
||||
@ -65,22 +65,22 @@ interface IFilterProps {
|
||||
interface ISelectProps<T extends boolean> {
|
||||
className?: string;
|
||||
items: Option[];
|
||||
selectedOptions?: ValueType<Option, T>;
|
||||
selectedOptions?: OnChangeValue<Option, T>;
|
||||
creatable?: boolean;
|
||||
onCreateOption?: (value: string) => void;
|
||||
isLoading: boolean;
|
||||
isDisabled?: boolean;
|
||||
onChange: (item: ValueType<Option, T>) => void;
|
||||
onChange: (item: OnChangeValue<Option, T>) => void;
|
||||
initialIds?: string[];
|
||||
isMulti: T;
|
||||
isClearable?: boolean;
|
||||
onInputChange?: (input: string) => void;
|
||||
components?: Partial<SelectComponents<Option, T>>;
|
||||
components?: Partial<SelectComponents<Option, T, GroupBase<Option>>>;
|
||||
filterOption?: (option: Option, rawInput: string) => boolean;
|
||||
isValidNewOption?: (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, T>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
value: Options<Option>,
|
||||
options: OptionsOrGroups<Option, GroupBase<Option>>
|
||||
) => boolean;
|
||||
placeholder?: string;
|
||||
showDropdown?: boolean;
|
||||
@ -95,7 +95,16 @@ interface IFilterComponentProps extends IFilterProps {
|
||||
onCreate?: (name: string) => Promise<{ item: ValidTypes; message: string }>;
|
||||
}
|
||||
interface IFilterSelectProps<T extends boolean>
|
||||
extends Omit<ISelectProps<T>, "onChange" | "items" | "onCreateOption"> {}
|
||||
extends Pick<
|
||||
ISelectProps<T>,
|
||||
| "isLoading"
|
||||
| "isMulti"
|
||||
| "components"
|
||||
| "filterOption"
|
||||
| "isValidNewOption"
|
||||
| "placeholder"
|
||||
| "closeMenuOnSelect"
|
||||
> {}
|
||||
|
||||
type TitledObject = { id: string; title: string };
|
||||
interface ITitledSelect {
|
||||
@ -106,18 +115,21 @@ interface ITitledSelect {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const getSelectedItems = (selectedItems: ValueType<Option, boolean>) =>
|
||||
selectedItems
|
||||
? Array.isArray(selectedItems)
|
||||
? selectedItems
|
||||
: [selectedItems]
|
||||
: [];
|
||||
const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
if (Array.isArray(selectedItems)) {
|
||||
return selectedItems;
|
||||
} else if (selectedItems) {
|
||||
return [selectedItems];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) =>
|
||||
const getSelectedValues = (selectedItems: OnChangeValue<Option, boolean>) =>
|
||||
getSelectedItems(selectedItems).map((item) => item.value);
|
||||
|
||||
const LimitedSelectMenu = <T extends boolean>(
|
||||
props: MenuListComponentProps<Option, T, GroupTypeBase<Option>>
|
||||
props: MenuListProps<Option, T, GroupBase<Option>>
|
||||
) => {
|
||||
const maxOptionsShown = 200;
|
||||
const [hiddenCount, setHiddenCount] = useState<number>(0);
|
||||
@ -129,13 +141,13 @@ const LimitedSelectMenu = <T extends boolean>(
|
||||
if (Array.isArray(props.children)) {
|
||||
// limit the number of select options showing in the select dropdowns
|
||||
// always showing the 'Create "..."' option when it exists
|
||||
let creationOptionIndex = (props.children as React.ReactNodeArray).findIndex(
|
||||
let creationOptionIndex = (props.children as React.ReactNode[]).findIndex(
|
||||
(child: React.ReactNode) => {
|
||||
let maybeCreatableOption = child as React.ReactElement<
|
||||
OptionProps<
|
||||
Option & { __isNew__: boolean },
|
||||
T,
|
||||
GroupTypeBase<Option & { __isNew__: boolean }>
|
||||
GroupBase<Option & { __isNew__: boolean }>
|
||||
>,
|
||||
""
|
||||
>;
|
||||
@ -190,7 +202,7 @@ const SelectComponent = <T extends boolean>({
|
||||
noOptionsMessage = type !== "tags" ? "None" : null,
|
||||
}: ISelectProps<T> & ITypeProps) => {
|
||||
const values = items.filter((item) => initialIds?.indexOf(item.value) !== -1);
|
||||
const defaultValue = (isMulti ? values : values[0] ?? null) as ValueType<
|
||||
const defaultValue = (isMulti ? values : values[0] ?? null) as OnChangeValue<
|
||||
Option,
|
||||
T
|
||||
>;
|
||||
@ -204,18 +216,18 @@ const SelectComponent = <T extends boolean>({
|
||||
]
|
||||
: items;
|
||||
|
||||
const styles: Partial<Styles<Option, T>> = {
|
||||
const styles: StylesConfig<Option, T> = {
|
||||
option: (base) => ({
|
||||
...base,
|
||||
color: "#000",
|
||||
}),
|
||||
container: (base, props) => ({
|
||||
container: (base, state) => ({
|
||||
...base,
|
||||
zIndex: props.selectProps.isFocused ? 10 : base.zIndex,
|
||||
zIndex: state.isFocused ? 10 : base.zIndex,
|
||||
}),
|
||||
multiValueRemove: (base, props) => ({
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: props.selectProps.isFocused ? base.color : "#333333",
|
||||
color: state.isFocused ? base.color : "#333333",
|
||||
}),
|
||||
};
|
||||
|
||||
@ -279,11 +291,11 @@ const FilterSelectComponent = <T extends boolean>(
|
||||
const selected = options.filter((option) =>
|
||||
selectedIds.includes(option.value)
|
||||
);
|
||||
const selectedOptions = (isMulti
|
||||
? selected
|
||||
: selected[0] ?? null) as ValueType<Option, T>;
|
||||
const selectedOptions = (
|
||||
isMulti ? selected : selected[0] ?? null
|
||||
) as OnChangeValue<Option, T>;
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option, boolean>) => {
|
||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
const selectedValues = getSelectedValues(selectedItems);
|
||||
onSelect?.(items.filter((item) => selectedValues.includes(item.id)));
|
||||
};
|
||||
@ -342,7 +354,7 @@ export const GallerySelect: React.FC<ITitledSelect> = (props) => {
|
||||
setQuery(input);
|
||||
}, 500);
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option, boolean>) => {
|
||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
const selected = getSelectedItems(selectedItems);
|
||||
props.onSelect(
|
||||
selected.map((s) => ({
|
||||
@ -369,6 +381,7 @@ export const GallerySelect: React.FC<ITitledSelect> = (props) => {
|
||||
placeholder="Search for gallery..."
|
||||
noOptionsMessage={query === "" ? null : "No galleries found."}
|
||||
showDropdown={false}
|
||||
isDisabled={props.disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -394,7 +407,7 @@ export const SceneSelect: React.FC<ITitledSelect> = (props) => {
|
||||
setQuery(input);
|
||||
}, 500);
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option, boolean>) => {
|
||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
const selected = getSelectedItems(selectedItems);
|
||||
props.onSelect(
|
||||
(selected ?? []).map((s) => ({
|
||||
@ -446,7 +459,7 @@ export const ImageSelect: React.FC<ITitledSelect> = (props) => {
|
||||
setQuery(input);
|
||||
}, 500);
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option, boolean>) => {
|
||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
||||
const selected = getSelectedItems(selectedItems);
|
||||
props.onSelect(
|
||||
(selected ?? []).map((s) => ({
|
||||
@ -485,7 +498,7 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
|
||||
const { data, loading } = useMarkerStrings();
|
||||
const suggestions = data?.markerStrings ?? [];
|
||||
|
||||
const onChange = (selectedItem: ValueType<Option, false>) =>
|
||||
const onChange = (selectedItem: OnChangeValue<Option, false>) =>
|
||||
props.onChange(selectedItem?.value ?? "");
|
||||
|
||||
const items = suggestions.map((item) => ({
|
||||
@ -535,9 +548,10 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
const defaultCreatable =
|
||||
!configuration?.interface.disableDropdownCreate.performer ?? true;
|
||||
|
||||
const performers = useMemo(() => data?.allPerformers ?? [], [
|
||||
data?.allPerformers,
|
||||
]);
|
||||
const performers = useMemo(
|
||||
() => data?.allPerformers ?? [],
|
||||
[data?.allPerformers]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// build the tag aliases map
|
||||
@ -609,15 +623,15 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, boolean>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
value: Options<Option>,
|
||||
options: OptionsOrGroups<Option, GroupBase<Option>>
|
||||
) => {
|
||||
if (!inputValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(options as OptionsType<Option>).some((o: Option) => {
|
||||
(options as Options<Option>).some((o: Option) => {
|
||||
return o.label.toLowerCase() === inputValue.toLowerCase();
|
||||
})
|
||||
) {
|
||||
@ -752,15 +766,15 @@ export const StudioSelect: React.FC<
|
||||
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, boolean>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
value: OnChangeValue<Option, boolean>,
|
||||
options: OptionsOrGroups<Option, GroupBase<Option>>
|
||||
) => {
|
||||
if (!inputValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(options as OptionsType<Option>).some((o: Option) => {
|
||||
(options as Options<Option>).some((o: Option) => {
|
||||
return o.label.toLowerCase() === inputValue.toLowerCase();
|
||||
})
|
||||
) {
|
||||
@ -873,7 +887,9 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||
};
|
||||
}
|
||||
|
||||
const id = optionProps.data.__isNew__ ? "" : optionProps.data.value;
|
||||
const id = (optionProps.data as Option & { __isNew__: boolean }).__isNew__
|
||||
? ""
|
||||
: optionProps.data.value;
|
||||
|
||||
return (
|
||||
<TagPopover id={id}>
|
||||
@ -917,15 +933,15 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||
|
||||
const isValidNewOption = (
|
||||
inputValue: string,
|
||||
value: ValueType<Option, boolean>,
|
||||
options: OptionsType<Option> | GroupedOptionsType<Option>
|
||||
value: OnChangeValue<Option, boolean>,
|
||||
options: OptionsOrGroups<Option, GroupBase<Option>>
|
||||
) => {
|
||||
if (!inputValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
(options as OptionsType<Option>).some((o: Option) => {
|
||||
(options as Options<Option>).some((o: Option) => {
|
||||
return o.label.toLowerCase() === inputValue.toLowerCase();
|
||||
})
|
||||
) {
|
||||
@ -957,16 +973,17 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||
);
|
||||
};
|
||||
|
||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
|
||||
props.type === "performers" ? (
|
||||
<PerformerSelect {...props} creatable={false} />
|
||||
) : props.type === "studios" || props.type === "parent_studios" ? (
|
||||
<StudioSelect {...props} creatable={false} />
|
||||
) : props.type === "movies" ? (
|
||||
<MovieSelect {...props} creatable={false} />
|
||||
) : (
|
||||
<TagSelect {...props} creatable={false} />
|
||||
);
|
||||
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
|
||||
if (props.type === "performers") {
|
||||
return <PerformerSelect {...props} creatable={false} />;
|
||||
} else if (props.type === "studios" || props.type === "parent_studios") {
|
||||
return <StudioSelect {...props} creatable={false} />;
|
||||
} else if (props.type === "movies") {
|
||||
return <MovieSelect {...props} creatable={false} />;
|
||||
} else {
|
||||
return <TagSelect {...props} creatable={false} />;
|
||||
}
|
||||
};
|
||||
|
||||
interface IStringListSelect {
|
||||
options?: string[];
|
||||
@ -988,18 +1005,18 @@ export const StringListSelect: React.FC<IStringListSelect> = ({
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
const styles: Partial<Styles<Option, true>> = {
|
||||
const styles: StylesConfig<Option, true> = {
|
||||
option: (base) => ({
|
||||
...base,
|
||||
color: "#000",
|
||||
}),
|
||||
container: (base, props) => ({
|
||||
container: (base, state) => ({
|
||||
...base,
|
||||
zIndex: props.selectProps.isFocused ? 10 : base.zIndex,
|
||||
zIndex: state.isFocused ? 10 : base.zIndex,
|
||||
}),
|
||||
multiValueRemove: (base, props) => ({
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: props.selectProps.isFocused ? base.color : "#333333",
|
||||
color: state.isFocused ? base.color : "#333333",
|
||||
}),
|
||||
};
|
||||
|
||||
@ -1038,18 +1055,18 @@ export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
|
||||
return value.map(toOptionType);
|
||||
}, [value, toOptionType]);
|
||||
|
||||
const styles: Partial<Styles<{ label: string; value: string }, true>> = {
|
||||
const styles: StylesConfig<Option, true> = {
|
||||
option: (base) => ({
|
||||
...base,
|
||||
color: "#000",
|
||||
}),
|
||||
container: (base, p) => ({
|
||||
container: (base, state) => ({
|
||||
...base,
|
||||
zIndex: p.selectProps.isFocused ? 10 : base.zIndex,
|
||||
zIndex: state.isFocused ? 10 : base.zIndex,
|
||||
}),
|
||||
multiValueRemove: (base, p) => ({
|
||||
multiValueRemove: (base, state) => ({
|
||||
...base,
|
||||
color: p.selectProps.isFocused ? base.color : "#333333",
|
||||
color: state.isFocused ? base.color : "#333333",
|
||||
}),
|
||||
};
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
export const SweatDrops = () => (
|
||||
export const SweatDrops: React.FC = () => (
|
||||
<span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@ -65,7 +65,9 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("e");
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
@ -11,15 +11,13 @@ import { StudioEditPanel } from "./StudioEditPanel";
|
||||
|
||||
const StudioCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const Toast = useToast();
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const studio = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
@ -74,7 +72,7 @@ const StudioCreate: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<StudioEditPanel
|
||||
studio={{ name: nameQuery ?? "" }}
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onImageChange={setImage}
|
||||
onCancel={() => history.push("/studios")}
|
||||
|
||||
@ -35,10 +35,9 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const isNew = studio.id === undefined;
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
const isNew = !studio || !studio.id;
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
const schema = yup.object({
|
||||
@ -130,10 +129,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||
return () => onImageChange?.();
|
||||
}, [formik.values.image, onImageChange]);
|
||||
|
||||
useEffect(() => onImageEncoding?.(imageEncoding), [
|
||||
onImageEncoding,
|
||||
imageEncoding,
|
||||
]);
|
||||
useEffect(
|
||||
() => onImageEncoding?.(imageEncoding),
|
||||
[onImageEncoding, imageEncoding]
|
||||
);
|
||||
|
||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||
ImageUtils.onImageChange(event, onImageLoad);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindStudios } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { StudioCard } from "./StudioCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const StudioRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const StudioRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindStudios(props.filter);
|
||||
const cardCount = result.data?.findStudios.count;
|
||||
|
||||
|
||||
@ -201,9 +201,8 @@ export const TaggerContext: React.FC = ({ children }) => {
|
||||
});
|
||||
}
|
||||
|
||||
const [
|
||||
submitFingerprintsMutation,
|
||||
] = GQL.useSubmitStashBoxFingerprintsMutation();
|
||||
const [submitFingerprintsMutation] =
|
||||
GQL.useSubmitStashBoxFingerprintsMutation();
|
||||
|
||||
async function submitFingerprints() {
|
||||
const endpoint = currentSource?.stashboxEndpoint;
|
||||
|
||||
@ -30,13 +30,11 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
||||
onLink,
|
||||
endpoint,
|
||||
}) => {
|
||||
const {
|
||||
data: performerData,
|
||||
loading: stashLoading,
|
||||
} = GQL.useFindPerformerQuery({
|
||||
variables: { id: performer.stored_id ?? "" },
|
||||
skip: !performer.stored_id,
|
||||
});
|
||||
const { data: performerData, loading: stashLoading } =
|
||||
GQL.useFindPerformerQuery({
|
||||
variables: { id: performer.stored_id ?? "" },
|
||||
skip: !performer.stored_id,
|
||||
});
|
||||
|
||||
const matchedPerformer = performerData?.findPerformer;
|
||||
const matchedStashID = matchedPerformer?.stash_ids.some(
|
||||
|
||||
@ -181,14 +181,10 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const [
|
||||
nbPhashMatchSceneA,
|
||||
ratioPhashMatchSceneA,
|
||||
] = calculatePhashComparisonScore(stashScene, sceneA);
|
||||
const [
|
||||
nbPhashMatchSceneB,
|
||||
ratioPhashMatchSceneB,
|
||||
] = calculatePhashComparisonScore(stashScene, sceneB);
|
||||
const [nbPhashMatchSceneA, ratioPhashMatchSceneA] =
|
||||
calculatePhashComparisonScore(stashScene, sceneA);
|
||||
const [nbPhashMatchSceneB, ratioPhashMatchSceneB] =
|
||||
calculatePhashComparisonScore(stashScene, sceneB);
|
||||
|
||||
if (nbPhashMatchSceneA != nbPhashMatchSceneB) {
|
||||
return nbPhashMatchSceneB - nbPhashMatchSceneA;
|
||||
|
||||
@ -21,12 +21,11 @@ export interface ISceneTaggerModalsContextState {
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const SceneTaggerModalsState = React.createContext<ISceneTaggerModalsContextState>(
|
||||
{
|
||||
export const SceneTaggerModalsState =
|
||||
React.createContext<ISceneTaggerModalsContextState>({
|
||||
createPerformerModal: () => {},
|
||||
createStudioModal: () => {},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
export const SceneTaggerModals: React.FC = ({ children }) => {
|
||||
const { currentSource } = useContext(TaggerStateContext);
|
||||
|
||||
@ -87,7 +87,9 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => setIsEditing(true));
|
||||
Mousetrap.bind("d d", () => onDelete());
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@ -11,15 +11,13 @@ import { TagEditPanel } from "./TagEditPanel";
|
||||
|
||||
const TagCreate: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const Toast = useToast();
|
||||
|
||||
function useQuery() {
|
||||
const { search } = useLocation();
|
||||
return React.useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
||||
const query = useQuery();
|
||||
const nameQuery = query.get("name");
|
||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||
const tag = {
|
||||
name: query.get("q") ?? undefined,
|
||||
};
|
||||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
@ -86,7 +84,7 @@ const TagCreate: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<TagEditPanel
|
||||
tag={{ name: nameQuery ?? "" }}
|
||||
tag={tag}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => history.push("/tags")}
|
||||
onDelete={() => {}}
|
||||
|
||||
@ -6,12 +6,12 @@ import { DetailsEditNavbar, TagSelect } from "src/components/Shared";
|
||||
import { Form, Col, Row } from "react-bootstrap";
|
||||
import { FormUtils, ImageUtils } from "src/utils";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt, useParams } from "react-router-dom";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||
|
||||
interface ITagEditPanel {
|
||||
tag?: Partial<GQL.TagDataFragment>;
|
||||
tag: Partial<GQL.TagDataFragment>;
|
||||
// returns id
|
||||
onSubmit: (tag: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>) => void;
|
||||
onCancel: () => void;
|
||||
@ -19,10 +19,6 @@ interface ITagEditPanel {
|
||||
setImage: (image?: string | null) => void;
|
||||
}
|
||||
|
||||
interface ITagEditPanelParams {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
tag,
|
||||
onSubmit,
|
||||
@ -32,10 +28,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const params = useParams<ITagEditPanelParams>();
|
||||
const idParam = params.id;
|
||||
|
||||
const isNew = idParam === undefined;
|
||||
const isNew = tag.id === undefined;
|
||||
|
||||
const labelXS = 3;
|
||||
const labelXL = 3;
|
||||
|
||||
@ -33,10 +33,8 @@ interface ITagList {
|
||||
|
||||
export const TagList: React.FC<ITagList> = ({ filterHook }) => {
|
||||
const Toast = useToast();
|
||||
const [
|
||||
deletingTag,
|
||||
setDeletingTag,
|
||||
] = useState<Partial<GQL.TagDataFragment> | null>(null);
|
||||
const [deletingTag, setDeletingTag] =
|
||||
useState<Partial<GQL.TagDataFragment> | null>(null);
|
||||
|
||||
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import React from "react";
|
||||
import { useFindTags } from "src/core/StashService";
|
||||
import Slider from "react-slick";
|
||||
import Slider from "@ant-design/react-slick";
|
||||
import { TagCard } from "./TagCard";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getSlickSliderSettings } from "src/core/recommendations";
|
||||
@ -13,9 +13,7 @@ interface IProps {
|
||||
header: string;
|
||||
}
|
||||
|
||||
export const TagRecommendationRow: FunctionComponent<IProps> = (
|
||||
props: IProps
|
||||
) => {
|
||||
export const TagRecommendationRow: React.FC<IProps> = (props) => {
|
||||
const result = useFindTags(props.filter);
|
||||
const cardCount = result.data?.findTags.count;
|
||||
|
||||
|
||||
@ -53,16 +53,16 @@ export const WallPanel: React.FC<IWallPanelProps> = (
|
||||
/>
|
||||
));
|
||||
|
||||
const sceneMarkers = (
|
||||
props.sceneMarkers ?? []
|
||||
).map((marker, index, markerArray) => (
|
||||
<WallItem
|
||||
key={marker.id}
|
||||
sceneMarker={marker}
|
||||
clickHandler={props.clickHandler}
|
||||
className={calculateClass(index, markerArray.length)}
|
||||
/>
|
||||
));
|
||||
const sceneMarkers = (props.sceneMarkers ?? []).map(
|
||||
(marker, index, markerArray) => (
|
||||
<WallItem
|
||||
key={marker.id}
|
||||
sceneMarker={marker}
|
||||
clickHandler={props.clickHandler}
|
||||
className={calculateClass(index, markerArray.length)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
const images = (props.images ?? []).map((image, index, imageArray) => (
|
||||
<WallItem
|
||||
|
||||
@ -436,23 +436,13 @@ const updateSceneO = (
|
||||
cache: ApolloCache<SceneOMutation>,
|
||||
updatedOCount?: number
|
||||
) => {
|
||||
const scene = cache.readQuery<
|
||||
GQL.FindSceneQuery,
|
||||
GQL.FindSceneQueryVariables
|
||||
>({
|
||||
query: GQL.FindSceneDocument,
|
||||
variables: { id },
|
||||
});
|
||||
if (updatedOCount === undefined || !scene?.findScene) return;
|
||||
if (updatedOCount === undefined) return;
|
||||
|
||||
cache.writeQuery<GQL.FindSceneQuery, GQL.FindSceneQueryVariables>({
|
||||
query: GQL.FindSceneDocument,
|
||||
variables: { id },
|
||||
data: {
|
||||
...scene,
|
||||
findScene: {
|
||||
...scene.findScene,
|
||||
o_counter: updatedOCount,
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: "Scene", id }),
|
||||
fields: {
|
||||
o_counter() {
|
||||
return updatedOCount;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -6,7 +6,8 @@ import {
|
||||
ServerError,
|
||||
TypePolicies,
|
||||
} from "@apollo/client";
|
||||
import { WebSocketLink } from "@apollo/client/link/ws";
|
||||
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
|
||||
import { createClient as createWSClient } from "graphql-ws";
|
||||
import { onError } from "@apollo/client/link/error";
|
||||
import { getMainDefinition } from "@apollo/client/utilities";
|
||||
import { createUploadLink } from "apollo-upload-client";
|
||||
@ -105,7 +106,11 @@ export const getPlatformURL = (ws?: boolean) => {
|
||||
}
|
||||
|
||||
if (ws) {
|
||||
platformUrl.protocol = "ws:";
|
||||
if (platformUrl.protocol === "https:") {
|
||||
platformUrl.protocol = "wss:";
|
||||
} else {
|
||||
platformUrl.protocol = "ws:";
|
||||
}
|
||||
}
|
||||
|
||||
return platformUrl;
|
||||
@ -115,23 +120,20 @@ export const createClient = () => {
|
||||
const platformUrl = getPlatformURL();
|
||||
const wsPlatformUrl = getPlatformURL(true);
|
||||
|
||||
if (platformUrl.protocol === "https:") {
|
||||
wsPlatformUrl.protocol = "wss:";
|
||||
}
|
||||
|
||||
const url = `${platformUrl.toString()}graphql`;
|
||||
const wsUrl = `${wsPlatformUrl.toString()}graphql`;
|
||||
|
||||
const httpLink = createUploadLink({
|
||||
uri: url,
|
||||
});
|
||||
const httpLink = createUploadLink({ uri: url });
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri: wsUrl,
|
||||
options: {
|
||||
reconnect: true,
|
||||
},
|
||||
});
|
||||
const wsLink = new GraphQLWsLink(
|
||||
createWSClient({
|
||||
url: wsUrl,
|
||||
retryAttempts: Infinity,
|
||||
shouldRetry() {
|
||||
return true;
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const errorLink = onError(({ networkError }) => {
|
||||
// handle unauthorized error by redirecting to the login page
|
||||
@ -155,7 +157,6 @@ export const createClient = () => {
|
||||
);
|
||||
},
|
||||
wsLink,
|
||||
// @ts-ignore
|
||||
httpLink
|
||||
);
|
||||
|
||||
|
||||
@ -1,9 +1,7 @@
|
||||
import migration32 from "./32.md";
|
||||
import migration39 from "./39.md";
|
||||
|
||||
type Module = typeof migration32;
|
||||
|
||||
export const migrationNotes: Record<number, Module> = {
|
||||
export const migrationNotes: Record<number, string> = {
|
||||
32: migration32,
|
||||
39: migration39,
|
||||
};
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import v0170 from "./v0170.md";
|
||||
|
||||
export type Module = typeof v0170;
|
||||
|
||||
interface IReleaseNotes {
|
||||
// handle should be in the form of YYYYMMDD
|
||||
date: number;
|
||||
content: Module;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const releaseNotes: IReleaseNotes[] = [
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user