diff --git a/.oxlintrc.jsonc b/.oxlintrc.jsonc index 64e3f19a..826c0d66 100644 --- a/.oxlintrc.jsonc +++ b/.oxlintrc.jsonc @@ -1,461 +1,419 @@ { - "plugins": [ - "react", - "unicorn", - "typescript", - "import", - "jsx-a11y" - ], - "jsPlugins": [ - "eslint-plugin-lingui", - "eslint-plugin-no-relative-import-paths", - "eslint-plugin-unused-imports", - "eslint-plugin-no-only-tests", - "@tony.ganchev/eslint-plugin-header" - ], - "categories": { - "correctness": "off" - }, - "rules": { - // ── Core JS: correctness (from js.configs.recommended) ── - // Rules redundant with TypeScript strict mode have been removed. - "for-direction": "error", - "no-async-promise-executor": "error", - "no-case-declarations": "error", - "no-compare-neg-zero": "error", - "no-cond-assign": "error", - "no-constant-binary-expression": "error", - "no-constant-condition": "error", - "no-control-regex": "error", - "no-debugger": "error", - "no-delete-var": "error", - "no-dupe-else-if": "error", - "no-duplicate-case": "error", - "no-empty": "error", - "no-empty-character-class": "error", - "no-empty-pattern": "error", - "no-empty-static-block": "error", - "no-ex-assign": "error", - "no-extra-boolean-cast": "error", - "no-invalid-regexp": "error", - "no-irregular-whitespace": "error", - "no-loss-of-precision": "error", - "no-misleading-character-class": "error", - "no-nonoctal-decimal-escape": "error", - "no-prototype-builtins": "error", - "no-regex-spaces": "error", - "no-self-assign": "error", - "no-shadow-restricted-names": "error", - "no-sparse-arrays": "error", - "no-unsafe-finally": "error", - "no-unsafe-optional-chaining": "error", - "no-unused-labels": "error", - "no-unused-private-class-members": "error", - "no-unused-vars": [ - "error", - { - "vars": "all", - "args": "after-used", - "ignoreRestSiblings": true, - "argsIgnorePattern": "^_", - "varsIgnorePattern": "^_", - "caughtErrorsIgnorePattern": "^_|^e$|^error$" - } + "plugins": ["react", "unicorn", "typescript", "import", "jsx-a11y"], + "jsPlugins": [ + "eslint-plugin-lingui", + "eslint-plugin-no-relative-import-paths", + "eslint-plugin-unused-imports", + "eslint-plugin-no-only-tests", + "@tony.ganchev/eslint-plugin-header", ], - "no-useless-backreference": "error", - "no-useless-catch": "error", - "no-useless-escape": "error", - "no-with": "error", - "require-yield": "error", - "use-isnan": "error", - "no-array-constructor": "error", - "no-unused-expressions": "error", - - // ── Core JS: style & best practices ── - "no-console": "error", - "no-else-return": [ - "error", - { - "allowElseIf": false - } - ], - "no-lonely-if": "error", - "no-unneeded-ternary": [ - "error", - { - "defaultAssignment": false - } - ], - "no-multi-assign": "error", - "no-return-assign": [ - "error", - "always" - ], - "no-sequences": "error", - "no-new": "error", - "no-new-func": "error", - "no-new-wrappers": "error", - "no-eval": "error", - "no-script-url": "error", - "no-self-compare": "error", - "no-extend-native": "error", - "no-alert": "error", - "no-restricted-globals": [ - "error", - "isFinite", - "isNaN" - ], - "no-await-in-loop": "error", - "no-promise-executor-return": "error", - "no-template-curly-in-string": "error", - "no-constructor-return": "error", - "no-useless-concat": "error", - "no-useless-return": "error", - "no-useless-computed-key": "error", - "no-useless-rename": "error", - "eqeqeq": [ - "error", - "always", - { - "null": "ignore" - } - ], - "default-case": [ - "error", - { - "commentPattern": "^no default$" - } - ], - "default-case-last": "error", - "yoda": "error", - "prefer-template": "error", - "prefer-exponentiation-operator": "error", - "prefer-object-spread": "error", - "arrow-body-style": ["error", "as-needed"], - "prefer-destructuring": "error", - "no-nested-ternary": "error", - "prefer-promise-reject-errors": [ - "error", - { - "allowEmptyReject": true - } - ], - "no-param-reassign": [ - "error", - { - "props": true, - "ignorePropertyModificationsForRegex": [ - "^draft" - ] - } - ], - "no-restricted-imports": [ - "error", - { - "patterns": [ - { - "group": [ - "@mui/*", - "!@mui/material/", - "!@mui/icons-material/", - "!@mui/x-date-pickers/" - ], - }, - { - "group": [ - "@mui/*/*/*" - ] - }, - { - "group": [ - "lodash", - "lodash/*", - "!lodash/fp/*" - ], - "message": "Use lodash-fp instead." - } - ] - } - ], - - // ── New: modern JS essentials ── - "prefer-const": "error", - "no-var": "error", - "curly": "error", - "no-warning-comments": [ - "error", - { - "terms": [ - "@nocommit" - ] - } - ], - - // ── TypeScript rules (non-type-checked) ── - "no-shadow": "error", - "no-loop-func": "error", - "no-use-before-define": [ - "error", - { - "functions": false, - "classes": true, - "variables": true - } - ], - "default-param-last": "error", - - // ── TypeScript rules from recommended ── - "typescript/no-duplicate-enum-values": "error", - "typescript/no-extra-non-null-assertion": "error", - "typescript/no-misused-new": "error", - "typescript/no-namespace": "error", - "typescript/no-non-null-asserted-optional-chain": "error", - "typescript/no-require-imports": "error", - "typescript/no-this-alias": "error", - "typescript/no-unnecessary-type-constraint": "error", - "typescript/no-unsafe-declaration-merging": "error", - "typescript/no-wrapper-object-types": "error", - "typescript/prefer-as-const": "error", - "typescript/prefer-namespace-keyword": "error", - "typescript/triple-slash-reference": "error", - - // ── New: TypeScript safety ── - "typescript/no-explicit-any": "off", - "typescript/no-empty-object-type": "error", - "typescript/no-unsafe-function-type": "error", - "typescript/consistent-type-imports": "error", - "typescript/switch-exhaustiveness-check": "error", - - // ── TypeScript rules (type-checked) ── - "typescript/no-implied-eval": "error", - "typescript/only-throw-error": "error", - "typescript/dot-notation": "error", - "typescript/return-await": [ - "error", - "in-try-catch" - ], - - // ── Import rules ── - "import/namespace": "error", - "import/default": "error", - "import/export": "error", - "import/no-duplicates": "error", - "import/no-mutable-exports": "error", - "import/first": "error", - "import/no-amd": "error", - "import/no-absolute-path": "error", - "import/no-self-import": "error", - "import/no-default-export": "error", - - // ── Unicorn: modern JS (auto-fixable) ── - "unicorn/prefer-array-flat-map": "error", - "unicorn/prefer-structured-clone": "error", - "unicorn/prefer-string-replace-all": "error", - "unicorn/consistent-function-scoping": "error", - - // ── React rules ── - "react/jsx-key": "error", - "react/jsx-no-comment-textnodes": "error", - "react/jsx-no-duplicate-props": "error", - "react/jsx-no-target-blank": "error", - "react/jsx-no-undef": "error", - "react/no-children-prop": "error", - "react/no-danger-with-children": "error", - "react/no-render-return-value": "error", - "react/no-unescaped-entities": "error", - "react/no-unknown-property": "error", - "react/rules-of-hooks": "error", - "react/no-array-index-key": "error", - "react/jsx-no-useless-fragment": "error", - "react/jsx-no-constructed-context-values": "error", - "react/self-closing-comp": "error", - "react/jsx-boolean-value": [ - "error", - "never" - ], - "react/jsx-pascal-case": [ - "error", - { - "allowAllCaps": true - } - ], - "react/button-has-type": "error", - "react/jsx-curly-brace-presence": [ - "error", - { - "props": "never", - "children": "never" - } - ], - "react/jsx-fragments": [ - "error", - "syntax" - ], - "react/jsx-no-script-url": "error", - "react/style-prop-object": "error", - "react/void-dom-elements-no-children": "error", - "react/no-danger": "error", - - // ── jsx-a11y rules ── - "jsx_a11y/alt-text": "error", - "jsx_a11y/anchor-has-content": "error", - "jsx_a11y/anchor-is-valid": "error", - "jsx_a11y/aria-activedescendant-has-tabindex": "error", - "jsx_a11y/aria-props": "error", - "jsx_a11y/aria-proptypes": "error", - "jsx_a11y/aria-role": "error", - "jsx_a11y/aria-unsupported-elements": "error", - "jsx_a11y/autocomplete-valid": "error", - "jsx_a11y/click-events-have-key-events": "error", - "jsx_a11y/heading-has-content": "error", - "jsx_a11y/html-has-lang": "error", - "jsx_a11y/iframe-has-title": "error", - "jsx_a11y/img-redundant-alt": "error", - "jsx_a11y/label-has-associated-control": "error", - "jsx_a11y/media-has-caption": "error", - "jsx_a11y/mouse-events-have-key-events": "error", - "jsx_a11y/no-access-key": "error", - "jsx_a11y/no-autofocus": [ - "error", - { - "ignoreNonDOM": true - } - ], - "jsx_a11y/no-distracting-elements": "error", - "jsx_a11y/no-noninteractive-tabindex": [ - "error", - { - "tags": [], - "roles": [ - "tabpanel" + "categories": { + "correctness": "off", + }, + "rules": { + // ── Core JS: correctness (from js.configs.recommended) ── + // Rules redundant with TypeScript strict mode have been removed. + "for-direction": "error", + "no-async-promise-executor": "error", + "no-case-declarations": "error", + "no-compare-neg-zero": "error", + "no-cond-assign": "error", + "no-constant-binary-expression": "error", + "no-constant-condition": "error", + "no-control-regex": "error", + "no-debugger": "error", + "no-delete-var": "error", + "no-dupe-else-if": "error", + "no-duplicate-case": "error", + "no-empty": "error", + "no-empty-character-class": "error", + "no-empty-pattern": "error", + "no-empty-static-block": "error", + "no-ex-assign": "error", + "no-extra-boolean-cast": "error", + "no-invalid-regexp": "error", + "no-irregular-whitespace": "error", + "no-loss-of-precision": "error", + "no-misleading-character-class": "error", + "no-nonoctal-decimal-escape": "error", + "no-prototype-builtins": "error", + "no-regex-spaces": "error", + "no-self-assign": "error", + "no-shadow-restricted-names": "error", + "no-sparse-arrays": "error", + "no-unsafe-finally": "error", + "no-unsafe-optional-chaining": "error", + "no-unused-labels": "error", + "no-unused-private-class-members": "error", + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "after-used", + "ignoreRestSiblings": true, + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_|^e$|^error$", + }, ], - "allowExpressionValues": true - } + "no-useless-backreference": "error", + "no-useless-catch": "error", + "no-useless-escape": "error", + "no-with": "error", + "require-yield": "error", + "use-isnan": "error", + "no-array-constructor": "error", + "no-unused-expressions": "error", + + // ── Core JS: style & best practices ── + "no-console": "error", + "no-else-return": [ + "error", + { + "allowElseIf": false, + }, + ], + "no-lonely-if": "error", + "no-unneeded-ternary": [ + "error", + { + "defaultAssignment": false, + }, + ], + "no-multi-assign": "error", + "no-return-assign": ["error", "always"], + "no-sequences": "error", + "no-new": "error", + "no-new-func": "error", + "no-new-wrappers": "error", + "no-eval": "error", + "no-script-url": "error", + "no-self-compare": "error", + "no-extend-native": "error", + "no-alert": "error", + "no-restricted-globals": ["error", "isFinite", "isNaN"], + "no-await-in-loop": "error", + "no-promise-executor-return": "error", + "no-template-curly-in-string": "error", + "no-constructor-return": "error", + "no-useless-concat": "error", + "no-useless-return": "error", + "no-useless-computed-key": "error", + "no-useless-rename": "error", + "eqeqeq": [ + "error", + "always", + { + "null": "ignore", + }, + ], + "default-case": [ + "error", + { + "commentPattern": "^no default$", + }, + ], + "default-case-last": "error", + "yoda": "error", + "prefer-template": "error", + "prefer-exponentiation-operator": "error", + "prefer-object-spread": "error", + "arrow-body-style": ["error", "as-needed"], + "prefer-destructuring": "error", + "no-nested-ternary": "error", + "prefer-promise-reject-errors": [ + "error", + { + "allowEmptyReject": true, + }, + ], + "no-param-reassign": [ + "error", + { + "props": true, + "ignorePropertyModificationsForRegex": ["^draft"], + }, + ], + "no-restricted-imports": [ + "error", + { + "patterns": [ + { + "group": ["@mui/*", "!@mui/material/", "!@mui/icons-material/", "!@mui/x-date-pickers/"], + }, + { + "group": ["@mui/*/*/*"], + }, + { + "group": ["lodash", "lodash/*", "!lodash/fp/*"], + "message": "Use lodash-fp instead.", + }, + ], + "paths": [ + { + "name": "use-long-press", + "importNames": ["useLongPress"], + "message": "Use the wrapper \"usePress\" instead", + }, + ], + }, + ], + + // ── New: modern JS essentials ── + "prefer-const": "error", + "no-var": "error", + "curly": "error", + "no-warning-comments": [ + "error", + { + "terms": ["@nocommit"], + }, + ], + + // ── TypeScript rules (non-type-checked) ── + "no-shadow": "error", + "no-loop-func": "error", + "no-use-before-define": [ + "error", + { + "functions": false, + "classes": true, + "variables": true, + }, + ], + "default-param-last": "error", + + // ── TypeScript rules from recommended ── + "typescript/no-duplicate-enum-values": "error", + "typescript/no-extra-non-null-assertion": "error", + "typescript/no-misused-new": "error", + "typescript/no-namespace": "error", + "typescript/no-non-null-asserted-optional-chain": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unnecessary-type-constraint": "error", + "typescript/no-unsafe-declaration-merging": "error", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/prefer-namespace-keyword": "error", + "typescript/triple-slash-reference": "error", + + // ── New: TypeScript safety ── + "typescript/no-explicit-any": "off", + "typescript/no-empty-object-type": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/consistent-type-imports": "error", + "typescript/switch-exhaustiveness-check": "error", + + // ── TypeScript rules (type-checked) ── + "typescript/no-implied-eval": "error", + "typescript/only-throw-error": "error", + "typescript/dot-notation": "error", + "typescript/return-await": ["error", "in-try-catch"], + + // ── Import rules ── + "import/namespace": "error", + "import/default": "error", + "import/export": "error", + "import/no-duplicates": "error", + "import/no-mutable-exports": "error", + "import/first": "error", + "import/no-amd": "error", + "import/no-absolute-path": "error", + "import/no-self-import": "error", + "import/no-default-export": "error", + + // ── Unicorn: modern JS (auto-fixable) ── + "unicorn/prefer-array-flat-map": "error", + "unicorn/prefer-structured-clone": "error", + "unicorn/prefer-string-replace-all": "error", + "unicorn/consistent-function-scoping": "error", + + // ── React rules ── + "react/jsx-key": "error", + "react/jsx-no-comment-textnodes": "error", + "react/jsx-no-duplicate-props": "error", + "react/jsx-no-target-blank": "error", + "react/jsx-no-undef": "error", + "react/no-children-prop": "error", + "react/no-danger-with-children": "error", + "react/no-render-return-value": "error", + "react/no-unescaped-entities": "error", + "react/no-unknown-property": "error", + "react/rules-of-hooks": "error", + "react/no-array-index-key": "error", + "react/jsx-no-useless-fragment": "error", + "react/jsx-no-constructed-context-values": "error", + "react/self-closing-comp": "error", + "react/jsx-boolean-value": ["error", "never"], + "react/jsx-pascal-case": [ + "error", + { + "allowAllCaps": true, + }, + ], + "react/button-has-type": "error", + "react/jsx-curly-brace-presence": [ + "error", + { + "props": "never", + "children": "never", + }, + ], + "react/jsx-fragments": ["error", "syntax"], + "react/jsx-no-script-url": "error", + "react/style-prop-object": "error", + "react/void-dom-elements-no-children": "error", + "react/no-danger": "error", + + // ── jsx-a11y rules ── + "jsx_a11y/alt-text": "error", + "jsx_a11y/anchor-has-content": "error", + "jsx_a11y/anchor-is-valid": "error", + "jsx_a11y/aria-activedescendant-has-tabindex": "error", + "jsx_a11y/aria-props": "error", + "jsx_a11y/aria-proptypes": "error", + "jsx_a11y/aria-role": "error", + "jsx_a11y/aria-unsupported-elements": "error", + "jsx_a11y/autocomplete-valid": "error", + "jsx_a11y/click-events-have-key-events": "error", + "jsx_a11y/heading-has-content": "error", + "jsx_a11y/html-has-lang": "error", + "jsx_a11y/iframe-has-title": "error", + "jsx_a11y/img-redundant-alt": "error", + "jsx_a11y/label-has-associated-control": "error", + "jsx_a11y/media-has-caption": "error", + "jsx_a11y/mouse-events-have-key-events": "error", + "jsx_a11y/no-access-key": "error", + "jsx_a11y/no-autofocus": [ + "error", + { + "ignoreNonDOM": true, + }, + ], + "jsx_a11y/no-distracting-elements": "error", + "jsx_a11y/no-noninteractive-tabindex": [ + "error", + { + "tags": [], + "roles": ["tabpanel"], + "allowExpressionValues": true, + }, + ], + "jsx_a11y/no-redundant-roles": "error", + "jsx_a11y/no-static-element-interactions": [ + "error", + { + "allowExpressionValues": true, + "handlers": ["onClick", "onMouseDown", "onMouseUp", "onKeyPress", "onKeyDown", "onKeyUp"], + }, + ], + "jsx_a11y/role-has-required-aria-props": "error", + "jsx_a11y/role-supports-aria-props": "error", + "jsx_a11y/scope": "error", + "jsx_a11y/tabindex-no-positive": "error", + + // ── Lingui rules ── + "lingui/t-call-in-function": "error", + "lingui/no-single-tag-to-translate": "error", + "lingui/no-single-variables-to-translate": "error", + "lingui/no-trans-inside-trans": "error", + "lingui/no-expression-in-message": "off", + + // ── No relative import paths ── + "no-relative-import-paths/no-relative-import-paths": "error", + + // ── Unused imports ── + "unused-imports/no-unused-imports": "error", + + // ── No .only in tests ── + "no-only-tests/no-only-tests": "error", + + // ── License header ── + "@tony.ganchev/header/header": [ + "error", + "block", + [ + "", + " * Copyright (C) Contributors to the Suwayomi project", + " *", + " * This Source Code Form is subject to the terms of the Mozilla Public", + " * License, v. 2.0. If a copy of the MPL was not distributed with this", + " * file, You can obtain one at https://mozilla.org/MPL/2.0/.", + " ", + ], + 2, + ], + }, + "settings": { + "jsx-a11y": { + "components": {}, + "attributes": {}, + }, + "next": { + "rootDir": [], + }, + "react": { + "formComponents": [], + "linkComponents": [], + "version": "19.2.0", + "componentWrapperFunctions": [], + }, + "jsdoc": { + "ignorePrivate": false, + "ignoreInternal": false, + "ignoreReplacesDocs": true, + "overrideReplacesDocs": true, + "augmentsExtendsReplacesDocs": false, + "implementsReplacesDocs": false, + "exemptDestructuredRootsFromChecks": false, + "tagNamePreference": {}, + }, + "vitest": { + "typecheck": false, + }, + }, + "env": { + "browser": true, + "builtin": true, + }, + "globals": {}, + "overrides": [ + { + "files": [ + "src/base/utils/MediaQuery.tsx", + "src/features/authentication/AuthManager.ts", + "src/features/chapter/services/Chapters.ts", + "src/features/metadata/services/MetadataMigrations.ts", + "src/features/migration/MigrationManager.ts", + "src/features/reader/services/ReaderControls.ts", + "src/features/reader/services/ReaderService.ts", + "src/features/source/services/Sources.ts", + "src/lib/dnd-kit/DndKitUtil.ts", + "src/lib/requests/RequestManager.ts", + "src/lib/requests/client/GraphQLClient.ts", + "src/lib/virtuoso/Virtuoso.util.tsx", + ], + "rules": { + "react/rules-of-hooks": "off", + }, + }, + { + "files": ["**/*.config.ts", "**/*.config.js"], + "rules": { + "import/no-default-export": "off", + }, + }, + { + "files": ["tools/scripts/**/*"], + "env": { + "node": true, + }, + "rules": { + "no-console": "off", + "no-relative-import-paths/no-relative-import-paths": "off", + }, + }, ], - "jsx_a11y/no-redundant-roles": "error", - "jsx_a11y/no-static-element-interactions": [ - "error", - { - "allowExpressionValues": true, - "handlers": [ - "onClick", - "onMouseDown", - "onMouseUp", - "onKeyPress", - "onKeyDown", - "onKeyUp" - ] - } - ], - "jsx_a11y/role-has-required-aria-props": "error", - "jsx_a11y/role-supports-aria-props": "error", - "jsx_a11y/scope": "error", - "jsx_a11y/tabindex-no-positive": "error", - - // ── Lingui rules ── - "lingui/t-call-in-function": "error", - "lingui/no-single-tag-to-translate": "error", - "lingui/no-single-variables-to-translate": "error", - "lingui/no-trans-inside-trans": "error", - "lingui/no-expression-in-message": "off", - - // ── No relative import paths ── - "no-relative-import-paths/no-relative-import-paths": "error", - - // ── Unused imports ── - "unused-imports/no-unused-imports": "error", - - // ── No .only in tests ── - "no-only-tests/no-only-tests": "error", - - // ── License header ── - "@tony.ganchev/header/header": ["error", "block", [ - "", - " * Copyright (C) Contributors to the Suwayomi project", - " *", - " * This Source Code Form is subject to the terms of the Mozilla Public", - " * License, v. 2.0. If a copy of the MPL was not distributed with this", - " * file, You can obtain one at https://mozilla.org/MPL/2.0/.", - " " - ], 2] - }, - "settings": { - "jsx-a11y": { - "components": {}, - "attributes": {} - }, - "next": { - "rootDir": [] - }, - "react": { - "formComponents": [], - "linkComponents": [], - "version": "19.2.0", - "componentWrapperFunctions": [] - }, - "jsdoc": { - "ignorePrivate": false, - "ignoreInternal": false, - "ignoreReplacesDocs": true, - "overrideReplacesDocs": true, - "augmentsExtendsReplacesDocs": false, - "implementsReplacesDocs": false, - "exemptDestructuredRootsFromChecks": false, - "tagNamePreference": {} - }, - "vitest": { - "typecheck": false - } - }, - "env": { - "browser": true, - "builtin": true - }, - "globals": {}, - "overrides": [ - { - "files": [ - "src/base/utils/MediaQuery.tsx", - "src/features/authentication/AuthManager.ts", - "src/features/chapter/services/Chapters.ts", - "src/features/metadata/services/MetadataMigrations.ts", - "src/features/migration/MigrationManager.ts", - "src/features/reader/services/ReaderControls.ts", - "src/features/reader/services/ReaderService.ts", - "src/features/source/services/Sources.ts", - "src/lib/dnd-kit/DndKitUtil.ts", - "src/lib/requests/RequestManager.ts", - "src/lib/requests/client/GraphQLClient.ts", - "src/lib/virtuoso/Virtuoso.util.tsx" - ], - "rules": { - "react/rules-of-hooks": "off" - } - }, - { - "files": [ - "**/*.config.ts", - "**/*.config.js" - ], - "rules": { - "import/no-default-export": "off" - } - }, - { - "files": [ - "tools/scripts/**/*" - ], - "env": { - "node": true - }, - "rules": { - "no-console": "off", - "no-relative-import-paths/no-relative-import-paths": "off" - } - } - ], - "ignorePatterns": [ - "src/lib/graphql/generated/**", - "tools/eslint-plugin-header.cjs" - ] + "ignorePatterns": ["src/lib/graphql/generated/**", "tools/eslint-plugin-header.cjs"], } diff --git a/CHANGELOG.md b/CHANGELOG.md index 782d2898..c2de2b09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- (**General**) Fix long press actions on desktop - (**Migration**) Fix aborting bulk migration search/execution while webUI is served on a subpath - (**Migration**) Fix retry button never being shown for failed search/migration entries - (**Migration**) Fix showing an empty "Available" source group header when all sources are selected diff --git a/src/base/hooks/usePress.ts b/src/base/hooks/usePress.ts new file mode 100644 index 00000000..da30c1e9 --- /dev/null +++ b/src/base/hooks/usePress.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +import { useCallback, useRef } from 'react'; +import type { + LongPressCallbackMeta, + LongPressMouseHandlers, + LongPressPointerHandlers, + LongPressReactEvents, + LongPressResult, + LongPressTouchHandlers, +} from 'use-long-press'; +// oxlint-disable-next-line no-restricted-imports +import { useLongPress } from 'use-long-press'; + +export type UsePressResult = LongPressResult< + (LongPressPointerHandlers | LongPressMouseHandlers | LongPressTouchHandlers) & { + onClick: (event: React.MouseEvent | React.TouchEvent) => void; + } +>; + +export const usePress = ( + options: Omit[1], 'onCancel' | 'onStart'> & { + onLongPress: NonNullable[0]>; + onPress: (event: React.MouseEvent | React.TouchEvent) => void; + }, +): UsePressResult => { + const { onLongPress, onPress, ...actualOptions } = options; + + const hasLongPressRef = useRef(false); + + const onCancel = useCallback(() => { + if (hasLongPressRef.current) { + hasLongPressRef.current = false; + } + }, []); + + const bind = useLongPress( + useCallback( + (event: LongPressReactEvents, meta: LongPressCallbackMeta) => { + hasLongPressRef.current = true; + onLongPress(event, meta); + }, + [onLongPress], + ), + { + ...actualOptions, + onCancel, + onStart: onCancel, + }, + ); + + return useCallback( + (context?: unknown) => ({ + ...bind(context), + onClick: (event: React.MouseEvent | React.TouchEvent) => { + if (!hasLongPressRef.current) { + onPress(event); + } else { + event.preventDefault(); + } + }, + }), + [bind, onPress], + ); +}; diff --git a/src/features/chapter/components/cards/ChapterCard.tsx b/src/features/chapter/components/cards/ChapterCard.tsx index ad3bec93..1b1e3c3e 100644 --- a/src/features/chapter/components/cards/ChapterCard.tsx +++ b/src/features/chapter/components/cards/ChapterCard.tsx @@ -18,7 +18,6 @@ import type { MouseEvent, TouchEvent } from 'react'; import React, { memo, useRef } from 'react'; import { Link } from 'react-router-dom'; import PopupState, { bindMenu, bindTrigger } from 'material-ui-popup-state'; -import { useLongPress } from 'use-long-press'; import { useLingui } from '@lingui/react/macro'; import { CustomTooltip } from '@/base/components/CustomTooltip.tsx'; import { getDateString } from '@/base/utils/DateHelper.ts'; @@ -43,6 +42,7 @@ import type { } from '@/features/chapter/Chapter.types.ts'; import { MediaQuery } from '@/base/utils/MediaQuery.tsx'; import type { ChapterType } from '@/lib/graphql/generated/graphql-base.types.ts'; +import { usePress } from '@/base/hooks/usePress.ts'; type TChapter = ChapterIdInfo & ChapterMangaInfo & @@ -104,15 +104,18 @@ export const ChapterCard = memo((props: IProps) => { onSelect(chapter.id, !selected, event.shiftKey); }; - const longPressBind = useLongPress((event, { context: openMenu }) => { - if (!isSelecting && !!menuButtonRef.current) { - handleClickOpenMenu(event, () => (openMenu as (event: Element) => void)?.(menuButtonRef.current!)); - return; - } + const longPressBind = usePress({ + onLongPress: (event, { context: openMenu }) => { + if (!isSelecting && !!menuButtonRef.current) { + handleClickOpenMenu(event, () => (openMenu as (event: Element) => void)?.(menuButtonRef.current!)); + return; + } - // oxlint-disable-next-line no-param-reassign - event.shiftKey = true; - handleClick(event); + // oxlint-disable-next-line no-param-reassign + event.shiftKey = true; + handleClick(event); + }, + onPress: handleClick, }); return ( @@ -136,7 +139,6 @@ export const ChapterCard = memo((props: IProps) => { }} state={Chapters.getReaderOpenChapterLocationState(chapter, true)} replace={mode === 'reader'} - onClick={(e) => handleClick(e)} {...longPressBind(popupState.open)} > diff --git a/src/features/manga/Manga.types.ts b/src/features/manga/Manga.types.ts index add823a4..20a5b29b 100644 --- a/src/features/manga/Manga.types.ts +++ b/src/features/manga/Manga.types.ts @@ -6,7 +6,6 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import type { LongPressPointerHandlers, LongPressResult } from 'use-long-press'; import type { PopupState } from 'material-ui-popup-state/hooks'; import type { JSX } from 'react'; import type { SelectableCollectionReturnType } from '@/base/collection/hooks/useSelectableCollection.ts'; @@ -29,6 +28,7 @@ import type { SourceType, TrackRecordType, } from '@/lib/graphql/generated/graphql-base.types.ts'; +import type { UsePressResult } from '@/base/hooks/usePress.ts'; export type MangaCardMode = 'default' | 'source' | 'migrate.select.bulk' | 'migrate.select.single' | 'duplicate'; @@ -75,9 +75,8 @@ export interface MangaCardProps { export type SpecificMangaCardProps = Omit & Pick, 'isInLibrary'> & { manga: MangaCardSpecificProps; - longPressBind: LongPressResult; + longPressBind: UsePressResult; popupState: PopupState; - handleClick: (event: React.MouseEvent | React.TouchEvent) => void; mangaLinkTo: string; continueReadingButton: JSX.Element; mangaBadges: JSX.Element; diff --git a/src/features/manga/components/cards/MangaCard.tsx b/src/features/manga/components/cards/MangaCard.tsx index f6982547..99f5f67e 100644 --- a/src/features/manga/components/cards/MangaCard.tsx +++ b/src/features/manga/components/cards/MangaCard.tsx @@ -8,7 +8,6 @@ import PopupState, { bindMenu } from 'material-ui-popup-state'; import { memo, useCallback, useMemo } from 'react'; -import { useLongPress } from 'use-long-press'; import type { SingleModeProps } from '@/features/manga/components/MangaActionMenuItems.tsx'; import { MangaActionMenuItems } from '@/features/manga/components/MangaActionMenuItems.tsx'; import { Menu } from '@/base/components/menu/Menu.tsx'; @@ -32,6 +31,7 @@ import { MangaMigration } from '@/features/migration/MangaMigration.ts'; import { MANGA_ACTION_TO_TRANSLATION } from '@/features/manga/Manga.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { assertIsDefined } from '@/base/Asserts.ts'; +import { usePress } from '@/base/hooks/usePress.ts'; const getMangaLinkTo = (mode: MangaCardMode, mangaId: number): string => { switch (mode) { @@ -149,8 +149,8 @@ export const MangaCard = memo((props: MangaCardProps) => { [mode, selected, updateLibraryState, handleSelection, migrationSourceMangaId], ); - const longPressBind = useLongPress( - useCallback( + const longPressBind = usePress({ + onLongPress: useCallback( (e: any, { context }: any) => { // oxlint-disable-next-line no-param-reassign e.shiftKey = true; @@ -158,7 +158,8 @@ export const MangaCard = memo((props: MangaCardProps) => { }, [handleClick], ), - ); + onPress: handleClick, + }); const MangaCardComponent = useMemo( () => (gridLayout === GridLayout.List ? MangaListCard : MangaGridCard), @@ -173,7 +174,6 @@ export const MangaCard = memo((props: MangaCardProps) => { {...props} longPressBind={longPressBind} popupState={popupState} - handleClick={handleClick} mangaLinkTo={mangaLinkTo} isInLibrary={isInLibrary} inLibraryIndicator={inLibraryIndicator} diff --git a/src/features/manga/components/cards/MangaGridCard.tsx b/src/features/manga/components/cards/MangaGridCard.tsx index a6ba9e9e..4a282eaf 100644 --- a/src/features/manga/components/cards/MangaGridCard.tsx +++ b/src/features/manga/components/cards/MangaGridCard.tsx @@ -45,7 +45,6 @@ export const MangaGridCard = memo( manga, longPressBind, popupState, - handleClick, mangaLinkTo, selected, inLibraryIndicator, @@ -65,7 +64,6 @@ export const MangaGridCard = memo( popupState.open(optionButtonRef.current))} - onClick={handleClick} to={mangaLinkTo} state={Mangas.createLocationState(manga, mode)} onContextMenu={preventMobileContextMenu} diff --git a/src/features/manga/components/cards/MangaListCard.tsx b/src/features/manga/components/cards/MangaListCard.tsx index 4ef06063..70374f29 100644 --- a/src/features/manga/components/cards/MangaListCard.tsx +++ b/src/features/manga/components/cards/MangaListCard.tsx @@ -26,7 +26,6 @@ export const MangaListCard = memo( manga, longPressBind, popupState, - handleClick, mangaLinkTo, selected, inLibraryIndicator, @@ -48,7 +47,6 @@ export const MangaListCard = memo( component={RouterLink} to={mangaLinkTo} state={Mangas.createLocationState(manga, mode)} - onClick={handleClick} {...longPressBind(() => popupState.open(optionButtonRef.current))} onContextMenu={preventMobileContextMenu} sx={{