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