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
internal/api/generated_*.go
ui/v2.5/src/core/generated-*.tsx
####
# Jetbrains

View File

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

View File

@ -9,61 +9,69 @@
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"jsx-a11y"
],
"plugins": ["@typescript-eslint", "jsx-a11y"],
"extends": [
"airbnb-typescript",
"airbnb/hooks",
"plugin:react/recommended",
"plugin:import/recommended",
"prettier",
"prettier/prettier"
"airbnb-typescript",
"plugin:import/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"airbnb/hooks",
"prettier"
],
"settings": {
"react": {
"version": "detect"
}
},
"ignorePatterns": ["node_modules/", "src/core/generated-graphql.tsx"],
"rules": {
"@typescript-eslint/no-explicit-any": 2,
"@typescript-eslint/naming-convention": [
"error",
{
"@typescript-eslint/no-explicit-any": 2,
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "interface",
"format": ["PascalCase"],
"custom": {
"regex": "^I[A-Z]",
"match": true
}
}
],
"lines-between-class-members": "off",
"@typescript-eslint/lines-between-class-members": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"import/named": "off",
"import/namespace": "off",
"import/no-unresolved": "off",
"react/display-name": "off",
"react/prop-types": "off",
"react/style-prop-object": ["error", {
}
],
"lines-between-class-members": "off",
"@typescript-eslint/lines-between-class-members": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
],
"import/named": "off",
"import/namespace": "off",
"import/no-unresolved": "off",
"react/display-name": "off",
"react/prop-types": "off",
"react/style-prop-object": [
"error",
{
"allow": ["FormattedNumber"]
}],
"spaced-comment": ["error", "always", {
"markers": ["/"]
}],
"prefer-destructuring": ["error", {"object": true, "array": false}],
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": true }],
"no-nested-ternary": "off"
}
],
"spaced-comment": [
"error",
"always",
{
"markers": ["/"]
}
],
"prefer-destructuring": ["error", { "object": true, "array": false }],
"@typescript-eslint/no-use-before-define": [
"error",
{ "functions": false, "classes": true }
],
"no-nested-ternary": "off"
}
}

5
ui/v2.5/.gitignore vendored
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
/node_modules
@ -12,6 +13,7 @@
/build
# misc
.gitignore
.DS_Store
.env.local
.env.development.local
@ -23,3 +25,4 @@ yarn-debug.log*
yarn-error.log*
.eslintcache
.stylelintcache

18
ui/v2.5/.prettierignore Normal file
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": [
"stylelint-order"
],
"plugins": ["stylelint-order"],
"extends": "stylelint-config-prettier",
"customSyntax": "postcss-scss",
"rules": {
"indentation": null,
"at-rule-empty-line-before": [ "always", {
except: ["after-same-name", "first-nested" ],
ignore: ["after-comment"],
} ],
"at-rule-empty-line-before": [
"always",
{
"except": ["after-same-name", "first-nested"],
"ignore": ["after-comment"]
}
],
"at-rule-no-vendor-prefix": true,
"selector-no-vendor-prefix": true,
"block-closing-brace-newline-after": "always",
@ -21,10 +23,13 @@
"color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [ "always", {
except: ["first-nested"],
ignore: ["stylelint-commands"],
} ],
"comment-empty-line-before": [
"always",
{
"except": ["first-nested"],
"ignore": ["stylelint-commands"]
}
],
"comment-whitespace-inside": "always",
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
@ -63,15 +68,15 @@
"no-missing-end-of-source-newline": true,
"number-max-precision": 3,
"number-no-trailing-zeros": true,
"order/order": [
"custom-properties",
"declarations"
],
"order/order": ["custom-properties", "declarations"],
"order/properties-alphabetical-order": true,
"rule-empty-line-before": ["always-multi-line", {
except: ["after-single-line-comment", "first-nested" ],
ignore: ["after-comment"],
}],
"rule-empty-line-before": [
"always-multi-line",
{
"except": ["after-single-line-comment", "first-nested"],
"ignore": ["after-comment"]
}
],
"selector-max-id": 1,
"selector-max-type": 2,
"selector-class-pattern": "^(\\.*[A-Z]*[a-z]+)+(-[a-z0-9]+)*$",
@ -87,5 +92,5 @@
"time-min-milliseconds": 100,
"value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never"
},
}
}

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,11 +4,9 @@ documents: "../../graphql/documents/**/*.graphql"
generates:
src/core/generated-graphql.tsx:
plugins:
- add:
content: "/* eslint-disable */"
- time
- typescript
- typescript-operations
- typescript-react-apollo
config:
withRefetchFn: true
withRefetchFn: true

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/">
<base href="/" />
<meta charset="utf-8" />
<link rel="shortcut icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
@ -10,27 +10,15 @@
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta name="theme-color" content="%COLOR%" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" crossorigin="use-credentials" href="manifest.json" />
<title>Stash</title>
<script>window.STASH_BASE_URL = "/%BASE_URL%/"</script>
<script>
window.STASH_BASE_URL = "/%BASE_URL%/";
</script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@ -3,125 +3,117 @@
"version": "0.1.0",
"private": true,
"homepage": "./",
"sideEffects": false,
"scripts": {
"start": "vite",
"build": "vite build",
"build-ci": "yarn validate && yarn build",
"validate": "yarn lint && tsc --noEmit && yarn format-check",
"lint": "yarn lint:css && yarn lint:js",
"lint:js": "eslint --cache src/**/*.{ts,tsx}",
"lint:css": "stylelint \"src/**/*.scss\"",
"format": "prettier --write \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
"format-check": "prettier --check \"src/**/!(generated-graphql).{js,jsx,ts,tsx,scss}\"",
"build-ci": "yarn run validate && yarn run build",
"validate": "yarn run lint && yarn run check && yarn run format-check",
"lint": "yarn run lint:css && yarn run lint:js",
"lint:css": "stylelint --cache \"src/**/*.scss\"",
"lint:js": "eslint --cache src/",
"check": "tsc --noEmit",
"format": "prettier --write .",
"format-check": "prettier --check .",
"gqlgen": "gql-gen --config codegen.yml",
"extract": "NODE_ENV=development extract-messages -l=en,de -o src/locale -d en --flat false 'src/**/!(*.test).tsx'"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
">0.5% and supports es6-module-dynamic-import"
],
"dependencies": {
"@apollo/client": "^3.3.7",
"@formatjs/intl-getcanonicallocales": "^1.5.3",
"@formatjs/intl-locale": "^2.4.14",
"@formatjs/intl-numberformat": "^6.1.3",
"@formatjs/intl-pluralrules": "^4.0.6",
"@fortawesome/fontawesome-svg-core": "^1.2.34",
"@fortawesome/free-regular-svg-icons": "^5.15.2",
"@fortawesome/free-solid-svg-icons": "^5.15.2",
"@fortawesome/react-fontawesome": "^0.1.14",
"@types/react-select": "^4.0.8",
"ansi-regex": "^5.0.1",
"apollo-upload-client": "^14.1.3",
"axios": "^1.1.3",
"@ant-design/react-slick": "^1.0.0",
"@apollo/client": "^3.7.4",
"@formatjs/intl-getcanonicallocales": "^2.0.5",
"@formatjs/intl-locale": "^3.0.11",
"@formatjs/intl-numberformat": "^8.3.3",
"@formatjs/intl-pluralrules": "^5.1.8",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"apollo-upload-client": "^17.0.0",
"axios": "^1.2.5",
"base64-blob": "^1.4.1",
"bootstrap": "^4.6.0",
"classnames": "^2.2.6",
"flag-icon-css": "^3.5.0",
"bootstrap": "^4.6.2",
"classnames": "^2.3.2",
"flag-icons": "^6.6.6",
"flexbin": "^0.2.0",
"formik": "^2.2.6",
"graphql": "^15.4.0",
"graphql-tag": "^2.11.0",
"i18n-iso-countries": "^6.4.0",
"intersection-observer": "^0.12.0",
"localforage": "^1.9.0",
"formik": "^2.2.9",
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.11.2",
"i18n-iso-countries": "^7.5.0",
"intersection-observer": "^0.12.2",
"localforage": "^1.10.0",
"lodash-es": "^4.17.21",
"mousetrap": "^1.6.5",
"mousetrap-pause": "^1.0.0",
"normalize-url": "^4.5.1",
"postcss": "^8.2.10",
"query-string": "6.13.8",
"react": "17.0.2",
"react-bootstrap": "1.4.3",
"react-dom": "17.0.2",
"react": "^17.0.2",
"react-bootstrap": "^1.6.6",
"react-dom": "^17.0.2",
"react-helmet": "^6.1.0",
"react-intl": "^5.10.16",
"react-markdown": "^7.1.0",
"react-intl": "^6.2.6",
"react-markdown": "^8.0.5",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.2.0",
"react-router-hash-link": "^2.3.1",
"react-select": "^4.0.2",
"react-slick": "^0.29.0",
"remark-gfm": "^1.0.0",
"react-router-dom": "^5.3.4",
"react-router-hash-link": "^2.4.3",
"react-select": "^5.6.1",
"remark-gfm": "^3.0.1",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.32.5",
"slick-carousel": "^1.8.1",
"string.prototype.replaceall": "^1.0.4",
"subscriptions-transport-ws": "^0.9.18",
"string.prototype.replaceall": "^1.0.7",
"thehandy": "^1.0.3",
"universal-cookie": "^4.0.4",
"video.js": "^7.20.3",
"video.js": "^7.21.1",
"videojs-mobile-ui": "^0.8.0",
"videojs-seek-buttons": "^3.0.1",
"videojs-vtt.js": "^0.15.4",
"vite": "^2.9.13",
"vite-plugin-compression": "^0.3.5",
"vite-tsconfig-paths": "^3.3.17",
"ws": "^7.4.6",
"yup": "^0.32.9"
"yup": "^0.32.11"
},
"devDependencies": {
"@graphql-codegen/add": "^2.0.2",
"@graphql-codegen/cli": "^1.20.0",
"@graphql-codegen/time": "^2.0.2",
"@graphql-codegen/typescript": "^1.20.00",
"@graphql-codegen/typescript-operations": "^1.17.13",
"@graphql-codegen/typescript-react-apollo": "^2.2.1",
"@types/apollo-upload-client": "^14.1.0",
"@types/classnames": "^2.2.11",
"@types/fslightbox-react": "^1.4.0",
"@babel/core": "^7.20.5",
"@graphql-codegen/cli": "^2.16.4",
"@graphql-codegen/time": "^3.2.3",
"@graphql-codegen/typescript": "^2.8.7",
"@graphql-codegen/typescript-operations": "^2.5.12",
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
"@types/apollo-upload-client": "^17.0.2",
"@types/lodash-es": "^4.17.6",
"@types/mousetrap": "^1.6.5",
"@types/node": "14.14.22",
"@types/react": "17.0.31",
"@types/react-dom": "^17.0.10",
"@types/react-helmet": "^6.1.3",
"@types/mousetrap": "^1.6.11",
"@types/node": "^14.18.36",
"@types/react": "^17.0.53",
"@types/react-dom": "^17.0.18",
"@types/react-helmet": "^6.1.6",
"@types/react-router-bootstrap": "^0.24.5",
"@types/react-router-dom": "5.1.7",
"@types/react-router-hash-link": "^1.2.1",
"@types/react-slick": "^0.23.8",
"@types/video.js": "^7.3.49",
"@types/react-router-hash-link": "^2.4.5",
"@types/video.js": "^7.3.50",
"@types/videojs-mobile-ui": "^0.5.0",
"@types/videojs-seek-buttons": "^2.1.0",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-airbnb-typescript": "^14.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"@typescript-eslint/eslint-plugin": "^5.49.0",
"@typescript-eslint/parser": "^5.49.0",
"@vitejs/plugin-react": "^3.0.1",
"eslint": "^8.32.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.0.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-react": "^7.32.1",
"eslint-plugin-react-hooks": "^4.6.0",
"extract-react-intl-messages": "^4.1.1",
"postcss-safe-parser": "^5.0.2",
"prettier": "2.2.1",
"stylelint": "^13.9.0",
"stylelint-config-prettier": "^8.0.2",
"stylelint-order": "^4.1.0",
"typescript": "~4.4.4"
"postcss": "^8.4.21",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.3",
"sass": "^1.57.1",
"stylelint": "^14.16.1",
"stylelint-config-prettier": "^9.0.4",
"stylelint-order": "^6.0.1",
"ts-node": "^10.9.1",
"typescript": "~4.8.4",
"vite": "^4.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-tsconfig-paths": "^4.0.5"
}
}

24
ui/v2.5/src/@types/mousetrap-pause.d.ts vendored Normal file
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,111 +1,111 @@
/* eslint-disable @typescript-eslint/naming-convention */
declare module "videojs-vtt.js" {
namespace vttjs {
/**
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
*
* There are two error codes that can be reported back currently:
* * 0 BadSignature
* * 1 BadTimeStamp
*
* Note: Exceptions other then ParsingError will be thrown and not reported.
*/
class ParsingError extends Error {
readonly name: string;
readonly code: number;
readonly message: string;
}
namespace WebVTT {
/**
* A parser for the WebVTT spec in JavaScript.
*/
class Parser {
/**
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
*
* @param window the window object to use
* @param vttjs the vtt.js module
* @param decoder the decoder to decode `parse()` data with
*/
constructor(window: Window);
constructor(window: Window, decoder: TextDecoder);
constructor(window: Window, vttjs: vttjs, decoder: TextDecoder);
/**
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
*/
onregion?: (cue: VTTRegion) => void;
/**
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
*/
oncue?: (cue: VTTCue) => void;
/**
* Is invoked in response to `flush()` and after the content was parsed completely.
*/
onflush?: () => void;
/**
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
* Is passed a `ParsingError` object.
*/
onparsingerror?: (e: ParsingError) => void;
/**
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
*
* @param data data to be parsed
*/
parse(data: string): this;
/**
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
* Will also trigger `onflush`.
*/
flush(): this;
}
/**
* Helper to allow strings to be decoded instead of the default binary utf8 data.
*/
function StringDecoder(): TextDecoder;
/**
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
*
* @param window window object to use
* @param cuetext cue text to parse
*/
function convertCueToDOMTree(
window: Window,
cuetext: string
): HTMLDivElement | null;
/**
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
*
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
*/
function processCues(
window: Window,
cues: VTTCue[],
overlay: Element
): void;
}
/**
* A custom JS error object that is reported through the parser's `onparsingerror` callback.
* It has a name, code, and message property, along with all the regular properties that come with a JavaScript error object.
*
* There are two error codes that can be reported back currently:
* * 0 BadSignature
* * 1 BadTimeStamp
*
* Note: Exceptions other then ParsingError will be thrown and not reported.
*/
class ParsingError extends Error {
readonly name: string;
readonly code: number;
readonly message: string;
}
export = vttjs;
export namespace WebVTT {
/**
* A parser for the WebVTT spec in JavaScript.
*/
class Parser {
/**
* The Parser constructor is passed a window object with which it will create new `VTTCues` and `VTTRegions`
* as well as an optional `StringDecoder` object which it will use to decode the data that the `parse()` function receives.
* For ease of use, a `StringDecoder` is provided via `WebVTT.StringDecoder()`.
* If a custom `StringDecoder` object is passed in it must support the API specified by the #whatwg string encoding spec.
*
* @param window the window object to use
* @param vttjs the vtt.js module
* @param decoder the decoder to decode `parse()` data with
*/
constructor(window: Window);
constructor(window: Window, decoder: TextDecoder);
constructor(
window: Window,
vttjs: typeof import("videojs-vtt.js"),
decoder: TextDecoder
);
/**
* Callback that is invoked for every region that is correctly parsed. Is passed a `VTTRegion` object.
*/
onregion?: (cue: VTTRegion) => void;
/**
* Callback that is invoked for every cue that is fully parsed. In case of streaming parsing,
* `oncue` is delayed until the cue has been completely received. Is passed a `VTTCue` object.
*/
oncue?: (cue: VTTCue) => void;
/**
* Is invoked in response to `flush()` and after the content was parsed completely.
*/
onflush?: () => void;
/**
* Is invoked when a parsing error has occurred. This means that some part of the WebVTT file markup is badly formed.
* Is passed a `ParsingError` object.
*/
onparsingerror?: (e: ParsingError) => void;
/**
* Hands data in some format to the parser for parsing. The passed data format is expected to be decodable by the
* StringDecoder object that it has. The parser decodes the data and reassembles partial data (streaming), even across line breaks.
*
* @param data data to be parsed
*/
parse(data: string): this;
/**
* Indicates that no more data is expected and will force the parser to parse any unparsed data that it may have.
* Will also trigger `onflush`.
*/
flush(): this;
}
/**
* Helper to allow strings to be decoded instead of the default binary utf8 data.
*/
function StringDecoder(): TextDecoder;
/**
* Parses the cue text handed to it into a tree of DOM nodes that mirrors the internal WebVTT node structure of the cue text.
* It uses the window object handed to it to construct new HTMLElements and returns a tree of DOM nodes attached to a top level div.
*
* @param window window object to use
* @param cuetext cue text to parse
*/
function convertCueToDOMTree(
window: Window,
cuetext: string
): HTMLDivElement | null;
/**
* Converts the cuetext of the cues passed to it to DOM trees - by calling convertCueToDOMTree - and then runs the
* processing model steps of the WebVTT specification on the divs. The processing model applies the necessary CSS styles
* to the cue divs to prepare them for display on the web page. During this process the cue divs get added to a block level element (overlay).
* The overlay should be a part of the live DOM as the algorithm will use the computed styles (only of the divs) to do overlap avoidance.
*
* @param overlay A block level element (usually a div) that the computed cues and regions will be placed into.
*/
function processCues(
window: Window,
cues: VTTCue[],
overlay: Element
): void;
}
}

View File

@ -91,10 +91,12 @@ export const App: React.FC = () => {
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
const chosenMessages = (await locales[messageLanguage]()).default;
const res = await fetch(getPlatformURL() + "customlocales");
let customMessages = {};
try {
customMessages = res.ok ? await res.json() : {};
const res = await fetch(getPlatformURL() + "customlocales");
if (res.ok) {
customMessages = await res.json();
}
} catch (err) {
console.log(err);
}

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 { MarkdownPage } from "../Shared/MarkdownPage";
// to avoid use of explicit any
type Module = typeof V010;
const Changelog: React.FC = () => {
const [{ data, loading }, setOpenState] = useChangelogStorage();
@ -55,7 +52,7 @@ const Changelog: React.FC = () => {
interface IStashRelease {
version: string;
date?: string;
page: Module;
page: string;
defaultOpen?: boolean;
}

View File

@ -261,9 +261,8 @@ export const FieldOptionsList: React.FC<IFieldOptionsList> = ({
allowSetDefault = true,
defaultOptions,
}) => {
const [localFieldOptions, setLocalFieldOptions] = useState<
GQL.IdentifyFieldOptions[]
>();
const [localFieldOptions, setLocalFieldOptions] =
useState<GQL.IdentifyFieldOptions[]>();
const [editField, setEditField] = useState<string | undefined>();
useEffect(() => {

View File

@ -202,9 +202,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
if (s.options) {
const sourceOptions = withoutTypename(s.options);
sourceOptions.fieldOptions = sourceOptions.fieldOptions?.map(
withoutTypename
);
sourceOptions.fieldOptions =
sourceOptions.fieldOptions?.map(withoutTypename);
ret.options = sourceOptions;
}
@ -215,9 +214,8 @@ export const IdentifyDialog: React.FC<IIdentifyDialogProps> = ({
setSources(mappedSources);
if (identifyDefaults.options) {
const defaultOptions = withoutTypename(identifyDefaults.options);
defaultOptions.fieldOptions = defaultOptions.fieldOptions?.map(
withoutTypename
);
defaultOptions.fieldOptions =
defaultOptions.fieldOptions?.map(withoutTypename);
setOptions(defaultOptions);
}
} else {

View File

@ -20,7 +20,7 @@ export const sceneFields = [
"tags",
"stash_ids",
] as const;
export type SceneField = typeof sceneFields[number];
export type SceneField = (typeof sceneFields)[number];
export const multiValueSceneFields: SceneField[] = [
"studio",

View File

@ -4,10 +4,9 @@ import { Modal } from "src/components/Shared";
import { faCogs } from "@fortawesome/free-solid-svg-icons";
import { useIntl } from "react-intl";
import { MarkdownPage } from "../Shared/MarkdownPage";
import { Module } from "src/docs/en/ReleaseNotes";
interface IReleaseNotesDialog {
notes: Module[];
notes: string[];
onClose: () => void;
}

View File

@ -31,10 +31,8 @@ export const EditGalleriesDialog: React.FC<IListOperationProps> = (
const Toast = useToast();
const [rating100, setRating] = useState<number>();
const [studioId, setStudioId] = useState<string>();
const [
performerMode,
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerMode, setPerformerMode] =
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(

View File

@ -8,7 +8,7 @@ import Gallery from "./GalleryDetails/Gallery";
import GalleryCreate from "./GalleryDetails/GalleryCreate";
import { GalleryList } from "./GalleryList";
const Galleries = () => {
const Galleries: React.FC = () => {
const intl = useIntl();
const title_template = `${intl.formatMessage({

View File

@ -214,7 +214,6 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
<Tab.Pane eventKey="gallery-edit-panel">
<GalleryEditPanel
isVisible={activeTabKey === "gallery-edit-panel"}
isNew={false}
gallery={gallery}
onDelete={() => setIsDeleteAlertOpen(true)}
/>

View File

@ -1,18 +1,15 @@
import React from "react";
import React, { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router-dom";
import { GalleryEditPanel } from "./GalleryEditPanel";
const GalleryCreate: React.FC = () => {
const intl = useIntl();
function useQuery() {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
}
const query = useQuery();
const nameQuery = query.get("name");
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const gallery = {
title: query.get("q") ?? undefined,
};
return (
<div className="row new-view">
@ -23,12 +20,7 @@ const GalleryCreate: React.FC = () => {
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
/>
</h2>
<GalleryEditPanel
isNew
gallery={{ title: nameQuery ?? "" }}
isVisible
onDelete={() => {}}
/>
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
</div>
</div>
);

View File

@ -40,23 +40,16 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
import { ConfigurationContext } from "src/hooks/Config";
interface IProps {
gallery: Partial<GQL.GalleryDataFragment>;
isVisible: boolean;
onDelete: () => void;
}
interface INewProps {
isNew: true;
gallery?: Partial<GQL.GalleryDataFragment>;
}
interface IExistingProps {
isNew: false;
gallery: GQL.GalleryDataFragment;
}
export const GalleryEditPanel: React.FC<
IProps & (INewProps | IExistingProps)
> = ({ gallery, isNew, isVisible, onDelete }) => {
export const GalleryEditPanel: React.FC<IProps> = ({
gallery,
isVisible,
onDelete,
}) => {
const intl = useIntl();
const Toast = useToast();
const history = useHistory();
@ -67,15 +60,14 @@ export const GalleryEditPanel: React.FC<
}))
);
const isNew = gallery.id === undefined;
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const Scrapers = useListGalleryScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
const [
scrapedGallery,
setScrapedGallery,
] = useState<GQL.ScrapedGallery | null>();
const [scrapedGallery, setScrapedGallery] =
useState<GQL.ScrapedGallery | null>();
// Network state
const [isLoading, setIsLoading] = useState(false);

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { useFindGalleries } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { GalleryCard } from "./GalleryCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
@ -13,9 +13,7 @@ interface IProps {
header: string;
}
export const GalleryRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const GalleryRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindGalleries(props.filter);
const cardCount = result.data?.findGalleries.count;

View File

@ -1,3 +1,5 @@
@use "sass:math";
.gallery-image {
&:hover {
cursor: pointer;
@ -138,14 +140,14 @@ $galleryTabWidth: 450px;
}
@mixin galleryWidth($width) {
height: ($width / 3) * 2;
height: math.div($width, 3) * 2;
&-landscape {
width: $width;
}
&-portrait {
width: $width / 2;
width: math.div($width, 2);
}
}

View File

@ -173,11 +173,9 @@ export const Manual: React.FC<IManualProps> = ({
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) {
if (event.target instanceof HTMLAnchorElement) {
const href = (event.target as HTMLAnchorElement).getAttribute("href");
const href = event.target.getAttribute("href");
if (href && href.startsWith("/help")) {
const newKey = (event.target as HTMLAnchorElement).pathname.substring(
"/help/".length
);
const newKey = event.target.pathname.substring("/help/".length);
setActiveTab(newKey);
event.preventDefault();
}

View File

@ -31,10 +31,8 @@ export const EditImagesDialog: React.FC<IListOperationProps> = (
const Toast = useToast();
const [rating100, setRating] = useState<number>();
const [studioId, setStudioId] = useState<string>();
const [
performerMode,
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerMode, setPerformerMode] =
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(

View File

@ -239,7 +239,9 @@ export const Image: React.FC = () => {
Mousetrap.bind("a", () => setActiveTabKey("image-details-panel"));
Mousetrap.bind("e", () => setActiveTabKey("image-edit-panel"));
Mousetrap.bind("f", () => setActiveTabKey("image-file-info-panel"));
Mousetrap.bind("o", () => onIncrementClick());
Mousetrap.bind("o", () => {
onIncrementClick();
});
return () => {
Mousetrap.unbind("a");

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { useFindImages } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
@ -13,9 +13,7 @@ interface IProps {
header: string;
}
export const ImageRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const ImageRecommendationRow: React.FC<IProps> = (props: IProps) => {
const result = useFindImages(props.filter);
const cardCount = result.data?.findImages.count;

View File

@ -10,10 +10,9 @@ interface IHierarchicalLabelValueFilterProps {
onValueChanged: (value: IHierarchicalLabelValue) => void;
}
export const HierarchicalLabelValueFilter: React.FC<IHierarchicalLabelValueFilterProps> = ({
criterion,
onValueChanged,
}) => {
export const HierarchicalLabelValueFilter: React.FC<
IHierarchicalLabelValueFilterProps
> = ({ criterion, onValueChanged }) => {
const intl = useIntl();
if (

View File

@ -18,7 +18,7 @@ import { ManualStateContext } from "./Help/context";
import { SettingsButton } from "./SettingsButton";
import {
faBars,
faChartBar,
faChartColumn,
faFilm,
faHeart,
faImage,
@ -220,10 +220,10 @@ export const MainNavbar: React.FC = () => {
const pathname = location.pathname.replace(/\/$/, "");
let newPath = newPathsList.includes(pathname) ? `${pathname}/new` : null;
if (newPath != null) {
if (newPath !== null) {
let queryParam = new URLSearchParams(location.search).get("q");
if (queryParam != null) {
newPath += "?name=" + encodeURIComponent(queryParam);
if (queryParam) {
newPath += "?q=" + encodeURIComponent(queryParam);
}
}
@ -296,7 +296,7 @@ export const MainNavbar: React.FC = () => {
className="minimal d-flex align-items-center h-100"
title={intl.formatMessage({ id: "statistics" })}
>
<Icon icon={faChartBar} />
<Icon icon={faChartColumn} />
</Button>
</NavLink>
<NavLink

View File

@ -1,4 +1,4 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { Button, ButtonGroup } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import {
@ -20,7 +20,7 @@ interface IProps {
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
export const MovieCard: React.FC<IProps> = (props: IProps) => {
function maybeRenderSceneNumber() {
if (!props.sceneIndex) return;

View File

@ -51,7 +51,9 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("d d", () => onDelete());
Mousetrap.bind("d d", () => {
onDelete();
});
return () => {
Mousetrap.unbind("e");

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 { useMovieCreate } from "src/core/StashService";
import { useHistory } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import { LoadingIndicator } from "src/components/Shared";
import { useToast } from "src/hooks";
import { MovieEditPanel } from "./MovieEditPanel";
const MovieCreate: React.FC = () => {
const history = useHistory();
const location = useLocation();
const Toast = useToast();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const movie = {
name: query.get("q") ?? undefined,
};
// Editing movie state
const [frontImage, setFrontImage] = useState<string | undefined | null>(
undefined
);
const [backImage, setBackImage] = useState<string | undefined | null>(
undefined
);
const [frontImage, setFrontImage] = useState<string | null>();
const [backImage, setBackImage] = useState<string | null>();
const [encodingImage, setEncodingImage] = useState<boolean>(false);
const [createMovie] = useMovieCreate();
@ -84,6 +86,7 @@ const MovieCreate: React.FC = () => {
</div>
<MovieEditPanel
movie={movie}
onSubmit={onSave}
onCancel={() => history.push("/movies")}
onDelete={() => {}}

View File

@ -25,7 +25,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
import { ConfigurationContext } from "src/hooks/Config";
interface IMovieEditPanel {
movie?: Partial<GQL.MovieDataFragment>;
movie: Partial<GQL.MovieDataFragment>;
onSubmit: (
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
) => void;
@ -49,19 +49,15 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
const Toast = useToast();
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const isNew = movie === undefined;
const isNew = movie.id === undefined;
const [isLoading, setIsLoading] = useState(false);
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
undefined
);
const [imageClipboard, setImageClipboard] = useState<string>();
const Scrapers = useListMovieScrapers();
const [scrapedMovie, setScrapedMovie] = useState<
GQL.ScrapedMovie | undefined
>();
const [scrapedMovie, setScrapedMovie] = useState<GQL.ScrapedMovie>();
const schema = yup.object({
name: yup.string().required(),
@ -113,10 +109,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
setBackImage(formik.values.back_image);
}, [formik.values.back_image, setBackImage]);
useEffect(() => onImageEncoding(encodingImage), [
onImageEncoding,
encodingImage,
]);
useEffect(
() => onImageEncoding(encodingImage),
[onImageEncoding, encodingImage]
);
function setRating(v: number) {
formik.setFieldValue("rating100", v);

View File

@ -1,6 +1,6 @@
import React from "react";
import { useFindMovies } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { MovieCard } from "./MovieCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";

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

View File

@ -60,10 +60,8 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
mode: GQL.BulkUpdateIdMode.Add,
});
const [existingTagIds, setExistingTagIds] = useState<string[]>();
const [
aggregateState,
setAggregateState,
] = useState<GQL.BulkPerformerUpdateInput>({});
const [aggregateState, setAggregateState] =
useState<GQL.BulkPerformerUpdateInput>({});
// weight needs conversion to/from number
const [weight, setWeight] = useState<string | undefined>();
const [updateInput, setUpdateInput] = useState<GQL.BulkPerformerUpdateInput>(

View File

@ -32,12 +32,8 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel";
import { PerformerEditPanel } from "./PerformerEditPanel";
import { PerformerSubmitButton } from "./PerformerSubmitButton";
import GenderIcon from "../GenderIcon";
import {
faCamera,
faDove,
faHeart,
faLink,
} from "@fortawesome/free-solid-svg-icons";
import { faHeart, faLink } from "@fortawesome/free-solid-svg-icons";
import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons";
import { IUIConfig } from "src/core/config";
import { useRatingKeybinds } from "src/hooks/keybinds";
@ -247,7 +243,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
<PerformerEditPanel
performer={performer}
isVisible={isEditing}
isNew={false}
onImageChange={onImageChange}
onImageEncoding={onImageEncoding}
onCancelEditing={() => {
@ -351,7 +346,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faDove} />
<Icon icon={faTwitter} />
</a>
</Button>
)}
@ -366,7 +361,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
target="_blank"
rel="noopener noreferrer"
>
<Icon icon={faCamera} />
<Icon icon={faInstagram} />
</a>
</Button>
)}
@ -405,7 +400,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
<h2>
<GenderIcon
gender={performer.gender}
className="gender-icon mr-2 flag-icon"
className="gender-icon mr-2 fi"
/>
<CountryFlag country={performer.country} className="mr-2" />
<span className="performer-name">{performer.name}</span>

View File

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared";
import { PerformerEditPanel } from "./PerformerEditPanel";
@ -8,13 +8,11 @@ const PerformerCreate: React.FC = () => {
const [imagePreview, setImagePreview] = useState<string | null>();
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
function useQuery() {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
}
const query = useQuery();
const nameQuery = query.get("name");
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const performer = {
name: query.get("q") ?? undefined,
};
const activeImage = imagePreview ?? "";
const intl = useIntl();
@ -50,9 +48,8 @@ const PerformerCreate: React.FC = () => {
/>
</h2>
<PerformerEditPanel
performer={{ name: nameQuery ?? "" }}
performer={performer}
isVisible
isNew
onImageChange={onImageChange}
onImageEncoding={onImageEncoding}
/>

View File

@ -50,7 +50,6 @@ const isScraper = (
interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>;
isNew?: boolean;
isVisible: boolean;
onImageChange?: (image?: string | null) => void;
onImageEncoding?: (loading?: boolean) => void;
@ -59,7 +58,6 @@ interface IPerformerDetails {
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
performer,
isNew,
isVisible,
onImageChange,
onImageEncoding,
@ -68,8 +66,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const Toast = useToast();
const history = useHistory();
const isNew = performer.id === undefined;
// Editing state
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox | undefined>();
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox>();
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
@ -447,10 +447,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return () => onImageChange?.();
}, [formik.values.image, onImageChange]);
useEffect(() => onImageEncoding?.(imageEncoding), [
onImageEncoding,
imageEncoding,
]);
useEffect(
() => onImageEncoding?.(imageEncoding),
[onImageEncoding, imageEncoding]
);
useEffect(() => {
const newQueryableScrapers = (

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { useFindPerformers } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { PerformerCard } from "./PerformerCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
@ -13,9 +13,7 @@ interface IProps {
header: string;
}
export const PerformerRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const PerformerRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindPerformers(props.filter);
const cardCount = result.data?.findPerformers.count;

View File

@ -76,7 +76,7 @@
width: 100%;
}
.flag-icon {
.fi {
bottom: 1rem;
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
height: 2rem;

View File

@ -12,7 +12,6 @@ import {
} from "react-bootstrap";
import { Link, useHistory } from "react-router-dom";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import querystring from "query-string";
import * as GQL from "src/core/generated-graphql";
import {
@ -47,20 +46,13 @@ const CLASSNAME = "duplicate-checker";
export const SceneDuplicateChecker: React.FC = () => {
const intl = useIntl();
const history = useHistory();
const { page, size, distance } = querystring.parse(history.location.search);
const currentPage = Number.parseInt(
Array.isArray(page) ? page[0] : page ?? "1",
10
);
const pageSize = Number.parseInt(
Array.isArray(size) ? size[0] : size ?? "20",
10
);
const query = new URLSearchParams(history.location.search);
const currentPage = Number.parseInt(query.get("page") ?? "1", 10);
const pageSize = Number.parseInt(query.get("size") ?? "20", 10);
const hashDistance = Number.parseInt(query.get("distance") ?? "0", 10);
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
const hashDistance = Number.parseInt(
Array.isArray(distance) ? distance[0] : distance ?? "0",
10
);
const [isMultiDelete, setIsMultiDelete] = useState(false);
const [deletingScenes, setDeletingScenes] = useState(false);
const [editingScenes, setEditingScenes] = useState(false);
@ -90,9 +82,8 @@ export const SceneDuplicateChecker: React.FC = () => {
GQL.SlimSceneDataFragment[] | null
>(null);
const [mergeScenes, setMergeScenes] = useState<
{ id: string; title: string }[] | undefined
>(undefined);
const [mergeScenes, setMergeScenes] =
useState<{ id: string; title: string }[]>();
if (loading) return <LoadingIndicator />;
if (!data) return <ErrorMessage error="Error searching for duplicates." />;
@ -107,12 +98,16 @@ export const SceneDuplicateChecker: React.FC = () => {
).length;
const setQuery = (q: Record<string, string | number | undefined>) => {
history.push({
search: querystring.stringify({
...querystring.parse(history.location.search),
...q,
}),
});
const newQuery = new URLSearchParams(query);
for (const key of Object.keys(q)) {
const value = q[key];
if (value !== undefined) {
newQuery.set(key, String(value));
} else {
newQuery.delete(key);
}
}
history.push({ search: newQuery.toString() });
};
function onDeleteDialogClosed(deleted: boolean) {
@ -504,7 +499,7 @@ export const SceneDuplicateChecker: React.FC = () => {
page: undefined,
})
}
defaultValue={distance ?? 0}
defaultValue={hashDistance}
className="input-control ml-4"
>
<option value={0}>

View File

@ -40,9 +40,8 @@ export const SceneFilenameParser: React.FC = () => {
const intl = useIntl();
const Toast = useToast();
const [parserResult, setParserResult] = useState<SceneParserResult[]>([]);
const [parserInput, setParserInput] = useState<IParserInput>(
initialParserInput
);
const [parserInput, setParserInput] =
useState<IParserInput>(initialParserInput);
const prevParserInputRef = useRef<IParserInput>();
const prevParserInput = prevParserInputRef.current;

View File

@ -14,7 +14,7 @@ interface IShowFieldsProps {
onShowFieldsChanged: (fields: Map<string, boolean>) => void;
}
export const ShowFields = (props: IShowFieldsProps) => {
export const ShowFields: React.FC<IShowFieldsProps> = (props) => {
const intl = useIntl();
const [open, setOpen] = useState(false);

View File

@ -10,12 +10,10 @@ class TrackActivityPlugin extends videojs.getPlugin("plugin") {
incrementPlayCount: () => Promise<void> = () => {
return Promise.resolve();
};
saveActivity: (
resumeTime: number,
playDuration: number
) => Promise<void> = () => {
return Promise.resolve();
};
saveActivity: (resumeTime: number, playDuration: number) => Promise<void> =
() => {
return Promise.resolve();
};
private enabled = false;
private playCountIncremented = false;

View File

@ -32,10 +32,8 @@ export const EditScenesDialog: React.FC<IListOperationProps> = (
const Toast = useToast();
const [rating100, setRating] = useState<number>();
const [studioId, setStudioId] = useState<string>();
const [
performerMode,
setPerformerMode,
] = React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerMode, setPerformerMode] =
React.useState<GQL.BulkUpdateIdMode>(GQL.BulkUpdateIdMode.Add);
const [performerIds, setPerformerIds] = useState<string[]>();
const [existingPerformerIds, setExistingPerformerIds] = useState<string[]>();
const [tagMode, setTagMode] = React.useState<GQL.BulkUpdateIdMode>(

View File

@ -99,9 +99,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
);
// studio image is missing if it uses the default
const missingStudioImage = props.scene.studio?.image_path?.endsWith(
"?default=true"
);
const missingStudioImage =
props.scene.studio?.image_path?.endsWith("?default=true");
const showStudioAsText =
missingStudioImage || (configuration?.interface.showStudioAsText ?? false);

View File

@ -1,5 +1,4 @@
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
import queryString from "query-string";
import React, {
useEffect,
useState,
@ -144,7 +143,9 @@ const ScenePage: React.FC<IProps> = ({
Mousetrap.bind("e", () => setActiveTabKey("scene-edit-panel"));
Mousetrap.bind("k", () => setActiveTabKey("scene-markers-panel"));
Mousetrap.bind("i", () => setActiveTabKey("scene-file-info-panel"));
Mousetrap.bind("o", () => onIncrementClick());
Mousetrap.bind("o", () => {
onIncrementClick();
});
Mousetrap.bind("p n", () => onQueueNext());
Mousetrap.bind("p p", () => onQueuePrevious());
Mousetrap.bind("p r", () => onQueueRandom());
@ -521,7 +522,7 @@ const SceneLoader: React.FC = () => {
const { data, loading, error } = useFindScene(id ?? "");
const queryParams = useMemo(
() => queryString.parse(location.search, { decode: false }),
() => new URLSearchParams(location.search),
[location.search]
);
const sceneQueue = useMemo(
@ -529,13 +530,13 @@ const SceneLoader: React.FC = () => {
[queryParams]
);
const queryContinue = useMemo(() => {
let cont = queryParams.continue;
if (cont !== undefined) {
let cont = queryParams.get("continue");
if (cont) {
return cont === "true";
} else {
return !!configuration?.interface.continuePlaylistDefault;
}
}, [configuration?.interface.continuePlaylistDefault, queryParams.continue]);
}, [configuration?.interface.continuePlaylistDefault, queryParams]);
const [queueScenes, setQueueScenes] = useState<QueuedScene[]>([]);
@ -547,14 +548,13 @@ const SceneLoader: React.FC = () => {
const _setTimestamp = useRef<(value: number) => void>();
const initialTimestamp = useMemo(() => {
const t = Array.isArray(queryParams.t) ? queryParams.t[0] : queryParams.t;
return Number.parseInt(t ?? "0", 10);
return Number.parseInt(queryParams.get("t") ?? "0", 10);
}, [queryParams]);
const [queueTotal, setQueueTotal] = useState(0);
const [queueStart, setQueueStart] = useState(1);
const autoplay = queryParams.autoplay === "true";
const autoplay = queryParams.get("autoplay") === "true";
const currentQueueIndex = queueScenes
? queueScenes.findIndex((s) => s.id === id)
: -1;

View File

@ -1,7 +1,7 @@
import React, { useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router-dom";
import { SceneEditPanel } from "./SceneEditPanel";
import queryString from "query-string";
import { useFindScene } from "src/core/StashService";
import { ImageUtils } from "src/utils";
import { LoadingIndicator } from "src/components/Shared";
@ -9,13 +9,13 @@ import { LoadingIndicator } from "src/components/Shared";
const SceneCreate: React.FC = () => {
const intl = useIntl();
// create scene from provided scene id if applicable
const queryParams = queryString.parse(location.search);
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const fromSceneID = (queryParams?.from_scene_id ?? "") as string;
const { data, loading } = useFindScene(fromSceneID ?? "");
// create scene from provided scene id if applicable
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "");
const [loadingCoverImage, setLoadingCoverImage] = useState(false);
const [coverImage, setCoverImage] = useState<string | undefined>(undefined);
const [coverImage, setCoverImage] = useState<string>();
const scene = useMemo(() => {
if (data?.findScene) {
@ -26,8 +26,10 @@ const SceneCreate: React.FC = () => {
};
}
return {};
}, [data?.findScene]);
return {
title: query.get("q") ?? undefined,
};
}, [data?.findScene, query]);
useEffect(() => {
async function fetchCoverImage() {
@ -62,6 +64,7 @@ const SceneCreate: React.FC = () => {
</h2>
<SceneEditPanel
scene={scene}
fileID={query.get("file_id") ?? undefined}
initialCoverImage={coverImage}
isVisible
isNew

View File

@ -36,7 +36,6 @@ import { ImageUtils, FormUtils, getStashIDs } from "src/utils";
import { MovieSelect } from "src/components/Shared/Select";
import { useFormik } from "formik";
import { Prompt, useHistory } from "react-router-dom";
import queryString from "query-string";
import { ConfigurationContext } from "src/hooks/Config";
import { stashboxDisplayName } from "src/utils/stashbox";
import { SceneMovieTable } from "./SceneMovieTable";
@ -54,6 +53,7 @@ const SceneQueryModal = lazy(() => import("./SceneQueryModal"));
interface IProps {
scene: Partial<GQL.SceneDataFragment>;
fileID?: string;
initialCoverImage?: string;
isNew?: boolean;
isVisible: boolean;
@ -62,6 +62,7 @@ interface IProps {
export const SceneEditPanel: React.FC<IProps> = ({
scene,
fileID,
initialCoverImage,
isNew = false,
isVisible,
@ -71,10 +72,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
const Toast = useToast();
const history = useHistory();
const queryParams = queryString.parse(location.search);
const fileID = (queryParams?.file_id ?? "") as string;
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
[]
);
@ -83,17 +80,13 @@ export const SceneEditPanel: React.FC<IProps> = ({
const [fragmentScrapers, setFragmentScrapers] = useState<GQL.Scraper[]>([]);
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
const [scraper, setScraper] = useState<GQL.ScraperSourceInput | undefined>();
const [
isScraperQueryModalOpen,
setIsScraperQueryModalOpen,
] = useState<boolean>(false);
const [scraper, setScraper] = useState<GQL.ScraperSourceInput>();
const [isScraperQueryModalOpen, setIsScraperQueryModalOpen] =
useState<boolean>(false);
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
const [endpoint, setEndpoint] = useState<string | undefined>();
const [endpoint, setEndpoint] = useState<string>();
const [coverImagePreview, setCoverImagePreview] = useState<
string | undefined
>();
const [coverImagePreview, setCoverImagePreview] = useState<string>();
useEffect(() => {
setCoverImagePreview(
@ -286,7 +279,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
const createValues = getCreateValues(input);
const result = await mutateCreateScene({
...createValues,
file_ids: fileID ? [fileID as string] : undefined,
file_ids: fileID ? [fileID] : undefined,
});
if (result.data?.sceneCreate?.id) {
history.push(`/scenes/${result.data?.sceneCreate.id}`);
@ -901,9 +894,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
</Form.Label>
<ul className="pl-0">
{formik.values.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(
/https?:\/\/.*?\//
)?.[0];
const base =
stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
const link = base ? (
<a
href={`${base}scenes/${stashID.stash_id}`}

View File

@ -178,12 +178,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
const Toast = useToast();
const [loading, setLoading] = useState(false);
const [deletingFile, setDeletingFile] = useState<
GQL.VideoFileDataFragment | undefined
>();
const [reassigningFile, setReassigningFile] = useState<
GQL.VideoFileDataFragment | undefined
>();
const [deletingFile, setDeletingFile] = useState<GQL.VideoFileDataFragment>();
const [reassigningFile, setReassigningFile] =
useState<GQL.VideoFileDataFragment>();
function renderStashIDs() {
if (!props.scene.stash_ids.length) {

View File

@ -22,10 +22,8 @@ export const SceneMarkersPanel: React.FC<ISceneMarkersPanelProps> = (
},
});
const [isEditorOpen, setIsEditorOpen] = useState<boolean>(false);
const [
editingMarker,
setEditingMarker,
] = useState<GQL.SceneMarkerDataFragment>();
const [editingMarker, setEditingMarker] =
useState<GQL.SceneMarkerDataFragment>();
// set up hotkeys
useEffect(() => {

View File

@ -1,4 +1,4 @@
import React, { FunctionComponent } from "react";
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieCard } from "src/components/Movies/MovieCard";
@ -6,7 +6,7 @@ interface ISceneMoviePanelProps {
scene: GQL.SceneDataFragment;
}
export const SceneMoviePanel: FunctionComponent<ISceneMoviePanelProps> = (
export const SceneMoviePanel: React.FC<ISceneMoviePanelProps> = (
props: ISceneMoviePanelProps
) => {
const cards = props.scene.movies.map((sceneMovie) => (

View File

@ -1,4 +1,4 @@
import * as React from "react";
import React from "react";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { useAllMoviesForFilter } from "src/core/StashService";
@ -11,9 +11,7 @@ export interface IProps {
onUpdate: (value: GQL.SceneMovieInput[]) => void;
}
export const SceneMovieTable: React.FunctionComponent<IProps> = (
props: IProps
) => {
export const SceneMovieTable: React.FC<IProps> = (props) => {
const intl = useIntl();
const { data } = useAllMoviesForFilter();

View File

@ -97,14 +97,8 @@ interface IScrapedObjectsRow<T> {
export const ScrapedObjectsRow = <T extends IHasName>(
props: IScrapedObjectsRow<T>
) => {
const {
title,
result,
onChange,
newObjects,
onCreateNew,
renderObjects,
} = props;
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
props;
return (
<ScrapeDialogRow

View File

@ -43,9 +43,8 @@ export const SceneList: React.FC<ISceneList> = ({
const history = useHistory();
const config = React.useContext(ConfigurationContext);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const [mergeScenes, setMergeScenes] = useState<
{ id: string; title: string }[] | undefined
>(undefined);
const [mergeScenes, setMergeScenes] =
useState<{ id: string; title: string }[]>();
const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false);
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
const [isExportAll, setIsExportAll] = useState(false);

View File

@ -36,7 +36,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
const renderMovies = (scene: GQL.SlimSceneDataFragment) =>
scene.movies.map((sceneMovie) =>
!sceneMovie.movie ? undefined : (
<Link to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}>
<Link
key={sceneMovie.movie.id}
to={NavUtils.makeMovieScenesUrl(sceneMovie.movie)}
>
<h6>{sceneMovie.movie.name}</h6>
</Link>
)

View File

@ -133,7 +133,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
setLoading(true);
const destData = await ImageUtils.imageToDataURL(dest.paths.screenshot);
const srcData = await ImageUtils.imageToDataURL(src.paths!.screenshot!);
const srcData = await ImageUtils.imageToDataURL(src.paths.screenshot!);
// keep destination image by default
const useNewValue = false;

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent, useMemo } from "react";
import React, { useMemo } from "react";
import { useFindScenes } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { SceneCard } from "./SceneCard";
import { SceneQueue } from "src/models/sceneQueue";
import { ListFilterModel } from "src/models/list-filter/filter";
@ -14,9 +14,7 @@ interface IProps {
header: string;
}
export const SceneRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const SceneRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindScenes(props.filter);
const cardCount = result.data?.findScenes.count;

View File

@ -463,7 +463,7 @@ input[type="range"].blue-slider {
}
@media (min-width: 1200px), (max-width: 575px) {
.performer-card .flag-icon {
.performer-card .fi {
height: 1.33rem;
width: 2rem;
}

View File

@ -1,8 +1,7 @@
import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
import React, { useState } from "react";
import React, { PropsWithChildren, useState } from "react";
import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { PropsWithChildren } from "react-router/node_modules/@types/react";
import { Icon } from "../Shared";
import { StringListInput } from "../Shared/StringListInput";
@ -154,7 +153,7 @@ export const BooleanSetting: React.FC<IBooleanSetting> = (props) => {
};
interface ISelectSetting extends ISetting {
value?: string | number | string[] | undefined;
value?: string | number | string[];
onChange: (v: string) => void;
}

View File

@ -1,7 +1,6 @@
import React from "react";
import React, { PropsWithChildren } from "react";
import { Card } from "react-bootstrap";
import { useIntl } from "react-intl";
import { PropsWithChildren } from "react-router/node_modules/@types/react";
interface ISettingGroup {
id?: string;

View File

@ -1,5 +1,4 @@
import React from "react";
import queryString from "query-string";
import { Tab, Nav, Row, Col } from "react-bootstrap";
import { useHistory, useLocation } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl";
@ -23,7 +22,7 @@ export const Settings: React.FC = () => {
const intl = useIntl();
const location = useLocation();
const history = useHistory();
const defaultTab = queryString.parse(location.search).tab ?? "tasks";
const defaultTab = new URLSearchParams(location.search).get("tab") ?? "tasks";
const onSelect = (val: string) => history.push(`?tab=${val}`);

View File

@ -9,14 +9,8 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
export const SettingsLibraryPanel: React.FC = () => {
const intl = useIntl();
const {
general,
loading,
error,
saveGeneral,
defaults,
saveDefaults,
} = React.useContext(SettingStateContext);
const { general, loading, error, saveGeneral, defaults, saveDefaults } =
React.useContext(SettingStateContext);
function commaDelimitedToList(value: string | undefined) {
if (value) {

View File

@ -75,31 +75,17 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
export const SettingsScrapingPanel: React.FC = () => {
const Toast = useToast();
const intl = useIntl();
const {
data: performerScrapers,
loading: loadingPerformers,
} = useListPerformerScrapers();
const {
data: sceneScrapers,
loading: loadingScenes,
} = useListSceneScrapers();
const {
data: galleryScrapers,
loading: loadingGalleries,
} = useListGalleryScrapers();
const {
data: movieScrapers,
loading: loadingMovies,
} = useListMovieScrapers();
const { data: performerScrapers, loading: loadingPerformers } =
useListPerformerScrapers();
const { data: sceneScrapers, loading: loadingScenes } =
useListSceneScrapers();
const { data: galleryScrapers, loading: loadingGalleries } =
useListGalleryScrapers();
const { data: movieScrapers, loading: loadingMovies } =
useListMovieScrapers();
const {
general,
scraping,
loading,
error,
saveGeneral,
saveScraping,
} = React.useContext(SettingStateContext);
const { general, scraping, loading, error, saveGeneral, saveScraping } =
React.useContext(SettingStateContext);
async function onReloadScrapers() {
await mutateReloadScrapers().catch((e) => Toast.error(e));

View File

@ -71,14 +71,8 @@ export const SettingsSecurityPanel: React.FC = () => {
const intl = useIntl();
const Toast = useToast();
const {
general,
apiKey,
loading,
error,
saveGeneral,
refetch,
} = React.useContext(SettingStateContext);
const { general, apiKey, loading, error, saveGeneral, refetch } =
React.useContext(SettingStateContext);
const [generateAPIKey] = useGenerateAPIKey();

View File

@ -23,9 +23,12 @@ export const SettingsServicesPanel: React.FC = () => {
const intl = useIntl();
const Toast = useToast();
const { dlna, loading: configLoading, error, saveDLNA } = React.useContext(
SettingStateContext
);
const {
dlna,
loading: configLoading,
error,
saveDLNA,
} = React.useContext(SettingStateContext);
// undefined to hide dialog, true for enable, false for disable
const [enableDisable, setEnableDisable] = useState<boolean | undefined>(

View File

@ -17,9 +17,8 @@ import {
} from "./GeneratePreviewOptions";
export const SettingsConfigurationPanel: React.FC = () => {
const { general, loading, error, saveGeneral } = React.useContext(
SettingStateContext
);
const { general, loading, error, saveGeneral } =
React.useContext(SettingStateContext);
const transcodeQualities = [
GQL.StreamingResolutionEnum.Low,

View File

@ -17,12 +17,9 @@ interface IDirectorySelectionDialogProps {
onClose: (paths?: string[]) => void;
}
export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps> = ({
animation,
allowEmpty = false,
initialPaths = [],
onClose,
}) => {
export const DirectorySelectionDialog: React.FC<
IDirectorySelectionDialogProps
> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => {
const intl = useIntl();
const { configuration } = React.useContext(ConfigurationContext);

View File

@ -81,14 +81,12 @@ export const LibraryTasks: React.FC = () => {
});
const [scanOptions, setScanOptions] = useState<GQL.ScanMetadataInput>({});
const [
autoTagOptions,
setAutoTagOptions,
] = useState<GQL.AutoTagMetadataInput>({
performers: ["*"],
studios: ["*"],
tags: ["*"],
});
const [autoTagOptions, setAutoTagOptions] =
useState<GQL.AutoTagMetadataInput>({
performers: ["*"],
studios: ["*"],
tags: ["*"],
});
function getDefaultGenerateOptions(): GQL.GenerateMetadataInput {
return {
@ -104,10 +102,8 @@ export const LibraryTasks: React.FC = () => {
};
}
const [
generateOptions,
setGenerateOptions,
] = useState<GQL.GenerateMetadataInput>(getDefaultGenerateOptions());
const [generateOptions, setGenerateOptions] =
useState<GQL.GenerateMetadataInput>(getDefaultGenerateOptions());
type DialogOpenState = typeof dialogOpen;

View File

@ -22,9 +22,8 @@ import {
} from "@fortawesome/free-solid-svg-icons";
export const Setup: React.FC = () => {
const { configuration, loading: configLoading } = useContext(
ConfigurationContext
);
const { configuration, loading: configLoading } =
useContext(ConfigurationContext);
const [step, setStep] = useState(0);
const [configLocation, setConfigLocation] = useState("");

View File

@ -19,9 +19,7 @@ const CountryFlag: React.FC<ICountryFlag> = ({
return (
<span
className={`${
className ?? ""
} flag-icon flag-icon-${isoCountry.toLowerCase()}`}
className={`${className ?? ""} fi fi-${isoCountry.toLowerCase()}`}
title={country}
/>
);

View File

@ -5,7 +5,7 @@ import { getCountries } from "src/utils";
import CountryLabel from "./CountryLabel";
interface IProps {
value?: string | undefined;
value?: string;
onChange?: (value: string) => void;
disabled?: boolean;
className?: string;

View File

@ -20,9 +20,8 @@ export const FolderSelect: React.FC<IProps> = ({
defaultDirectories,
appendButton,
}) => {
const [debouncedDirectory, setDebouncedDirectory] = useState(
currentDirectory
);
const [debouncedDirectory, setDebouncedDirectory] =
useState(currentDirectory);
const { data, error, loading } = useDirectory(debouncedDirectory);
const intl = useIntl();

View File

@ -1,11 +1,10 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import gfm from "remark-gfm";
import remarkGfm from "remark-gfm";
interface IPageProps {
// page is a markdown module
// eslint-disable-next-line @typescript-eslint/no-explicit-any
page: any;
page: string;
}
export const MarkdownPage: React.FC<IPageProps> = ({ page }) => {
@ -20,7 +19,7 @@ export const MarkdownPage: React.FC<IPageProps> = ({ page }) => {
}, [page, markdown]);
return (
<ReactMarkdown className="markdown" plugins={[gfm]}>
<ReactMarkdown className="markdown" remarkPlugins={[remarkGfm]}>
{markdown}
</ReactMarkdown>
);

View File

@ -1,4 +1,4 @@
import * as React from "react";
import React from "react";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
@ -21,9 +21,7 @@ interface IMultiSetProps {
onSetMode: (mode: GQL.BulkUpdateIdMode) => void;
}
const MultiSet: React.FunctionComponent<IMultiSetProps> = (
props: IMultiSetProps
) => {
const MultiSet: React.FC<IMultiSetProps> = (props) => {
const intl = useIntl();
const modes = [
GQL.BulkUpdateIdMode.Set,

View File

@ -1,13 +1,13 @@
import React, { useEffect, useMemo, useState } from "react";
import Select, {
ValueType,
Styles,
OnChangeValue,
StylesConfig,
OptionProps,
components as reactSelectComponents,
GroupedOptionsType,
OptionsType,
MenuListComponentProps,
GroupTypeBase,
Options,
MenuListProps,
GroupBase,
OptionsOrGroups,
} from "react-select";
import CreatableSelect from "react-select/creatable";
import debounce from "lodash-es/debounce";
@ -24,7 +24,7 @@ import {
usePerformerCreate,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import { SelectComponents } from "react-select/src/components";
import { SelectComponents } from "react-select/dist/declarations/src/components";
import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
import { objectTitle } from "src/core/files";
@ -65,22 +65,22 @@ interface IFilterProps {
interface ISelectProps<T extends boolean> {
className?: string;
items: Option[];
selectedOptions?: ValueType<Option, T>;
selectedOptions?: OnChangeValue<Option, T>;
creatable?: boolean;
onCreateOption?: (value: string) => void;
isLoading: boolean;
isDisabled?: boolean;
onChange: (item: ValueType<Option, T>) => void;
onChange: (item: OnChangeValue<Option, T>) => void;
initialIds?: string[];
isMulti: T;
isClearable?: boolean;
onInputChange?: (input: string) => void;
components?: Partial<SelectComponents<Option, T>>;
components?: Partial<SelectComponents<Option, T, GroupBase<Option>>>;
filterOption?: (option: Option, rawInput: string) => boolean;
isValidNewOption?: (
inputValue: string,
value: ValueType<Option, T>,
options: OptionsType<Option> | GroupedOptionsType<Option>
value: Options<Option>,
options: OptionsOrGroups<Option, GroupBase<Option>>
) => boolean;
placeholder?: string;
showDropdown?: boolean;
@ -95,7 +95,16 @@ interface IFilterComponentProps extends IFilterProps {
onCreate?: (name: string) => Promise<{ item: ValidTypes; message: string }>;
}
interface IFilterSelectProps<T extends boolean>
extends Omit<ISelectProps<T>, "onChange" | "items" | "onCreateOption"> {}
extends Pick<
ISelectProps<T>,
| "isLoading"
| "isMulti"
| "components"
| "filterOption"
| "isValidNewOption"
| "placeholder"
| "closeMenuOnSelect"
> {}
type TitledObject = { id: string; title: string };
interface ITitledSelect {
@ -106,18 +115,21 @@ interface ITitledSelect {
disabled?: boolean;
}
const getSelectedItems = (selectedItems: ValueType<Option, boolean>) =>
selectedItems
? Array.isArray(selectedItems)
? selectedItems
: [selectedItems]
: [];
const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
if (Array.isArray(selectedItems)) {
return selectedItems;
} else if (selectedItems) {
return [selectedItems];
} else {
return [];
}
};
const getSelectedValues = (selectedItems: ValueType<Option, boolean>) =>
const getSelectedValues = (selectedItems: OnChangeValue<Option, boolean>) =>
getSelectedItems(selectedItems).map((item) => item.value);
const LimitedSelectMenu = <T extends boolean>(
props: MenuListComponentProps<Option, T, GroupTypeBase<Option>>
props: MenuListProps<Option, T, GroupBase<Option>>
) => {
const maxOptionsShown = 200;
const [hiddenCount, setHiddenCount] = useState<number>(0);
@ -129,13 +141,13 @@ const LimitedSelectMenu = <T extends boolean>(
if (Array.isArray(props.children)) {
// limit the number of select options showing in the select dropdowns
// always showing the 'Create "..."' option when it exists
let creationOptionIndex = (props.children as React.ReactNodeArray).findIndex(
let creationOptionIndex = (props.children as React.ReactNode[]).findIndex(
(child: React.ReactNode) => {
let maybeCreatableOption = child as React.ReactElement<
OptionProps<
Option & { __isNew__: boolean },
T,
GroupTypeBase<Option & { __isNew__: boolean }>
GroupBase<Option & { __isNew__: boolean }>
>,
""
>;
@ -190,7 +202,7 @@ const SelectComponent = <T extends boolean>({
noOptionsMessage = type !== "tags" ? "None" : null,
}: ISelectProps<T> & ITypeProps) => {
const values = items.filter((item) => initialIds?.indexOf(item.value) !== -1);
const defaultValue = (isMulti ? values : values[0] ?? null) as ValueType<
const defaultValue = (isMulti ? values : values[0] ?? null) as OnChangeValue<
Option,
T
>;
@ -204,18 +216,18 @@ const SelectComponent = <T extends boolean>({
]
: items;
const styles: Partial<Styles<Option, T>> = {
const styles: StylesConfig<Option, T> = {
option: (base) => ({
...base,
color: "#000",
}),
container: (base, props) => ({
container: (base, state) => ({
...base,
zIndex: props.selectProps.isFocused ? 10 : base.zIndex,
zIndex: state.isFocused ? 10 : base.zIndex,
}),
multiValueRemove: (base, props) => ({
multiValueRemove: (base, state) => ({
...base,
color: props.selectProps.isFocused ? base.color : "#333333",
color: state.isFocused ? base.color : "#333333",
}),
};
@ -279,11 +291,11 @@ const FilterSelectComponent = <T extends boolean>(
const selected = options.filter((option) =>
selectedIds.includes(option.value)
);
const selectedOptions = (isMulti
? selected
: selected[0] ?? null) as ValueType<Option, T>;
const selectedOptions = (
isMulti ? selected : selected[0] ?? null
) as OnChangeValue<Option, T>;
const onChange = (selectedItems: ValueType<Option, boolean>) => {
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selectedValues = getSelectedValues(selectedItems);
onSelect?.(items.filter((item) => selectedValues.includes(item.id)));
};
@ -342,7 +354,7 @@ export const GallerySelect: React.FC<ITitledSelect> = (props) => {
setQuery(input);
}, 500);
const onChange = (selectedItems: ValueType<Option, boolean>) => {
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
props.onSelect(
selected.map((s) => ({
@ -369,6 +381,7 @@ export const GallerySelect: React.FC<ITitledSelect> = (props) => {
placeholder="Search for gallery..."
noOptionsMessage={query === "" ? null : "No galleries found."}
showDropdown={false}
isDisabled={props.disabled}
/>
);
};
@ -394,7 +407,7 @@ export const SceneSelect: React.FC<ITitledSelect> = (props) => {
setQuery(input);
}, 500);
const onChange = (selectedItems: ValueType<Option, boolean>) => {
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
props.onSelect(
(selected ?? []).map((s) => ({
@ -446,7 +459,7 @@ export const ImageSelect: React.FC<ITitledSelect> = (props) => {
setQuery(input);
}, 500);
const onChange = (selectedItems: ValueType<Option, boolean>) => {
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
const selected = getSelectedItems(selectedItems);
props.onSelect(
(selected ?? []).map((s) => ({
@ -485,7 +498,7 @@ export const MarkerTitleSuggest: React.FC<IMarkerSuggestProps> = (props) => {
const { data, loading } = useMarkerStrings();
const suggestions = data?.markerStrings ?? [];
const onChange = (selectedItem: ValueType<Option, false>) =>
const onChange = (selectedItem: OnChangeValue<Option, false>) =>
props.onChange(selectedItem?.value ?? "");
const items = suggestions.map((item) => ({
@ -535,9 +548,10 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const defaultCreatable =
!configuration?.interface.disableDropdownCreate.performer ?? true;
const performers = useMemo(() => data?.allPerformers ?? [], [
data?.allPerformers,
]);
const performers = useMemo(
() => data?.allPerformers ?? [],
[data?.allPerformers]
);
useEffect(() => {
// build the tag aliases map
@ -609,15 +623,15 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const isValidNewOption = (
inputValue: string,
value: ValueType<Option, boolean>,
options: OptionsType<Option> | GroupedOptionsType<Option>
value: Options<Option>,
options: OptionsOrGroups<Option, GroupBase<Option>>
) => {
if (!inputValue) {
return false;
}
if (
(options as OptionsType<Option>).some((o: Option) => {
(options as Options<Option>).some((o: Option) => {
return o.label.toLowerCase() === inputValue.toLowerCase();
})
) {
@ -752,15 +766,15 @@ export const StudioSelect: React.FC<
const isValidNewOption = (
inputValue: string,
value: ValueType<Option, boolean>,
options: OptionsType<Option> | GroupedOptionsType<Option>
value: OnChangeValue<Option, boolean>,
options: OptionsOrGroups<Option, GroupBase<Option>>
) => {
if (!inputValue) {
return false;
}
if (
(options as OptionsType<Option>).some((o: Option) => {
(options as Options<Option>).some((o: Option) => {
return o.label.toLowerCase() === inputValue.toLowerCase();
})
) {
@ -873,7 +887,9 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
};
}
const id = optionProps.data.__isNew__ ? "" : optionProps.data.value;
const id = (optionProps.data as Option & { __isNew__: boolean }).__isNew__
? ""
: optionProps.data.value;
return (
<TagPopover id={id}>
@ -917,15 +933,15 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
const isValidNewOption = (
inputValue: string,
value: ValueType<Option, boolean>,
options: OptionsType<Option> | GroupedOptionsType<Option>
value: OnChangeValue<Option, boolean>,
options: OptionsOrGroups<Option, GroupBase<Option>>
) => {
if (!inputValue) {
return false;
}
if (
(options as OptionsType<Option>).some((o: Option) => {
(options as Options<Option>).some((o: Option) => {
return o.label.toLowerCase() === inputValue.toLowerCase();
})
) {
@ -957,16 +973,17 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
);
};
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) =>
props.type === "performers" ? (
<PerformerSelect {...props} creatable={false} />
) : props.type === "studios" || props.type === "parent_studios" ? (
<StudioSelect {...props} creatable={false} />
) : props.type === "movies" ? (
<MovieSelect {...props} creatable={false} />
) : (
<TagSelect {...props} creatable={false} />
);
export const FilterSelect: React.FC<IFilterProps & ITypeProps> = (props) => {
if (props.type === "performers") {
return <PerformerSelect {...props} creatable={false} />;
} else if (props.type === "studios" || props.type === "parent_studios") {
return <StudioSelect {...props} creatable={false} />;
} else if (props.type === "movies") {
return <MovieSelect {...props} creatable={false} />;
} else {
return <TagSelect {...props} creatable={false} />;
}
};
interface IStringListSelect {
options?: string[];
@ -988,18 +1005,18 @@ export const StringListSelect: React.FC<IStringListSelect> = ({
});
}, [value]);
const styles: Partial<Styles<Option, true>> = {
const styles: StylesConfig<Option, true> = {
option: (base) => ({
...base,
color: "#000",
}),
container: (base, props) => ({
container: (base, state) => ({
...base,
zIndex: props.selectProps.isFocused ? 10 : base.zIndex,
zIndex: state.isFocused ? 10 : base.zIndex,
}),
multiValueRemove: (base, props) => ({
multiValueRemove: (base, state) => ({
...base,
color: props.selectProps.isFocused ? base.color : "#333333",
color: state.isFocused ? base.color : "#333333",
}),
};
@ -1038,18 +1055,18 @@ export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
return value.map(toOptionType);
}, [value, toOptionType]);
const styles: Partial<Styles<{ label: string; value: string }, true>> = {
const styles: StylesConfig<Option, true> = {
option: (base) => ({
...base,
color: "#000",
}),
container: (base, p) => ({
container: (base, state) => ({
...base,
zIndex: p.selectProps.isFocused ? 10 : base.zIndex,
zIndex: state.isFocused ? 10 : base.zIndex,
}),
multiValueRemove: (base, p) => ({
multiValueRemove: (base, state) => ({
...base,
color: p.selectProps.isFocused ? base.color : "#333333",
color: state.isFocused ? base.color : "#333333",
}),
};

View File

@ -1,6 +1,6 @@
import React from "react";
export const SweatDrops = () => (
export const SweatDrops: React.FC = () => (
<span>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@ -65,7 +65,9 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("d d", () => onDelete());
Mousetrap.bind("d d", () => {
onDelete();
});
return () => {
Mousetrap.unbind("e");

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 { useIntl } from "react-intl";
@ -11,15 +11,13 @@ import { StudioEditPanel } from "./StudioEditPanel";
const StudioCreate: React.FC = () => {
const history = useHistory();
const location = useLocation();
const Toast = useToast();
function useQuery() {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
}
const query = useQuery();
const nameQuery = query.get("name");
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const studio = {
name: query.get("q") ?? undefined,
};
const intl = useIntl();
@ -74,7 +72,7 @@ const StudioCreate: React.FC = () => {
)}
</div>
<StudioEditPanel
studio={{ name: nameQuery ?? "" }}
studio={studio}
onSubmit={onSave}
onImageChange={setImage}
onCancel={() => history.push("/studios")}

View File

@ -35,10 +35,9 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
}) => {
const intl = useIntl();
const isNew = studio.id === undefined;
const { configuration } = React.useContext(ConfigurationContext);
const isNew = !studio || !studio.id;
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
const schema = yup.object({
@ -130,10 +129,10 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
return () => onImageChange?.();
}, [formik.values.image, onImageChange]);
useEffect(() => onImageEncoding?.(imageEncoding), [
onImageEncoding,
imageEncoding,
]);
useEffect(
() => onImageEncoding?.(imageEncoding),
[onImageEncoding, imageEncoding]
);
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
ImageUtils.onImageChange(event, onImageLoad);

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { useFindStudios } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { StudioCard } from "./StudioCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
@ -13,9 +13,7 @@ interface IProps {
header: string;
}
export const StudioRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const StudioRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindStudios(props.filter);
const cardCount = result.data?.findStudios.count;

View File

@ -201,9 +201,8 @@ export const TaggerContext: React.FC = ({ children }) => {
});
}
const [
submitFingerprintsMutation,
] = GQL.useSubmitStashBoxFingerprintsMutation();
const [submitFingerprintsMutation] =
GQL.useSubmitStashBoxFingerprintsMutation();
async function submitFingerprints() {
const endpoint = currentSource?.stashboxEndpoint;

View File

@ -30,13 +30,11 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
onLink,
endpoint,
}) => {
const {
data: performerData,
loading: stashLoading,
} = GQL.useFindPerformerQuery({
variables: { id: performer.stored_id ?? "" },
skip: !performer.stored_id,
});
const { data: performerData, loading: stashLoading } =
GQL.useFindPerformerQuery({
variables: { id: performer.stored_id ?? "" },
skip: !performer.stored_id,
});
const matchedPerformer = performerData?.findPerformer;
const matchedStashID = matchedPerformer?.stash_ids.some(

View File

@ -181,14 +181,10 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
return -1;
}
const [
nbPhashMatchSceneA,
ratioPhashMatchSceneA,
] = calculatePhashComparisonScore(stashScene, sceneA);
const [
nbPhashMatchSceneB,
ratioPhashMatchSceneB,
] = calculatePhashComparisonScore(stashScene, sceneB);
const [nbPhashMatchSceneA, ratioPhashMatchSceneA] =
calculatePhashComparisonScore(stashScene, sceneA);
const [nbPhashMatchSceneB, ratioPhashMatchSceneB] =
calculatePhashComparisonScore(stashScene, sceneB);
if (nbPhashMatchSceneA != nbPhashMatchSceneB) {
return nbPhashMatchSceneB - nbPhashMatchSceneA;

View File

@ -21,12 +21,11 @@ export interface ISceneTaggerModalsContextState {
) => void;
}
export const SceneTaggerModalsState = React.createContext<ISceneTaggerModalsContextState>(
{
export const SceneTaggerModalsState =
React.createContext<ISceneTaggerModalsContextState>({
createPerformerModal: () => {},
createStudioModal: () => {},
}
);
});
export const SceneTaggerModals: React.FC = ({ children }) => {
const { currentSource } = useContext(TaggerStateContext);

View File

@ -87,7 +87,9 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("d d", () => onDelete());
Mousetrap.bind("d d", () => {
onDelete();
});
return () => {
if (isEditing) {

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 * as GQL from "src/core/generated-graphql";
@ -11,15 +11,13 @@ import { TagEditPanel } from "./TagEditPanel";
const TagCreate: React.FC = () => {
const history = useHistory();
const location = useLocation();
const Toast = useToast();
function useQuery() {
const { search } = useLocation();
return React.useMemo(() => new URLSearchParams(search), [search]);
}
const query = useQuery();
const nameQuery = query.get("name");
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const tag = {
name: query.get("q") ?? undefined,
};
// Editing tag state
const [image, setImage] = useState<string | null>();
@ -86,7 +84,7 @@ const TagCreate: React.FC = () => {
)}
</div>
<TagEditPanel
tag={{ name: nameQuery ?? "" }}
tag={tag}
onSubmit={onSave}
onCancel={() => history.push("/tags")}
onDelete={() => {}}

View File

@ -6,12 +6,12 @@ import { DetailsEditNavbar, TagSelect } from "src/components/Shared";
import { Form, Col, Row } from "react-bootstrap";
import { FormUtils, ImageUtils } from "src/utils";
import { useFormik } from "formik";
import { Prompt, useParams } from "react-router-dom";
import { Prompt } from "react-router-dom";
import Mousetrap from "mousetrap";
import { StringListInput } from "src/components/Shared/StringListInput";
interface ITagEditPanel {
tag?: Partial<GQL.TagDataFragment>;
tag: Partial<GQL.TagDataFragment>;
// returns id
onSubmit: (tag: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>) => void;
onCancel: () => void;
@ -19,10 +19,6 @@ interface ITagEditPanel {
setImage: (image?: string | null) => void;
}
interface ITagEditPanelParams {
id?: string;
}
export const TagEditPanel: React.FC<ITagEditPanel> = ({
tag,
onSubmit,
@ -32,10 +28,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
}) => {
const intl = useIntl();
const params = useParams<ITagEditPanelParams>();
const idParam = params.id;
const isNew = idParam === undefined;
const isNew = tag.id === undefined;
const labelXS = 3;
const labelXL = 3;

View File

@ -33,10 +33,8 @@ interface ITagList {
export const TagList: React.FC<ITagList> = ({ filterHook }) => {
const Toast = useToast();
const [
deletingTag,
setDeletingTag,
] = useState<Partial<GQL.TagDataFragment> | null>(null);
const [deletingTag, setDeletingTag] =
useState<Partial<GQL.TagDataFragment> | null>(null);
const [deleteTag] = useTagDestroy(getDeleteTagInput() as GQL.TagDestroyInput);

View File

@ -1,6 +1,6 @@
import React, { FunctionComponent } from "react";
import React from "react";
import { useFindTags } from "src/core/StashService";
import Slider from "react-slick";
import Slider from "@ant-design/react-slick";
import { TagCard } from "./TagCard";
import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
@ -13,9 +13,7 @@ interface IProps {
header: string;
}
export const TagRecommendationRow: FunctionComponent<IProps> = (
props: IProps
) => {
export const TagRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindTags(props.filter);
const cardCount = result.data?.findTags.count;

View File

@ -53,16 +53,16 @@ export const WallPanel: React.FC<IWallPanelProps> = (
/>
));
const sceneMarkers = (
props.sceneMarkers ?? []
).map((marker, index, markerArray) => (
<WallItem
key={marker.id}
sceneMarker={marker}
clickHandler={props.clickHandler}
className={calculateClass(index, markerArray.length)}
/>
));
const sceneMarkers = (props.sceneMarkers ?? []).map(
(marker, index, markerArray) => (
<WallItem
key={marker.id}
sceneMarker={marker}
clickHandler={props.clickHandler}
className={calculateClass(index, markerArray.length)}
/>
)
);
const images = (props.images ?? []).map((image, index, imageArray) => (
<WallItem

View File

@ -436,23 +436,13 @@ const updateSceneO = (
cache: ApolloCache<SceneOMutation>,
updatedOCount?: number
) => {
const scene = cache.readQuery<
GQL.FindSceneQuery,
GQL.FindSceneQueryVariables
>({
query: GQL.FindSceneDocument,
variables: { id },
});
if (updatedOCount === undefined || !scene?.findScene) return;
if (updatedOCount === undefined) return;
cache.writeQuery<GQL.FindSceneQuery, GQL.FindSceneQueryVariables>({
query: GQL.FindSceneDocument,
variables: { id },
data: {
...scene,
findScene: {
...scene.findScene,
o_counter: updatedOCount,
cache.modify({
id: cache.identify({ __typename: "Scene", id }),
fields: {
o_counter() {
return updatedOCount;
},
},
});

View File

@ -6,7 +6,8 @@ import {
ServerError,
TypePolicies,
} from "@apollo/client";
import { WebSocketLink } from "@apollo/client/link/ws";
import { GraphQLWsLink } from "@apollo/client/link/subscriptions";
import { createClient as createWSClient } from "graphql-ws";
import { onError } from "@apollo/client/link/error";
import { getMainDefinition } from "@apollo/client/utilities";
import { createUploadLink } from "apollo-upload-client";
@ -105,7 +106,11 @@ export const getPlatformURL = (ws?: boolean) => {
}
if (ws) {
platformUrl.protocol = "ws:";
if (platformUrl.protocol === "https:") {
platformUrl.protocol = "wss:";
} else {
platformUrl.protocol = "ws:";
}
}
return platformUrl;
@ -115,23 +120,20 @@ export const createClient = () => {
const platformUrl = getPlatformURL();
const wsPlatformUrl = getPlatformURL(true);
if (platformUrl.protocol === "https:") {
wsPlatformUrl.protocol = "wss:";
}
const url = `${platformUrl.toString()}graphql`;
const wsUrl = `${wsPlatformUrl.toString()}graphql`;
const httpLink = createUploadLink({
uri: url,
});
const httpLink = createUploadLink({ uri: url });
const wsLink = new WebSocketLink({
uri: wsUrl,
options: {
reconnect: true,
},
});
const wsLink = new GraphQLWsLink(
createWSClient({
url: wsUrl,
retryAttempts: Infinity,
shouldRetry() {
return true;
},
})
);
const errorLink = onError(({ networkError }) => {
// handle unauthorized error by redirecting to the login page
@ -155,7 +157,6 @@ export const createClient = () => {
);
},
wsLink,
// @ts-ignore
httpLink
);

View File

@ -1,9 +1,7 @@
import migration32 from "./32.md";
import migration39 from "./39.md";
type Module = typeof migration32;
export const migrationNotes: Record<number, Module> = {
export const migrationNotes: Record<number, string> = {
32: migration32,
39: migration39,
};

View File

@ -1,11 +1,9 @@
import v0170 from "./v0170.md";
export type Module = typeof v0170;
interface IReleaseNotes {
// handle should be in the form of YYYYMMDD
date: number;
content: Module;
content: string;
}
export const releaseNotes: IReleaseNotes[] = [

Some files were not shown because too many files have changed in this diff Show More