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:
DingDongSoLong4 2023-02-16 05:06:44 +02:00 committed by GitHub
parent 0c9eeef143
commit a1851b3713
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
137 changed files with 5102 additions and 5729 deletions

1
.gitignore vendored
View File

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

View File

@ -1,3 +1,2 @@
BROWSER=none BROWSER=none
PORT=3000
ESLINT_NO_DEV_ERRORS=true ESLINT_NO_DEV_ERRORS=true

View File

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

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

View File

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

View File

@ -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}/*"
}
}
]
}

View File

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

View File

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

View File

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

View File

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

View 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;
}

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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={() => {}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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",
}), }),
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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={() => {}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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