diff --git a/.gitignore b/.gitignore index d3c9431..a9557e1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,5 @@ yarn-error.log* .vscode/* .idea stats.html + +/public/moeflow-runtime-config.json diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..626e2f3 --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +locale-json: src/locales/en.json src/locales/zh-cn.json + +locale-json-watch: + watch make locale-json + +format: + npm run format:fix + +build: .PHONY + npm run build + +src/locales/en.json: src/locales/messages.yaml + node_modules/.bin/tsx scripts/generate-locale-json.ts + +src/locales/zh-cn.json: src/locales/messages.yaml + node_modules/.bin/tsx scripts/generate-locale-json.ts + + +.PHONY: diff --git a/package-lock.json b/package-lock.json index 9531b86..065c3db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,6 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.19", - "@gradio/client": "1.14.0", "@jokester/ts-commonutil": "^0.6.1", "@reduxjs/toolkit": "^1.9.7", "@zip.js/zip.js": "^2.7.60", @@ -653,31 +652,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", - "dependencies": { - "cookie": "^0.7.2" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@bundled-es-modules/tough-cookie": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", - "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", - "dependencies": { - "@types/tough-cookie": "^4.0.5", - "tough-cookie": "^4.1.4" - } - }, "node_modules/@ctrl/tinycolor": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", @@ -1468,25 +1442,6 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, - "node_modules/@gradio/client": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@gradio/client/-/client-1.14.0.tgz", - "integrity": "sha512-BqM4D6RNCjInJG0OcGrAbik+DX4kRht2XrB7Mkd6bT5rtP3pUStZFRqb+mOuw+wvZvDfm4K0SngB/9X2i+RtTg==", - "dependencies": { - "@types/eventsource": "^1.1.15", - "bufferutil": "^4.0.7", - "eventsource": "^2.0.2", - "fetch-event-stream": "^0.1.5", - "msw": "^2.2.1", - "semiver": "^1.1.0", - "textlinestream": "^1.1.1", - "typescript": "^5.0.0", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1544,87 +1499,6 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@inquirer/confirm": { - "version": "5.1.9", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.9.tgz", - "integrity": "sha512-NgQCnHqFTjF7Ys2fsqK2WtnA8X1kHyInyG+nMIuHowVTIgIuS10T4AznI/PvbqSpJqjCUqNBlKGh1v3bwLFL4w==", - "dependencies": { - "@inquirer/core": "^10.1.10", - "@inquirer/type": "^3.0.6" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core": { - "version": "10.1.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.10.tgz", - "integrity": "sha512-roDaKeY1PYY0aCqhRmXihrHjoSW2A00pV3Ke5fTpMCkzcGF64R8e0lw3dK+eLEHwS4vB5RnW1wuQmvzoRul8Mw==", - "dependencies": { - "@inquirer/figures": "^1.0.11", - "@inquirer/type": "^3.0.6", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "node_modules/@inquirer/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.11.tgz", - "integrity": "sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.6.tgz", - "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2071,22 +1945,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mswjs/interceptors": { - "version": "0.37.6", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.6.tgz", - "integrity": "sha512-wK+5pLK5XFmgtH3aQ2YVvA3HohS3xqV/OxuVOdNx9Wpnz7VE/fnC+e1A7ln6LFYeck7gOJ/dsZV6OLplOtAJ2w==", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2149,25 +2007,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" - }, "node_modules/@rc-component/portal": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", @@ -2604,11 +2443,6 @@ "integrity": "sha512-e7s6CdDCUoBQdCe62Q6OS+DF68M8+ABxCEMh2Isjt4Fl3xuddljCHMN8mak48AMSVGGwUUtNRaZbkzgL5PEWew==", "dev": true }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -2624,11 +2458,6 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true }, - "node_modules/@types/eventsource": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", - "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==" - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -2739,7 +2568,7 @@ "version": "20.17.41", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.41.tgz", "integrity": "sha512-bOB0a6u/e7Ey/Gyc+ghRg+xoXFGYug4I7pdvwxudh+Ewmk93Z4wTudn4NIKiIRYQyujf9jm2uTBzQK8tg8oUeQ==", - "devOptional": true, + "dev": true, "dependencies": { "undici-types": "~6.19.2" } @@ -2852,22 +2681,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, - "node_modules/@types/statuses": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", - "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==" - }, "node_modules/@types/store": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/store/-/store-2.0.5.tgz", "integrity": "sha512-5NmTKe3GWdOaykzq7no+Ahf6mafJu0oLc9JNhJ3E26+0oFvd6GnksnZQpMXcH526mfG4xDYjFiKzyDL51PzeWQ==", "dev": true }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" - }, "node_modules/@types/uuid": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.8.tgz", @@ -3236,6 +3055,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, "dependencies": { "type-fest": "^0.21.3" }, @@ -3250,6 +3070,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -3258,6 +3079,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4113,18 +3935,6 @@ "node": ">= 6" } }, - "node_modules/bufferutil": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", - "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", - "hasInstallScript": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/cacache": { "version": "16.1.3", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz", @@ -4388,18 +4198,11 @@ "node": ">=6" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "engines": { - "node": ">= 12" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -4413,6 +4216,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4495,6 +4299,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4505,7 +4310,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -4592,14 +4398,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", @@ -4652,6 +4450,7 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -5083,7 +4882,8 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/encoding": { "version": "0.1.13", @@ -5352,6 +5152,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "engines": { "node": ">=6" } @@ -5685,14 +5486,6 @@ "node": ">=0.10.0" } }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -5869,11 +5662,6 @@ "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js." }, - "node_modules/fetch-event-stream": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/fetch-event-stream/-/fetch-event-stream-0.1.5.tgz", - "integrity": "sha512-V1PWovkspxQfssq/NnxoEyQo1DV+MRK/laPuPblIZmSjMN8P5u46OhlFQznSr9p/t0Sp8Uc6SbM3yCMfr0KU8g==" - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6216,6 +6004,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -6482,14 +6271,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/graphql": { - "version": "16.11.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", - "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, "node_modules/hard-rejection": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", @@ -6589,11 +6370,6 @@ "node": ">= 0.4" } }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==" - }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", @@ -7066,6 +6842,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } @@ -7136,11 +6913,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7661,41 +7433,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/babel-plugin-macros": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", - "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "cosmiconfig": "^7.0.0", - "resolve": "^1.19.0" - }, - "engines": { - "node": ">=10", - "npm": ">=6" - } - }, - "node_modules/jest-circus/node_modules/cosmiconfig": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", - "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-circus/node_modules/dedent": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", @@ -7710,17 +7447,6 @@ } } }, - "node_modules/jest-circus/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -8882,68 +8608,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/msw": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.6.tgz", - "integrity": "sha512-P+rwn43ktxN8ghcl8q+hSAUlEi0PbJpDhGmDkw4zeUnRj3hBCVynWD+dTu38yLYKCE9ZF1OYcvpy7CTBRcqkZA==", - "hasInstallScript": true, - "dependencies": { - "@bundled-es-modules/cookie": "^2.0.1", - "@bundled-es-modules/statuses": "^1.0.1", - "@bundled-es-modules/tough-cookie": "^0.1.6", - "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.37.0", - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", - "@types/statuses": "^2.0.4", - "graphql": "^16.8.1", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.3", - "path-to-regexp": "^6.3.0", - "picocolors": "^1.1.1", - "strict-event-emitter": "^0.5.1", - "type-fest": "^4.26.1", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.8.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/msw/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/nan": { "version": "2.22.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", @@ -9066,16 +8730,6 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -9352,11 +9006,6 @@ "readable-stream": "^2.0.1" } }, - "node_modules/outvariant": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", - "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==" - }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -9520,11 +9169,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -9821,17 +9465,6 @@ "dev": true, "optional": true }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" - } - }, "node_modules/pump": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", @@ -9857,6 +9490,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "engines": { "node": ">=6" } @@ -9891,11 +9525,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -11072,15 +10701,11 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" - }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", @@ -11641,14 +11266,6 @@ "compute-scroll-into-view": "^1.0.20" } }, - "node_modules/semiver": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", - "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", - "engines": { - "node": ">=6" - } - }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -12026,14 +11643,6 @@ "stacktrace-gps": "^3.0.4" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/store": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/store/-/store-2.0.12.tgz", @@ -12058,11 +11667,6 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "dev": true }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" - }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -12094,6 +11698,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -12200,6 +11805,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12366,11 +11972,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/textlinestream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/textlinestream/-/textlinestream-1.1.1.tgz", - "integrity": "sha512-iBHbi7BQxrFmwZUQJsT0SjNzlLLsXhvW/kg7EyOMVMBIrlnj/qYofwo1LVLZi+3GbUEo96Iu2eqToI2+lZoAEQ==" - }, "node_modules/throttle-debounce": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", @@ -12538,20 +12139,6 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/trim-newlines": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-4.1.1.tgz", @@ -12724,6 +12311,7 @@ "version": "0.21.3", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, "engines": { "node": ">=10" }, @@ -12815,6 +12403,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12900,7 +12489,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "devOptional": true + "dev": true }, "node_modules/unique-filename": { "version": "2.0.1", @@ -12958,14 +12547,6 @@ "node": ">= 0.6" } }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -13005,15 +12586,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/use-debounce": { "version": "10.0.4", "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", @@ -13520,19 +13092,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -13552,26 +13111,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/ws": { - "version": "8.18.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", - "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -13585,6 +13124,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "engines": { "node": ">=10" } @@ -13595,24 +13135,11 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -13639,6 +13166,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "engines": { "node": ">=12" } @@ -13655,17 +13183,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/zscroller": { "version": "0.4.8", "resolved": "https://registry.npmjs.org/zscroller/-/zscroller-0.4.8.tgz", diff --git a/package.json b/package.json index 079d09e..85e9fe8 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "@fortawesome/free-regular-svg-icons": "^5.15.4", "@fortawesome/free-solid-svg-icons": "^5.15.4", "@fortawesome/react-fontawesome": "^0.1.19", - "@gradio/client": "1.14.0", "@jokester/ts-commonutil": "^0.6.1", "@reduxjs/toolkit": "^1.9.7", "@zip.js/zip.js": "^2.7.60", diff --git a/public/moeflow-runtime-config.sample.json b/public/moeflow-runtime-config.sample.json new file mode 100644 index 0000000..0d063c0 --- /dev/null +++ b/public/moeflow-runtime-config.sample.json @@ -0,0 +1,7 @@ +{ + "comment": "Runtime configuration overrides. See src/configs.tsx", + "moeflowCompanion": { + "gradioUrl": "http://localhost:7860", + "defaultMultimodalModel": "gemini-2.5-flash" + } +} \ No newline at end of file diff --git a/src/apis/index.ts b/src/apis/index.ts index e0c1888..263941a 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -9,7 +9,7 @@ import axios, { import qs from 'qs'; import { createElement } from 'react'; import { Icon } from '../components'; -import { configs, runtimeConfig } from '@/configs'; +import { runtimeConfig } from '@/configs'; import { createDebugLogger } from '@/utils/debug-logger'; import { getIntl } from '@/locales'; import store from '../store'; diff --git a/src/components/admin/AdminSiteSetting.tsx b/src/components/admin/AdminSiteSetting.tsx index f5280fd..2f75daf 100644 --- a/src/components/admin/AdminSiteSetting.tsx +++ b/src/components/admin/AdminSiteSetting.tsx @@ -8,8 +8,8 @@ import { api } from '@/apis'; import { APISiteSetting } from '@/apis/siteSetting'; import { FC } from '@/interfaces'; import { toLowerCamelCase } from '@/utils'; -import { Form } from '@/components/Form'; -import { FormItem } from '@/components/FormItem'; +import { Form } from '@/components/shared-form/Form'; +import { FormItem } from '@/components/shared-form/FormItem'; function textareaToArray(textarea: string): string[] { return textarea.trim() === '' diff --git a/src/components/admin/AdminUserList.tsx b/src/components/admin/AdminUserList.tsx index d03e5ca..b6faf31 100644 --- a/src/components/admin/AdminUserList.tsx +++ b/src/components/admin/AdminUserList.tsx @@ -17,9 +17,9 @@ import type { FilterValue, SorterResult } from 'antd/es/table/interface'; import { api } from '@/apis'; import { toLowerCamelCase } from '@/utils'; import { APIUser } from '@/apis/user'; -import { FormItem } from '@/components/FormItem'; -import { Form } from '@/components/Form'; -import { EmailInput } from '@/components/EmailInput'; +import { FormItem } from '@/components/shared-form/FormItem'; +import { Form } from '@/components/shared-form/Form'; +import { EmailInput } from '@/components/shared-form/EmailInput'; import { EMAIL_REGEX, USER_NAME_REGEX } from '@/utils/regex'; interface TableParams { diff --git a/src/components/DashboardMenu.tsx b/src/components/dashboard/DashboardMenu.tsx similarity index 99% rename from src/components/DashboardMenu.tsx rename to src/components/dashboard/DashboardMenu.tsx index cc12d71..ee0605f 100644 --- a/src/components/DashboardMenu.tsx +++ b/src/components/dashboard/DashboardMenu.tsx @@ -5,12 +5,12 @@ import React from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; -import { Avatar, Dropdown, Icon, ListItem, TeamList, Tooltip } from '.'; +import { Avatar, Dropdown, Icon, ListItem, TeamList, Tooltip } from '..'; import { FC } from '@/interfaces'; import { AppState } from '@/store'; import { resetProjectsState } from '@/store/project/slice'; import { setUserToken, UserState } from '@/store/user/slice'; -import style from '../style'; +import style from '../../style'; import { clickEffect } from '@/utils/style'; import { routes } from '@/pages/routes'; diff --git a/src/components/index.ts b/src/components/index.ts index bff7626..6d99775 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,67 +1,60 @@ export { Icon } from './icon'; -export { ApplicationList } from './ApplicationList'; -export { AuthFormWrapper } from './AuthFormWrapper'; -export { AuthLoginedTip } from './AuthLoginedTip'; -export { Avatar } from './Avatar'; -export { Button } from './Button'; -export { CAPTCHAInput } from './CAPTCHAInput'; -export { CAPTCHAModal } from './CAPTCHAModal'; -export { Content } from './Content'; -export { ContentItem } from './ContentItem'; -export { ContentTitle } from './ContentTitle'; -export { DashboardBox } from './DashboardBox'; -export { DashboardMenu } from './DashboardMenu'; -export { DebounceStatus } from './DebounceStatus'; -export { Dropdown } from './Dropdown'; -export { EmailInput } from './EmailInput'; -export { EmailVCodeInputItem } from './EmailVCodeInputItem'; -export { EmptyTip } from './EmptyTip'; -export { FileCover } from './FileCover'; -export { FileItem } from './FileItem'; -export { FileList } from './FileList'; -export { FileUploadProgress } from './FileUploadProgress'; -export { Form } from './Form'; -export { FormItem } from './FormItem'; -export { GroupJoinForm } from './GroupJoinForm'; -export { Header } from './Header'; +export { ApplicationList } from './shared-member/ApplicationList'; +export { AuthFormWrapper } from './shared-form/AuthFormWrapper'; +export { Avatar } from './shared/Avatar'; +export { Button } from './shared/Button'; +export { CAPTCHAInput } from './shared-form/CAPTCHAInput'; +export { CAPTCHAModal } from './shared-form/CAPTCHAModal'; +export { Content } from './shared/Content'; +export { ContentItem } from './shared/ContentItem'; +export { ContentTitle } from './shared/ContentTitle'; +export { DashboardBox } from './shared/DashboardBox'; +export { DashboardMenu } from './dashboard/DashboardMenu'; +export { DebounceStatus } from './shared/DebounceStatus'; +export { Dropdown } from './shared/Dropdown'; +export { EmailInput } from './shared-form/EmailInput'; +export { EmailVCodeInputItem } from './shared-form/EmailVCodeInputItem'; +export { EmptyTip } from './shared/EmptyTip'; +export { FileList } from './project/FileList'; +export { Form } from './shared-form/Form'; +export { FormItem } from './shared-form/FormItem'; +export { GroupJoinForm } from './shared-form/GroupJoinForm'; +export { Header } from './shared/Header'; export { useHotKey } from './HotKey'; -export { ImageOCRProgress } from './ImageOCRProgress'; -export { InvitationList } from './InvitationList'; -export { InviteUser } from './InviteUser'; -export { Label } from './Label'; -export { LanguageSelect } from './LanguageSelect'; -export { List } from './List'; -export { ListItem, LIST_ITEM_DEFAULT_HEIGHT } from './ListItem'; -export { ListSearchInput } from './ListSearchInput'; -export { ListSkeletonItem } from './ListSkeletonItem'; -export { LoadingIcon } from './LoadingIcon'; -export { MemberList } from './MemberList'; -export { MovableArea, MovableItem } from './Movable'; -export { NavTab } from './NavTab'; -export { NavTabs } from './NavTabs'; -export { Output } from './Output'; -export { OutputList } from './OutputList'; -export { ProjectItem } from './ProjectItem'; -export { ProjectList } from './ProjectList'; -export { ProjectSetCreateForm } from './ProjectSetCreateForm'; -export { ProjectSetEditForm } from './ProjectSetEditForm'; -export { ProjectSetList } from './ProjectSetList'; -export { ProjectSetSettingBase } from './ProjectSetSettingBase'; -export { RoleRadioGroup } from './RoleRadioGroup'; -export { RoleSelect } from './RoleSelect'; -export { Spin } from './Spin'; -export { TabBarM } from './TabBarM'; -export { TeamCreateForm } from './TeamCreateForm'; -export { TeamEditForm } from './TeamEditForm'; -export { TeamInsightProjectList } from './TeamInsightProjectList'; -export { TeamInsightUserList } from './TeamInsightUserList'; -export { TeamList } from './TeamList'; -export { TeamSearchList } from './TeamSearchList'; -export { TeamSettingBase } from './TeamSettingBase'; -export { Tooltip } from './Tooltip'; -export { TranslationProgress } from './TranslationProgress'; -export { TypeRadioGroup } from './TypeRadioGroup'; -export { UserEmailEditForm } from './UserEmailEditForm'; -export { UserInvitationList } from './UserInvitationList'; -export { UserPasswordEditForm } from './UserPasswordEditForm'; -export { VCodeInput } from './VCodeInput'; +export { InvitationList } from './shared-member/InvitationList'; +export { InviteUser } from './shared-member/InviteUser'; +export { Label } from './shared/Label'; +export { LanguageSelect } from './project/LanguageSelect'; +export { List } from './shared/List'; +export { ListItem, LIST_ITEM_DEFAULT_HEIGHT } from './shared/ListItem'; +export { ListSearchInput } from './shared/ListSearchInput'; +export { ListSkeletonItem } from './shared/ListSkeletonItem'; +export { LoadingIcon } from './shared/LoadingIcon'; +export { MemberList } from './shared-form/MemberList'; +export { MovableArea, MovableItem } from './shared/Movable'; +export { NavTab } from './shared/NavTab'; +export { NavTabs } from './shared/NavTabs'; +export { OutputList } from './project/OutputList'; +export { ProjectList } from './project-list/ProjectList'; +export { ProjectSetCreateForm } from './project-set/ProjectSetCreateForm'; +export { ProjectSetEditForm } from './project-set/ProjectSetEditForm'; +export { ProjectSetList } from './project-set/ProjectSetList'; +export { ProjectSetSettingBase } from './project-set/ProjectSetSettingBase'; +export { RoleRadioGroup } from './shared-form/RoleRadioGroup'; +export { RoleSelect } from './shared-form/RoleSelect'; +export { Spin } from './shared/Spin'; +export { TabBarM } from './shared/TabBarM'; +export { TeamCreateForm } from './team/TeamCreateForm'; +export { TeamEditForm } from './team/TeamEditForm'; +export { TeamInsightProjectList } from './team/TeamInsightProjectList'; +export { TeamInsightUserList } from './team/TeamInsightUserList'; +export { TeamList } from './team/TeamList'; +export { TeamSearchList } from './team/TeamSearchList'; +export { TeamSettingBase } from './team/TeamSettingBase'; +export { Tooltip } from './shared/Tooltip'; +export { TranslationProgress } from './shared/TranslationProgress'; +export { TypeRadioGroup } from './shared-form/TypeRadioGroup'; +export { UserEmailEditForm } from './shared-form/UserEmailEditForm'; +export { UserInvitationList } from './shared-member/UserInvitationList'; +export { UserPasswordEditForm } from './shared-form/UserPasswordEditForm'; +export { VCodeInput } from './shared-form/VCodeInput'; diff --git a/src/components/project-file/ImageSelect.tsx b/src/components/project-file/ImageSelect.tsx index dfa48e1..480e3d1 100644 --- a/src/components/project-file/ImageSelect.tsx +++ b/src/components/project-file/ImageSelect.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/core'; import { Button } from 'antd'; import classNames from 'classnames'; -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { useClickAway } from 'react-use'; -import apis from '@/apis'; +import { api } from '@/apis'; import { FC, File } from '@/interfaces'; import { AppState } from '@/store'; import style from '@/style'; @@ -53,7 +53,7 @@ export const ImageSelect: FC = ({ }) => { setLoading(true); if (!currentProject) return; - apis + api.file .getProjectFiles({ projectID: currentProject.id, params: { page, limit }, diff --git a/src/components/project-file/ImageViewer.tsx b/src/components/project-file/ImageViewer.tsx index 7aa439b..ebf40a5 100644 --- a/src/components/project-file/ImageViewer.tsx +++ b/src/components/project-file/ImageViewer.tsx @@ -3,7 +3,7 @@ import { css } from '@emotion/core'; import { Modal, Spin } from 'antd'; import Bowser from 'bowser'; import { debounce } from 'lodash-es'; -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; @@ -18,7 +18,7 @@ import { OnZoomEnd, OnZooming, OnZoomStart, -} from '@/components/Movable'; +} from '@/components/shared/Movable'; import { SOURCE_POSITION_TYPE } from '@/constants/source'; import { FC, File, Source } from '@/interfaces'; import { AppState } from '@/store'; @@ -32,9 +32,11 @@ import { ImageViewerZoomPanel } from './ImageViewerZoomPanel'; import { MovableAreaColorBackground } from './MovableAreaColorBackground'; import { MovableAreaImageBackground } from './MovableAreaImageBackground'; import { MovableLabel } from './MovableLabel'; -import { Tooltip } from '@/components/Tooltip'; +import { Tooltip } from '@/components/shared/Tooltip'; import { routes } from '@/pages/routes'; +import { createDebugLogger } from '@/utils/debug-logger'; +const debugLogger = createDebugLogger('components:project-file:ImageViewer'); /** * 🖥浏览器识别 */ @@ -671,7 +673,7 @@ export const ImageViewer: FC = ({ + /> void; className?: string; } + +const width = IMAGE_COVER.WIDTH; +const height = 240; +const imageHeight = IMAGE_COVER.HEIGHT; + +const fileItemStyle = css` + position: relative; + width: ${width}px; + height: ${height}px; + border-radius: ${style.borderRadiusBase}; + overflow: hidden; + transition: + box-shadow 100ms, + border-color 100ms; + border: 1px solid ${style.borderColorLight}; + .FileItem__ImageOCRProgressWrapper { + display: none; + position: absolute; + top: ${imageHeight - 17}px; + left: 6px; + padding: 3px 5px; + background-color: rgba(255, 255, 255, 0.8); + border-radius: 4px; + border: 1px solid #fff; + } + .FileItem__ImageWrapper { + display: block; + width: ${width - 2}px; + height: ${imageHeight}px; + overflow: hidden; + } + .FileItem__Image { + display: block; + width: ${width - 2}px; + height: ${imageHeight}px; + transition: transform 400ms; + user-select: none; + /* 禁止 iOS 上 Safari/Chrome/Firefox,重按/长按图片弹出菜单 */ + -webkit-touch-callout: none; + } + .FileItem__ImageTip { + width: 100%; + height: 100%; + padding: 20px 10px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + background-color: #f3f3f3; + font-weight: bold; + } + .FileItem__Name { + font-size: 14px; + line-height: 18px; + } + .FileItem__SelectWrapper { + position: absolute; + top: 0; + left: 0; + width: 36px; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + border-radius: ${style.borderRadiusBase} 0 ${style.borderRadiusBase} 0; + background-color: rgba(0, 0, 0, 0.04); + ${clickEffect( + css` + background-color: rgba(0, 0, 0, 0.2); + `, + css` + background-color: rgba(0, 0, 0, 0.4); + `, + )}; + } + .FileItem__Select { + padding: 7px 10px; + } + .FileItem__DeleteButton { + position: absolute; + top: 0; + right: 0; + width: 36px; + height: 36px; + display: flex; + justify-content: center; + align-items: center; + border-radius: 0 ${style.borderRadiusBase} 0 ${style.borderRadiusBase}; + background-color: rgba(0, 0, 0, 0.04); + ${clickEffect( + css` + background-color: rgba(0, 0, 0, 0.2); + `, + css` + background-color: rgba(0, 0, 0, 0.4); + `, + )}; + } + .FileItem__DeleteButtonIcon { + width: 18px; + height: 18px; + color: rgba(255, 255, 255, 0.8); + } + .FileItem__Info { + display: flex; + flex-direction: column; + justify-content: space-between; + height: ${height - imageHeight - 2}px; + padding: 10px; + } + .FileItem__Name { + height: 36px; + overflow: hidden; + word-break: break-all; + } + ${cardClickEffect()}; + ${clickEffect( + css` + .FileItem__Image { + transform: scale(1.1); + } + `, + css` + .FileItem__Image { + transform: scale(1.08); + transition: transform 100ms; + } + `, + )}; +`; /** * 文件条目 */ @@ -53,138 +180,10 @@ export const FileItem: FC = ({ ? file.fileTargetCache!.checkedSourceCount : file.checkedSourceCount; - const width = IMAGE_COVER.WIDTH; - const height = 240; - const imageHeight = IMAGE_COVER.HEIGHT; - return (
diff --git a/src/components/FileList.tsx b/src/components/project/FileList.tsx similarity index 76% rename from src/components/FileList.tsx rename to src/components/project/FileList.tsx index d1dd6a0..0cedc7a 100644 --- a/src/components/FileList.tsx +++ b/src/components/project/FileList.tsx @@ -1,16 +1,16 @@ import { css, Global } from '@emotion/core'; import { Button as AntdButton, Drawer, message, Modal, Spin } from 'antd'; -import { CancelToken } from 'axios'; import loadImage from 'blueimp-load-image'; import classNames from 'classnames'; -import React, { useEffect, useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { FilePond } from 'react-filepond'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Button, EmptyTip, FileItem, List, OutputList } from '.'; -import { api, resultTypes } from '../apis'; -import { runtimeConfig } from '@/configs'; +import { Button, EmptyTip, List } from '@/components'; +import { OutputList } from './OutputList'; +import { FileItem } from './FileItem'; +import { api, resultTypes } from '@/apis'; import { FILE_NOT_EXIST_REASON, FILE_SAFE_STATUS, @@ -19,13 +19,16 @@ import { PARSE_STATUS, PROJECT_PERMISSION, } from '@/constants'; -import { FC, File, Project, Target, Team } from '@/interfaces'; +import { FC, File as MFile, Project, Target } from '@/interfaces'; import { AppState } from '@/store'; import { setFilesState } from '@/store/file/slice'; -import style from '../style'; +import style from '@/style'; import { toLowerCamelCase } from '@/utils'; import { can } from '@/utils/user'; -import { usePromised } from '@jokester/ts-commonutil/lib/react/hook/use-promised'; +import { routes } from '@/pages/routes'; +import { ListPageSpec } from '@/components/shared/List'; +import { FilePondFile } from 'filepond'; +import { createDebugLogger } from '@/utils/debug-logger'; /** 文件列表的属性接口 */ interface FileListProps { @@ -34,6 +37,8 @@ interface FileListProps { target: Target; className?: string; } + +const debugLogger = createDebugLogger('components:project:FileList'); /** * 文件列表 */ @@ -49,23 +54,22 @@ export const FileList: FC = ({ const [loading, setLoading] = useState(true); const [listMode] = useState<'image' | 'text'>('image'); const [total, setTotal] = useState(0); // 元素总个数 - const runtimeConfigLoaded = usePromised(runtimeConfig); - const uploadAPI = - runtimeConfigLoaded.fulfilled && - `${runtimeConfigLoaded.value.baseURL}/v1/projects/${project.id}/files`; + const runtimeConfig = useSelector( + (state: AppState) => state.site.runtimeConfig, + ); + const uploadAPI = `${runtimeConfig.baseURL}/v1/projects/${project.id}/files`; const token = useSelector((state: AppState) => state.user.token); const platform = useSelector((state: AppState) => state.site.platform); const isMobile = platform === 'mobile'; const [outputDrawerVisible, setOutputDrawerVisible] = useState(false); const coverWidth = IMAGE_COVER.WIDTH; const coverHeight = IMAGE_COVER.HEIGHT; + // const [aiTranslateAvailable, startAiTranslate, modalContextHolder] = useMoeflowCompanionAiTranslate(); - const [items, setItems] = useState([]); + const [items, setItems] = useState([]); const [spinningIDs, setSpinningIDs] = useState([]); // 删除请求中 const filePondRef = useRef(); - - const [team, setTeam] = useState(); - const currentTeam = useSelector((state: AppState) => state.team.currentTeam); + const currentPageSpecRef = useRef(null); const defaultPage = useSelector( (state: AppState) => state.file.filesState.page, @@ -80,23 +84,11 @@ export const FileList: FC = ({ (state: AppState) => state.file.filesState.selectedFileIds, ); - useEffect(() => { - if (!currentTeam) { - api.project.getProject({ id: project.id }).then((result) => { - const data = toLowerCamelCase(result.data); - setTeam(data.team); - }); - } else { - setTeam(currentTeam); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [project.id]); - - const toTranslator = (file: File) => { - history.push(`/image-translator/${file.id}-${target?.id}`); + const openInTranslator = (file: MFile) => { + history.push(routes.imageTranslator.build(file.id, target.id)); }; - const deleteFile = (file: File) => { + const deleteFile = (file: MFile) => { Modal.confirm({ title: formatMessage({ id: 'project.deleteFile' }), content: formatMessage( @@ -129,19 +121,71 @@ export const FileList: FC = ({ }); }; + /** 处理文件添加 */ + const handleFileAdd = (error: any, file: FilePondFile) => { + // 加载预览图 + const realFile = file.file; + if (/^image.+/.test(file.file.type)) { + loadImage( + realFile, + (img) => { + setItems((items) => + items.map((item) => { + if (item.id === file.id && img) { + return { + ...item, + coverUrl: (img as HTMLCanvasElement).toDataURL(), + }; + } + return item; + }), + ); + }, + { + maxWidth: coverWidth, + maxHeight: coverHeight, + crop: true, + canvas: true, + ...({ imageSmoothingQuality: 'high' } as any), // @type 版本太旧 + }, + ); + } + const uploadingFile: MFile = { + // this is the random id generated by filepond + id: file.id, + name: file.filename, + saveName: '', + type: FILE_TYPE.IMAGE, + sourceCount: 0, + translatedSourceCount: 0, + checkedSourceCount: 0, + fileNotExistReason: FILE_NOT_EXIST_REASON.UNKNOWN, + safeStatus: FILE_SAFE_STATUS.NEED_MACHINE_CHECK, + parseStatus: PARSE_STATUS.NOT_START, + parseStatusDetailName: formatMessage({ id: 'file.parseNotStart' }), + parseErrorTypeDetailName: '', + url: '', + parentID: null, + fileTargetCache: { + translatedSourceCount: 0, + checkedSourceCount: 0, + }, + uploading: true, + uploadState: 'uploading', + uploadPercent: 0, + }; + setItems((items) => [uploadingFile, ...items]); + }; + /** 获取元素 */ - const handleChange = ({ - page, - pageSize, - word, - cancelToken, - }: { - page: number; - pageSize: number; - word?: string; - cancelToken: CancelToken; - }) => { + const loadPage = ({ page, pageSize, word, cancelToken }: ListPageSpec) => { setLoading(true); + currentPageSpecRef.current = { + page, + pageSize, + word: word || '', + cancelToken, + }; return api.file .getProjectFiles({ projectID: project.id, @@ -149,14 +193,14 @@ export const FileList: FC = ({ page, limit: pageSize, word, - target: target?.id, + target: target.id, }, configs: { cancelToken, }, }) .then((result) => { - const data = (result.data as File[]).map((d) => toLowerCamelCase(d)); + const data = (result.data as MFile[]).map((d) => toLowerCamelCase(d)); setItems(data); setTotal(Number(result.headers['x-pagination-count'])); setLoading(false); @@ -170,6 +214,7 @@ export const FileList: FC = ({ }); }; /* + const startOCR = () => { Modal.confirm({ title: formatMessage({ id: 'project.startOCR' }), @@ -195,10 +240,7 @@ export const FileList: FC = ({ }; // */ - const handleOutputDrawerOpen = () => { - setOutputDrawerVisible(true); - }; - + debugLogger('items', items); return (
= ({ flex-direction: column; justify-content: flex-start; align-items: stretch; - .FileList__ImageOCRProgressWrapper { - width: 100%; - } - .FileList__ImageOCRProgress { - padding: 0 ${style.paddingBase}px; - } .FileList__Header { width: 100%; height: 45px; @@ -267,62 +303,16 @@ export const FileList: FC = ({ dropOnElement={false} allowMultiple maxParallelUploads={5} - onaddfile={(_, file) => { - // 加载预览图 - const realFile = file.file; - if (/^image.+/.test(file.file.type)) { - loadImage( - realFile, - (img) => { - setItems((items) => { - return items.map((item) => { - if (item.id === file.id && img) { - item.coverUrl = (img as HTMLCanvasElement).toDataURL(); - } - return item; - }); - }); - }, - { - maxWidth: coverWidth, - maxHeight: coverHeight, - crop: true, - canvas: true, - ...({ imageSmoothingQuality: 'high' } as any), // @type 版本太旧 - }, - ); - } - const uploadingFile: File = { - id: file.id, - name: file.filename, - saveName: '', - type: FILE_TYPE.IMAGE, - sourceCount: 0, - translatedSourceCount: 0, - checkedSourceCount: 0, - fileNotExistReason: FILE_NOT_EXIST_REASON.UNKNOWN, - safeStatus: FILE_SAFE_STATUS.NEED_MACHINE_CHECK, - parseStatus: PARSE_STATUS.NOT_START, - parseStatusDetailName: formatMessage({ id: 'file.parseNotStart' }), - parseErrorTypeDetailName: '', - url: '', - parentID: null, - fileTargetCache: { - translatedSourceCount: 0, - checkedSourceCount: 0, - }, - uploading: true, - uploadState: 'uploading', - uploadPercent: 0, - }; - setItems((items) => [uploadingFile, ...items]); - }} + onaddfile={handleFileAdd} // 上传中 onprocessfileprogress={(file, progress) => { setItems((items) => items.map((item) => { if (item.id === file.id) { - item.uploadPercent = Math.floor(progress * 100); + return { + ...item, + uploadPercent: Math.floor(progress * 100), + }; } return item; }), @@ -331,22 +321,25 @@ export const FileList: FC = ({ // 上传成功 onprocessfile={(error, file) => { if (error) return; - const result = toLowerCamelCase(JSON.parse(file.serverId) as File); + const result = toLowerCamelCase(JSON.parse(file.serverId) as MFile); setItems((items) => { // 覆盖时删除列表中原来的文件 const itemsWithoutSameID = items.filter( (item) => item.id !== result.id, ); - return itemsWithoutSameID.map((item) => { + const updated = itemsWithoutSameID.map((item) => { if (item.id === file.id) { - item = { + // return a new MFile item , overwriting its (Filepond-created) id + return { ...item, ...result, uploadState: 'success', - }; + } as MFile; } return item; }); + debugLogger('updated on upload success', updated); + return updated; }); }} // 上传失败 @@ -355,7 +348,10 @@ export const FileList: FC = ({ setItems((items) => items.map((item) => { if (item.id === file.id) { - item.uploadState = 'failure'; + return { + ...item, + uploadState: 'failure', + }; } return item; }), @@ -376,22 +372,23 @@ export const FileList: FC = ({ iconProps={{ style: { height: '16px', width: '16px' }, }} - onClick={() => { - onChangeTargetClick && onChangeTargetClick(); - }} + onClick={onChangeTargetClick} > {(!isMobile ? formatMessage({ id: 'project.changeTarget' }) + ' - ' : '') + target?.language.i18nName} - {/* {can(team, TEAM_PERMISSION.USE_OCR_QUOTA) && ( + {false && ( + > + {formatMessage({ id: 'fileList.aiTranslate' })} + + )} + {/* {can(team, TEAM_PERMISSION.USE_OCR_QUOTA) && ( )} */} {/* */} {can(project, PROJECT_PERMISSION.OUTPUT_TRA) && ( - )} {can(project, PROJECT_PERMISSION.ADD_FILE) && ( @@ -475,7 +472,7 @@ export const FileList: FC = ({ = ({ onClick={() => { (file.uploadState === undefined || file.uploadState === 'success') && - toTranslator(file); + openInTranslator(file); }} selectVisible={can(project, PROJECT_PERMISSION.OUTPUT_TRA)} selected={selectedFileIds.includes(file.id)} @@ -566,7 +563,7 @@ export const FileList: FC = ({ ; + +interface TranslatorFunc { + (files: MFile[], target: Target): void; +} +function openTranslateModal( + files: MFile[], + target: Target, + service: MoeflowCompanionService, + modal: ModalStaticFunctions, +) { + const handle = modal.confirm({ + content: ( + handle} + /> + ), + okButtonProps: { disabled: true }, + onOk: () => { + console.log('ok'); + }, + onCancel: () => { + console.log('cancel'); + }, + }); +} + +export function useMoeflowCompanionAiTranslate(): + | [true, TranslatorFunc, React.ReactNode] + | [false, null, null] { + const [serviceState, service] = useMoeflowCompanion(); + const [modal, contextHolder] = Modal.useModal(); + + debugLogger('service', serviceState, service); + if (serviceState !== moeflowCompanionServiceState.connected) { + return [false, null, null]; + } + + return [ + true, + (files, target) => + openTranslateModal( + files, + target, + service!, + modal as ModalStaticFunctions, + ), + contextHolder, + ]; +} + +interface TranslateTaskState { + file: MFile; + status: string; +} + +function clipTo01(x: number) { + return Math.max(0, Math.min(1, x)); +} + +const ModalContent: FC<{ + service: MoeflowCompanionService; + files: MFile[]; + target: Target; + getHandle(): ModalHandle; +}> = ({ + service: { client, serviceConf, multimodalTranslate }, + files, + target, + getHandle, +}) => { + const intl = useIntl(); + const [fileStates, setFileStates] = useState(() => + files.map((file) => ({ file, status: 'waiting' })), + ); + useAsyncEffect(async (running, released) => { + const [cancelToken, fillCancelToken] = getCancelToken(); + const fileLimiter = ResourcePool.multiple([1, 2]); + const moeflowApiLimiter = ResourcePool.multiple([1, 2, 3, 4]); + const abort = new AbortController(); + released.then(() => fillCancelToken('unmounted')); + released.then(() => abort.abort('unmounted')); + + if (!running.current) { + debugLogger('canceled'); + return; + } + const tasksEnded = Promise.allSettled([ + files.map((f, idx) => fileLimiter.use(() => translateFile(f, idx))), + ]); + const cancelled = await Promise.race([ + released.then(() => true), + tasksEnded.then(() => false), + ]); + if (!cancelled) { + const handle = getHandle(); + handle.update({ okButtonProps: { disabled: false } }); + } + return; + + function setFileState(f: MFile, status: string) { + setFileStates((prev) => + prev.map((state) => (state.file === f ? { ...state, status } : state)), + ); + } + + async function translateFile(f: MFile, idx: number) { + setFileState(f, 'working'); + if (![undefined, null, 'success'].includes(f.uploadState)) { + setFileState(f, 'skip: upload not finished'); + return; + } + const refetchRes = await api.file + .getFile({ fileID: f.id }) + .catch(() => null); + if (refetchRes?.type !== resultTypes.SUCCESS) { + setFileState(f, 'skip: fetch file failed'); + return; + } + const resData = toLowerCamelCase(refetchRes.data); + if (resData.sourceCount) { + setFileState(f, 'skip: source count not 0'); + } + const imgBlob = await fetch(resData.url!, { + // mode: 'no-cors', + }).then( + (r) => r.blob(), + () => null, + ); + if (!imgBlob) { + setFileState(f, 'skip: fetch image blob failed'); + return; + } + + const result = await multimodalTranslate( + client, + [imgBlob], + target.language.enName, + serviceConf!.defaultMultimodalModel!, + ).catch((e) => { + debugLogger('translate failed', e); + return []; + }); + debugLogger('translate result', result); + + const [r] = result; + + if (r) { + await saveTranslations(f, r); + } else { + setFileState(f, 'error: translate failed'); + } + } + + async function saveTextBlock( + f: MFile, + tf: TranslatedFile, + tb: TranslatedFile['text_blocks'][number], + ) { + const src = await api.source.createSource({ + fileID: f.id, + data: { + x: clipTo01((tb.left + tb.right) / 2 / tf.image_w), + y: clipTo01((tb.top + tb.bottom) / 2 / tf.image_h), + content: tb.source, + }, + configs: { cancelToken }, + }); + await api.translation.createTranslation({ + sourceID: src.data.id, + data: { + content: tb.translated, + targetID: target.id, + }, + configs: { cancelToken }, + }); + } + + async function saveTranslations(f: MFile, r: TranslatedFile) { + if (r.text_blocks.length === 0) { + setFileState(f, 'done: no text blocks'); + } + setFileState(f, 'saving'); + try { + await Promise.all( + r.text_blocks.map((tb) => + moeflowApiLimiter.use(() => saveTextBlock(f, r, tb)), + ), + ); + setFileState( + f, + `success: translated ${r.text_blocks.length} text marks`, + ); + } catch (e) { + debugLogger('save text block failed', e); + setFileState(f, 'save file failed'); + } + } + }, []); + return ( +
+ {files.length} files to translate +
    + {fileStates.map((state) => ( +
  • + {state.file.name} - {state.status} +
  • + ))} +
+
+ ); +}; diff --git a/src/components/FileUploadProgress.tsx b/src/components/project/FileUploadProgress.tsx similarity index 95% rename from src/components/FileUploadProgress.tsx rename to src/components/project/FileUploadProgress.tsx index 2e5e07d..3e7a54d 100644 --- a/src/components/FileUploadProgress.tsx +++ b/src/components/project/FileUploadProgress.tsx @@ -1,10 +1,9 @@ import { css } from '@emotion/core'; -import { Icon } from '.'; +import { Icon } from '@/components'; import classNames from 'classnames'; -import React from 'react'; import { useIntl } from 'react-intl'; -import { FC, File } from '../interfaces'; -import style from '../style'; +import { FC, File } from '@/interfaces'; +import style from '@/style'; /** 文件上传进度的属性接口 */ interface FileUploadProgressProps { file: File; diff --git a/src/components/LanguageSelect.tsx b/src/components/project/LanguageSelect.tsx similarity index 97% rename from src/components/LanguageSelect.tsx rename to src/components/project/LanguageSelect.tsx index 25ccdd5..2900dea 100644 --- a/src/components/LanguageSelect.tsx +++ b/src/components/project/LanguageSelect.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/core'; import { Select } from 'antd'; import { SelectProps } from 'antd/lib/select'; import classNames from 'classnames'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { api } from '@/apis'; import { FC } from '@/interfaces'; diff --git a/src/components/Output.tsx b/src/components/project/Output.tsx similarity index 91% rename from src/components/Output.tsx rename to src/components/project/Output.tsx index acdbc20..a0937b8 100644 --- a/src/components/Output.tsx +++ b/src/components/project/Output.tsx @@ -1,14 +1,13 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; import dayjs from 'dayjs'; -import React from 'react'; import { useIntl } from 'react-intl'; -import { Avatar } from '.'; -import { APIOutput } from '../apis/output'; -import { OUTPUT_STATUS, OUTPUT_TYPE } from '../constants/output'; -import { FC } from '../interfaces'; -import style from '../style'; -import { Button } from './Button'; +import { Avatar } from '..'; +import { APIOutput } from '../../apis/output'; +import { OUTPUT_STATUS, OUTPUT_TYPE } from '../../constants/output'; +import { FC } from '../../interfaces'; +import style from '../../style'; +import { Button } from '../shared/Button'; /** 导出的属性接口 */ interface OutputProps { diff --git a/src/components/OutputList.tsx b/src/components/project/OutputList.tsx similarity index 93% rename from src/components/OutputList.tsx rename to src/components/project/OutputList.tsx index a39ce7a..5bafa93 100644 --- a/src/components/OutputList.tsx +++ b/src/components/project/OutputList.tsx @@ -2,17 +2,17 @@ import { css } from '@emotion/core'; import { Switch } from 'antd'; import { Canceler } from 'axios'; import classNames from 'classnames'; -import React, { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; -import apis from '../apis'; -import { APIOutput, CreateOutputData } from '../apis/output'; -import { OUTPUT_STATUS, OUTPUT_TYPE } from '../constants/output'; -import { FC } from '../interfaces'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; -import { getCancelToken } from '../utils/api'; -import { useStateRef } from '../hooks'; -import { Button } from './Button'; +import apis from '@/apis'; +import { APIOutput, CreateOutputData } from '@/apis/output'; +import { OUTPUT_STATUS, OUTPUT_TYPE } from '@/constants/output'; +import { FC } from '@/interfaces'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; +import { getCancelToken } from '@/utils/api'; +import { useStateRef } from '@/hooks'; +import { Button } from '@/components'; import { Output } from './Output'; /** 模板的属性接口 */ diff --git a/src/components/project-set/ProjectCreateForm.tsx b/src/components/project/ProjectCreateForm.tsx similarity index 96% rename from src/components/project-set/ProjectCreateForm.tsx rename to src/components/project/ProjectCreateForm.tsx index d022bee..354563e 100644 --- a/src/components/project-set/ProjectCreateForm.tsx +++ b/src/components/project/ProjectCreateForm.tsx @@ -2,14 +2,9 @@ import { css } from '@emotion/core'; import { Button, Form as AntdForm, Input, message, Modal, Upload } from 'antd'; import { useState } from 'react'; import { useIntl } from 'react-intl'; -import { - Form, - FormItem, - RoleRadioGroup, - TypeRadioGroup, - LanguageSelect, -} from '..'; -import api from '@/apis'; +import { Form, FormItem, RoleRadioGroup, TypeRadioGroup } from '@/components'; +import { LanguageSelect } from './LanguageSelect'; +import { api } from '@/apis'; import { FC, UserProjectSet, UserTeam } from '@/interfaces'; import { useDispatch, useSelector } from 'react-redux'; import { createProject, resetProjectsState } from '@/store/project/slice'; @@ -17,10 +12,9 @@ import { useHistory } from 'react-router-dom'; import { AppState } from '@/store'; import { toLowerCamelCase } from '@/utils'; import { GROUP_ALLOW_APPLY_TYPE } from '@/constants'; -import { configs, runtimeConfig } from '@/configs'; -import style from '../../style'; +import { configs } from '@/configs'; +import style from '@/style'; import { resetFilesState } from '@/store/file/slice'; -import { usePromised } from '@jokester/ts-commonutil/lib/react/hook/use-promised'; /** 创建项目表单的属性接口 */ interface ProjectCreateFormProps { @@ -36,7 +30,6 @@ export const ProjectCreateForm: FC = ({ projectSetID, className, }) => { - const runtimeConfigLoaded = usePromised(runtimeConfig); const { formatMessage } = useIntl(); // i18n const [form] = AntdForm.useForm(); const dispatch = useDispatch(); @@ -76,7 +69,7 @@ export const ProjectCreateForm: FC = ({ const handleFinish = (values: any) => { setSubmitting(true); - api + api.project .createProject({ teamID: currentTeam.id, data: { ...values, labelplusTXT }, @@ -114,6 +107,7 @@ export const ProjectCreateForm: FC = ({ margin-bottom: 0; } .ProjectCreateForm__Label { + margin-right: 8px; color: rgba(0, 0, 0, 0.85); } .ProjectCreateForm__Tip { @@ -142,7 +136,7 @@ export const ProjectCreateForm: FC = ({ sourceLanguage: configs.default.project.sourceLanugageCode, targetLanguages: configs.default.project.targetLanguageCodes, }} - hideRequiredMark + requiredMark={false} onValuesChange={(values) => { // 关闭加入时,隐藏加入选项 if (values.allowApplyType) { @@ -158,7 +152,7 @@ export const ProjectCreateForm: FC = ({ if (values.targetLanguages) { setDisableSourceLanugageIDs(values.targetLanguages); } - // 当幕布语言大于 1 个的时候,不显示从“翻译数据.txt”导入 + // 当目标语言大于 1 个的时候,不显示从“翻译数据.txt”导入 if (values.targetLanguages && values.targetLanguages.length > 1) { setSupportLabelplusTXT(false); setLabelplusTXT(undefined); diff --git a/src/components/project-set/ProjectImportForm.tsx b/src/components/project/ProjectImportForm.tsx similarity index 98% rename from src/components/project-set/ProjectImportForm.tsx rename to src/components/project/ProjectImportForm.tsx index c89896b..0a657e6 100644 --- a/src/components/project-set/ProjectImportForm.tsx +++ b/src/components/project/ProjectImportForm.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/core'; import { Button, message, Upload } from 'antd'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { api } from '@/apis'; import { FC, UserProjectSet, UserTeam } from '@/interfaces'; @@ -30,7 +30,6 @@ export const ProjectImportForm: FC = ({ }) => { const { formatMessage } = useIntl(); // i18n const dispatch = useDispatch(); - const history = useHistory(); const [importing, setImporting] = useState(false); const [importStatuses, setImportStatuses] = useState([]); const [importFileList, setImportFileList] = useState(); diff --git a/src/components/project/ProjectSettingTarget.tsx b/src/components/project/ProjectSettingTarget.tsx index aed669b..6fb42dc 100644 --- a/src/components/project/ProjectSettingTarget.tsx +++ b/src/components/project/ProjectSettingTarget.tsx @@ -2,19 +2,19 @@ import { css } from '@emotion/core'; import { Button, message, Modal } from 'antd'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import api, { resultTypes } from '@/apis'; import { EmptyTip, Icon, - LanguageSelect, List, ListItem, LIST_ITEM_DEFAULT_HEIGHT, Spin, } from '@/components'; +import { LanguageSelect } from './LanguageSelect'; import { FC, Target, Project } from '@/interfaces'; import { AppState } from '@/store'; import { increaseCurrentProjectTargetCount } from '@/store/project/slice'; diff --git a/src/components/project/ProjectTargetList.tsx b/src/components/project/ProjectTargetList.tsx index 0fa37ba..4384fb7 100644 --- a/src/components/project/ProjectTargetList.tsx +++ b/src/components/project/ProjectTargetList.tsx @@ -1,21 +1,21 @@ import { css } from '@emotion/core'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; -import api, { resultTypes } from '../../apis'; +import { api, resultTypes } from '@/apis'; import { EmptyTip, List, ListItem, LIST_ITEM_DEFAULT_HEIGHT, TranslationProgress, -} from '..'; -import { FC, Project, Target } from '../../interfaces'; -import { AppState } from '../../store'; -import { toLowerCamelCase } from '../../utils'; -import style from '../../style'; +} from '@/components'; +import { FC, Project, Target } from '@/interfaces'; +import { AppState } from '@/store'; +import { toLowerCamelCase } from '@/utils'; +import style from '@/style'; /** 项目目标列表页的属性接口 */ interface ProjectTargetListProps { @@ -38,7 +38,7 @@ export const ProjectTargetList: FC = ({ const isMobile = platform === 'mobile'; const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); // 元素总个数 - const [items, setItems] = useState([]); // 元素 + const [targetLangs, setTargetLangs] = useState([]); // 元素 /** 获取元素 */ const handleChange = ({ @@ -53,7 +53,7 @@ export const ProjectTargetList: FC = ({ cancelToken: CancelToken; }) => { setLoading(true); - api + api.target .getProjectTargets({ projectID: project.id, params: { @@ -71,7 +71,7 @@ export const ProjectTargetList: FC = ({ setLoading(false); // 转成大写 const items = result.data.map((item: any) => toLowerCamelCase(item)); - setItems(items); + setTargetLangs(items); onLoad && onLoad(items); }) .catch((error) => { @@ -95,7 +95,7 @@ export const ProjectTargetList: FC = ({ searchInputVisible={false} loading={loading} total={total} - items={items} + items={targetLangs} itemHeight={LIST_ITEM_DEFAULT_HEIGHT} itemCreater={(item) => { return ( @@ -123,9 +123,9 @@ export const ProjectTargetList: FC = ({ columnWidth={250} autoPageSize={false} defaultPageSize={100000} - emptyTipCreater={() => { - return ; - }} + emptyTipCreater={() => ( + + )} /> ); }; diff --git a/src/components/setting/LocalePicker.tsx b/src/components/setting/LocalePicker.tsx index db915ed..f7c58a3 100644 --- a/src/components/setting/LocalePicker.tsx +++ b/src/components/setting/LocalePicker.tsx @@ -1,8 +1,7 @@ -import { FC } from '../../interfaces'; +import { FC } from '@/interfaces'; import { MenuProps } from 'antd'; -import { availableLocales, setLocale } from '../../locales'; -import { Dropdown } from '../Dropdown'; -import { Icon } from '../icon'; +import { availableLocales, setLocale } from '@/locales'; +import { Dropdown, Icon } from '@/components'; import { css } from '@emotion/core'; const dropDownMenuItemStyle = css` diff --git a/src/components/setting/UserBasicSettings.tsx b/src/components/setting/UserBasicSettings.tsx index 2f66996..9e9fac6 100644 --- a/src/components/setting/UserBasicSettings.tsx +++ b/src/components/setting/UserBasicSettings.tsx @@ -4,7 +4,7 @@ import { useIntl } from 'react-intl'; import { Content, ContentItem, ContentTitle } from '@/components'; import { FC } from '@/interfaces'; import style from '../../style'; -import { AvatarUpload } from '../AvatarUpload'; +import { AvatarUpload } from '../shared/AvatarUpload'; import { UserEditForm } from './UserEditForm'; import { LocalePicker } from './LocalePicker'; diff --git a/src/components/AuthFormWrapper.tsx b/src/components/shared-form/AuthFormWrapper.tsx similarity index 94% rename from src/components/AuthFormWrapper.tsx rename to src/components/shared-form/AuthFormWrapper.tsx index 6b3c059..c93f376 100644 --- a/src/components/AuthFormWrapper.tsx +++ b/src/components/shared-form/AuthFormWrapper.tsx @@ -1,16 +1,15 @@ import { css, Global } from '@emotion/core'; -import { Icon } from '.'; +import { Icon } from '@/components'; import { Button } from 'antd'; import { FormProps } from 'antd/lib/form'; import classNames from 'classnames'; -import React from 'react'; import { useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { AuthLoginedTip } from './AuthLoginedTip'; -import mascot from '../images/brand/mascot-jump1.png'; -import { AppState } from '../store'; -import style from '../style'; -import { FC } from '../interfaces'; +import mascot from '@/images/brand/mascot-jump1.png'; +import { AppState } from '@/store'; +import style from '@/style'; +import { FC } from '@/interfaces'; /** 身份验证页面通用的表单外框的属性接口 */ interface AuthFormWrapperProps { diff --git a/src/components/AuthLoginedTip.tsx b/src/components/shared-form/AuthLoginedTip.tsx similarity index 89% rename from src/components/AuthLoginedTip.tsx rename to src/components/shared-form/AuthLoginedTip.tsx index 9040037..13da6a9 100644 --- a/src/components/AuthLoginedTip.tsx +++ b/src/components/shared-form/AuthLoginedTip.tsx @@ -1,14 +1,13 @@ import { css } from '@emotion/core'; import { Button } from 'antd'; -import React from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router'; -import { Avatar } from './Avatar'; -import { AppState } from '../store'; -import { setUserToken } from '../store/user/slice'; -import style from '../style'; -import { FC } from '../interfaces'; +import { Avatar } from '@/components'; +import { AppState } from '@/store'; +import { setUserToken } from '@/store/user/slice'; +import style from '@/style'; +import { FC } from '@/interfaces'; /** 已经登陆提示的属性接口 */ interface AuthLoginedTipProps { diff --git a/src/components/CAPTCHAInput.tsx b/src/components/shared-form/CAPTCHAInput.tsx similarity index 95% rename from src/components/CAPTCHAInput.tsx rename to src/components/shared-form/CAPTCHAInput.tsx index fdcf3b7..449bead 100644 --- a/src/components/CAPTCHAInput.tsx +++ b/src/components/shared-form/CAPTCHAInput.tsx @@ -4,12 +4,12 @@ import { CancelToken } from 'axios'; import classNames from 'classnames'; import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; -import api from '../apis'; -import { LoadingIcon } from './LoadingIcon'; -import { getIntl } from '../locales'; -import style from '../style'; -import { getCancelToken } from '../utils/api'; -import { clickEffect, imageClickEffect } from '../utils/style'; +import api from '@/apis'; +import { LoadingIcon } from '@/components'; +import { getIntl } from '@/locales'; +import style from '@/style'; +import { getCancelToken } from '@/utils/api'; +import { clickEffect, imageClickEffect } from '@/utils/style'; /** 用于 Form.Item 校验验证码,rules={[{validator: checkCAPTCHA}]} */ export const checkCAPTCHA = (rule: any, captcha: CAPTCHAInputValue) => { diff --git a/src/components/CAPTCHAModal.tsx b/src/components/shared-form/CAPTCHAModal.tsx similarity index 95% rename from src/components/CAPTCHAModal.tsx rename to src/components/shared-form/CAPTCHAModal.tsx index a9d34e6..363bc50 100644 --- a/src/components/CAPTCHAModal.tsx +++ b/src/components/shared-form/CAPTCHAModal.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/core'; -import { Icon } from '.'; +import { Icon } from '@/components'; import { Button } from 'antd'; import { ValidateStatus as AntdValidateStatus } from 'antd/lib/form/FormItem'; import React, { useEffect, useRef, useState } from 'react'; import { useIntl } from 'react-intl'; -import { CAPTCHAInput } from '../components'; -import { CAPTCHAInputRef, CAPTCHAInputValue } from '../components/CAPTCHAInput'; -import style from '../style'; -import { FC } from '../interfaces'; -import { clickEffect } from '../utils/style'; +import { CAPTCHAInput } from '@/components'; +import { CAPTCHAInputRef, CAPTCHAInputValue } from './CAPTCHAInput'; +import style from '@/style'; +import { FC } from '@/interfaces'; +import { clickEffect } from '@/utils/style'; export type ValidateStatus = AntdValidateStatus; export interface OnSubmit { diff --git a/src/components/EmailInput.tsx b/src/components/shared-form/EmailInput.tsx similarity index 100% rename from src/components/EmailInput.tsx rename to src/components/shared-form/EmailInput.tsx diff --git a/src/components/EmailVCodeInputItem.tsx b/src/components/shared-form/EmailVCodeInputItem.tsx similarity index 98% rename from src/components/EmailVCodeInputItem.tsx rename to src/components/shared-form/EmailVCodeInputItem.tsx index 6659777..a39805a 100644 --- a/src/components/EmailVCodeInputItem.tsx +++ b/src/components/shared-form/EmailVCodeInputItem.tsx @@ -5,11 +5,11 @@ import { InputProps } from 'antd/lib/input'; import classNames from 'classnames'; import React, { useRef, useState, useEffect } from 'react'; import { useIntl } from 'react-intl'; -import api, { FailureResults, resultTypes } from '../apis'; -import style from '../style'; -import { FC } from '../interfaces'; -import { EMAIL_REGEX } from '../utils/regex'; -import { clickEffect } from '../utils/style'; +import api, { FailureResults, resultTypes } from '@/apis'; +import style from '@/style'; +import { FC } from '@/interfaces'; +import { EMAIL_REGEX } from '@/utils/regex'; +import { clickEffect } from '@/utils/style'; import { CAPTCHAInputValue } from './CAPTCHAInput'; import { CAPTCHAModal, OnSubmit, ValidateStatus } from './CAPTCHAModal'; import { EmailInput } from './EmailInput'; diff --git a/src/components/Form.tsx b/src/components/shared-form/Form.tsx similarity index 92% rename from src/components/Form.tsx rename to src/components/shared-form/Form.tsx index f8a4402..eb3c9da 100644 --- a/src/components/Form.tsx +++ b/src/components/shared-form/Form.tsx @@ -1,8 +1,7 @@ import { Form as AntdForm } from 'antd'; import { FormProps as AntdFormProps } from 'antd/lib/form'; import { Store } from 'rc-field-form/es/interface'; -import React from 'react'; -import { FC } from '../interfaces'; +import { FC } from '@/interfaces'; /** 表单的属性接口 */ interface FormProps {} diff --git a/src/components/FormItem.tsx b/src/components/shared-form/FormItem.tsx similarity index 86% rename from src/components/FormItem.tsx rename to src/components/shared-form/FormItem.tsx index 5103771..307061d 100644 --- a/src/components/FormItem.tsx +++ b/src/components/shared-form/FormItem.tsx @@ -1,7 +1,6 @@ import { Form as AntdForm } from 'antd'; import { FormItemProps as AntdFormItemProps } from 'antd/lib/form'; -import React from 'react'; -import { FC } from '../interfaces'; +import { FC } from '@/interfaces'; /** 表单的属性接口 */ interface FormItemProps {} diff --git a/src/components/GroupJoinForm.tsx b/src/components/shared-form/GroupJoinForm.tsx similarity index 91% rename from src/components/GroupJoinForm.tsx rename to src/components/shared-form/GroupJoinForm.tsx index e793138..53f9699 100644 --- a/src/components/GroupJoinForm.tsx +++ b/src/components/shared-form/GroupJoinForm.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/core'; import { Button, Form as AntdForm, Input } from 'antd'; import classNames from 'classnames'; -import React from 'react'; import { useIntl } from 'react-intl'; import { useHistory } from 'react-router-dom'; -import { GroupTypes } from '../apis/type'; -import { FC } from '../interfaces'; -import { ID_REGEX } from '../utils/regex'; +import { GroupTypes } from '@/apis/type'; +import { FC } from '@/interfaces'; +import { ID_REGEX } from '@/utils/regex'; import { Form } from './Form'; import { FormItem } from './FormItem'; diff --git a/src/components/MemberList.tsx b/src/components/shared-form/MemberList.tsx similarity index 92% rename from src/components/MemberList.tsx rename to src/components/shared-form/MemberList.tsx index e079afc..484b140 100644 --- a/src/components/MemberList.tsx +++ b/src/components/shared-form/MemberList.tsx @@ -5,18 +5,25 @@ import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; -import { Avatar, EmptyTip, Icon, List, ListItem, RoleSelect } from '.'; -import api, { resultTypes } from '../apis'; -import { GroupTypes } from '../apis/type'; -import { TEAM_PERMISSION } from '../constants'; -import { FC, Project, Role, UserTeam } from '../interfaces'; -import { AppState } from '../store'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; -import { getCancelToken } from '../utils/api'; -import { can } from '../utils/user'; -import { Spin } from './Spin'; -import { TypeData } from './TypeRadioGroup'; +import { + Avatar, + EmptyTip, + Icon, + List, + ListItem, + RoleSelect, +} from '@/components'; +import api, { resultTypes } from '@/apis'; +import { GroupTypes } from '@/apis/type'; +import { TEAM_PERMISSION } from '@/constants'; +import { FC, Project, Role, UserTeam } from '@/interfaces'; +import { AppState } from '@/store'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; +import { getCancelToken } from '@/utils/api'; +import { can } from '../../utils/user'; +import { Spin } from '@/components'; +import { TypeData } from '@/components/shared-form/TypeRadioGroup'; /** 成员设置页的属性接口 */ interface MemberListProps { diff --git a/src/components/RoleRadioGroup.tsx b/src/components/shared-form/RoleRadioGroup.tsx similarity index 95% rename from src/components/RoleRadioGroup.tsx rename to src/components/shared-form/RoleRadioGroup.tsx index 80010a6..0488970 100644 --- a/src/components/RoleRadioGroup.tsx +++ b/src/components/shared-form/RoleRadioGroup.tsx @@ -2,9 +2,9 @@ import { css, Global } from '@emotion/core'; import { Button, Tag } from 'antd'; import React, { useState } from 'react'; import { useIntl } from 'react-intl'; -import { Icon, Tooltip, TypeRadioGroup } from '.'; -import { GroupTypes } from '../apis/type'; -import { FC } from '../interfaces'; +import { Icon, Tooltip, TypeRadioGroup } from '@/components'; +import { GroupTypes } from '@/apis/type'; +import { FC } from '@/interfaces'; import { TypeRadioGroupProps } from './TypeRadioGroup'; export interface RoleData { diff --git a/src/components/RoleSelect.tsx b/src/components/shared-form/RoleSelect.tsx similarity index 89% rename from src/components/RoleSelect.tsx rename to src/components/shared-form/RoleSelect.tsx index 76a6d0d..fc27d5b 100644 --- a/src/components/RoleSelect.tsx +++ b/src/components/shared-form/RoleSelect.tsx @@ -3,10 +3,10 @@ import { Select } from 'antd'; import { SelectValue } from 'antd/lib/select'; import classNames from 'classnames'; import React from 'react'; -import { TEAM_PERMISSION } from '../constants'; -import { FC, Project, Role, UserTeam } from '../interfaces'; -import { User } from '../interfaces/user'; -import { can } from '../utils/user'; +import { TEAM_PERMISSION } from '@/constants'; +import { FC, Project, Role, UserTeam } from '@/interfaces'; +import { User } from '@/interfaces/user'; +import { can } from '@/utils/user'; const { Option } = Select; diff --git a/src/components/TypeRadioGroup.tsx b/src/components/shared-form/TypeRadioGroup.tsx similarity index 98% rename from src/components/TypeRadioGroup.tsx rename to src/components/shared-form/TypeRadioGroup.tsx index 839f55b..c3d8ae9 100644 --- a/src/components/TypeRadioGroup.tsx +++ b/src/components/shared-form/TypeRadioGroup.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/core'; import { Radio, Skeleton } from 'antd'; import { RadioChangeEvent, RadioGroupProps } from 'antd/lib/radio'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { api } from '@/apis'; import { GroupTypes, TypeNames } from '@/apis/type'; import { configs } from '@/configs'; diff --git a/src/components/UserEmailEditForm.tsx b/src/components/shared-form/UserEmailEditForm.tsx similarity index 93% rename from src/components/UserEmailEditForm.tsx rename to src/components/shared-form/UserEmailEditForm.tsx index 6c39ef5..a2def29 100644 --- a/src/components/UserEmailEditForm.tsx +++ b/src/components/shared-form/UserEmailEditForm.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/core'; import { Button, Form as AntdForm, type InputRef, message } from 'antd'; -import React, { useRef, useState } from 'react'; +import { useRef, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { EmailVCodeInputItem, Form, FormItem, VCodeInput } from '.'; -import api from '../apis'; -import { FC } from '../interfaces'; -import { AppState } from '../store'; -import { setUserInfo } from '../store/user/slice'; -import { toLowerCamelCase } from '../utils'; +import { EmailVCodeInputItem, Form, FormItem, VCodeInput } from '@/components'; +import api from '@/apis'; +import { FC } from '@/interfaces'; +import { AppState } from '@/store'; +import { setUserInfo } from '@/store/user/slice'; +import { toLowerCamelCase } from '../../utils'; /** 修改项目表单的属性接口 */ interface UserEmailEditFormProps { diff --git a/src/components/UserPasswordEditForm.tsx b/src/components/shared-form/UserPasswordEditForm.tsx similarity index 90% rename from src/components/UserPasswordEditForm.tsx rename to src/components/shared-form/UserPasswordEditForm.tsx index df8c9a2..eb7c2d1 100644 --- a/src/components/UserPasswordEditForm.tsx +++ b/src/components/shared-form/UserPasswordEditForm.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/core'; import { Button, Form as AntdForm, Input, message } from 'antd'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Form, FormItem } from '.'; -import api from '../apis'; -import { FC } from '../interfaces'; -import { setUserToken } from '../store/user/slice'; -import { toLowerCamelCase } from '../utils'; +import { Form, FormItem } from '@/components'; +import api from '@/apis'; +import { FC } from '@/interfaces'; +import { setUserToken } from '@/store/user/slice'; +import { toLowerCamelCase } from '@/utils'; /** 修改项目表单的属性接口 */ interface UserPasswordEditFormProps { diff --git a/src/components/VCodeInput.tsx b/src/components/shared-form/VCodeInput.tsx similarity index 100% rename from src/components/VCodeInput.tsx rename to src/components/shared-form/VCodeInput.tsx diff --git a/src/components/ApplicationList.tsx b/src/components/shared-member/ApplicationList.tsx similarity index 98% rename from src/components/ApplicationList.tsx rename to src/components/shared-member/ApplicationList.tsx index 1622dea..87acf77 100644 --- a/src/components/ApplicationList.tsx +++ b/src/components/shared-member/ApplicationList.tsx @@ -2,10 +2,17 @@ import { css } from '@emotion/core'; import { message } from 'antd'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { Avatar, EmptyTip, Icon, List, ListItem, RoleSelect } from '.'; +import { + Avatar, + EmptyTip, + Icon, + List, + ListItem, + RoleSelect, +} from '@/components'; import { api, resultTypes } from '@/apis'; import { APIApplication } from '@/apis/application'; import { GroupTypes } from '@/apis/type'; @@ -17,12 +24,12 @@ import { import { FC, Project, UserTeam } from '@/interfaces'; import { AppState } from '@/store'; import { setRelatedApplicationsCount } from '@/store/site/slice'; -import style from '../style'; +import style from '@/style'; import { toLowerCamelCase } from '@/utils'; import { formatGroupType } from '@/utils/i18n'; import { clickEffect } from '@/utils/style'; import { can } from '@/utils/user'; -import { Spin } from './Spin'; +import { Spin } from '@/components'; /** 申请管理页的属性接口 */ interface ApplicationListProps { diff --git a/src/components/InvitationList.tsx b/src/components/shared-member/InvitationList.tsx similarity index 94% rename from src/components/InvitationList.tsx rename to src/components/shared-member/InvitationList.tsx index 3c29fc2..783de8c 100644 --- a/src/components/InvitationList.tsx +++ b/src/components/shared-member/InvitationList.tsx @@ -1,24 +1,23 @@ import { css } from '@emotion/core'; -import { Icon } from '.'; +import { Icon, Spin } from '@/components'; import { Drawer, message, Modal, Select } from 'antd'; import { SelectValue } from 'antd/lib/select'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; -import { Avatar, EmptyTip, InviteUser, List, ListItem } from '.'; -import api, { resultTypes } from '../apis'; -import { GroupTypes } from '../apis/type'; -import { AppState } from '../store'; -import style from '../style'; -import { UserTeam, Role, Project } from '../interfaces'; -import { toLowerCamelCase } from '../utils'; -import { getCancelToken } from '../utils/api'; -import { FC } from '../interfaces'; -import { clickEffect } from '../utils/style'; -import { Spin } from './Spin'; -import { INVITATION_STATUS } from '../constants'; +import { Avatar, EmptyTip, InviteUser, List, ListItem } from '@/components'; +import api, { resultTypes } from '@/apis'; +import { GroupTypes } from '@/apis/type'; +import { AppState } from '@/store'; +import style from '@/style'; +import { UserTeam, Role, Project } from '@/interfaces'; +import { toLowerCamelCase } from '@/utils'; +import { getCancelToken } from '@/utils/api'; +import { FC } from '@/interfaces'; +import { clickEffect } from '@/utils/style'; +import { INVITATION_STATUS } from '@/constants'; const { Option } = Select; diff --git a/src/components/InviteUser.tsx b/src/components/shared-member/InviteUser.tsx similarity index 93% rename from src/components/InviteUser.tsx rename to src/components/shared-member/InviteUser.tsx index e68090a..6930aa1 100644 --- a/src/components/InviteUser.tsx +++ b/src/components/shared-member/InviteUser.tsx @@ -1,20 +1,20 @@ import { css } from '@emotion/core'; -import { Icon } from '.'; +import { Icon } from '@/components'; import { message, Select } from 'antd'; import { SelectValue } from 'antd/lib/select'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; -import { Avatar, EmptyTip, List, ListItem } from '.'; -import api, { resultTypes } from '../apis'; -import { GroupTypes } from '../apis/type'; -import { FC, Role, Project, UserTeam } from '../interfaces'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; -import { getCancelToken } from '../utils/api'; -import { Spin } from './Spin'; -import { LIST_ITEM_DEFAULT_HEIGHT } from './ListItem'; +import { Avatar, EmptyTip, List, ListItem } from '@/components'; +import api, { resultTypes } from '@/apis'; +import { GroupTypes } from '@/apis/type'; +import { FC, Role, Project, UserTeam } from '@/interfaces'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; +import { getCancelToken } from '@/utils/api'; +import { Spin } from '@/components'; +import { LIST_ITEM_DEFAULT_HEIGHT } from '@/components/shared/ListItem'; const { Option } = Select; diff --git a/src/components/UserInvitationList.tsx b/src/components/shared-member/UserInvitationList.tsx similarity index 94% rename from src/components/UserInvitationList.tsx rename to src/components/shared-member/UserInvitationList.tsx index aaa0fb2..0b71d70 100644 --- a/src/components/UserInvitationList.tsx +++ b/src/components/shared-member/UserInvitationList.tsx @@ -2,21 +2,21 @@ import { css } from '@emotion/core'; import { message } from 'antd'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { Avatar, EmptyTip, Icon, List, ListItem } from '.'; -import api, { resultTypes } from '../apis'; -import { APIInvitation } from '../apis/invitation'; -import { INVITATION_STATUS } from '../constants'; -import { FC, UserTeam } from '../interfaces'; -import { AppState } from '../store'; -import { setNewInvitationsCount } from '../store/site/slice'; -import { createTeam } from '../store/team/slice'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; -import { clickEffect } from '../utils/style'; -import { Spin } from './Spin'; +import { Avatar, EmptyTip, Icon, List, ListItem } from '@/components'; +import api, { resultTypes } from '@/apis'; +import { APIInvitation } from '@/apis/invitation'; +import { INVITATION_STATUS } from '@/constants'; +import { FC, UserTeam } from '@/interfaces'; +import { AppState } from '@/store'; +import { setNewInvitationsCount } from '@/store/site/slice'; +import { createTeam } from '@/store/team/slice'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; +import { clickEffect } from '@/utils/style'; +import { Spin } from '@/components'; /** 申请管理页的属性接口 */ interface UserInvitationListProps { diff --git a/src/components/Avatar.tsx b/src/components/shared/Avatar.tsx similarity index 90% rename from src/components/Avatar.tsx rename to src/components/shared/Avatar.tsx index 032b50c..aa68191 100644 --- a/src/components/Avatar.tsx +++ b/src/components/shared/Avatar.tsx @@ -3,8 +3,8 @@ import { Avatar as AntdAvatar, Badge } from 'antd'; import { AvatarProps as AntdAvatarProps } from 'antd/lib/avatar'; import React from 'react'; import { useIntl } from 'react-intl'; -import defaultTeamAvatar from '../images/common/default-team-avatar.jpg'; -import defaultUserAvatar from '../images/common/default-user-avatar.jpg'; +import defaultTeamAvatar from '@/images/common/default-team-avatar.jpg'; +import defaultUserAvatar from '@/images/common/default-user-avatar.jpg'; /** 头像的属性接口 */ interface AvatarProps { diff --git a/src/components/AvatarUpload.tsx b/src/components/shared/AvatarUpload.tsx similarity index 91% rename from src/components/AvatarUpload.tsx rename to src/components/shared/AvatarUpload.tsx index 482bca0..7be510b 100644 --- a/src/components/AvatarUpload.tsx +++ b/src/components/shared/AvatarUpload.tsx @@ -1,18 +1,18 @@ import { css } from '@emotion/core'; import React, { useState } from 'react'; import { useIntl } from 'react-intl'; -import { FC } from '../interfaces'; +import { FC } from '../../interfaces'; import classNames from 'classnames'; import ImgCrop from 'antd-img-crop'; import { Button, Upload } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; -import { runtimeConfig } from '../configs'; +import { runtimeConfig } from '../../configs'; import { useDispatch, useSelector } from 'react-redux'; -import { AppState } from '../store'; -import { setUserInfo } from '../store/user/slice'; -import { setCurrentTeamInfo } from '../store/team/slice'; -import { Avatar } from '.'; +import { AppState } from '../../store'; +import { setUserInfo } from '../../store/user/slice'; +import { setCurrentTeamInfo } from '../../store/team/slice'; +import { Avatar } from '..'; import { usePromised } from '@jokester/ts-commonutil/lib/react/hook/use-promised'; /** 头像上传的属性接口 */ interface AvatarUploadProps { diff --git a/src/components/Button.tsx b/src/components/shared/Button.tsx similarity index 98% rename from src/components/Button.tsx rename to src/components/shared/Button.tsx index c0104ce..72b5f87 100644 --- a/src/components/Button.tsx +++ b/src/components/shared/Button.tsx @@ -3,9 +3,9 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import type React from 'react'; -import { Icon } from '.'; +import { Icon } from '..'; import { FC } from '@/interfaces'; -import style from '../style'; +import style from '../../style'; import { clickEffect } from '@/utils/style'; import { Tooltip, TooltipProps } from './Tooltip'; diff --git a/src/components/Content.tsx b/src/components/shared/Content.tsx similarity index 83% rename from src/components/Content.tsx rename to src/components/shared/Content.tsx index 75fe1c6..1ecea69 100644 --- a/src/components/Content.tsx +++ b/src/components/shared/Content.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; +import { FC } from '@/interfaces'; +import style from '@/style'; /** 一般内容 Body 的属性接口 */ interface ContentProps { diff --git a/src/components/ContentItem.tsx b/src/components/shared/ContentItem.tsx similarity index 84% rename from src/components/ContentItem.tsx rename to src/components/shared/ContentItem.tsx index 2adffae..c0ae494 100644 --- a/src/components/ContentItem.tsx +++ b/src/components/shared/ContentItem.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; +import { FC } from '@/interfaces'; +import style from '@/style'; /** 一般内容体中一行 的属性接口 */ interface ContentItemProps { diff --git a/src/components/ContentTitle.tsx b/src/components/shared/ContentTitle.tsx similarity index 86% rename from src/components/ContentTitle.tsx rename to src/components/shared/ContentTitle.tsx index feb7112..389d675 100644 --- a/src/components/ContentTitle.tsx +++ b/src/components/shared/ContentTitle.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; +import { FC } from '@/interfaces'; +import style from '@/style'; /** 一般内容标题的属性接口 */ interface ContentTitleProps { diff --git a/src/components/DashboardBox.tsx b/src/components/shared/DashboardBox.tsx similarity index 97% rename from src/components/DashboardBox.tsx rename to src/components/shared/DashboardBox.tsx index 8abdaf8..99ee461 100644 --- a/src/components/DashboardBox.tsx +++ b/src/components/shared/DashboardBox.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/core'; import React from 'react'; import { FC } from '@/interfaces'; -import style from '../style'; +import style from '../../style'; /** 带有导航栏的布局的属性接口 */ interface DashboardBoxProps { diff --git a/src/components/DebounceStatus.tsx b/src/components/shared/DebounceStatus.tsx similarity index 97% rename from src/components/DebounceStatus.tsx rename to src/components/shared/DebounceStatus.tsx index fb7fddd..618c1f8 100644 --- a/src/components/DebounceStatus.tsx +++ b/src/components/shared/DebounceStatus.tsx @@ -1,10 +1,9 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; import { useIntl } from 'react-intl'; -import { Icon } from '.'; +import { Icon } from '@/components'; import { FC, InputDebounceStatus } from '@/interfaces'; -import style from '../style'; +import style from '@/style'; /** 输入框防抖状态的属性接口 */ interface DebounceStatusProps { diff --git a/src/components/Dropdown.tsx b/src/components/shared/Dropdown.tsx similarity index 94% rename from src/components/Dropdown.tsx rename to src/components/shared/Dropdown.tsx index a7248d9..69a57c1 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/shared/Dropdown.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/core'; import { Dropdown as AntdDropdown } from 'antd'; import { DropDownProps as AntdDropdownProps } from 'antd/lib/dropdown'; import React from 'react'; -import { FC } from '../interfaces'; +import { FC } from '@/interfaces'; /** 下拉菜单的属性接口 */ interface DropdownProps { diff --git a/src/components/EmptyTip.tsx b/src/components/shared/EmptyTip.tsx similarity index 93% rename from src/components/EmptyTip.tsx rename to src/components/shared/EmptyTip.tsx index 2f4a40b..71a1c71 100644 --- a/src/components/EmptyTip.tsx +++ b/src/components/shared/EmptyTip.tsx @@ -1,8 +1,8 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; import React from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; +import { FC } from '@/interfaces'; +import style from '@/style'; /** 空提示的属性接口 */ interface EmptyTipProps { diff --git a/src/components/Header.tsx b/src/components/shared/Header.tsx similarity index 92% rename from src/components/Header.tsx rename to src/components/shared/Header.tsx index cdc7057..529c629 100644 --- a/src/components/Header.tsx +++ b/src/components/shared/Header.tsx @@ -1,19 +1,19 @@ import { css } from '@emotion/core'; import { MenuProps } from 'antd'; -import { Icon } from './icon'; +import { Icon } from '@/components'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import { Avatar } from './Avatar'; import { Dropdown } from './Dropdown'; -import { AppState } from '../store'; -import { setUserToken } from '../store/user/slice'; -import style from '../style'; -import { FC } from '../interfaces'; -import { clickEffect } from '../utils/style'; +import { AppState } from '@/store'; +import { setUserToken } from '@/store/user/slice'; +import style from '@/style'; +import { FC } from '@/interfaces'; +import { clickEffect } from '@/utils/style'; import classNames from 'classnames'; -import { routes } from '../pages/routes'; -import { LocalePicker } from './setting/LocalePicker'; +import { routes } from '@/pages/routes'; +import { LocalePicker } from '@/components/setting/LocalePicker'; /** 头部的属性接口 */ interface HeaderProps { diff --git a/src/components/Label.tsx b/src/components/shared/Label.tsx similarity index 97% rename from src/components/Label.tsx rename to src/components/shared/Label.tsx index b09f682..e4804b9 100644 --- a/src/components/Label.tsx +++ b/src/components/shared/Label.tsx @@ -1,18 +1,17 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; import { useSelector } from 'react-redux'; -import { AppState } from '../store'; -import style from '../style'; +import { AppState } from '@/store'; +import style from '@/style'; import { Direction, FC, labelSavingStatuses, LabelStatus, WritingMode, -} from '../interfaces'; -import { Spin } from './Spin'; -import { SOURCE_POSITION_TYPE } from '../constants/source'; +} from '@/interfaces'; +import { Spin } from '@/components/shared/Spin'; +import { SOURCE_POSITION_TYPE } from '@/constants/source'; import { useIntl } from 'react-intl'; /** 标签的属性接口 */ diff --git a/src/components/List.tsx b/src/components/shared/List.tsx similarity index 97% rename from src/components/List.tsx rename to src/components/shared/List.tsx index 6030bcc..cff85ba 100644 --- a/src/components/List.tsx +++ b/src/components/shared/List.tsx @@ -8,11 +8,18 @@ import React, { useEffect, useRef, useState } from 'react'; import { useSelector } from 'react-redux'; import ReactResizeDetector from 'react-resize-detector'; import { useDebouncedCallback } from 'use-debounce'; -import { ListSearchInput, ListSkeletonItem } from '.'; -import { AppState } from '../store'; -import { getCancelToken } from '../utils/api'; +import { ListSearchInput, ListSkeletonItem } from '@/components'; +import { AppState } from '@/store'; +import { getCancelToken } from '@/utils/api'; import { ListSearchInputProps } from './ListSearchInput'; +export interface ListPageSpec { + page: number; + pageSize: number; + word: string; + cancelToken: CancelToken; +} + /** 列表的属性接口 */ interface ListProps { id?: string; @@ -21,12 +28,7 @@ interface ListProps { pageSize, word, cancelToken, - }: { - page: number; - pageSize: number; - word: string; - cancelToken: CancelToken; - }) => Promise | void; + }: ListPageSpec) => Promise | void; loading: boolean; onSearchRightButtonClick?: (e: React.MouseEvent) => void; total: number; diff --git a/src/components/ListItem.tsx b/src/components/shared/ListItem.tsx similarity index 97% rename from src/components/ListItem.tsx rename to src/components/shared/ListItem.tsx index a3c32ff..6e7e05a 100644 --- a/src/components/ListItem.tsx +++ b/src/components/shared/ListItem.tsx @@ -2,9 +2,9 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; import React, { useEffect, useState } from 'react'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; -import { FC } from '../interfaces'; -import style from '../style'; -import { clickEffect } from '../utils/style'; +import { FC } from '@/interfaces'; +import style from '@/style'; +import { clickEffect } from '@/utils/style'; export const LIST_ITEM_DEFAULT_HEIGHT = 45; /** 列表元素的属性接口 */ diff --git a/src/components/ListSearchInput.tsx b/src/components/shared/ListSearchInput.tsx similarity index 96% rename from src/components/ListSearchInput.tsx rename to src/components/shared/ListSearchInput.tsx index 5d5d70a..ebbb7c3 100644 --- a/src/components/ListSearchInput.tsx +++ b/src/components/shared/ListSearchInput.tsx @@ -3,9 +3,9 @@ import { Input } from 'antd'; import { SearchProps } from 'antd/lib/input/Search'; import classNames from 'classnames'; import React, { useState } from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; -import { clickEffect } from '../utils/style'; +import { FC } from '@/interfaces'; +import style from '@/style'; +import { clickEffect } from '@/utils/style'; /** 列表搜索框的属性接口 */ export interface ListSearchInputProps extends SearchProps { diff --git a/src/components/ListSkeletonItem.tsx b/src/components/shared/ListSkeletonItem.tsx similarity index 87% rename from src/components/ListSkeletonItem.tsx rename to src/components/shared/ListSkeletonItem.tsx index 438a132..7688b2c 100644 --- a/src/components/ListSkeletonItem.tsx +++ b/src/components/shared/ListSkeletonItem.tsx @@ -1,9 +1,8 @@ import { css } from '@emotion/core'; import { Skeleton } from 'antd'; -import React from 'react'; -import { FC } from '../interfaces'; -import style from '../style'; -import { listItemStyle } from '../utils/style'; +import { FC } from '@/interfaces'; +import style from '@/style'; +import { listItemStyle } from '@/utils/style'; /** 列表元素骨架的属性接口 */ interface ListSkeletonItemProps { diff --git a/src/components/LoadingIcon.tsx b/src/components/shared/LoadingIcon.tsx similarity index 100% rename from src/components/LoadingIcon.tsx rename to src/components/shared/LoadingIcon.tsx diff --git a/src/components/Movable.tsx b/src/components/shared/Movable.tsx similarity index 99% rename from src/components/Movable.tsx rename to src/components/shared/Movable.tsx index 10b0c9a..c05abbf 100644 --- a/src/components/Movable.tsx +++ b/src/components/shared/Movable.tsx @@ -10,8 +10,8 @@ import React, { useMemo, useRef, } from 'react'; -import { FC } from '../interfaces'; -import { useStateRef } from '../hooks'; +import { FC } from '../../interfaces'; +import { useStateRef } from '../../hooks'; /** * ========== Context ========= @@ -963,7 +963,7 @@ const MovableItemWithoutRef: React.ForwardRefRenderFunction< clientY, }; // 设置当前激活的子组件 - onFocusIndexChange && onFocusIndexChange(itemIndex); + onFocusIndexChange && onFocusIndexChange(itemIndex!); // 执行移动开始回调 if (allowMoveRef.current && onMoveStart && button === 0) { onMoveStart(state); diff --git a/src/components/NavTab.tsx b/src/components/shared/NavTab.tsx similarity index 91% rename from src/components/NavTab.tsx rename to src/components/shared/NavTab.tsx index a4c277b..dc71d14 100644 --- a/src/components/NavTab.tsx +++ b/src/components/shared/NavTab.tsx @@ -1,12 +1,11 @@ import { css } from '@emotion/core'; -import React from 'react'; -import { FC } from '../interfaces'; +import { FC } from '@/interfaces'; import { NavLinkProps, NavLink } from 'react-router-dom'; -import style from '../style'; -import { clickEffect } from '../utils/style'; +import style from '@/style'; +import { clickEffect } from '@/utils/style'; import { useSelector } from 'react-redux'; -import { AppState } from '../store'; -import { Icon } from '.'; +import { AppState } from '@/store'; +import { Icon } from '@/components'; import classNames from 'classnames'; /** 带导航的 Tab 的属性接口 */ diff --git a/src/components/NavTabs.tsx b/src/components/shared/NavTabs.tsx similarity index 91% rename from src/components/NavTabs.tsx rename to src/components/shared/NavTabs.tsx index 21beeac..dcab8bf 100644 --- a/src/components/NavTabs.tsx +++ b/src/components/shared/NavTabs.tsx @@ -1,14 +1,14 @@ import { css } from '@emotion/core'; -import { Dropdown, Menu, MenuProps } from 'antd'; +import { Dropdown, MenuProps } from 'antd'; import classNames from 'classnames'; import React, { isValidElement } from 'react'; import { useSelector } from 'react-redux'; import { useMeasure } from 'react-use'; -import { Icon } from '.'; -import { FC } from '../interfaces'; -import { AppState } from '../store'; -import style from '../style'; -import { clickEffect } from '../utils/style'; +import { Icon } from '@/components'; +import { FC } from '@/interfaces'; +import { AppState } from '@/store'; +import style from '@/style'; +import { clickEffect } from '@/utils/style'; /** 标签栏的属性接口 */ interface NavTabsProps { diff --git a/src/components/Spin.tsx b/src/components/shared/Spin.tsx similarity index 92% rename from src/components/Spin.tsx rename to src/components/shared/Spin.tsx index af438ea..a9e0c21 100644 --- a/src/components/Spin.tsx +++ b/src/components/shared/Spin.tsx @@ -1,9 +1,8 @@ import { css } from '@emotion/core'; import { Spin as AntdSpin } from 'antd'; import { SpinProps as AntdSpinProps } from 'antd/lib/spin'; -import React from 'react'; import { LoadingIcon } from './LoadingIcon'; -import { FC } from '../interfaces'; +import { FC } from '@/interfaces'; /** Spin 的属性接口 */ interface SpinProps extends AntdSpinProps { diff --git a/src/components/TabBarM.tsx b/src/components/shared/TabBarM.tsx similarity index 95% rename from src/components/TabBarM.tsx rename to src/components/shared/TabBarM.tsx index 5f44639..4fb2d69 100644 --- a/src/components/TabBarM.tsx +++ b/src/components/shared/TabBarM.tsx @@ -1,13 +1,12 @@ import { css } from '@emotion/core'; import { TabBar } from 'antd-mobile'; -import React from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; -import { Icon } from '.'; -import { FC } from '../interfaces'; -import { AppState } from '../store'; -import style from '../style'; +import { Icon } from '@/components'; +import { FC } from '@/interfaces'; +import { AppState } from '@/store'; +import style from '@/style'; /** 手机版首页底部 TabBar 的属性接口 */ interface TabBarProps { diff --git a/src/components/Tooltip.tsx b/src/components/shared/Tooltip.tsx similarity index 96% rename from src/components/Tooltip.tsx rename to src/components/shared/Tooltip.tsx index 1694288..9e11c26 100644 --- a/src/components/Tooltip.tsx +++ b/src/components/shared/Tooltip.tsx @@ -3,12 +3,11 @@ import { TooltipPropsWithTitle as AntdTooltipPropsWithTitle, TooltipPropsWithOverlay as AntdTooltipPropsWithOverlay, } from 'antd/lib/tooltip'; -import React from 'react'; import { useSelector } from 'react-redux'; import { AppState } from '@/store'; import { FC } from '@/interfaces'; import { css, Global } from '@emotion/core'; -import style from '../style'; +import style from '@/style'; /** * 手机版自动隐藏的 Tooltip diff --git a/src/components/TranslationProgress.tsx b/src/components/shared/TranslationProgress.tsx similarity index 97% rename from src/components/TranslationProgress.tsx rename to src/components/shared/TranslationProgress.tsx index 5db0f90..12ae40c 100644 --- a/src/components/TranslationProgress.tsx +++ b/src/components/shared/TranslationProgress.tsx @@ -1,10 +1,9 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; import { useIntl } from 'react-intl'; -import { Icon, Tooltip } from '.'; -import { FC } from '../interfaces'; -import style from '../style'; +import { Icon, Tooltip } from '@/components'; +import { FC } from '@/interfaces'; +import style from '@/style'; /** 文件翻译的属性接口 */ interface TranslationProgressProps { sourceCount: number; diff --git a/src/components/TeamCreateForm.tsx b/src/components/team/TeamCreateForm.tsx similarity index 91% rename from src/components/TeamCreateForm.tsx rename to src/components/team/TeamCreateForm.tsx index 9f37ba2..a56cf54 100644 --- a/src/components/TeamCreateForm.tsx +++ b/src/components/team/TeamCreateForm.tsx @@ -1,17 +1,17 @@ import { css } from '@emotion/core'; import { Button, Form as AntdForm, Input, message } from 'antd'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Form, FormItem, RoleRadioGroup, TypeRadioGroup } from '.'; -import api from '../apis'; -import { GROUP_ALLOW_APPLY_TYPE } from '../constants'; -import { FC, UserTeam } from '../interfaces'; -import { resetProjectSetsState } from '../store/projectSet/slice'; -import { createTeam } from '../store/team/slice'; -import { toLowerCamelCase } from '../utils'; -import { TEAM_NAME_REGEX } from '../utils/regex'; +import { Form, FormItem, RoleRadioGroup, TypeRadioGroup } from '@/components'; +import api from '@/apis'; +import { GROUP_ALLOW_APPLY_TYPE } from '@/constants'; +import { FC, UserTeam } from '@/interfaces'; +import { resetProjectSetsState } from '@/store/projectSet/slice'; +import { createTeam } from '@/store/team/slice'; +import { toLowerCamelCase } from '@/utils'; +import { TEAM_NAME_REGEX } from '@/utils/regex'; /** 创建团队表单的属性接口 */ interface TeamCreateFormProps { diff --git a/src/components/TeamEditForm.tsx b/src/components/team/TeamEditForm.tsx similarity index 92% rename from src/components/TeamEditForm.tsx rename to src/components/team/TeamEditForm.tsx index 1af4111..ae1d5d5 100644 --- a/src/components/TeamEditForm.tsx +++ b/src/components/team/TeamEditForm.tsx @@ -3,15 +3,15 @@ import { Button, Form as AntdForm, Input, message } from 'antd'; import React, { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; -import { Form, FormItem, RoleRadioGroup, TypeRadioGroup } from '.'; -import api from '../apis'; -import { GROUP_ALLOW_APPLY_TYPE, TEAM_PERMISSION } from '../constants'; -import { FC, UserTeam } from '../interfaces'; -import { AppState } from '../store'; -import { editTeam, setCurrentTeam } from '../store/team/slice'; -import { toLowerCamelCase } from '../utils'; -import { TEAM_NAME_REGEX } from '../utils/regex'; -import { can } from '../utils/user'; +import { Form, FormItem, RoleRadioGroup, TypeRadioGroup } from '@/components'; +import api from '@/apis'; +import { GROUP_ALLOW_APPLY_TYPE, TEAM_PERMISSION } from '@/constants'; +import { FC, UserTeam } from '@/interfaces'; +import { AppState } from '@/store'; +import { editTeam, setCurrentTeam } from '@/store/team/slice'; +import { toLowerCamelCase } from '@/utils'; +import { TEAM_NAME_REGEX } from '@/utils/regex'; +import { can } from '@/utils/user'; /** 修改团队表单的属性接口 */ interface TeamEditFormProps { diff --git a/src/components/TeamInsightProjectList.tsx b/src/components/team/TeamInsightProjectList.tsx similarity index 95% rename from src/components/TeamInsightProjectList.tsx rename to src/components/team/TeamInsightProjectList.tsx index 12f2830..9e18446 100644 --- a/src/components/TeamInsightProjectList.tsx +++ b/src/components/team/TeamInsightProjectList.tsx @@ -8,25 +8,25 @@ import { Tag, message, } from 'antd'; -import { Button as CustomButton } from './Button'; +import { Button as CustomButton } from '../shared/Button'; import classNames from 'classnames'; import produce from 'immer'; import qs from 'qs'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom'; -import { Icon, ListSearchInput } from '.'; -import apis from '../apis'; -import { APIInsightProject, APIInsightUserProject } from '../apis/insight'; -import { usePagination } from '../hooks'; -import { FC, Team } from '../interfaces'; -import { AppState } from '../store'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; +import { Icon, ListSearchInput } from '..'; +import apis from '@/apis'; +import { APIInsightProject, APIInsightUserProject } from '@/apis/insight'; +import { usePagination } from '@/hooks'; +import { FC, Team } from '@/interfaces'; +import { AppState } from '@/store'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; import dayjs from 'dayjs'; -import { OUTPUT_STATUS, OUTPUT_TYPE } from '../constants/output'; -import { clickEffect } from '../utils/style'; +import { OUTPUT_STATUS, OUTPUT_TYPE } from '@/constants/output'; +import { clickEffect } from '@/utils/style'; const { Column } = Table; @@ -324,7 +324,7 @@ export const TeamInsightProjectList: FC = ({ }} /> { diff --git a/src/components/TeamInsightUserList.tsx b/src/components/team/TeamInsightUserList.tsx similarity index 95% rename from src/components/TeamInsightUserList.tsx rename to src/components/team/TeamInsightUserList.tsx index f7aba73..1f7d383 100644 --- a/src/components/TeamInsightUserList.tsx +++ b/src/components/team/TeamInsightUserList.tsx @@ -3,19 +3,19 @@ import { Button, Pagination, Result, Table, Tag } from 'antd'; import classNames from 'classnames'; import produce from 'immer'; import qs from 'qs'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom'; -import { Icon, ListSearchInput } from '.'; -import apis from '../apis'; -import { APIInsightUser } from '../apis/insight'; -import { APIUser } from '../apis/user'; -import { usePagination } from '../hooks'; -import { FC, Team } from '../interfaces'; -import { AppState } from '../store'; -import style from '../style'; -import { toLowerCamelCase } from '../utils'; +import { Icon, ListSearchInput } from '@/components'; +import apis from '@/apis'; +import { APIInsightUser } from '@/apis/insight'; +import { APIUser } from '@/apis/user'; +import { usePagination } from '@/hooks'; +import { FC, Team } from '@/interfaces'; +import { AppState } from '@/store'; +import style from '@/style'; +import { toLowerCamelCase } from '@/utils'; const { Column } = Table; diff --git a/src/components/TeamList.tsx b/src/components/team/TeamList.tsx similarity index 88% rename from src/components/TeamList.tsx rename to src/components/team/TeamList.tsx index bc60dbd..a91287e 100644 --- a/src/components/TeamList.tsx +++ b/src/components/team/TeamList.tsx @@ -1,18 +1,18 @@ import { Button } from 'antd'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { matchPath, useHistory, useLocation } from 'react-router-dom'; -import { Avatar, EmptyTip, Icon, List, ListItem } from '.'; -import api, { resultTypes } from '../apis'; -import { FC, UserTeam } from '../interfaces'; -import { AppState } from '../store'; -import { resetProjectSetsState } from '../store/projectSet/slice'; -import { clearTeams, createTeam, setTeamsState } from '../store/team/slice'; -import { toLowerCamelCase } from '../utils'; -import { LIST_ITEM_DEFAULT_HEIGHT } from './ListItem'; +import { Avatar, EmptyTip, Icon, List, ListItem } from '@/components'; +import { api, resultTypes } from '@/apis'; +import { FC, UserTeam } from '@/interfaces'; +import { AppState } from '@/store'; +import { resetProjectSetsState } from '@/store/projectSet/slice'; +import { clearTeams, createTeam, setTeamsState } from '@/store/team/slice'; +import { toLowerCamelCase } from '@/utils'; +import { LIST_ITEM_DEFAULT_HEIGHT } from '@/components/shared/ListItem'; /** 团队列表的属性接口 */ interface TeamListProps { @@ -54,7 +54,7 @@ export const TeamList: FC = ({ className } = {}) => { }) => { setLoading(true); dispatch(clearTeams()); - return api + return api.team .getUserTeams({ params: { page, diff --git a/src/components/TeamSearchList.tsx b/src/components/team/TeamSearchList.tsx similarity index 93% rename from src/components/TeamSearchList.tsx rename to src/components/team/TeamSearchList.tsx index 563350d..619e082 100644 --- a/src/components/TeamSearchList.tsx +++ b/src/components/team/TeamSearchList.tsx @@ -1,21 +1,21 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Icon } from '.'; +import { Icon } from '@/components'; import { Button, Input, message, Modal } from 'antd'; import { CancelToken } from 'axios'; import classNames from 'classnames'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Avatar, EmptyTip, List, ListItem } from '.'; -import api, { resultTypes } from '../apis'; -import { TEAM_ALLOW_APPLY_TYPE } from '../constants'; -import { AppState } from '../store'; -import { createTeam } from '../store/team/slice'; -import { toLowerCamelCase } from '../utils'; -import { FC, UserTeam } from '../interfaces'; -import { Team } from '../interfaces'; -import { LIST_ITEM_DEFAULT_HEIGHT } from './ListItem'; +import { Avatar, EmptyTip, List, ListItem } from '@/components'; +import api, { resultTypes } from '@/apis'; +import { TEAM_ALLOW_APPLY_TYPE } from '@/constants'; +import { AppState } from '@/store'; +import { createTeam } from '@/store/team/slice'; +import { toLowerCamelCase } from '@/utils'; +import { FC, UserTeam } from '@/interfaces'; +import { Team } from '@/interfaces'; +import { LIST_ITEM_DEFAULT_HEIGHT } from '@/components/shared/ListItem'; /** 搜索团队的属性接口 */ interface TeamSearchListProps { diff --git a/src/components/TeamSettingBase.tsx b/src/components/team/TeamSettingBase.tsx similarity index 94% rename from src/components/TeamSettingBase.tsx rename to src/components/team/TeamSettingBase.tsx index 146be2d..4032856 100644 --- a/src/components/TeamSettingBase.tsx +++ b/src/components/team/TeamSettingBase.tsx @@ -1,23 +1,23 @@ import { css } from '@emotion/core'; -import { FormItem, Icon, Tooltip } from '.'; +import { FormItem, Icon, Tooltip } from '@/components'; import { Button, message, Modal, Tag } from 'antd'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; -import { Content, ContentItem, ContentTitle, TeamEditForm } from '.'; -import api from '../apis'; -import { TEAM_PERMISSION } from '../constants'; -import { AppState } from '../store'; +import { Content, ContentItem, ContentTitle, TeamEditForm } from '@/components'; +import api from '@/apis'; +import { TEAM_PERMISSION } from '@/constants'; +import { AppState } from '@/store'; import { clearCurrentTeam, deleteTeam as deleteTeamActionCreator, -} from '../store/team/slice'; -import style from '../style'; -import { FC, UserTeam } from '../interfaces'; -import { can } from '../utils/user'; +} from '@/store/team/slice'; +import style from '@/style'; +import { FC, UserTeam } from '@/interfaces'; +import { can } from '@/utils/user'; import copy from 'copy-to-clipboard'; -import { AvatarUpload } from './AvatarUpload'; +import { AvatarUpload } from '@/components/shared/AvatarUpload'; /** 团队基础设置的属性接口 */ interface TeamSettingBaseProps { diff --git a/src/components/FileCover.tsx b/src/components/unused/FileCover.tsx similarity index 93% rename from src/components/FileCover.tsx rename to src/components/unused/FileCover.tsx index cd2fca6..9d18ffc 100644 --- a/src/components/FileCover.tsx +++ b/src/components/unused/FileCover.tsx @@ -1,8 +1,7 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; -import React from 'react'; -import { FC, File } from '../interfaces'; -import style from '../style'; +import { FC, File } from '../../interfaces'; +import style from '../../style'; /** 文件封面的属性接口 */ interface FileCoverProps { diff --git a/src/components/ImageOCRProgress.tsx b/src/components/unused/ImageOCRProgress.tsx similarity index 91% rename from src/components/ImageOCRProgress.tsx rename to src/components/unused/ImageOCRProgress.tsx index 747c129..71adb40 100644 --- a/src/components/ImageOCRProgress.tsx +++ b/src/components/unused/ImageOCRProgress.tsx @@ -1,10 +1,10 @@ import { css } from '@emotion/core'; import classNames from 'classnames'; import React from 'react'; -import { Icon, Tooltip } from '.'; -import { ParseStatuses, PARSE_STATUS } from '../constants'; -import { FC } from '../interfaces'; -import style from '../style'; +import { Icon, Tooltip } from '..'; +import { ParseStatuses, PARSE_STATUS } from '../../constants'; +import { FC } from '../../interfaces'; +import style from '../../style'; const HEIGHT = 12; export const IMAGE_OCR_PROGRESS_HEIGHT = HEIGHT; diff --git a/src/configs.tsx b/src/configs.tsx index ee316ab..00e7f8f 100644 --- a/src/configs.tsx +++ b/src/configs.tsx @@ -1,8 +1,10 @@ import { lazyThenable } from '@jokester/ts-commonutil/lib/concurrency/lazy-thenable'; -interface RuntimeConfig { +export interface RuntimeConfig { // base URL for API requests baseURL: string; + + // TODO: more fields can be added here } /** @@ -16,7 +18,7 @@ export const runtimeConfig = lazyThenable(async () => { const overriden: RuntimeConfig = await fetch('/moeflow-runtime-config.json') .then((res) => res.json()) .catch(() => null); - const merged = { + const merged: RuntimeConfig = { ...{ // defaults baseURL: process.env.REACT_APP_BASE_URL || '/api/', diff --git a/src/index.tsx b/src/index.tsx index 9a47988..e7701dd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -11,10 +11,11 @@ import App from './App'; import './fontAwesome'; // Font Awesome import './index.css'; import store from './store'; -import { setOSName, setPlatform } from './store/site/slice'; +import { setOSName, setPlatform, setRuntimeConfig } from './store/site/slice'; import { setUserToken } from './store/user/slice'; import { getToken } from './utils/cookie'; import { OSName, Platform } from './interfaces'; +import { runtimeConfig } from './configs'; import { getDefaultHotKey, hotKeyInitialState, @@ -22,6 +23,8 @@ import { setHotKey, } from './store/hotKey/slice'; import { loadHotKey } from './utils/storage'; +import { createDebugLogger } from './utils/debug-logger'; +const debugLogger = createDebugLogger('app'); // 时间插件 if (false && process.env.NODE_ENV === 'development') { @@ -55,8 +58,10 @@ for (const hotKeyName in hotKeyInitialState) { } async function mountApp() { + store.dispatch(setRuntimeConfig(await runtimeConfig)); const { intlMessages, locale, antdLocale, antdValidateMessages } = await initI18n; + debugLogger('initial state', store.getState()); /** * Set user token from cookie */ diff --git a/src/interfaces/file.ts b/src/interfaces/file.ts index 6127d55..039368b 100644 --- a/src/interfaces/file.ts +++ b/src/interfaces/file.ts @@ -35,9 +35,14 @@ export interface File { prevImage?: File; imageOcrPercent?: number; imageOcrPercentDetailName?: string; + + /** + * NOTE fields below are browser only + */ // 上传中的文件 uploading?: boolean; uploadOverwrite?: boolean; + /** undefined when fetched from server */ uploadState?: 'uploading' | 'success' | 'failure'; uploadPercent?: number; // 1-100 } diff --git a/src/locales/en.json b/src/locales/en.json index 2930c36..792ab6d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,8 +2,8 @@ "imageTranslator.imageViewerZoomPanel.back": "Back", "imageTranslator.imageViewerZoomPanel.setting": "Setting", "imageTranslator.imageViewerZoomPanel.originSize": "Origin Size", - "imageTranslator.imageViewerZoomPanel.fixWidth": "Fix Width", - "imageTranslator.imageViewerZoomPanel.fixHeight": "Fix Height", + "imageTranslator.imageViewerZoomPanel.fixWidth": "Fit Width", + "imageTranslator.imageViewerZoomPanel.fixHeight": "Fit Height", "imageTranslator.imageViewerZoomPanel.zoomIn": "Zoom In", "imageTranslator.imageViewerZoomPanel.zoomOut": "Zoom Out", "auth.login": "Log In", @@ -149,7 +149,7 @@ "projectSet.emptySearchTip": "No Project set contain \"{word}\". Please try a different search term.", "projectSet.default": "Ungrouped", "site.projectSet": "Project sets", - "site.createProjectSet": "Create project set", + "projectSet.createProjectSet": "Create project set", "projectSet.name": "Project set name", "projectSet.info": "Projection set Information", "projectSet.intro": "Project set description", @@ -166,8 +166,8 @@ "project.finish": "Mark Project as complete", "project.name": "Project Name", "project.intro": "Project Description", - "site.createProject": "Create Project", - "site.importProject": "Import Project", + "project.createProject": "Create Project", + "project.importProject": "Import Project", "project.startImport": "Start Import", "project.finishTip": "This action will delete all files and images for this item.", "project.deleteTip": "\"Delete this item permanently after one day, no information will be retained. It is recommended to use the completion function. (You can cancel this operation before it is deleted)\"", @@ -232,7 +232,7 @@ "imageTranslator.sourceViewer.rightClickToMarkSource": "Right click on image to mark out-of-balloon text", "imageTranslator.sourceViewer.rightClickMarkToRemove": "Right click a mark to remove it", "site.noPermission": "No permission", - "site.output": "Export", + "project.export": "Export", "site.loading": "Loading", "output.empty": "No export available", "output.refresh": "Refresh", @@ -263,6 +263,8 @@ "fileList.changeMode": "Toggle Display Mode", "file.parseNotStart": "Auto Mark as Not Started", "fileList.ocrButtonTip": "Image Auto Tagging", + "fileList.aiTranslate": "Auto Translate", + "fileList.aiTranslateTip": "Detect marks and Translate", "project.startOCR": "Start Auto Tagging", "project.startOCRTip": "Do you want to start auto-tagging images? (Only images marked as \"Not Started\" or \"Tagging Failed\" will be auto-tagged, and the quota will only be deducted upon successful tagging)", "site.ocrQuota": "Auto Tagging Limit", @@ -321,13 +323,13 @@ "output.outputPartial": "Export {count} selected files", "output.outputPartialExclude": "Export all except the selected {count} files", "output.invert": "Invert selection", - "project.createViaLabelplus": "Import from LabelPlus txt file:", + "project.createViaLabelplus": "(Optional) Import marks from LabelPlus txt:", "site.delete": "Delete", "project.createViaLabelplusNotSupport": "Unavailable for project of multiple target languages", "file.needUploadTip": "Pending Upload", - "site.selectPageAll": "Select All on This Page", - "site.selectPageInverse": "Deselect This Page", - "site.selectCancel": "Deselect", + "site.selectPageAll": "Select All", + "site.selectPageInverse": "Invert selection", + "site.selectCancel": "Deselect All", "site.selectFile": "Select File", "imageTranslator.translatorMode": "Translations", "imageTranslator.godMode": "Overview", diff --git a/src/locales/messages.yaml b/src/locales/messages.yaml index b4fa568..8902bc0 100644 --- a/src/locales/messages.yaml +++ b/src/locales/messages.yaml @@ -11,10 +11,10 @@ imageTranslator.imageViewerZoomPanel.originSize: en: Origin Size imageTranslator.imageViewerZoomPanel.fixWidth: zhCn: 适应宽度 - en: Fix Width + en: Fit Width imageTranslator.imageViewerZoomPanel.fixHeight: zhCn: 适应高度 - en: Fix Height + en: Fit Height imageTranslator.imageViewerZoomPanel.zoomIn: zhCn: 放大 en: Zoom In @@ -455,7 +455,7 @@ projectSet.default: site.projectSet: zhCn: 项目集 en: Project sets -site.createProjectSet: +projectSet.createProjectSet: zhCn: 创建项目集 en: Create project set projectSet.name: @@ -506,10 +506,10 @@ project.name: project.intro: zhCn: 项目介绍 en: Project Description -site.createProject: +project.createProject: zhCn: 创建项目 en: Create Project -site.importProject: +project.importProject: zhCn: 导入项目 en: Import Project project.startImport: @@ -711,7 +711,7 @@ imageTranslator.sourceViewer.rightClickMarkToRemove: site.noPermission: zhCn: 没有权限 en: No permission -site.output: +project.export: zhCn: 导出 en: Export site.loading: @@ -804,6 +804,12 @@ file.parseNotStart: fileList.ocrButtonTip: zhCn: 图片自动标记 en: Image Auto Tagging +fileList.aiTranslate: + zhCn: 自动翻译 + en: Auto Translate +fileList.aiTranslateTip: + zhCn: 自动识别文字并翻译 + en: Detect marks and Translate project.startOCR: zhCn: 开始自动标记 en: Start Auto Tagging @@ -981,8 +987,8 @@ output.invert: zhCn: 反选 en: Invert selection project.createViaLabelplus: - zhCn: 通过 LabalPlus “翻译数据.txt”创建: - en: 'Import from LabelPlus txt file:' + zhCn: (可选) 通过 LabalPlus “翻译数据.txt”创建: + en: '(Optional) Import marks from LabelPlus txt:' site.delete: zhCn: 删除 en: Delete @@ -994,13 +1000,13 @@ file.needUploadTip: en: Pending Upload site.selectPageAll: zhCn: 全选本页 - en: Select All on This Page + en: Select All site.selectPageInverse: zhCn: 反选本页 - en: Deselect This Page + en: Invert selection site.selectCancel: zhCn: 取消选择 - en: Deselect + en: Deselect All site.selectFile: zhCn: 选择文件 en: Select File diff --git a/src/locales/zh-cn.json b/src/locales/zh-cn.json index 65bfd69..788302f 100644 --- a/src/locales/zh-cn.json +++ b/src/locales/zh-cn.json @@ -149,7 +149,7 @@ "projectSet.emptySearchTip": "没有名称含有 “{word}” 的项目集,请换个搜索词试试。", "projectSet.default": "未分组", "site.projectSet": "项目集", - "site.createProjectSet": "创建项目集", + "projectSet.createProjectSet": "创建项目集", "projectSet.name": "项目集名称", "projectSet.info": "项目集信息", "projectSet.intro": "项目集介绍", @@ -166,8 +166,8 @@ "project.finish": "完结项目", "project.name": "项目名称", "project.intro": "项目介绍", - "site.createProject": "创建项目", - "site.importProject": "导入项目", + "project.createProject": "创建项目", + "project.importProject": "导入项目", "project.startImport": "开始导入", "project.finishTip": "此操作将删除此项目的所有文件和图片。", "project.deleteTip": "一天后彻底删除此项目,不会留存任何信息,推荐使用完结功能。(您可以在正式删除前取消此操作)", @@ -232,7 +232,7 @@ "imageTranslator.sourceViewer.rightClickToMarkSource": "右键点击图片新增框外标记", "imageTranslator.sourceViewer.rightClickMarkToRemove": "右键点击标记来删除标记", "site.noPermission": "没有权限", - "site.output": "导出", + "project.export": "导出", "site.loading": "加载中", "output.empty": "暂无导出", "output.refresh": "刷新", @@ -263,6 +263,8 @@ "fileList.changeMode": "切换显示模式", "file.parseNotStart": "自动标记未开始", "fileList.ocrButtonTip": "图片自动标记", + "fileList.aiTranslate": "自动翻译", + "fileList.aiTranslateTip": "自动识别文字并翻译", "project.startOCR": "开始自动标记", "project.startOCRTip": "您要开始图片自动标记吗?(仅自动标记“未开始”、“标记失败”的图片,且仅成功时扣减限额)", "site.ocrQuota": "自动标记限额", @@ -321,7 +323,7 @@ "output.outputPartial": "导出已选中的 {count} 个文件", "output.outputPartialExclude": "导出除了已选中的 {count} 个文件", "output.invert": "反选", - "project.createViaLabelplus": "通过 LabalPlus “翻译数据.txt”创建:", + "project.createViaLabelplus": "(可选) 通过 LabalPlus “翻译数据.txt”创建:", "site.delete": "删除", "project.createViaLabelplusNotSupport": "多个目标语言时不支持", "file.needUploadTip": "待上传", diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index e3e6530..8e98f8a 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -34,7 +34,7 @@ import MyProject from './MyProject'; import Team from './Team'; import TeamSetting from './TeamSetting'; import UserSetting from './UserSetting'; -import { MENU_COLLAPSED_WIDTH } from '@/components/DashboardMenu'; +import { MENU_COLLAPSED_WIDTH } from '@/components/dashboard/DashboardMenu'; /** 仪表盘的属性接口 */ interface DashboardProps {} diff --git a/src/pages/ImageTranslator.tsx b/src/pages/ImageTranslator.tsx index fd507c2..77aba19 100644 --- a/src/pages/ImageTranslator.tsx +++ b/src/pages/ImageTranslator.tsx @@ -1,13 +1,13 @@ import { css, Global } from '@emotion/core'; import { Modal } from 'antd'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; import { api } from '@/apis'; import { useHotKey } from '@/components'; import { ImageViewer, ImageSourceViewer } from '@/components/project-file'; -import { FC } from '@/interfaces'; +import { FC, Source } from '@/interfaces'; import { AppState } from '@/store'; import { setCurrentProjectSaga } from '@/store/project/slice'; import { fetchSourcesSaga, focusSource } from '@/store/source/slice'; @@ -15,20 +15,20 @@ import style from '../style'; import { toLowerCamelCase } from '@/utils'; import { getCancelToken } from '@/utils/api'; import { useTitle } from '@/hooks'; -import { ImageTranslatorSettingMouse } from '@/components/project-file/ImageTranslatorSettingMouse'; -import { ImageTranslatorSettingHotKey } from '@/components/project-file/ImageTranslatorSettingHotKey'; +import { ImageTranslatorSettingMouse } from '@/components/project-file'; +import { ImageTranslatorSettingHotKey } from '@/components/project-file'; import { GetFileReturn } from '@/apis/file'; /** * 全屏显示的图片翻译器 */ const ImageTranslator: FC = () => { + const dispatch = useDispatch(); const { formatMessage } = useIntl(); const { fileID, targetID } = useParams<{ fileID: string; targetID: string; }>(); - const dispatch = useDispatch(); const sources = useSelector((state: AppState) => state.source.sources); const sourcesLoading = useSelector((state: AppState) => state.source.loading); const focusedSourceID = useSelector( @@ -36,8 +36,6 @@ const ImageTranslator: FC = () => { ); const platform = useSelector((state: AppState) => state.site.platform); const isMobile = platform === 'mobile'; - const osName = useSelector((state: AppState) => state.site.osName); - const isIOS = osName === 'ios'; const [file, setFile] = useState(); const sourceListWidth = 400; const sourceListHeightMobile = 200; @@ -48,144 +46,13 @@ const ImageTranslator: FC = () => { useTitle({ prefix: file?.name }, [file?.name]); // 设置标题 - const focusNextSource = () => { - if (sources.length === 0) { - return; - } - let nextFocusedSourceIndex = 0; - if (focusedSourceID) { - const focusedSourceIndex = sources.findIndex( - (source) => source.id === focusedSourceID, - ); - if (focusedSourceIndex + 1 >= sources.length) { - nextFocusedSourceIndex = 0; - } else { - nextFocusedSourceIndex = focusedSourceIndex + 1; - } - } - const nextFocusedSourceID = sources[nextFocusedSourceIndex].id; - dispatch( - focusSource({ - id: nextFocusedSourceID, - effects: ['focusInput', 'focusLabel', 'scrollIntoView'], - noises: ['focusInput', 'focusLabel'], - }), - ); - }; - - const focusPrevSource = () => { - if (sources.length === 0) { - return; - } - let prevFocusedSourceIndex = sources.length - 1; - if (focusedSourceID) { - const focusedSourceIndex = sources.findIndex( - (source) => source.id === focusedSourceID, - ); - if (focusedSourceIndex - 1 < 0) { - prevFocusedSourceIndex = sources.length - 1; - } else { - prevFocusedSourceIndex = focusedSourceIndex - 1; - } - } - const prevFocusedSourceID = sources[prevFocusedSourceIndex].id; - dispatch( - focusSource({ - id: prevFocusedSourceID, - effects: ['focusInput', 'focusLabel', 'scrollIntoView'], - noises: ['focusInput', 'focusLabel'], - }), - ); - }; - - // 快捷键 - 当 ImageViewer 未加载完成是,忽略所有快捷键 - useHotKey( - { - disabled: Boolean(file?.id), - ignoreKeyboardElement: false, - }, - () => {}, - [file?.id], - ); - - // 快捷键 - 下一个输入框 - const focusNextSourceHotKeyOptions = useSelector( - (state: AppState) => state.hotKey.focusNextSource, - ); - useHotKey( - { - disabled: !Boolean(focusNextSourceHotKeyOptions[0]), - ...focusNextSourceHotKeyOptions[0], - }, - focusNextSource, - [focusedSourceID, sources.length], - ); - useHotKey( - { - disabled: !Boolean(focusNextSourceHotKeyOptions[1]), - ...focusNextSourceHotKeyOptions[1], - }, - focusNextSource, - [focusedSourceID, sources.length], - ); - - // 快捷键 - 上一个输入框 - const focusPrevSourceHotKeyOptions = useSelector( - (state: AppState) => state.hotKey.focusPrevSource, - ); - useHotKey( - { - disabled: !Boolean(focusPrevSourceHotKeyOptions[0]), - ...focusPrevSourceHotKeyOptions[0], - }, - focusPrevSource, - [focusedSourceID, sources.length], - ); - useHotKey( - { - disabled: !Boolean(focusPrevSourceHotKeyOptions[1]), - ...focusPrevSourceHotKeyOptions[1], - }, - focusPrevSource, - [focusedSourceID, sources.length], - ); - - // 页面尺寸 - const [windowSize, setWindowSize] = useState({ - width: 0, - height: 0, - }); - - useEffect(() => { - const handleResize = () => { - const width = window.innerWidth; - const height = window.innerHeight; - setWindowSize({ width, height }); - }; - handleResize(); - window.addEventListener('resize', handleResize); - const setTimeoutHandleResize = () => { - setTimeout(handleResize, 250); - }; - if (isIOS) { - window.addEventListener('focusin', setTimeoutHandleResize); - window.addEventListener('focusout', setTimeoutHandleResize); - } - return () => { - window.removeEventListener('resize', handleResize); - if (isIOS) { - window.removeEventListener('focusin', setTimeoutHandleResize); - window.removeEventListener('focusout', setTimeoutHandleResize); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - + useImageTranslatorHotkeys(file, sources, focusedSourceID); // 翻译器尺寸 const [imageTranslatorSize, setImageTranslatorSize] = useState({ width: 0, height: 0, }); + const windowSize = useWindowSize(); useEffect(() => { setImageTranslatorSize({ @@ -322,4 +189,150 @@ const ImageTranslator: FC = () => {
); }; + +function useImageTranslatorHotkeys( + file: GetFileReturn | undefined, + sources: Source[], + focusedSourceID: string | null, +) { + const dispatch = useDispatch(); + const focusNextSource = () => { + if (sources.length === 0) { + return; + } + let nextFocusedSourceIndex = 0; + if (focusedSourceID) { + const focusedSourceIndex = sources.findIndex( + (source) => source.id === focusedSourceID, + ); + if (focusedSourceIndex + 1 >= sources.length) { + nextFocusedSourceIndex = 0; + } else { + nextFocusedSourceIndex = focusedSourceIndex + 1; + } + } + const nextFocusedSourceID = sources[nextFocusedSourceIndex].id; + dispatch( + focusSource({ + id: nextFocusedSourceID, + effects: ['focusInput', 'focusLabel', 'scrollIntoView'], + noises: ['focusInput', 'focusLabel'], + }), + ); + }; + const focusPrevSource = () => { + if (sources.length === 0) { + return; + } + let prevFocusedSourceIndex = sources.length - 1; + if (focusedSourceID) { + const focusedSourceIndex = sources.findIndex( + (source) => source.id === focusedSourceID, + ); + if (focusedSourceIndex - 1 < 0) { + prevFocusedSourceIndex = sources.length - 1; + } else { + prevFocusedSourceIndex = focusedSourceIndex - 1; + } + } + const prevFocusedSourceID = sources[prevFocusedSourceIndex].id; + dispatch( + focusSource({ + id: prevFocusedSourceID, + effects: ['focusInput', 'focusLabel', 'scrollIntoView'], + noises: ['focusInput', 'focusLabel'], + }), + ); + }; + + // 快捷键 - 下一个输入框 + const focusNextSourceHotKeyOptions = useSelector( + (state: AppState) => state.hotKey.focusNextSource, + ); + useHotKey( + { + disabled: !Boolean(focusNextSourceHotKeyOptions[0]), + ...focusNextSourceHotKeyOptions[0], + }, + focusNextSource, + [focusedSourceID, sources.length], + ); + useHotKey( + { + disabled: !Boolean(focusNextSourceHotKeyOptions[1]), + ...focusNextSourceHotKeyOptions[1], + }, + focusNextSource, + [focusedSourceID, sources.length], + ); + + // 快捷键 - 上一个输入框 + const focusPrevSourceHotKeyOptions = useSelector( + (state: AppState) => state.hotKey.focusPrevSource, + ); + useHotKey( + { + disabled: !Boolean(focusPrevSourceHotKeyOptions[0]), + ...focusPrevSourceHotKeyOptions[0], + }, + focusPrevSource, + [focusedSourceID, sources.length], + ); + useHotKey( + { + disabled: !Boolean(focusPrevSourceHotKeyOptions[1]), + ...focusPrevSourceHotKeyOptions[1], + }, + focusPrevSource, + [focusedSourceID, sources.length], + ); + + // 快捷键 - 当 ImageViewer 未加载完成是,忽略所有快捷键 + useHotKey( + { + disabled: Boolean(file?.id), + ignoreKeyboardElement: false, + }, + () => {}, + [file?.id], + ); +} + +function useWindowSize() { + const osName = useSelector((state: AppState) => state.site.osName); + const isIOS = osName === 'ios'; + + // 页面尺寸 + const [windowSize, setWindowSize] = useState({ + width: 0, + height: 0, + }); + + useEffect(() => { + const handleResize = () => { + const width = window.innerWidth; + const height = window.innerHeight; + setWindowSize({ width, height }); + }; + handleResize(); + window.addEventListener('resize', handleResize); + const setTimeoutHandleResize = () => { + setTimeout(handleResize, 250); + }; + if (isIOS) { + window.addEventListener('focusin', setTimeoutHandleResize); + window.addEventListener('focusout', setTimeoutHandleResize); + } + return () => { + window.removeEventListener('resize', handleResize); + if (isIOS) { + window.removeEventListener('focusin', setTimeoutHandleResize); + window.removeEventListener('focusout', setTimeoutHandleResize); + } + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return windowSize; +} export default ImageTranslator; diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 49eaaca..12535ff 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -13,7 +13,10 @@ import { Header, Form, } from '../components'; -import { CAPTCHAInputRef, checkCAPTCHA } from '../components/CAPTCHAInput'; +import { + CAPTCHAInputRef, + checkCAPTCHA, +} from '../components/shared-form/CAPTCHAInput'; import { setUserToken } from '../store/user/slice'; import { useTitle } from '../hooks'; import { FC } from '../interfaces'; diff --git a/src/pages/Project.tsx b/src/pages/Project.tsx index e39c265..21c5ad3 100644 --- a/src/pages/Project.tsx +++ b/src/pages/Project.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Route, Switch, useParams, useRouteMatch } from 'react-router-dom'; import { useTitle } from '@/hooks'; diff --git a/src/pages/ProjectFiles.tsx b/src/pages/ProjectFiles.tsx index 198e9c9..5e2e5a0 100644 --- a/src/pages/ProjectFiles.tsx +++ b/src/pages/ProjectFiles.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/core'; import { message, Spin } from 'antd'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState, cloneElement } from 'react'; import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { FileList, Icon, ListItem } from '@/components'; @@ -45,7 +45,23 @@ const ProjectFiles: FC = ({ project }) => { return ; } - return project ? ( + if (!project) { + return ( +
+ +
+ ); + } + + const wrapper = (
= ({ project }) => { } } `} - > - {!currentTarget ? ( - <> - {targets && ( - - ) - } - name={formatMessage({ id: 'project.selectTarget' })} - /> - )} - { + /> + ); + + if (!currentTarget) { + // target selector + return cloneElement( + wrapper, + undefined, + targets && ( + + ) + } + name={formatMessage({ id: 'project.selectTarget' })} + /> + ), + { + setCurrentTarget(target); + saveDefaultTargetID({ + projectID: project.id, + targetID: target.id, + }); + }} + onLoad={(targets) => { + setTargets(targets); + // 只有一个时候,直接选中 + if (targets.length === 1) { + setCurrentTarget(targets[0]); + } + // 自动选中默认的 + const defaultTargetID = loadDefaultTargetID({ + projectID: project.id, + }); + if (defaultTargetID) { + const target = targets.find( + (target) => target.id === defaultTargetID, + ); + if (target) { setCurrentTarget(target); - saveDefaultTargetID({ - projectID: project.id, - targetID: target.id, - }); - }} - onLoad={(targets) => { - setTargets(targets); - // 只有一个时候,直接选中 - if (targets.length === 1) { - setCurrentTarget(targets[0]); - } - // 自动选中默认的 - const defaultTargetID = loadDefaultTargetID({ - projectID: project.id, - }); - if (defaultTargetID) { - const target = targets.find( - (target) => target.id === defaultTargetID, - ); - if (target) { - setCurrentTarget(target); - } - } - }} - /> - - ) : project.importFromLabelplusStatus === - IMPORT_FROM_LABELPLUS_STATUS.SUCCEEDED ? ( - { - if (targets.length > 1) { - setCurrentTarget(undefined); - clearDefaultTargetID({ projectID: project.id }); - } else { - message.info( - formatMessage({ id: 'project.onlyOneTargetTip' }), - 1, - ); } - }} - /> - ) : ( - - )} -
- ) : ( - + } + }} + />, + ); + } + + if ( + project.importFromLabelplusStatus !== IMPORT_FROM_LABELPLUS_STATUS.SUCCEEDED + ) { + return cloneElement( + wrapper, + undefined, + , + ); + } + + return cloneElement( + wrapper, + undefined, + { + if (targets.length > 1) { + setCurrentTarget(undefined); + clearDefaultTargetID({ projectID: project.id }); + } else { + message.info(formatMessage({ id: 'project.onlyOneTargetTip' }), 1); + } + }} + />, ); }; export default ProjectFiles; diff --git a/src/pages/ProjectSet.tsx b/src/pages/ProjectSet.tsx index bce971b..4e09839 100644 --- a/src/pages/ProjectSet.tsx +++ b/src/pages/ProjectSet.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/core'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { useIntl } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import { @@ -28,8 +28,8 @@ import { setCurrentProjectSetSaga } from '@/store/projectSet/slice'; import style from '../style'; import { can } from '@/utils/user'; import Project from './Project'; -import { ProjectCreateForm } from '@/components/project-set/ProjectCreateForm'; -import { ProjectImportForm } from '@/components/project-set/ProjectImportForm'; +import { ProjectCreateForm } from '@/components/project/ProjectCreateForm'; +import { ProjectImportForm } from '@/components/project/ProjectImportForm'; /** 项目集页的属性接口 */ interface ProjectSetProps {} @@ -60,7 +60,7 @@ const ProjectSet: FC = () => { const rightButton = (
@@ -172,7 +172,7 @@ const ProjectSet: FC = () => { `} > - {formatMessage({ id: 'site.createProject' })} + {formatMessage({ id: 'project.createProject' })} = () => { border-top: 1px solid ${style.borderColorBase}; `} > - {formatMessage({ id: 'site.importProject' })} + {formatMessage({ id: 'project.importProject' })} = () => { const rightButton = (
@@ -166,7 +166,7 @@ const Team: FC = () => { `} > - {formatMessage({ id: 'site.createProjectSet' })} + {formatMessage({ id: 'projectSet.createProjectSet' })} diff --git a/src/services/moeflow_companion/TranslateCompanion.tsx b/src/services/ai/TranslateCompanion.tsx similarity index 98% rename from src/services/moeflow_companion/TranslateCompanion.tsx rename to src/services/ai/TranslateCompanion.tsx index 3371989..7688434 100644 --- a/src/services/moeflow_companion/TranslateCompanion.tsx +++ b/src/services/ai/TranslateCompanion.tsx @@ -2,12 +2,12 @@ import { FC } from '@/interfaces'; import { RefObject, useRef, useState } from 'react'; import { FilePond } from 'react-filepond'; import { css } from '@emotion/core'; -import { Button } from '@/components/Button'; +import { Button } from '@/components/shared/Button'; import { createMoeflowProjectZip, LPFile } from '../labelplus_packager'; import { FailureResults } from '@/apis'; import { measureImgSize } from '@jokester/ts-commonutil/lib/web/measure-img'; import { clamp } from 'lodash-es'; -import { BBox, mitPreprocess, TextQuad } from '@/apis/mit_preprocess'; +import { BBox, mitPreprocess, TextQuad } from './mit_preprocess'; import { ResourcePool } from '@jokester/ts-commonutil/lib/concurrency/resource-pool'; const MAX_FILE_COUNT = 30; diff --git a/src/apis/mit_preprocess.ts b/src/services/ai/mit_preprocess.ts similarity index 96% rename from src/apis/mit_preprocess.ts rename to src/services/ai/mit_preprocess.ts index 0013e15..f1574a5 100644 --- a/src/apis/mit_preprocess.ts +++ b/src/services/ai/mit_preprocess.ts @@ -1,5 +1,5 @@ -import { request } from '.'; -import { uploadRequest } from './_request'; +import { request } from '../../apis'; +import { uploadRequest } from '../../apis/_request'; import { wait } from '@jokester/ts-commonutil/lib/concurrency/timing'; const mitApiPrefix = `/v1/mit`; diff --git a/src/services/ai/multimodal_recognize.ts b/src/services/ai/multimodal_recognize.ts new file mode 100644 index 0000000..46bf401 --- /dev/null +++ b/src/services/ai/multimodal_recognize.ts @@ -0,0 +1,22 @@ +interface MultimodalModelConf { + provider: string; + model: string; + baseUrl: string; +} + +export const multimodalPresets: readonly MultimodalModelConf[] = [ + // gemini: + // see https://ai.google.dev/gemini-api/docs/openai + { + provider: 'gemini', + model: 'gemini-2.5-flash', + baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/', + }, + { + provider: 'gemini', + model: 'gemini-2.5-pro', + baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai/', + }, +]; + +export async function x(); diff --git a/src/services/ai/use_moeflow_companion.ts b/src/services/ai/use_moeflow_companion.ts new file mode 100644 index 0000000..582b798 --- /dev/null +++ b/src/services/ai/use_moeflow_companion.ts @@ -0,0 +1,107 @@ +import { useState, useRef } from 'react'; +import { Client } from '@gradio/client'; +import { useAsyncEffect } from '@jokester/ts-commonutil/lib/react/hook/use-async-effect'; +import { useSelector } from 'react-redux'; +import { AppState } from '@/store'; +import { createDebugLogger } from '@/utils/debug-logger'; +import { RuntimeConfig } from '@/configs'; + +export const moeflowCompanionServiceState = { + disabled: 'disabled', + connecting: 'connecting', + connected: 'connected', + disconnected: 'disconnected', +} as const; + +const debugLogger = createDebugLogger('service:moeflow_companion'); + +export interface MoeflowCompanionService { + client: Client; + serviceConf: RuntimeConfig['moeflowCompanion']; + multimodalTranslate: typeof multimodalTranslate; +} + +export function useMoeflowCompanion(): [ + string, + MoeflowCompanionService | null, +] { + const serviceRef = useRef(null); + const [clientState, setClientState] = useState( + moeflowCompanionServiceState.connecting, + ); + const serviceConf = useSelector( + (s: AppState) => s.site.runtimeConfig.moeflowCompanion, + ); + + useAsyncEffect( + async (_, released) => { + if ( + !( + serviceConf && + serviceConf.gradioUrl && + serviceConf.defaultMultimodalModel + ) + ) { + serviceRef.current = null; + setClientState(moeflowCompanionServiceState.disabled); + return; + } + try { + const client = await Client.connect(serviceConf.gradioUrl); + serviceRef.current = { + client, + multimodalTranslate, + serviceConf, + }; + setClientState(moeflowCompanionServiceState.connected); + released.then(() => client.close()); + } catch (e) { + debugLogger('error connecting', e, serviceConf.gradioUrl); + serviceRef.current = null; + setClientState(moeflowCompanionServiceState.disconnected); + } + }, + [serviceConf], + ); + return [clientState, serviceRef.current] as const; +} + +async function multimodalTranslate( + client: Client, + files: Blob[], + targetLang: string, + model: string, +): Promise { + // const uploadRes = await client.upload_files(hfSpaceUrl, files) + // files.forEach(file => formData.append('files[]', file)); + // debugLogger('Upload response:', uploadRes); + const predictRes = await client.predict( + '/multimodal_llm_translate_file_api', + { + gradio_temp_files: files, // uploadRes.files!.map(handle_file), + model, + target_language: targetLang, + }, + ); + const [{ files: translated }] = predictRes.data as MoeflowMultimodalResData; + + debugLogger('Predict response:', predictRes, translated); + return translated; +} +export interface TranslatedFile { + local_path: string; + image_w: number; + image_h: number; + text_blocks: Array<{ + left: number; + top: number; + right: number; + bottom: number; + source: string; + translated: string; + }>; +} +/** + * the type in gradio https://github.com/moeflow-com/manga-image-translator/blob/moeflow-companion-main/moeflow_companion/gradio/multimodal.py#L62 + */ +type MoeflowMultimodalResData = [{ files: TranslatedFile[] }]; diff --git a/src/store/project/sagas.ts b/src/store/project/sagas.ts index 50e39e6..4a23dc3 100644 --- a/src/store/project/sagas.ts +++ b/src/store/project/sagas.ts @@ -34,7 +34,7 @@ function* setCurrentProjectWorker( configs: { cancelToken }, }); yield put(setCurrentProject(toLowerCamelCase(result.data))); - } catch (error) { + } catch (error: any) { error.default(); } finally { if (yield cancelled()) { diff --git a/src/store/projectSet/sagas.ts b/src/store/projectSet/sagas.ts index a97b3c5..3cbc420 100644 --- a/src/store/projectSet/sagas.ts +++ b/src/store/projectSet/sagas.ts @@ -16,7 +16,7 @@ function* setCurrentProjectSetWorker( ) { // 清空当前 projectSet yield put(clearCurrentProjectSet()); - const projectSets = yield select( + const projectSets: UserProjectSet[] = yield select( (state: AppState) => state.projectSet.projectSets, ); const projectSet = projectSets.find( @@ -35,7 +35,7 @@ function* setCurrentProjectSetWorker( configs: { cancelToken }, }); yield put(setCurrentProjectSet(toLowerCamelCase(result.data))); - } catch (error) { + } catch (error: any) { error.default(); } finally { if (yield cancelled()) { diff --git a/src/store/site/slice.ts b/src/store/site/slice.ts index 56bce4e..e7c4d34 100644 --- a/src/store/site/slice.ts +++ b/src/store/site/slice.ts @@ -1,18 +1,21 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { OSName, Platform } from '../../interfaces'; +import { OSName, Platform } from '@/interfaces'; +import { RuntimeConfig } from '@/configs'; export interface SiteState { - osName?: OSName; - platform?: Platform; + osName: OSName; + platform: Platform; newInvitationsCount: number; relatedApplicationsCount: number; + runtimeConfig: RuntimeConfig; } const initialState: SiteState = { - osName: 'windows', - platform: 'desktop', + osName: null!, + platform: null!, relatedApplicationsCount: 0, newInvitationsCount: 0, + runtimeConfig: null!, }; const slice = createSlice({ name: 'site', @@ -30,6 +33,9 @@ const slice = createSlice({ setNewInvitationsCount(state, action: PayloadAction) { state.newInvitationsCount = action.payload; }, + setRuntimeConfig(state, action: PayloadAction) { + state.runtimeConfig = action.payload; + }, }, }); @@ -38,5 +44,6 @@ export const { setOSName, setRelatedApplicationsCount, setNewInvitationsCount, + setRuntimeConfig, } = slice.actions; export default slice.reducer; diff --git a/src/style.ts b/src/style.ts index e5f223f..8f035e8 100644 --- a/src/style.ts +++ b/src/style.ts @@ -98,7 +98,7 @@ const antdVarsM = { export default { ...antdVars, ...otherVars, -}; +} as const; // 供 config-overrides.js 引用,转换成 antd Less 连字符格式,用于覆盖其 Less 配置 export const antdLessVars = toHyphenCase(antdVars) as Record; export const antdLessVarsM = toHyphenCase(antdVarsM) as Record;