options页面组件

This commit is contained in:
王一之 2024-12-30 18:06:53 +08:00
parent 78152222f3
commit 9876c1cbcb
45 changed files with 3318 additions and 410 deletions

View File

@ -26,6 +26,7 @@ export default [
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-expressions": "off",
...reactHooks.configs.recommended.rules,
},
},

View File

@ -17,6 +17,9 @@
},
"dependencies": {
"@arco-design/web-react": "^2.64.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@reduxjs/toolkit": "^2.3.0",
"cron": "^3.2.1",
"dayjs": "^1.11.13",
@ -29,7 +32,9 @@
"react-dom": "^18.2.0",
"react-i18next": "^15.1.0",
"react-icons": "^5.3.0",
"react-joyride": "^2.9.3",
"react-redux": "^9.1.2",
"react-router-dom": "^7.1.1",
"semver": "^7.6.3",
"uuid": "^11.0.3",
"yaml": "^2.6.1"

View File

@ -1,3 +1,4 @@
import EventEmitter from "eventemitter3";
import { connect } from "./client";
import { ApiFunction, Server } from "./server";
@ -24,6 +25,8 @@ export class Broker {
export class MessageQueue {
topicConMap: Map<string, { name: string; con: chrome.runtime.Port }[]> = new Map();
private EE: EventEmitter = new EventEmitter();
constructor(api: Server) {
api.on("messageQueue", this.handler());
}
@ -69,5 +72,16 @@ export class MessageQueue {
list?.forEach((item) => {
item.con.postMessage({ action: "message", topic, message });
});
this.EE.emit(topic, message);
}
// 只发布给当前环境
emit(topic: string, message: any) {
this.EE.emit(topic, message);
}
// 同环境下使用addListener
addListener(topic: string, handler: (message: any) => void) {
this.EE.on(topic, handler);
}
}

238
pnpm-lock.yaml generated
View File

@ -11,6 +11,15 @@ importers:
'@arco-design/web-react':
specifier: ^2.64.1
version: 2.64.1(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/sortable':
specifier: ^10.0.0
version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@18.3.1)
'@reduxjs/toolkit':
specifier: ^2.3.0
version: 2.3.0(react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1))(react@18.3.1)
@ -47,9 +56,15 @@ importers:
react-icons:
specifier: ^5.3.0
version: 5.3.0(react@18.3.1)
react-joyride:
specifier: ^2.9.3
version: 2.9.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-redux:
specifier: ^9.1.2
version: 9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1)
react-router-dom:
specifier: ^7.1.1
version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
semver:
specifier: ^7.6.3
version: 7.6.3
@ -309,6 +324,28 @@ packages:
resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==}
engines: {node: '>=10.0.0'}
'@dnd-kit/accessibility@3.1.1':
resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==}
peerDependencies:
react: '>=16.8.0'
'@dnd-kit/core@6.3.1':
resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@dnd-kit/sortable@10.0.0':
resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==}
peerDependencies:
'@dnd-kit/core': ^6.3.0
react: '>=16.8.0'
'@dnd-kit/utilities@3.2.2':
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'}
@ -634,6 +671,12 @@ packages:
resolution: {integrity: sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@gilbarbara/deep-equal@0.1.2':
resolution: {integrity: sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==}
'@gilbarbara/deep-equal@0.3.1':
resolution: {integrity: sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==}
'@humanfs/core@0.19.0':
resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==}
engines: {node: '>=18.18.0'}
@ -983,6 +1026,9 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/eslint-scope@3.7.7':
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
@ -1644,6 +1690,10 @@ packages:
resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
engines: {node: '>= 0.6'}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
core-js@3.38.1:
resolution: {integrity: sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==}
@ -1724,6 +1774,9 @@ packages:
decimal.js@10.4.3:
resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==}
deep-diff@1.0.2:
resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
@ -1731,6 +1784,10 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
default-browser-id@5.0.0:
resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==}
engines: {node: '>=18'}
@ -2367,6 +2424,12 @@ packages:
engines: {node: '>=14.16'}
hasBin: true
is-lite@0.8.2:
resolution: {integrity: sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==}
is-lite@1.2.1:
resolution: {integrity: sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==}
is-map@2.0.3:
resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
engines: {node: '>= 0.4'}
@ -2852,6 +2915,10 @@ packages:
pkg-types@1.2.1:
resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==}
popper.js@1.16.1:
resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==}
deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1
possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
@ -2927,6 +2994,12 @@ packages:
peerDependencies:
react: ^18.3.1
react-floater@0.7.9:
resolution: {integrity: sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==}
peerDependencies:
react: 15 - 18
react-dom: 15 - 18
react-focus-lock@2.13.2:
resolution: {integrity: sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ==}
peerDependencies:
@ -2954,12 +3027,24 @@ packages:
peerDependencies:
react: '*'
react-innertext@1.1.5:
resolution: {integrity: sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==}
peerDependencies:
'@types/react': '>=0.0.0 <=99'
react: '>=0.0.0 <=99'
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-joyride@2.9.3:
resolution: {integrity: sha512-1+Mg34XK5zaqJ63eeBhqdbk7dlGCFp36FXwsEvgpjqrtyywX2C6h9vr3jgxP0bGHCw8Ilsp/nRDzNVq6HJ3rNw==}
peerDependencies:
react: 15 - 18
react-dom: 15 - 18
react-redux@9.1.2:
resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==}
peerDependencies:
@ -2972,6 +3057,23 @@ packages:
redux:
optional: true
react-router-dom@7.1.1:
resolution: {integrity: sha512-vSrQHWlJ5DCfyrhgo0k6zViOe9ToK8uT5XGSmnuC2R3/g261IdIMpZVqfjD6vWSXdnf5Czs4VA/V60oVR6/jnA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
react-router@7.1.1:
resolution: {integrity: sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react-transition-group@4.4.5:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
@ -3123,6 +3225,12 @@ packages:
scroll-into-view-if-needed@2.2.31:
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
scroll@3.0.1:
resolution: {integrity: sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==}
scrollparent@2.1.0:
resolution: {integrity: sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==}
select-hose@2.0.0:
resolution: {integrity: sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==}
@ -3154,6 +3262,9 @@ packages:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@ -3399,6 +3510,12 @@ packages:
resolution: {integrity: sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==}
engines: {node: '>=18'}
tree-changes@0.11.2:
resolution: {integrity: sha512-4gXlUthrl+RabZw6lLvcCDl6KfJOCmrC16BC5CRdut1EAH509Omgg0BfKLY+ViRlzrvYOTWR0FMS2SQTwzumrw==}
tree-changes@0.9.3:
resolution: {integrity: sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==}
tree-dump@1.0.2:
resolution: {integrity: sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==}
engines: {node: '>=10.0'}
@ -3433,10 +3550,17 @@ packages:
engines: {node: '>=18.0.0'}
hasBin: true
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@4.31.0:
resolution: {integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==}
engines: {node: '>=16'}
type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
@ -3882,6 +4006,31 @@ snapshots:
'@discoveryjs/json-ext@0.5.7': {}
'@dnd-kit/accessibility@3.1.1(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.8.0
'@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/accessibility': 3.1.1(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.8.0
'@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
tslib: 2.8.0
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.8.0
'@esbuild/aix-ppc64@0.21.5':
optional: true
@ -4075,6 +4224,10 @@ snapshots:
dependencies:
levn: 0.4.1
'@gilbarbara/deep-equal@0.1.2': {}
'@gilbarbara/deep-equal@0.3.1': {}
'@humanfs/core@0.19.0': {}
'@humanfs/node@0.16.5':
@ -4448,6 +4601,8 @@ snapshots:
dependencies:
'@types/node': 22.10.2
'@types/cookie@0.6.0': {}
'@types/eslint-scope@3.7.7':
dependencies:
'@types/eslint': 9.6.1
@ -5420,6 +5575,8 @@ snapshots:
cookie@0.7.1: {}
cookie@1.0.2: {}
core-js@3.38.1: {}
core-util-is@1.0.3: {}
@ -5496,10 +5653,14 @@ snapshots:
decimal.js@10.4.3: {}
deep-diff@1.0.2: {}
deep-eql@5.0.2: {}
deep-is@0.1.4: {}
deepmerge@4.3.1: {}
default-browser-id@5.0.0: {}
default-browser@5.2.1:
@ -6354,6 +6515,10 @@ snapshots:
dependencies:
is-docker: 3.0.0
is-lite@0.8.2: {}
is-lite@1.2.1: {}
is-map@2.0.3: {}
is-negative-zero@2.0.3: {}
@ -6453,7 +6618,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 22.8.1
'@types/node': 22.10.2
merge-stream: 2.0.0
supports-color: 8.1.1
optional: true
@ -6813,6 +6978,8 @@ snapshots:
mlly: 1.7.3
pathe: 1.1.2
popper.js@1.16.1: {}
possible-typed-array-names@1.0.0: {}
postcss-loader@8.1.1(@rspack/core@1.0.14(@swc/helpers@0.5.13))(postcss@8.4.49)(typescript@5.6.3)(webpack@5.96.1(esbuild@0.23.1)):
@ -6885,6 +7052,16 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-floater@0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
deepmerge: 4.3.1
is-lite: 0.8.2
popper.js: 1.16.1
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tree-changes: 0.9.3
react-focus-lock@2.13.2(@types/react@18.3.12)(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
@ -6910,10 +7087,33 @@ snapshots:
dependencies:
react: 18.3.1
react-innertext@1.1.5(@types/react@18.3.12)(react@18.3.1):
dependencies:
'@types/react': 18.3.12
react: 18.3.1
react-is@16.13.1: {}
react-is@18.3.1: {}
react-joyride@2.9.3(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@gilbarbara/deep-equal': 0.3.1
deep-diff: 1.0.2
deepmerge: 4.3.1
is-lite: 1.2.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-floater: 0.7.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-innertext: 1.1.5(@types/react@18.3.12)(react@18.3.1)
react-is: 16.13.1
scroll: 3.0.1
scrollparent: 2.1.0
tree-changes: 0.11.2
type-fest: 4.31.0
transitivePeerDependencies:
- '@types/react'
react-redux@9.1.2(@types/react@18.3.12)(react@18.3.1)(redux@5.0.1):
dependencies:
'@types/use-sync-external-store': 0.0.3
@ -6923,6 +7123,22 @@ snapshots:
'@types/react': 18.3.12
redux: 5.0.1
react-router-dom@7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router@7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@types/cookie': 0.6.0
cookie: 1.0.2
react: 18.3.1
set-cookie-parser: 2.7.1
turbo-stream: 2.4.0
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.26.0
@ -7105,6 +7321,10 @@ snapshots:
dependencies:
compute-scroll-into-view: 1.0.20
scroll@3.0.1: {}
scrollparent@2.1.0: {}
select-hose@2.0.0: {}
selfsigned@2.4.1:
@ -7160,6 +7380,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
set-cookie-parser@2.7.1: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@ -7425,6 +7647,16 @@ snapshots:
dependencies:
punycode: 2.3.1
tree-changes@0.11.2:
dependencies:
'@gilbarbara/deep-equal': 0.3.1
is-lite: 1.2.1
tree-changes@0.9.3:
dependencies:
'@gilbarbara/deep-equal': 0.1.2
is-lite: 0.8.2
tree-dump@1.0.2(tslib@2.8.0):
dependencies:
tslib: 2.8.0
@ -7478,10 +7710,14 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
turbo-stream@2.4.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1
type-fest@4.31.0: {}
type-is@1.6.18:
dependencies:
media-typer: 0.3.0

View File

@ -139,7 +139,7 @@ export default defineConfig({
}),
new rspack.HtmlRspackPlugin({
filename: `${dist}/ext/src/options.html`,
template: `${src}/pages/template.html`,
template: `${src}/pages/options.html`,
inject: "head",
title: "Home - ScriptCat",
minify: true,

View File

@ -1,15 +1,15 @@
export abstract class Repo<T> {
constructor(private prefix: string) {
if (!prefix.endsWith(":")) {
prefix += ":";
this.prefix += ":";
}
}
private joinKey(key: string) {
protected joinKey(key: string) {
return this.prefix + key;
}
public async _save(key: string, val: T) {
protected async _save(key: string, val: T) {
return new Promise((resolve) => {
const data = {
[this.joinKey(key)]: val,
@ -55,4 +55,39 @@ export abstract class Repo<T> {
});
});
}
public delete(key: string) {
return new Promise<void>((resolve) => {
chrome.storage.local.remove(this.joinKey(key), () => {
resolve();
});
});
}
update(key: string, val: Partial<T>) {
return new Promise((resolve) => {
this.get(key).then((result) => {
if (result) {
Object.assign(result, val);
this._save(key, result).then(() => {
resolve(result);
});
}
});
});
}
all(): Promise<T[]> {
return new Promise((resolve) => {
chrome.storage.local.get((result) => {
const ret = [];
for (const key in result) {
if (key.startsWith(this.prefix)) {
ret.push(result[key]);
}
}
resolve(ret);
});
});
}
}

View File

@ -46,7 +46,7 @@ export interface Config {
export type UserConfig = { [key: string]: { [key: string]: Config } };
export interface Script {
id: number; // 脚本id
// id: number; // 脚本id mv3迁移为chrome.storage后舍弃
uuid: string; // 脚本uuid,通过脚本uuid识别唯一脚本
name: string; // 脚本名称
code: string; // 脚本执行代码

View File

@ -1,5 +1,6 @@
import { Script } from "@App/app/repo/scripts";
import { Client } from "@Packages/message/client";
import { InstallSource } from ".";
export class ScriptClient extends Client {
constructor() {
@ -11,7 +12,7 @@ export class ScriptClient extends Client {
return this.do("getInstallInfo", uuid);
}
installScript(script: Script) {
return this.do("installScript", script);
installScript(script: Script, upsertBy: InstallSource = "user") {
return this.do("installScript", { script, upsertBy });
}
}

View File

@ -8,6 +8,7 @@ import CacheKey from "@App/app/cache_key";
import { openInCurrentTab } from "@App/pkg/utils/utils";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
import { MessageQueue } from "@Packages/message/message_queue";
import { InstallSource } from ".";
export class ScriptService {
logger: Logger;
@ -135,17 +136,34 @@ export class ScriptService {
}
// 安装脚本
async installScript(script: Script) {
async installScript(param: { script: Script; upsertBy: InstallSource }) {
param.upsertBy = param.upsertBy || "user";
const { script, upsertBy } = param;
const logger = this.logger.with({
name: script.name,
uuid: script.uuid,
version: script.metadata.version[0],
upsertBy,
});
const dao = new ScriptDAO();
// 判断是否已经安装
const oldScript = await dao.findByUUID(script.uuid);
if (!oldScript) {
// 执行安装逻辑
} else {
if (oldScript) {
// 执行更新逻辑
script.selfMetadata = oldScript.selfMetadata;
}
return dao
.save(script)
.then(() => {
logger.info("install success");
// 广播一下
this.mq.publish("installScript", script);
return {};
})
.catch((e) => {
logger.error("install error", Logger.E(e));
throw e;
});
}
init() {

View File

@ -4,6 +4,10 @@
"version": "0.17.0.1001",
"author": "CodFrm",
"description": "__MSG_scriptcat_description__",
"options_ui": {
"page": "src/options.html",
"open_in_tab": true
},
"background": {
"service_worker": "src/service_worker.js"
},

View File

@ -0,0 +1,198 @@
import { Export, ExportDAO, ExportTarget } from "@App/app/repo/export";
import { Script } from "@App/app/repo/scripts";
import { Button, Checkbox, Form, Input, Message, Modal, Select } from "@arco-design/web-react";
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
const FormItem = Form.Item;
function defaultParams(script: Script) {
return {
exportValue: script.metadata.exportvalue && script.metadata.exportvalue[0],
exportCookie: script.metadata.exportcookie && script.metadata.exportcookie[0],
};
}
const CloudScriptPlan: React.FC<{
// eslint-disable-next-line react/require-default-props
script?: Script;
onClose: () => void;
}> = ({ script, onClose }) => {
const [form] = Form.useForm();
const [visible, setVisible] = React.useState(false);
const [cloudScriptType, setCloudScriptType] = React.useState<ExportTarget>("local");
const [, setModel] = React.useState<Export>();
const { t } = useTranslation();
const CloudScriptList = [
{
key: "local",
name: t("local"),
},
];
useEffect(() => {
if (script) {
setVisible(true);
// 设置默认值
// 从数据库中获取导出数据
const dao = new ExportDAO();
dao.findByScriptID(script.uuid).then((data) => {
setModel(data);
if (data && data.params[data.target]) {
setCloudScriptType(data.target);
form.setFieldsValue(data.params[data.target]);
} else {
setCloudScriptType("local");
form.setFieldsValue(defaultParams(script));
}
});
}
}, [script]);
return (
<Modal
title={
<div>
<span
style={{
height: "32px",
lineHeight: "32px",
}}
>
{script?.name} {t("upload_to_cloud")}
</span>
<Button
type="text"
icon={
<IconQuestionCircleFill
style={{
margin: 0,
}}
/>
}
href="https://docs.scriptcat.org/docs/dev/cloudcat/"
target="_blank"
iconOnly
/>
</div>
}
okText={t("export")}
visible={visible}
onCancel={() => {
setVisible(false);
onClose();
}}
onConfirm={async () => {
// 保存并导出
const dao = new ExportDAO();
const params = form.getFieldsValue() as unknown as ExportParams;
if (!params || !script) {
return;
}
setModel((prevModel) => {
if (!prevModel) {
prevModel = {
id: 0,
scriptId: script!.id,
target: "local",
params: {},
};
}
prevModel.params[cloudScriptType] = params;
prevModel.target = cloudScriptType;
dao.save(prevModel).catch((err) => {
Message.error(`${t("save_failed")}: ${err}`);
});
return prevModel;
});
Message.info(t("exporting")!);
// 本地特殊处理
const values = await parseExportValue(script, params.exportValue);
const cookies = await parseExportCookie(params.exportCookie);
if (cloudScriptType === "local") {
const jszip = new JSZip();
const cloudScript = CloudScriptFactory.create("local", {
zip: jszip,
...params,
});
cloudScript.exportCloud(script, values, cookies);
// 生成文件,并下载
const files = await jszip.generateAsync({
type: "blob",
compression: "DEFLATE",
compressionOptions: {
level: 9,
},
comment: "Created by Scriptcat",
});
const url = URL.createObjectURL(files);
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
chrome.downloads.download({
url,
saveAs: true,
filename: `${script.uuid}.zip`,
});
}
}}
>
<Form
autoComplete="off"
style={{
width: "100%",
}}
layout="vertical"
form={form}
>
<FormItem label={t("upload_to")}>
<Select
value={cloudScriptType}
onChange={(value) => {
setCloudScriptType(value);
}}
>
{CloudScriptList.map((item) => (
<Select.Option key={item.key} value={item.key}>
{item.name}
</Select.Option>
))}
</Select>
</FormItem>
{/* {Object.keys(cloudScriptParams[cloudScriptType]).map((key) => {
const item = cloudScriptParams[cloudScriptType][key];
return (
<FormItem key={key} label={item.title}>
<Input />
</FormItem>
);
})} */}
<FormItem label={t("value_export_expression")} field="exportValue">
<Input.TextArea />
</FormItem>
<FormItem label="" field="overwriteValue">
<Checkbox>{t("overwrite_original_value_on_import")}</Checkbox>
</FormItem>
<FormItem label={t("cookie_export_expression")} field="exportCookie">
<Input.TextArea />
</FormItem>
<FormItem label="" field="overwriteCookie">
<Checkbox>{t("overwrite_original_cookie_on_import")}</Checkbox>
</FormItem>
<Button
type="primary"
onClick={() => {
if (script) {
form.setFieldsValue(defaultParams(script));
}
}}
>
{t("restore_default_values")}
</Button>
</Form>
</Modal>
);
};
export default CloudScriptPlan;

View File

@ -0,0 +1,35 @@
import React, { ReactNode } from "react";
import { useNavigate } from "react-router-dom";
const CustomLink: React.FC<{
children: ReactNode;
to: string;
className?: string;
search?: string;
}> = ({ children, to, search, className }) => {
const nav = useNavigate();
const click = () => {
if (window.onbeforeunload) {
if (confirm("当前正在编辑状态,跳转其它页面将会丢失当前内容,是否跳转?")) {
nav({
pathname: to,
search,
});
}
} else {
nav({
pathname: to,
search,
});
}
};
return (
<div className={className} onClick={click}>
{children}
</div>
);
};
export default CustomLink;

View File

@ -0,0 +1,45 @@
import { Link } from "@arco-design/web-react";
import React from "react";
import { useTranslation } from "react-i18next";
// 因为i18n的Trans组件打包后出现问题所以自己实现一个
export const CustomTrans: React.FC<{
i18nKey: string;
}> = ({ i18nKey }) => {
const { t } = useTranslation();
const children: (JSX.Element | string)[] = [];
let content = t(i18nKey);
for (;;) {
const i = content.indexOf("<");
if (i !== -1) {
children.push(content.substring(0, i));
const end = content.indexOf(">", i);
const key = content.substring(i + 1, end).split(" ")[0];
const tag = content.substring(i, end + 1);
const tagEnd = content.indexOf(`</${key}>`, end);
const element = content.substring(end + 1, content.indexOf(`</${key}>`, end));
switch (key) {
case "Link":
// eslint-disable-next-line no-case-declarations
const href = tag.match(/href="(.*)"/)![1];
children.push(
<Link key={`i${i}`} href={href} target="_black">
{element}
</Link>
);
break;
default:
children.push(element);
break;
}
content = content.substring(tagEnd + key.length + 3);
} else {
children.push(content);
break;
}
}
return <div>{children}</div>;
};
export default CustomTrans;

View File

@ -0,0 +1,122 @@
import React from "react";
import { Input, Select, Space } from "@arco-design/web-react";
const fileSystemList: {
key: FileSystemType;
name: string;
}[] = [
{
key: "webdav",
name: "WebDAV",
},
{
key: "baidu-netdsik",
name: "百度网盘",
},
{
key: "onedrive",
name: "OneDrive",
},
];
const FileSystemParams: React.FC<{
preNode: React.ReactNode | string;
onChangeFileSystemType: (type: FileSystemType) => void;
onChangeFileSystemParams: (params: any) => void;
actionButton: React.ReactNode[];
fileSystemType: FileSystemType;
fileSystemParams: any;
}> = ({
onChangeFileSystemType,
onChangeFileSystemParams,
preNode,
actionButton,
fileSystemType,
fileSystemParams,
}) => {
return (
<>
<Space>
{preNode}
<Select
value={fileSystemType}
style={{ width: 120 }}
onChange={(value) => {
onChangeFileSystemType(value as FileSystemType);
}}
>
{fileSystemList.map((item) => (
<Select.Option key={item.key} value={item.key}>
{item.name}
</Select.Option>
))}
</Select>
{actionButton.map((item) => item)}
</Space>
<Space
style={{
display: "flex",
marginTop: 4,
}}
>
{Object.keys(fsParams[fileSystemType]).map((key) => (
<div key={key}>
{fsParams[fileSystemType][key].type === "select" && (
<>
<span>{fsParams[fileSystemType][key].title}</span>
<Select
value={
fileSystemParams[key] ||
fsParams[fileSystemType][key].options![0]
}
onChange={(value) => {
onChangeFileSystemParams({
...fileSystemParams,
[key]: value,
});
}}
>
{fsParams[fileSystemType][key].options!.map((option) => (
<Select.Option value={option} key={option}>
{option}
</Select.Option>
))}
</Select>
</>
)}
{fsParams[fileSystemType][key].type === "password" && (
<>
<span>{fsParams[fileSystemType][key].title}</span>
<Input.Password
value={fileSystemParams[key]}
onChange={(value) => {
onChangeFileSystemParams({
...fileSystemParams,
[key]: value,
});
}}
/>
</>
)}
{!fsParams[fileSystemType][key].type && (
<>
<span>{fsParams[fileSystemType][key].title}</span>
<Input
value={fileSystemParams[key]}
onChange={(value) => {
onChangeFileSystemParams({
...fileSystemParams,
[key]: value,
});
}}
/>
</>
)}
</div>
))}
</Space>
</>
);
};
export default FileSystemParams;

View File

@ -0,0 +1,131 @@
import React, { useState } from "react";
import {
Button,
Card,
Collapse,
Link,
Message,
Space,
Typography,
} from "@arco-design/web-react";
import { useTranslation } from "react-i18next";
import FileSystemParams from "../FileSystemParams";
const CollapseItem = Collapse.Item;
const GMApiSetting: React.FC = () => {
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
const [status, setStatus] = useState(systemConfig.catFileStorage.status);
const [fileSystemType, setFilesystemType] = useState<FileSystemType>(
systemConfig.catFileStorage.filesystem
);
const [fileSystemParams, setFilesystemParam] = useState<{
[key: string]: any;
}>(systemConfig.catFileStorage.params[fileSystemType] || {});
const { t } = useTranslation();
return (
<Card title={t("gm_api")} bordered={false}>
<Collapse bordered={false} defaultActiveKey={["storage"]}>
<CollapseItem header={t("storage_api")} name="storage">
<Space direction="vertical">
<FileSystemParams
preNode={
<Typography.Text>
{t("settings")}
<Link
target="_black"
href="https://github.com/scriptscat/scriptcat/blob/main/example/cat_file_storage.js"
>
CAT_fileStorage
</Link>
{t("use_file_system")}
</Typography.Text>
}
actionButton={[
<Button
key="save"
type="primary"
onClick={async () => {
try {
await FileSystemFactory.create(
fileSystemType,
fileSystemParams
);
} catch (e) {
Message.error(`${t("account_validation_failed")}: ${e}`);
return;
}
const params = { ...systemConfig.catFileStorage.params };
params[fileSystemType] = fileSystemParams;
systemConfig.catFileStorage = {
status: "success",
filesystem: fileSystemType,
params,
};
setStatus("success");
Message.success(t("save_success")!);
}}
>
{t("save")}
</Button>,
<Button
key="reset"
onClick={() => {
const config = systemConfig.catFileStorage;
config.status = "unset";
systemConfig.catFileStorage = config;
setStatus("unset");
}}
type="primary"
status="danger"
>
{t("reset")}
</Button>,
<Button
key="open"
type="secondary"
onClick={async () => {
try {
let fs = await FileSystemFactory.create(
fileSystemType,
fileSystemParams
);
fs = await fs.openDir("ScriptCat/app");
window.open(await fs.getDirUrl(), "_black");
} catch (e) {
Message.error(`${t("account_validation_failed")}: ${e}`);
}
}}
>
{t("open_directory")}
</Button>,
]}
fileSystemType={fileSystemType}
fileSystemParams={fileSystemParams}
onChangeFileSystemType={(type) => {
setFilesystemType(type);
}}
onChangeFileSystemParams={(params) => {
setFilesystemParam(params);
}}
/>
{status === "unset" && (
<Typography.Text type="secondary">{t("not_set")}</Typography.Text>
)}
{status === "success" && (
<Typography.Text type="success">{t("in_use")}</Typography.Text>
)}
{status === "error" && (
<Typography.Text type="error">
{t("storage_error")}
</Typography.Text>
)}
</Space>
</CollapseItem>
</Collapse>
</Card>
);
};
export default GMApiSetting;

View File

@ -0,0 +1,26 @@
.log-query-label .arco-select-view {
border-radius: 0;
}
.log-query-label .arco-select:first-child {
border-left: 1px solid var(--color-neutral-3);
}
.log-query-label .arco-select {
width: auto;
border-top: 1px solid var(--color-neutral-3);
border-bottom: 1px solid var(--color-neutral-3);
}
.log-query-label .arco-btn {
height: 34px;
border-left: 0;
border-radius: 0;
border-top: 1px solid var(--color-neutral-3);
border-bottom: 1px solid var(--color-neutral-3);
border-right: 1px solid var(--color-neutral-3);
}
.log-query-label .arco-select {
border-right: 1px solid var(--color-neutral-3);
}

View File

@ -0,0 +1,80 @@
import { Button, Select } from "@arco-design/web-react";
import { IconClose } from "@arco-design/web-react/icon";
import React from "react";
import "./index.css";
export type Query = {
key: string;
condition: "=" | "=~" | "!=" | "!~";
value: string;
};
export type Labels = {
[key: string]: { [key: string | number]: boolean };
};
const LogLabel: React.FC<{
value: Query;
labels: Labels;
onChange: (value: Query) => void;
onClose: () => void;
}> = ({ value, labels, onChange, onClose }) => {
const values = labels[value.key] || {};
return (
<div className="log-query-label">
<Select
showSearch
placeholder="key"
value={value.key || undefined}
onChange={(opt) => {
onChange({ ...value, key: opt });
}}
triggerProps={{
autoAlignPopupWidth: false,
autoAlignPopupMinWidth: true,
position: "bl",
}}
>
{Object.keys(labels).map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
<Select
placeholder="condition"
value={value.condition || "="}
onChange={(opt) => {
onChange({ ...value, condition: opt });
}}
>
<Select.Option value="=">=</Select.Option>
<Select.Option value="=~">=~</Select.Option>
<Select.Option value="!=">!=</Select.Option>
<Select.Option value="!~">!~</Select.Option>
</Select>
<Select
showSearch
placeholder="value"
value={value.value || undefined}
onChange={(opt) => {
onChange({ ...value, value: opt });
}}
triggerProps={{
autoAlignPopupWidth: false,
autoAlignPopupMinWidth: true,
position: "bl",
}}
>
{Object.keys(values).map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
<Button iconOnly icon={<IconClose />} onClick={onClose} />
</div>
);
};
export default LogLabel;

View File

@ -0,0 +1,334 @@
/* eslint-disable no-nested-ternary */
import React, { useEffect, useState } from "react";
import MessageInternal from "@App/app/message/internal";
import { MessageSender } from "@App/app/message/message";
import { ScriptMenu } from "@App/runtime/background/runtime";
import {
Button,
Collapse,
Empty,
Message,
Popconfirm,
Space,
Switch,
} from "@arco-design/web-react";
import {
IconCaretDown,
IconCaretUp,
IconDelete,
IconEdit,
IconMenu,
IconMinus,
IconSettings,
} from "@arco-design/web-react/icon";
import IoC from "@App/app/ioc";
import ScriptController from "@App/app/service/script/controller";
import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts";
import { RiPlayFill, RiStopFill } from "react-icons/ri";
import RuntimeController from "@App/runtime/content/runtime";
import { useTranslation } from "react-i18next";
import { SystemConfig } from "@App/pkg/config/config";
import { ScriptIcons } from "@App/pages/options/routes/utils";
const CollapseItem = Collapse.Item;
function isExclude(script: ScriptMenu, host: string) {
if (!script.customExclude) {
return false;
}
for (let i = 0; i < script.customExclude.length; i += 1) {
if (script.customExclude[i] === `*://${host}*`) {
return true;
}
}
return false;
}
// 用于popup页的脚本操作列表
const ScriptMenuList: React.FC<{
script: ScriptMenu[];
isBackscript: boolean;
currentUrl: string;
}> = ({ script, isBackscript, currentUrl }) => {
const [list, setList] = useState([] as ScriptMenu[]);
const message = IoC.instance(MessageInternal) as MessageInternal;
const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const runtimeCtrl = IoC.instance(RuntimeController) as RuntimeController;
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
const [expandMenuIndex, setExpandMenuIndex] = useState<{
[key: string]: boolean;
}>({});
const { t } = useTranslation();
let url: URL;
try {
url = new URL(currentUrl);
} catch (e) {
// ignore error
}
useEffect(() => {
setList(script);
}, [script]);
useEffect(() => {
// 监听脚本运行状态
const channel = runtimeCtrl.watchRunStatus();
channel.setHandler(([id, status]: any) => {
setList((prev) => {
const newList = [...prev];
const index = newList.findIndex((item) => item.id === id);
if (index !== -1) {
newList[index].runStatus = status;
}
return newList;
});
});
return () => {
channel.disChannel();
};
}, []);
const sendMenuAction = (sender: MessageSender, channelFlag: string) => {
let id = sender.tabId;
if (sender.frameId) {
id = sender.frameId;
}
message.broadcastChannel(
{
tag: sender.targetTag,
id: [id!],
},
channelFlag,
"click"
);
window.close();
};
// 监听菜单按键
// 菜单展开
return (
<>
{list.length === 0 && <Empty />}
{list.map((item, index) => (
<Collapse bordered={false} expandIconPosition="right" key={item.id}>
<CollapseItem
header={
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
onClick={(e) => {
e.stopPropagation();
}}
title={
// eslint-disable-next-line no-nested-ternary
item.enable
? item.runNumByIframe
? t("script_total_runs", {
runNum: item.runNum,
runNumByIframe: item.runNumByIframe,
})!
: t("script_total_runs_single", { runNum: item.runNum })!
: t("script_disabled")!
}
>
<Space>
<Switch
size="small"
checked={item.enable}
onChange={(checked) => {
let p: Promise<any>;
if (checked) {
p = scriptCtrl.enable(item.id).then(() => {
item.enable = true;
});
} else {
p = scriptCtrl.disable(item.id).then(() => {
item.enable = false;
});
}
p.catch((err) => {
Message.error(err);
}).finally(() => {
setList([...list]);
});
}}
/>
<span
style={{
display: "block",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: item.runNum === 0 ? "rgb(var(--gray-5))" : "",
lineHeight: "20px",
}}
>
<ScriptIcons script={item} size={20} />
{item.name}
</span>
</Space>
</div>
}
name={item.id.toString()}
contentStyle={{ padding: "0 0 0 40px" }}
>
<div className="flex flex-col">
{isBackscript && (
<Button
className="text-left"
type="secondary"
icon={
item.runStatus !== SCRIPT_RUN_STATUS_RUNNING ? (
<RiPlayFill />
) : (
<RiStopFill />
)
}
onClick={() => {
if (item.runStatus !== SCRIPT_RUN_STATUS_RUNNING) {
runtimeCtrl.startScript(item.id);
} else {
runtimeCtrl.stopScript(item.id);
}
}}
>
{item.runStatus !== SCRIPT_RUN_STATUS_RUNNING
? t("run_once")
: t("stop")}
</Button>
)}
<Button
className="text-left"
type="secondary"
icon={<IconEdit />}
onClick={() => {
window.open(
`/src/options.html#/script/editor/${item.id}`,
"_blank"
);
window.close();
}}
>
{t("edit")}
</Button>
{url && (
<Button
className="text-left"
status="warning"
type="secondary"
icon={<IconMinus />}
onClick={() => {
scriptCtrl
.exclude(
item.id,
`*://${url.host}*`,
isExclude(item, url.host)
)
.finally(() => {
window.close();
});
}}
>
{isExclude(item, url.host)
? t("exclude_on")
: t("exclude_off")}
{` ${url.host} ${t("exclude_execution")}`}
</Button>
)}
<Popconfirm
title={t("confirm_delete_script")}
icon={<IconDelete />}
onOk={() => {
setList(list.filter((i) => i.id !== item.id));
scriptCtrl.delete(item.id).catch((e) => {
Message.error(`{t('delete_failed')}: ${e}`);
});
}}
>
<Button
className="text-left"
status="danger"
type="secondary"
icon={<IconDelete />}
>
{t("delete")}
</Button>
</Popconfirm>
</div>
</CollapseItem>
<div
className="arco-collapse-item-content-box flex flex-col"
style={{ padding: "0 0 0 40px" }}
>
{/* 判断菜单数量,再判断是否展开 */}
{(item.menus && item.menus?.length > systemConfig.menuExpandNum
? expandMenuIndex[index]
? item.menus
: item.menus?.slice(0, systemConfig.menuExpandNum)
: item.menus
)?.map((menu) => {
if (menu.accessKey) {
document.addEventListener("keypress", (e) => {
if (e.key.toUpperCase() === menu.accessKey!.toUpperCase()) {
sendMenuAction(menu.sender, menu.channelFlag);
}
});
}
return (
<Button
className="text-left"
key={menu.id}
type="secondary"
icon={<IconMenu />}
onClick={() => {
sendMenuAction(menu.sender, menu.channelFlag);
}}
>
{menu.name}
{menu.accessKey && `(${menu.accessKey.toUpperCase()})`}
</Button>
);
})}
{item.menus && item.menus?.length > systemConfig.menuExpandNum && (
<Button
className="text-left"
key="expand"
type="secondary"
icon={
expandMenuIndex[index] ? <IconCaretUp /> : <IconCaretDown />
}
onClick={() => {
setExpandMenuIndex({
...expandMenuIndex,
[index]: !expandMenuIndex[index],
});
}}
>
{expandMenuIndex[index] ? t("collapse") : t("expand")}
</Button>
)}
{item.hasUserConfig && (
<Button
className="text-left"
key="config"
type="secondary"
icon={<IconSettings />}
onClick={() => {
window.open(
`/src/options.html#/?userConfig=${item.id}`,
"_blank"
);
window.close();
}}
>
{t("user_config")}
</Button>
)}
</div>
</Collapse>
))}
</>
);
};
export default ScriptMenuList;

View File

@ -0,0 +1,177 @@
import { Resource } from "@App/app/repo/resource";
import { Script } from "@App/app/repo/scripts";
import { base64ToBlob } from "@App/pkg/utils/script";
import {
Button,
Drawer,
Input,
Message,
Popconfirm,
Space,
Table,
} from "@arco-design/web-react";
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { ColumnProps } from "@arco-design/web-react/es/Table";
import {
IconDelete,
IconDownload,
IconSearch,
} from "@arco-design/web-react/icon";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
type ResourceListItem = {
key: string;
} & Resource;
const ScriptResource: React.FC<{
// eslint-disable-next-line react/require-default-props
script?: Script;
visible: boolean;
onOk: () => void;
onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<ResourceListItem[]>([]);
const inputRef = useRef<RefInputType>(null);
// const resourceCtrl = IoC.instance(ResourceController) as ResourceController;
const { t } = useTranslation();
useEffect(() => {
if (!script) {
return () => {};
}
resourceCtrl.getResource(script).then((res) => {
const arr: ResourceListItem[] = [];
Object.keys(res).forEach((key) => {
// @ts-ignore
const item: ResourceListItem = res[key];
item.key = key;
arr.push(item);
});
setData(arr);
});
return () => {};
}, [script]);
const columns: ColumnProps[] = [
{
title: t("key"),
dataIndex: "key",
key: "key",
filterIcon: <IconSearch />,
// eslint-disable-next-line react/no-unstable-nested-components
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
return (
<div className="arco-table-custom-filter">
<Input.Search
ref={inputRef}
searchButton
placeholder={t("enter_key")!}
value={filterKeys[0] || ""}
onChange={(value) => {
setFilterKeys(value ? [value] : []);
}}
onSearch={() => {
confirm();
}}
/>
</div>
);
},
onFilter: (value, row) => (value ? row.key.indexOf(value) !== -1 : true),
onFilterDropdownVisibleChange: (v) => {
if (v) {
setTimeout(() => inputRef.current!.focus(), 150);
}
},
},
{
title: t("type"),
dataIndex: "contentType",
width: 140,
key: "type",
render(col, res: Resource) {
return `${res.type}/${col}`;
},
},
{
title: t("action"),
render(_col, value: Resource, index) {
return (
<Space>
<Button
type="text"
icon={<IconDownload />}
onClick={() => {
const url = URL.createObjectURL(base64ToBlob(value.base64));
setTimeout(() => {
URL.revokeObjectURL(url);
}, 60 * 1000);
const filename = value.url.split("/").pop();
chrome.downloads.download({
url,
saveAs: true,
filename,
});
}}
/>
<Popconfirm
focusLock
title={t("confirm_delete_resource")}
onOk={() => {
Message.info({
content: t("delete_success"),
});
resourceCtrl.deleteResource(value.id);
setData(data.filter((_, i) => i !== index));
}}
>
<Button type="text" iconOnly icon={<IconDelete />} />
</Popconfirm>
</Space>
);
},
},
];
return (
<Drawer
width={600}
title={
<span>
{script?.name} {t("script_resource")}
</span>
}
visible={visible}
onOk={onOk}
onCancel={onCancel}
>
<Space className="w-full" direction="vertical">
<Space className="!flex justify-end">
<Popconfirm
focusLock
title={t("confirm_clear_resource")}
onOk={() => {
setData((prev) => {
prev.forEach((v) => {
resourceCtrl.deleteResource(v.id);
});
Message.info({
content: t("clear_success"),
});
return [];
});
}}
>
<Button type="primary" status="warning">
{t("clear")}
</Button>
</Popconfirm>
</Space>
<Table columns={columns} data={data} rowKey="id" />
</Space>
</Drawer>
);
};
export default ScriptResource;

View File

@ -0,0 +1,325 @@
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Script } from "@App/app/repo/scripts";
import {
Space,
Popconfirm,
Button,
Divider,
Typography,
Modal,
Input,
} from "@arco-design/web-react";
import Table, { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete } from "@arco-design/web-react/icon";
type MatchItem = {
// id是为了避免match重复
id: number;
match: string;
self: boolean;
hasMatch: boolean;
isExclude: boolean;
};
const Match: React.FC<{
script: Script;
}> = ({ script }) => {
// const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const [match, setMatch] = useState<MatchItem[]>([]);
const [exclude, setExclude] = useState<MatchItem[]>([]);
const [matchValue, setMatchValue] = useState<string>("");
const [matchVisible, setMatchVisible] = useState<boolean>(false);
const [excludeValue, setExcludeValue] = useState<string>("");
const [excludeVisible, setExcludeVisible] = useState<boolean>(false);
const { t } = useTranslation(); // 使用 react-i18next 的 useTranslation 钩子函数获取翻译函数
useEffect(() => {
if (script) {
// 从数据库中获取是简单处理数据一致性的问题
scriptCtrl.scriptDAO.findById(script.id).then((res) => {
if (!res) {
return;
}
const matchArr = res.selfMetadata?.match || res.metadata.match || [];
const matchMap = new Map<string, boolean>();
res.metadata.match?.forEach((m) => {
matchMap.set(m, true);
});
const v: MatchItem[] = [];
matchArr.forEach((value, index) => {
if (matchMap.has(value)) {
v.push({
id: index,
match: value,
self: false,
hasMatch: false,
isExclude: false,
});
} else {
v.push({
id: index,
match: value,
self: true,
hasMatch: false,
isExclude: false,
});
}
});
setMatch(v);
const excludeArr =
res.selfMetadata?.exclude || res.metadata.exclude || [];
const excludeMap = new Map<string, boolean>();
res.metadata.exclude?.forEach((m) => {
excludeMap.set(m, true);
});
const e: MatchItem[] = [];
excludeArr.forEach((value, index) => {
const hasMatch = matchMap.has(value);
if (excludeMap.has(value)) {
e.push({
id: index,
match: value,
self: false,
hasMatch,
isExclude: true,
});
} else {
e.push({
id: index,
match: value,
self: true,
hasMatch,
isExclude: true,
});
}
});
setExclude(e);
});
}
}, [script, exclude, match]);
const columns: ColumnProps[] = [
{
title: t("match"),
dataIndex: "match",
key: "match",
},
{
title: t("user_setting"),
dataIndex: "self",
key: "self",
width: 100,
render(col) {
if (col) {
return <span style={{ color: "#52c41a" }}>{t("yes")}</span>;
}
return <span style={{ color: "#c4751a" }}>{t("no")}</span>;
},
},
{
title: t("action"),
render(_, item: MatchItem) {
if (item.isExclude) {
return (
<Space>
<Popconfirm
title={`${t("confirm_delete_exclude")}${
item.hasMatch ? ` ${t("after_deleting_match_item")}` : ""
}`}
onOk={() => {
exclude.splice(exclude.indexOf(item), 1);
scriptCtrl
.resetExclude(
script.id,
exclude.map((m) => m.match)
)
.then(() => {
setExclude([...exclude]);
if (item.hasMatch) {
match.push(item);
scriptCtrl
.resetMatch(
script.id,
match.map((m) => m.match)
)
.then(() => {
setMatch([...match]);
});
}
});
}}
>
<Button type="text" iconOnly icon={<IconDelete />} />
</Popconfirm>
</Space>
);
}
return (
<Space>
<Popconfirm
title={`${t("confirm_delete_match")}${
item.self ? "" : ` ${t("after_deleting_exclude_item")}`
}`}
onOk={() => {
match.splice(match.indexOf(item), 1);
scriptCtrl
.resetMatch(
script.id,
match.map((m) => m.match)
)
.then(() => {
setMatch([...match]);
// 添加到exclue
if (!item.self) {
exclude.push(item);
scriptCtrl
.resetExclude(
script.id,
exclude.map((m) => m.match)
)
.then(() => {
setExclude([...exclude]);
});
}
});
}}
>
<Button type="text" iconOnly icon={<IconDelete />} />
</Popconfirm>
</Space>
);
},
},
];
return (
<>
<Modal
title={t("add_match")}
visible={matchVisible}
onCancel={() => setMatchVisible(false)}
onOk={() => {
if (matchValue) {
match.push({
id: Math.random(),
match: matchValue,
self: true,
hasMatch: false,
isExclude: false,
});
scriptCtrl
.resetMatch(
script.id,
match.map((m) => m.match)
)
.then(() => {
setMatch([...match]);
setMatchVisible(false);
});
}
}}
>
<Input
value={matchValue}
onChange={(e) => {
setMatchValue(e);
}}
/>
</Modal>
<Modal
title={t("add_exclude")}
visible={excludeVisible}
onCancel={() => setExcludeVisible(false)}
onOk={() => {
if (excludeValue) {
exclude.push({
id: Math.random(),
match: excludeValue,
self: true,
hasMatch: false,
isExclude: true,
});
scriptCtrl
.resetExclude(
script.id,
exclude.map((m) => m.match)
)
.then(() => {
setExclude([...exclude]);
setExcludeVisible(false);
});
}
}}
>
<Input
value={excludeValue}
onChange={(e) => {
setExcludeValue(e);
}}
/>
</Modal>
<div className="flex flex-row justify-between pb-2">
<Typography.Title heading={6}>{t("website_match")}</Typography.Title>
<Space>
<Button
type="primary"
size="small"
onClick={() => {
setMatchValue("");
setMatchVisible(true);
}}
>
{t("add_match")}
</Button>
<Popconfirm
title={t("confirm_reset")}
onOk={() => {
scriptCtrl.resetMatch(script.id, undefined).then(() => {
setMatch([]);
});
}}
>
<Button type="primary" size="small" status="warning">
{t("reset")}
</Button>
</Popconfirm>
</Space>
</div>
<Table columns={columns} data={match} rowKey="id" pagination={false} />
<Divider />
<div className="flex flex-row justify-between pb-2">
<Typography.Title heading={6}>{t("website_exclude")}</Typography.Title>
<Space>
<Button
type="primary"
size="small"
onClick={() => {
setExcludeValue("");
setExcludeVisible(true);
}}
>
{t("add_exclude")}
</Button>
<Popconfirm
title={t("confirm_reset")}
onOk={() => {
scriptCtrl.resetExclude(script.id, undefined).then(() => {
setExclude([]);
});
}}
>
<Button type="primary" size="small" status="warning">
{t("reset")}
</Button>
</Popconfirm>
</Space>
</div>
<Table columns={columns} data={exclude} rowKey="id" pagination={false} />
<Divider />
</>
);
};
export default Match;

View File

@ -0,0 +1,196 @@
import React, { useEffect, useState } from "react";
import { Permission } from "@App/app/repo/permission";
import { Script } from "@App/app/repo/scripts";
import { useTranslation } from "react-i18next";
import {
Space,
Popconfirm,
Message,
Button,
Checkbox,
Input,
Modal,
Select,
Typography,
} from "@arco-design/web-react";
import Table, { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete } from "@arco-design/web-react/icon";
const PermissionManager: React.FC<{
script: Script;
}> = ({ script }) => {
// const permissionCtrl = IoC.instance(
// PermissionController
// ) as PermissionController;
const [permission, setPermission] = useState<Permission[]>([]);
const [permissionVisible, setPermissionVisible] = useState<boolean>(false);
const [permissionValue, setPermissionValue] = useState<Permission>();
const { t } = useTranslation();
const columns: ColumnProps[] = [
{
title: t("type"),
dataIndex: "permission",
key: "permission",
width: 100,
},
{
title: t("permission_value"),
dataIndex: "permissionValue",
key: "permissionValue",
},
{
title: t("allow"),
dataIndex: "allow",
key: "allow",
render(col) {
if (col) {
return <span style={{ color: "#52c41a" }}>{t("yes")}</span>;
}
return <span style={{ color: "#f5222d" }}>{t("no")}</span>;
},
},
{
title: t("action"),
render(_, item: Permission) {
return (
<Space>
<Popconfirm
title={t("confirm_delete_permission")}
onOk={() => {
permissionCtrl
.deletePermission(script!.id, {
permission: item.permission,
permissionValue: item.permissionValue,
})
.then(() => {
Message.success(t("delete_success")!);
setPermission(permission.filter((i) => i.id !== item.id));
})
.catch(() => {
Message.error(t("delete_failed")!);
});
}}
>
<Button type="text" iconOnly icon={<IconDelete />} />
</Popconfirm>
</Space>
);
},
},
];
useEffect(() => {
if (script) {
permissionCtrl.getPermissions(script.id).then((list) => {
setPermission(list);
});
}
}, [script]);
return (
<>
<Modal
title={t("add_permission")}
visible={permissionVisible}
onCancel={() => setPermissionVisible(false)}
onOk={() => {
if (permissionValue) {
permission.push({
id: 0,
scriptId: script.id,
permission: permissionValue.permission,
permissionValue: permissionValue.permissionValue,
allow: permissionValue.allow,
createtime: new Date().getTime(),
updatetime: 0,
});
permissionCtrl
.addPermission(script.id, permissionValue)
.then(() => {
setPermission([...permission]);
setPermissionVisible(false);
});
}
}}
>
<Space className="w-full" direction="vertical">
<Select
value={permissionValue?.permission}
onChange={(e) => {
permissionValue &&
setPermissionValue({ ...permissionValue, permission: e });
}}
>
<Select.Option value="cors">{t("permission_cors")}</Select.Option>
<Select.Option value="cookie">
{t("permission_cookie")}
</Select.Option>
</Select>
<Input
value={permissionValue?.permissionValue}
onChange={(e) => {
permissionValue &&
setPermissionValue({ ...permissionValue, permissionValue: e });
}}
/>
<Checkbox
checked={permissionValue?.allow}
onChange={(e) => {
permissionValue &&
setPermissionValue({ ...permissionValue, allow: e });
}}
>
{t("allow")}
</Checkbox>
</Space>
</Modal>
<div className="flex flex-row justify-between pb-2">
<Typography.Title heading={6}>
{t("permission_management")}
</Typography.Title>
<Space>
<Button
type="primary"
size="small"
onClick={() => {
setPermissionValue({
id: 0,
scriptId: script.id,
permission: "cors",
permissionValue: "",
allow: true,
createtime: 0,
updatetime: 0,
});
setPermissionVisible(true);
}}
>
{t("add_permission")}
</Button>
<Popconfirm
title={t("confirm_reset")}
onOk={() => {
permissionCtrl.resetPermission(script.id).then(() => {
setPermission([]);
});
}}
>
<Button type="primary" size="small" status="warning">
{t("reset")}
</Button>
</Popconfirm>
</Space>
</div>
<Table
columns={columns}
data={permission}
rowKey="id"
pagination={false}
/>
</>
);
};
export default PermissionManager;

View File

@ -0,0 +1,106 @@
import { Script } from "@App/app/repo/scripts";
import { formatUnixTime } from "@App/pkg/utils/utils";
import {
Descriptions,
Divider,
Drawer,
Empty,
Input,
Message,
} from "@arco-design/web-react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import Match from "./Match";
import PermissionManager from "./Permission";
const ScriptSetting: React.FC<{
script: Script;
visible: boolean;
onOk: () => void;
onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => {
// const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const [checkUpdateUrl, setCheckUpdateUrl] = useState<string>("");
const { t } = useTranslation();
useEffect(() => {
if (script) {
scriptCtrl.scriptDAO.findById(script.id).then((v) => {
setCheckUpdateUrl(v?.downloadUrl || "");
});
}
}, [script]);
return (
<Drawer
width={600}
title={
<span>
{script?.name} {t("script_setting")}
</span>
}
autoFocus={false}
focusLock={false}
visible={visible}
onOk={() => {
onOk();
}}
onCancel={() => {
onCancel();
}}
>
<Descriptions
column={1}
title={t("basic_info")}
data={[
{
label: t("last_updated"),
value: formatUnixTime(
(script?.updatetime || script?.createtime || 0) / 1000
),
},
{
label: "UUID",
value: script?.uuid,
},
]}
style={{ marginBottom: 20 }}
labelStyle={{ paddingRight: 36 }}
/>
<Divider />
{script && <Match script={script} />}
<Descriptions
column={1}
title={t("update")}
data={[
{
label: t("update_url"),
value: (
<Input
value={checkUpdateUrl}
onChange={(e) => {
setCheckUpdateUrl(e);
}}
onBlur={() => {
scriptCtrl
.updateCheckUpdateUrl(script!.id, checkUpdateUrl)
.then(() => {
Message.success(t("update_success")!);
});
}}
/>
),
},
]}
style={{ marginBottom: 20 }}
labelStyle={{ paddingRight: 36 }}
/>
<Divider />
{script && <PermissionManager script={script} />}
<Empty description={t("under_construction")} />
</Drawer>
);
};
export default ScriptSetting;

View File

@ -0,0 +1,286 @@
import { Script } from "@App/app/repo/scripts";
import { Value } from "@App/app/repo/value";
import { valueType } from "@App/pkg/utils/utils";
import { Button, Drawer, Form, Input, Message, Modal, Popconfirm, Select, Space, Table } from "@arco-design/web-react";
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { ColumnProps } from "@arco-design/web-react/es/Table";
import { IconDelete, IconEdit, IconSearch } from "@arco-design/web-react/icon";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
const FormItem = Form.Item;
const ScriptStorage: React.FC<{
// eslint-disable-next-line react/require-default-props
script?: Script;
visible: boolean;
onOk: () => void;
onCancel: () => void;
}> = ({ script, visible, onCancel, onOk }) => {
const [data, setData] = useState<Value[]>([]);
const inputRef = useRef<RefInputType>(null);
const [currentValue, setCurrentValue] = useState<Value>();
const [visibleEdit, setVisibleEdit] = useState(false);
const [form] = Form.useForm();
const { t } = useTranslation();
useEffect(() => {
if (!script) {
return () => {};
}
// valueCtrl.getValues(script).then((values) => {
// setData(values);
// });
// Monitor value changes
// const channel = valueCtrl.watchValue(script);
// channel.setHandler((value: Value) => {
// setData((prev) => {
// const index = prev.findIndex((item) => item.key === value.key);
// if (index === -1) {
// if (value.value === undefined) {
// return prev;
// }
// return [value, ...prev];
// }
// if (value.value === undefined) {
// prev.splice(index, 1);
// return [...prev];
// }
// prev[index] = value;
// return [...prev];
// });
// });
return () => {
// channel.disChannel();
};
}, [script]);
const columns: ColumnProps[] = [
{
title: t("key"),
dataIndex: "key",
key: "key",
filterIcon: <IconSearch />,
width: 140,
// eslint-disable-next-line react/no-unstable-nested-components
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
return (
<div className="arco-table-custom-filter">
<Input.Search
ref={inputRef}
searchButton
placeholder={t("enter_key")!}
value={filterKeys[0] || ""}
onChange={(value) => {
setFilterKeys(value ? [value] : []);
}}
onSearch={() => {
confirm();
}}
/>
</div>
);
},
onFilter: (value, row) => (value ? row.key.indexOf(value) !== -1 : true),
onFilterDropdownVisibleChange: (v) => {
if (v) {
setTimeout(() => inputRef.current!.focus(), 150);
}
},
},
{
title: t("value"),
dataIndex: "value",
key: "value",
className: "max-table-cell",
render(col) {
switch (typeof col) {
case "string":
return col;
default:
return (
<span
style={{
whiteSpace: "break-spaces",
}}
>
{JSON.stringify(col, null, 2)}
</span>
);
}
},
},
{
title: t("type"),
dataIndex: "value",
width: 90,
key: "type",
render(col) {
return valueType(col);
},
},
{
title: t("action"),
render(_col, value: Value, index) {
return (
<Space>
<Button
type="text"
icon={<IconEdit />}
onClick={() => {
setCurrentValue(value);
setVisibleEdit(true);
}}
/>
<Button
type="text"
iconOnly
icon={<IconDelete />}
onClick={() => {
valueCtrl.setValue(script!.id, value.key, undefined);
Message.info({
content: t("delete_success"),
});
setData(data.filter((_, i) => i !== index));
}}
/>
</Space>
);
},
},
];
return (
<Drawer
width={600}
title={
<span>
{script?.name} {t("script_storage")}
</span>
}
visible={visible}
onOk={onOk}
onCancel={onCancel}
>
<Modal
title={currentValue ? t("edit_value") : t("add_value")}
visible={visibleEdit}
onOk={() => {
form.validate().then((value: { key: string; value: any; type: string }) => {
switch (value.type) {
case "number":
value.value = Number(value.value);
break;
case "boolean":
value.value = value.value === "true";
break;
case "object":
value.value = JSON.parse(value.value);
break;
default:
break;
}
valueCtrl.setValue(script!.id, value.key, value.value);
if (currentValue) {
Message.info({
content: t("update_success"),
});
setData(
data.map((v) => {
if (v.key === value.key) {
return {
...v,
value: value.value,
};
}
return v;
})
);
} else {
Message.info({
content: t("add_success"),
});
setData([
{
id: 0,
scriptId: script!.id,
storageName: (script?.metadata.storagename && script?.metadata.storagename[0]) || "",
key: value.key,
value: value.value,
createtime: Date.now(),
updatetime: 0,
},
...data,
]);
}
setVisibleEdit(false);
});
}}
onCancel={() => setVisibleEdit(false)}
>
{visibleEdit && (
<Form
form={form}
initialValues={{
key: currentValue?.key,
value:
typeof currentValue?.value === "string"
? currentValue?.value
: JSON.stringify(currentValue?.value, null, 2),
type: valueType(currentValue?.value || "string"),
}}
>
<FormItem label="Key" field="key" rules={[{ required: true }]}>
<Input placeholder={t("key_placeholder")!} disabled={!!currentValue} />
</FormItem>
<FormItem label="Value" field="value" rules={[{ required: true }]}>
<Input.TextArea rows={6} placeholder={t("value_placeholder")!} />
</FormItem>
<FormItem label={t("type")} field="type" rules={[{ required: true }]}>
<Select>
<Select.Option value="string">{t("type_string")}</Select.Option>
<Select.Option value="number">{t("type_number")}</Select.Option>
<Select.Option value="boolean">{t("type_boolean")}</Select.Option>
<Select.Option value="object">{t("type_object")}</Select.Option>
</Select>
</FormItem>
</Form>
)}
</Modal>
<Space className="w-full" direction="vertical">
<Space className="!flex justify-end">
<Popconfirm
focusLock
title={t("confirm_clear")}
onOk={() => {
setData((prev) => {
prev.forEach((v) => {
valueCtrl.setValue(script!.id, v.key, undefined);
});
Message.info({
content: t("clear_success"),
});
return [];
});
}}
>
<Button type="primary" status="warning">
{t("clear")}
</Button>
</Popconfirm>
<Button
type="primary"
onClick={() => {
setCurrentValue(undefined);
setVisibleEdit(true);
}}
>
{t("add")}
</Button>
</Space>
<Table columns={columns} data={data} rowKey="id" />
</Space>
</Drawer>
);
};
export default ScriptStorage;

View File

@ -0,0 +1,200 @@
import React, { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; // 添加这行导入语句
import { Script, UserConfig } from "@App/app/repo/scripts";
import {
Checkbox,
Form,
FormInstance,
Input,
InputNumber,
Message,
Modal,
Select,
Tabs,
} from "@arco-design/web-react";
import TabPane from "@arco-design/web-react/es/Tabs/tab-pane";
const FormItem = Form.Item;
const UserConfigPanel: React.FC<{
script: Script;
userConfig: UserConfig;
values: { [key: string]: any };
}> = ({ script, userConfig, values }) => {
const formRefs = useRef<{ [key: string]: FormInstance }>({});
const [visible, setVisible] = React.useState(true);
const [tab, setTab] = React.useState(Object.keys(userConfig)[0]);
useEffect(() => {
setTab(Object.keys(userConfig)[0]);
setVisible(true);
}, [script, userConfig]);
const { t } = useTranslation();
return (
<Modal
visible={visible}
title={`${script.name} ${t("config")}`} // 替换为键值对应的英文文本
okText={t("save")} // 替换为键值对应的英文文本
cancelText={t("close")} // 替换为键值对应的英文文本
onOk={() => {
if (formRefs.current[tab]) {
const saveValues = formRefs.current[tab].getFieldsValue();
// 更新value
const valueCtrl = IoC.instance(ValueController) as ValueController;
Object.keys(saveValues).forEach((key) => {
Object.keys(saveValues[key]).forEach((valueKey) => {
if (saveValues[key][valueKey] === undefined) {
return;
}
valueCtrl.setValue(
script.id,
`${key}.${valueKey}`,
saveValues[key][valueKey]
);
});
});
Message.success(t("save_success")!); // 替换为键值对应的英文文本
setVisible(false);
}
}}
onCancel={() => {
setVisible(false);
}}
>
<Tabs
activeTab={tab}
onChange={(value) => {
setTab(value);
}}
>
{Object.keys(userConfig).map((itemKey) => {
const value = userConfig[itemKey];
return (
<TabPane key={itemKey} title={itemKey}>
<Form
key={script.id}
style={{
width: "100%",
}}
autoComplete="off"
layout="vertical"
initialValues={values}
ref={(el: FormInstance) => {
formRefs.current[itemKey] = el;
}}
>
{Object.keys(value).map((key) => (
<FormItem
key={key}
label={value[key].title}
field={`${itemKey}.${key}`}
>
{() => {
const item = value[key];
let { type } = item;
if (!type) {
// 根据其他值判断类型
if (typeof item.default === "boolean") {
type = "checkbox";
} else if (item.values) {
if (typeof item.values === "object") {
type = "mult-select";
} else {
type = "select";
}
} else if (typeof item.default === "number") {
type = "number";
} else {
type = "text";
}
}
switch (type) {
case "text":
if (item.password) {
return (
<Input.Password
placeholder={item.description}
maxLength={item.max}
/>
);
}
return (
<Input
placeholder={item.description}
maxLength={item.max}
showWordLimit
/>
);
case "number":
return (
<InputNumber
placeholder={item.description}
min={item.min}
max={item.max}
suffix={item.unit}
/>
);
case "checkbox":
return (
<Checkbox
defaultChecked={values[`${itemKey}.${key}`]}
>
{item.description}
</Checkbox>
);
case "select":
case "mult-select":
// eslint-disable-next-line no-case-declarations
let options: any[];
if (item.bind) {
const bindKey = item.bind.substring(1);
if (values[bindKey]) {
options = values[bindKey]!;
} else {
options = [];
}
} else {
options = item.values!;
}
return (
<Select
mode={
item.type === "mult-select"
? "multiple"
: undefined
}
placeholder={item.description}
>
{options!.map((option) => (
<Select.Option key={option} value={option}>
{option}
</Select.Option>
))}
</Select>
);
case "textarea":
return (
<Input.TextArea
placeholder={item.description}
maxLength={item.max}
rows={item.rows}
showWordLimit
/>
);
default:
return null;
}
}}
</FormItem>
))}
</Form>
</TabPane>
);
})}
</Tabs>
</Modal>
);
};
export default UserConfigPanel;

View File

@ -0,0 +1,210 @@
import Logger from "@App/pages/options/routes/Logger";
import ScriptEditor from "@App/pages/options/routes/script/ScriptEditor";
import ScriptList from "@App/pages/options/routes/ScriptList";
import Setting from "@App/pages/options/routes/Setting";
import SubscribeList from "@App/pages/options/routes/SubscribeList";
import Tools from "@App/pages/options/routes/Tools";
import { Layout, Menu } from "@arco-design/web-react";
import {
IconCode,
IconFile,
IconGithub,
IconLeft,
IconLink,
IconQuestion,
IconRight,
IconSettings,
IconSubscribe,
IconTool,
} from "@arco-design/web-react/icon";
import React, { useRef, useState } from "react";
import { HashRouter, Route, Routes } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { RiFileCodeLine, RiGuideLine, RiLinkM } from "react-icons/ri";
import SiderGuide from "./SiderGuide";
import CustomLink from "../CustomLink";
const MenuItem = Menu.Item;
let { hash } = window.location;
if (!hash.length) {
hash = "/";
} else {
hash = hash.substring(1);
}
const Sider: React.FC = () => {
const [menuSelect, setMenuSelect] = useState(hash);
const [collapsed, setCollapsed] = useState(localStorage.collapsed === "true");
const { t } = useTranslation();
const guideRef = useRef<{ open: () => void }>(null);
return (
<HashRouter>
<SiderGuide ref={guideRef} />
<Layout.Sider className="h-full" collapsed={collapsed} width={170}>
<div className="flex flex-col justify-between h-full">
<Menu
style={{ width: "100%" }}
selectedKeys={[menuSelect]}
selectable
onClickMenuItem={(key) => {
setMenuSelect(key);
}}
>
<CustomLink to="/">
<MenuItem key="/" className="menu-script">
<IconCode /> {t("installed_scripts")}
</MenuItem>
</CustomLink>
<CustomLink to="/subscribe">
<MenuItem key="/subscribe">
<IconSubscribe /> {t("subscribe")}
</MenuItem>
</CustomLink>
<CustomLink to="/logger">
<MenuItem key="/logger">
<IconFile /> {t("logs")}
</MenuItem>
</CustomLink>
<CustomLink to="/tools" className="menu-tools">
<MenuItem key="/tools">
<IconTool /> {t("tools")}
</MenuItem>
</CustomLink>
<CustomLink to="/setting" className="menu-setting">
<MenuItem key="/setting">
<IconSettings /> {t("settings")}
</MenuItem>
</CustomLink>
</Menu>
<Menu
style={{ width: "100%", borderTop: "1px solid var(--color-bg-5)" }}
selectedKeys={[]}
selectable
onClickMenuItem={(key) => {
setMenuSelect(key);
}}
mode="pop"
>
<Menu.SubMenu
key="/help"
title={
<>
<IconQuestion /> {t("helpcenter")}
</>
}
triggerProps={{
trigger: "hover",
}}
>
<Menu.SubMenu
key="/external_links"
title={
<>
<RiLinkM /> {t("external_links")}
</>
}
>
<Menu.Item key="scriptcat/docs/dev/">
<a
href="https://docs.scriptcat.org/docs/dev/"
target="_blank"
rel="noreferrer"
>
<RiFileCodeLine /> {t("api_docs")}
</a>
</Menu.Item>
<Menu.Item key="scriptcat/docs/learn/">
<a
href="https://learn.scriptcat.org/docs/%E7%AE%80%E4%BB%8B/"
target="_blank"
rel="noreferrer"
>
<RiFileCodeLine /> {t("development_guide")}
</a>
</Menu.Item>
<Menu.Item key="scriptcat/userscript">
<a
href="https://scriptcat.org/search"
target="_blank"
rel="noreferrer"
>
<IconLink /> {t("script_gallery")}
</a>
</Menu.Item>
<Menu.Item key="tampermonkey/bbs">
<a
href="https://bbs.tampermonkey.net.cn/"
target="_blank"
rel="noreferrer"
>
<IconLink /> {t("community_forum")}
</a>
</Menu.Item>
<Menu.Item key="GitHub">
<a
href="https://github.com/scriptscat/scriptcat"
target="_blank"
rel="noreferrer"
>
<IconGithub /> GitHub
</a>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item
key="/guide"
onClick={() => {
guideRef.current?.open();
}}
>
<RiGuideLine /> {t("guide")}
</Menu.Item>
<Menu.Item key="scriptcat/docs/use/">
<a
href="https://docs.scriptcat.org/docs/use/"
target="_blank"
rel="noreferrer"
>
<RiFileCodeLine /> {t("user_guide")}
</a>
</Menu.Item>
</Menu.SubMenu>
<MenuItem
key="/collapsible"
onClick={() => {
localStorage.collapsed = !collapsed;
setCollapsed(!collapsed);
}}
>
{collapsed ? <IconRight /> : <IconLeft />} {t("collapsible")}
</MenuItem>
</Menu>
</div>
</Layout.Sider>
<Layout.Content
style={{
borderLeft: "1px solid var(--color-bg-5)",
overflow: "hidden",
padding: 10,
height: "100%",
boxSizing: "border-box",
position: "relative",
}}
>
<Routes>
<Route index element={<ScriptList />} />
<Route path="/script/editor">
<Route path=":id" element={<ScriptEditor />} />
<Route path="" element={<ScriptEditor />} />
</Route>
<Route path="/subscribe" element={<SubscribeList />} />
<Route path="/logger" element={<Logger />} />
<Route path="/tools" element={<Tools />} />
<Route path="/setting" element={<Setting />} />
</Routes>
</Layout.Content>
</HashRouter>
);
};
export default Sider;

View File

@ -0,0 +1,157 @@
import React, { useEffect, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import Joyride, { Step } from "react-joyride";
import { Path, useLocation, useNavigate } from "react-router-dom";
import CustomTrans from "../CustomTrans";
const SiderGuide: React.ForwardRefRenderFunction<{ open: () => void }, object> = (
_props,
ref
) => {
const { t } = useTranslation();
const [stepIndex, setStepIndex] = useState(0);
const [initRoute, setInitRoute] = useState<Partial<Path>>({ pathname: "/" });
const [run, setRun] = useState(false);
const navigate = useNavigate();
const location = useLocation();
useImperativeHandle(ref, () => ({
open: () => setRun(true),
}));
useEffect(() => {
// 首次使用时,打开引导
if (localStorage.getItem("firstUse") === null) {
localStorage.setItem("firstUse", "false");
setRun(true);
}
}, []);
const steps: Array<Step> = [
{
title: t("start_guide_title"),
content: t("start_guide_content"),
target: "body",
placement: "center",
},
{
title: t("installed_scripts"),
content: t("guide_installed_scripts"),
target: ".menu-script",
},
{
content: <CustomTrans i18nKey="guide_script_list_content" />,
target: "#script-list",
title: t("guide_script_list_title"),
placement: "auto",
},
{
content: t("guide_script_list_enable_content"),
target: ".script-enable",
title: t("guide_script_list_enable_title"),
},
{
content: t("guide_script_list_apply_to_run_status_content"),
target: ".apply_to_run_status",
title: t("guide_script_list_apply_to_run_status_title"),
},
{
target: ".script-sort",
title: t("guide_script_list_sort_title"),
content: <CustomTrans i18nKey="guide_script_list_sort_content" />,
},
{
target: ".menu-tools",
title: t("guide_tools_title"),
content: t("guide_tools_content"),
placement: "auto",
},
{
target: ".tools .backup",
title: t("guide_tools_backup_title"),
content: t("guide_tools_backup_content"),
},
{
target: ".menu-setting",
title: t("guide_setting_title"),
content: t("guide_setting_content"),
placement: "auto",
},
{
target: ".setting .sync",
title: t("guide_setting_sync_title"),
content: t("guide_setting_sync_content"),
},
];
const gotoNavigate = (go: Partial<Path>) => {
if (go.pathname !== location.pathname) {
return navigate(go);
}
if (go.search !== location.search) {
return navigate(go);
}
if (go.hash !== location.hash) {
return navigate(go);
}
return true;
};
return (
<Joyride
callback={(data) => {
if (
data.action === "stop" ||
data.action === "close" ||
data.status === "finished"
) {
setRun(false);
setStepIndex(0);
gotoNavigate(initRoute);
} else if (data.action === "next" && data.lifecycle === "complete") {
switch (data.index) {
case 5:
gotoNavigate({ pathname: "/tools" });
break;
case 7:
gotoNavigate({ pathname: "/setting" });
break;
default:
break;
}
setStepIndex(data.index + 1);
} else if (data.action === "prev" && data.lifecycle === "complete") {
setStepIndex(data.index - 1);
} else if (data.action === "start" && data.lifecycle === "init") {
gotoNavigate({ pathname: "/" });
setInitRoute({
pathname: location.pathname,
search: location.search,
hash: location.hash,
});
}
}}
locale={{
next: t("next"),
skip: t("skip"),
back: t("back"),
last: t("last"),
}}
continuous
run={run}
scrollToFirstStep
showProgress
showSkipButton
stepIndex={stepIndex}
steps={steps}
disableOverlayClose
disableScrolling
spotlightPadding={0}
styles={{
options: {
zIndex: 10000,
},
}}
/>
);
};
export default React.forwardRef(SiderGuide);

View File

@ -6,7 +6,7 @@ import { Subscribe } from "@App/app/repo/subscribe";
import { i18nDescription, i18nName } from "@App/locales/locales";
import { useTranslation } from "react-i18next";
import { prepareScriptByCode, prepareSubscribeByCode, ScriptInfo } from "@App/pkg/utils/script";
import { isDebug, nextTime } from "@App/pkg/utils/utils";
import { nextTime } from "@App/pkg/utils/utils";
import { ScriptClient } from "@App/app/service/service_worker/client";
type Permission = { label: string; color?: string; value: string[] }[];
@ -156,11 +156,13 @@ function App() {
setScriptInfo(info);
setEnable(action.status === SCRIPT_STATUS_ENABLE);
setUpsertScript(action);
// 修改网页显示title
document.title = `${!isUpdate ? t("install_script") : t("update_script")} - ${i18nName(action)} - ScriptCat`;
})
.catch(() => {
Message.error(t("script_info_load_failed"));
});
}, [t]);
}, [isUpdate, t]);
return (
<div className="h-full">
@ -253,11 +255,9 @@ function App() {
Message.success(t("install_success")!);
setBtnText(t("install_success")!);
}
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
!isDebug() &&
setTimeout(() => {
closeWindow();
}, 200);
}, 500);
})
.catch((e) => {
Message.error(`${t("install_failed")}: ${e}`);

24
src/pages/options.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%= htmlRspackPlugin.options.title %></title>
</head>
<body>
<div id="root"></div>
<iframe src="/src/sandbox.html" name="sandbox" sandbox="allow-scripts" style="display: none"></iframe>
</body>
<style>
body {
height: 100%;
overflow: hidden;
background-color: var(--color-bg-2);
color: var(--color-text-1);
}
</style>
<% if rspackConfig.mode=="script" { %>
<script type="text/javascript" src="/_locales/i18n.js"></script>
<script type="text/javascript" src="https://cdn.crowdin.com/jipt/jipt.js"></script>
<% } %>
</html>

View File

@ -4,22 +4,26 @@ import MainLayout from "../components/layout/MainLayout.tsx";
import "@arco-design/web-react/dist/css/arco.css";
import "@App/locales/locales";
import "@App/index.css";
import "./index.css";
import { Provider } from "react-redux";
import { store } from "@App/store/store.ts";
import { Broker } from "@Packages/message/message_queue.ts";
import Sider from "../components/layout/Sider.tsx";
// 测试监听广播
// // 测试监听广播
const border = new Broker();
// const border = new Broker();
border.subscribe("installScript", (message) => {
console.log(message);
});
// border.subscribe("installScript", (message) => {
// console.log(message);
// });
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<MainLayout className="!flex-col !px-4 box-border">p</MainLayout>
<MainLayout className="!flex-row">
<Sider />
</MainLayout>
</Provider>
</React.StrictMode>
);

View File

@ -1,14 +1,5 @@
import React, { useEffect } from "react";
import {
BackTop,
Button,
Card,
DatePicker,
Input,
List,
Message,
Space,
} from "@arco-design/web-react";
import { BackTop, Button, Card, DatePicker, Input, List, Message, Space } from "@arco-design/web-react";
import dayjs from "dayjs";
import Text from "@arco-design/web-react/es/Typography/text";
import { Logger, LoggerDAO } from "@App/app/repo/logger";
@ -16,8 +7,6 @@ import LogLabel, { Labels, Query } from "@App/pages/components/LogLabel";
import { IconPlus } from "@arco-design/web-react/icon";
import { useSearchParams } from "react-router-dom";
import { formatUnixTime } from "@App/pkg/utils/utils";
import { SystemConfig } from "@App/pkg/config/config";
import IoC from "@App/app/ioc";
import { useTranslation } from "react-i18next";
function LoggerPage() {
@ -28,12 +17,10 @@ function LoggerPage() {
const [logs, setLogs] = React.useState<Logger[]>([]);
const [queryLogs, setQueryLogs] = React.useState<Logger[]>([]);
const [search, setSearch] = React.useState<string>("");
const [startTime, setStartTime] = React.useState(
dayjs().subtract(24, "hour").unix()
);
const [startTime, setStartTime] = React.useState(dayjs().subtract(24, "hour").unix());
const [endTime, setEndTime] = React.useState(dayjs().unix());
const loggerDAO = new LoggerDAO();
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
const systemConfig = { logCleanCycle: 1 };
const { t } = useTranslation();
const onQueryLog = () => {
@ -46,35 +33,27 @@ function LoggerPage() {
const value = log.label[query.key];
switch (query.condition) {
case "=":
// eslint-disable-next-line eqeqeq
if (value != query.value) {
return;
}
break;
case "=~":
if (
typeof value === "string" &&
value.indexOf(query.value) === -1
) {
if (typeof value === "string" && value.indexOf(query.value) === -1) {
return;
}
break;
case "!=":
// eslint-disable-next-line eqeqeq
if (value == query.value) {
return;
}
break;
case "!~":
if (
typeof value === "string" &&
value.indexOf(query.value) === -1
) {
if (typeof value === "string" && value.indexOf(query.value) === -1) {
return;
}
break;
default:
// eslint-disable-next-line eqeqeq
if (value != query.value) {
return;
}
@ -129,11 +108,7 @@ function LoggerPage() {
return (
<>
<BackTop
visibleHeight={30}
style={{ position: "absolute" }}
target={() => document.getElementById("backtop")!}
/>
<BackTop visibleHeight={30} style={{ position: "absolute" }} target={() => document.getElementById("backtop")!} />
<div
id="backtop"
style={{
@ -316,8 +291,7 @@ function LoggerPage() {
}}
>
<Text>
{formatUnixTime(startTime)} {t("to")} {formatUnixTime(endTime)}{" "}
{t("total_logs", { length: logs.length })}
{formatUnixTime(startTime)} {t("to")} {formatUnixTime(endTime)} {t("total_logs", { length: logs.length })}
{init === 4
? `, ${t("filtered_logs", { length: queryLogs.length })}`
: `, ${t("enter_filter_conditions")}`}
@ -334,9 +308,9 @@ function LoggerPage() {
key={index}
style={{
background:
// eslint-disable-next-line no-nested-ternary
item.level === "error"
? "var(--color-danger-light-2)" // eslint-disable-next-line no-nested-ternary
? "var(--color-danger-light-2)"
: item.level === "warn"
? "var(--color-warning-light-2)"
: item.level === "info"
@ -345,9 +319,7 @@ function LoggerPage() {
}}
>
{formatUnixTime(item.createtime / 1000)}{" "}
{typeof item.message === "object"
? JSON.stringify(item.message)
: item.message}{" "}
{typeof item.message === "object" ? JSON.stringify(item.message) : item.message}{" "}
{JSON.stringify(item.label)}
</List.Item>
)}

View File

@ -25,7 +25,6 @@ import {
SCRIPT_STATUS_ENABLE,
SCRIPT_TYPE_BACKGROUND,
SCRIPT_TYPE_NORMAL,
ScriptDAO,
UserConfig,
} from "@App/app/repo/scripts";
import {
@ -46,7 +45,6 @@ import {
RiUploadCloudFill,
} from "react-icons/ri";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import ScriptController from "@App/app/service/script/controller";
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import Text from "@arco-design/web-react/es/Typography/text";
import {
@ -59,28 +57,21 @@ import {
useSensors,
} from "@dnd-kit/core";
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import IoC from "@App/app/ioc";
import RuntimeController from "@App/runtime/content/runtime";
import UserConfigPanel from "@App/pages/components/UserConfigPanel";
import CloudScriptPlan from "@App/pages/components/CloudScriptPlan";
import SynchronizeController from "@App/app/service/synchronize/controller";
import { useTranslation } from "react-i18next";
import { nextTime, semTime } from "@App/pkg/utils/utils";
import { i18nName } from "@App/locales/locales";
import { SystemConfig } from "@App/pkg/config/config";
import {
getValues,
ListHomeRender,
ScriptIcons,
scriptListSort,
} from "./utils";
import { ListHomeRender, ScriptIcons } from "./utils";
import { useAppDispatch, useAppSelector } from "@App/store/hooks";
import { fetchScriptList, selectScripts } from "@App/store/features/script";
import { selectScriptListColumnWidth } from "@App/store/features/setting";
type ListType = Script & { loading?: boolean };
@ -91,43 +82,38 @@ function ScriptList() {
values: { [key: string]: any };
}>();
const [cloudScript, setCloudScript] = useState<Script>();
const scriptCtrl = IoC.instance(ScriptController) as ScriptController;
const synchronizeCtrl = IoC.instance(
SynchronizeController
) as SynchronizeController;
const runtimeCtrl = IoC.instance(RuntimeController) as RuntimeController;
const [scriptList, setScriptList] = useState<ListType[]>([]);
const dispatch = useAppDispatch();
const scriptList = useAppSelector(selectScripts);
const scriptListColumnWidth = useAppSelector(selectScriptListColumnWidth);
const inputRef = useRef<RefInputType>(null);
const navigate = useNavigate();
const openUserConfig = parseInt(
useSearchParams()[0].get("userConfig") || "",
10
);
const openUserConfig = parseInt(useSearchParams()[0].get("userConfig") || "", 10);
const [showAction, setShowAction] = useState(false);
const [action, setAction] = useState("");
const [select, setSelect] = useState<Script[]>([]);
const [selectColumn, setSelectColumn] = useState(0);
const systemConfig = IoC.instance(SystemConfig) as SystemConfig;
const { t } = useTranslation();
useEffect(() => {
dispatch(fetchScriptList());
// 监听脚本安装/运行
// Monitor script running status
const channel = runtimeCtrl.watchRunStatus();
channel.setHandler(([id, status]: any) => {
setScriptList((list) => {
return list.map((item) => {
if (item.id === id) {
item.runStatus = status;
}
return item;
});
});
});
return () => {
channel.disChannel();
};
}, []);
// const channel = runtimeCtrl.watchRunStatus();
// channel.setHandler(([id, status]: any) => {
// setScriptList((list) => {
// return list.map((item) => {
// if (item.id === id) {
// item.runStatus = status;
// }
// return item;
// });
// });
// });
// return () => {
// channel.disChannel();
// };
}, [dispatch]);
const columns: ColumnProps[] = [
{
@ -167,27 +153,27 @@ function ScriptList() {
loading={item.loading}
disabled={item.loading}
onChange={(checked) => {
setScriptList((list) => {
const index = list.findIndex((script) => script.id === item.id);
list[index].loading = true;
let p: Promise<any>;
if (checked) {
p = scriptCtrl.enable(item.id).then(() => {
list[index].status = SCRIPT_STATUS_ENABLE;
});
} else {
p = scriptCtrl.disable(item.id).then(() => {
list[index].status = SCRIPT_STATUS_DISABLE;
});
}
p.catch((err) => {
Message.error(err);
}).finally(() => {
list[index].loading = false;
setScriptList([...list]);
});
return list;
});
// setScriptList((list) => {
// const index = list.findIndex((script) => script.id === item.id);
// list[index].loading = true;
// let p: Promise<any>;
// if (checked) {
// p = scriptCtrl.enable(item.id).then(() => {
// list[index].status = SCRIPT_STATUS_ENABLE;
// });
// } else {
// p = scriptCtrl.disable(item.id).then(() => {
// list[index].status = SCRIPT_STATUS_DISABLE;
// });
// }
// p.catch((err) => {
// Message.error(err);
// }).finally(() => {
// list[index].loading = false;
// setScriptList([...list]);
// });
// return list;
// });
}}
/>
);
@ -199,7 +185,6 @@ function ScriptList() {
dataIndex: "name",
sorter: (a, b) => a.name.localeCompare(b.name),
filterIcon: <IconSearch />,
// eslint-disable-next-line react/no-unstable-nested-components
filterDropdown: ({ filterKeys, setFilterKeys, confirm }: any) => {
return (
<div className="arco-table-custom-filter">
@ -244,7 +229,7 @@ function ScriptList() {
return (
<Tooltip content={col} position="tl">
<Link
to={`/script/editor/${item.id}`}
to={`/script/editor/${item.uuid}`}
style={{
textDecoration: "none",
}}
@ -287,7 +272,7 @@ function ScriptList() {
pathname: "logger",
search: `query=${encodeURIComponent(
JSON.stringify([
{ key: "scriptId", value: item.id },
{ key: "scriptId", value: item.uuid },
{
key: "component",
value: "GM_log",
@ -317,9 +302,7 @@ function ScriptList() {
if (item.type === SCRIPT_TYPE_BACKGROUND) {
tooltip = t("background_script_tooltip");
} else {
tooltip = `${t("scheduled_script_tooltip")} ${nextTime(
item.metadata.crontab[0]
)}`;
tooltip = `${t("scheduled_script_tooltip")} ${nextTime(item.metadata.crontab[0])}`;
}
return (
<Tooltip content={tooltip}>
@ -332,9 +315,7 @@ function ScriptList() {
}}
onClick={toLogger}
>
{item.runStatus === SCRIPT_RUN_STATUS_RUNNING
? t("running")
: t("completed")}
{item.runStatus === SCRIPT_RUN_STATUS_RUNNING ? t("running") : t("completed")}
</Tag>
</Tooltip>
);
@ -351,8 +332,7 @@ function ScriptList() {
<Tooltip
content={
<p style={{ margin: 0 }}>
{t("subscription_link")}:{" "}
{decodeURIComponent(item.subscribeUrl)}
{t("subscription_link")}: {decodeURIComponent(item.subscribeUrl)}
</p>
}
>
@ -440,41 +420,40 @@ function ScriptList() {
sorter: (a, b) => a.updatetime - b.updatetime,
render(col, script: Script) {
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<span
style={{
cursor: "pointer",
}}
onClick={() => {
if (!script.checkUpdateUrl) {
Message.warning(t("update_not_supported")!);
return;
}
Message.info({
id: "checkupdate",
content: t("checking_for_updates"),
});
scriptCtrl
.checkUpdate(script.id)
.then((res) => {
if (res) {
Message.warning({
id: "checkupdate",
content: t("new_version_available"),
});
} else {
Message.success({
id: "checkupdate",
content: t("latest_version"),
});
}
})
.catch((e) => {
Message.error({
id: "checkupdate",
content: `${t("update_check_failed")}: ${e.message}`,
});
});
// if (!script.checkUpdateUrl) {
// Message.warning(t("update_not_supported")!);
// return;
// }
// Message.info({
// id: "checkupdate",
// content: t("checking_for_updates"),
// });
// scriptCtrl
// .checkUpdate(script.id)
// .then((res) => {
// if (res) {
// Message.warning({
// id: "checkupdate",
// content: t("new_version_available"),
// });
// } else {
// Message.success({
// id: "checkupdate",
// content: t("latest_version"),
// });
// }
// })
// .catch((e) => {
// Message.error({
// id: "checkupdate",
// content: `${t("update_check_failed")}: ${e.message}`,
// });
// });
}}
>
{semTime(new Date(col))}
@ -490,7 +469,7 @@ function ScriptList() {
render(col, item: Script) {
return (
<Button.Group>
<Link to={`/script/editor/${item.id}`}>
<Link to={`/script/editor/${item.uuid}`}>
<Button
type="text"
icon={<RiPencilFill />}
@ -503,12 +482,12 @@ function ScriptList() {
title={t("confirm_delete_script")}
icon={<RiDeleteBin5Fill />}
onOk={() => {
setScriptList((list) => {
return list.filter((i) => i.id !== item.id);
});
scriptCtrl.delete(item.id).catch((e) => {
Message.error(`${t("delete_failed")}: ${e}`);
});
// setScriptList((list) => {
// return list.filter((i) => i.id !== item.id);
// });
// scriptCtrl.delete(item.id).catch((e) => {
// Message.error(`${t("delete_failed")}: ${e}`);
// });
}}
>
<Button
@ -526,13 +505,13 @@ function ScriptList() {
icon={<RiSettings3Fill />}
onClick={() => {
// Get value
getValues(item).then((newValues) => {
setUserConfig({
userConfig: { ...item.config! },
script: item,
values: newValues,
});
});
// getValues(item).then((newValues) => {
// setUserConfig({
// userConfig: { ...item.config! },
// script: item,
// values: newValues,
// });
// });
}}
style={{
color: "var(--color-text-2)",
@ -546,16 +525,16 @@ function ScriptList() {
icon={<RiStopFill />}
onClick={async () => {
// Stop script
Message.loading({
id: "script-stop",
content: t("stopping_script"),
});
await runtimeCtrl.stopScript(item.id);
Message.success({
id: "script-stop",
content: t("script_stopped"),
duration: 3000,
});
// Message.loading({
// id: "script-stop",
// content: t("stopping_script"),
// });
// await runtimeCtrl.stopScript(item.id);
// Message.success({
// id: "script-stop",
// content: t("script_stopped"),
// duration: 3000,
// });
}}
style={{
color: "var(--color-text-2)",
@ -567,25 +546,25 @@ function ScriptList() {
icon={<RiPlayFill />}
onClick={async () => {
// Start script
Message.loading({
id: "script-run",
content: t("starting_script"),
});
await runtimeCtrl.startScript(item.id);
Message.success({
id: "script-run",
content: t("script_started"),
duration: 3000,
});
setScriptList((list) => {
for (let i = 0; i < list.length; i += 1) {
if (list[i].id === item.id) {
list[i].runStatus = SCRIPT_RUN_STATUS_RUNNING;
break;
}
}
return [...list];
});
// Message.loading({
// id: "script-run",
// content: t("starting_script"),
// });
// await runtimeCtrl.startScript(item.id);
// Message.success({
// id: "script-run",
// content: t("script_started"),
// duration: 3000,
// });
// setScriptList((list) => {
// for (let i = 0; i < list.length; i += 1) {
// if (list[i].id === item.id) {
// list[i].runStatus = SCRIPT_RUN_STATUS_RUNNING;
// break;
// }
// }
// return [...list];
// });
}}
style={{
color: "var(--color-text-2)",
@ -612,32 +591,31 @@ function ScriptList() {
const [newColumns, setNewColumns] = useState<ColumnProps[]>([]);
// 设置列和排序
useEffect(() => {
const dao = new ScriptDAO();
dao.table
.orderBy("sort")
.toArray()
.then(async (scripts) => {
// Sort when a new script is added (-1)
scriptListSort(scripts);
// Open user config panel
if (openUserConfig) {
const script = scripts.find((item) => item.id === openUserConfig);
if (script && script.config) {
setUserConfig({
script,
userConfig: script.config,
values: await getValues(script),
});
}
}
setScriptList(scripts);
});
// const dao = new ScriptDAO();
// dao.table
// .orderBy("sort")
// .toArray()
// .then(async (scripts) => {
// // Sort when a new script is added (-1)
// scriptListSort(scripts);
// // Open user config panel
// if (openUserConfig) {
// const script = scripts.find((item) => item.id === openUserConfig);
// if (script && script.config) {
// setUserConfig({
// script,
// userConfig: script.config,
// values: await getValues(script),
// });
// }
// }
// setScriptList(scripts);
// });
setNewColumns(
columns.map((item) => {
item.width =
systemConfig.scriptListColumnWidth[item.key!] ?? item.width;
item.width = scriptListColumnWidth[item.key!] ?? item.width;
return item;
})
);
@ -651,7 +629,6 @@ function ScriptList() {
})
);
// eslint-disable-next-line react/no-unstable-nested-components
const SortableWrapper = (props: any, ref: any) => {
return (
<DndContext
@ -663,27 +640,24 @@ function ScriptList() {
return;
}
if (active.id !== over.id) {
setScriptList((items) => {
let oldIndex = 0;
let newIndex = 0;
items.forEach((item, index) => {
if (item.id === active.id) {
oldIndex = index;
} else if (item.id === over.id) {
newIndex = index;
}
});
const newItems = arrayMove(items, oldIndex, newIndex);
scriptListSort(newItems);
return newItems;
});
// setScriptList((items) => {
// let oldIndex = 0;
// let newIndex = 0;
// items.forEach((item, index) => {
// if (item.id === active.id) {
// oldIndex = index;
// } else if (item.id === over.id) {
// newIndex = index;
// }
// });
// const newItems = arrayMove(items, oldIndex, newIndex);
// scriptListSort(newItems);
// return newItems;
// });
}
}}
>
<SortableContext
items={scriptList}
strategy={verticalListSortingStrategy}
>
<SortableContext items={scriptList} strategy={verticalListSortingStrategy}>
<table ref={ref} {...props} />
</SortableContext>
</DndContext>
@ -704,10 +678,8 @@ function ScriptList() {
const sortIndex = dealColumns.findIndex((item) => item.key === "sort");
// eslint-disable-next-line react/no-unstable-nested-components
const SortableItem = (props: any) => {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: props!.record.id });
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props!.record.uuid });
const style = {
transform: CSS.Transform.toString(transform),
@ -715,7 +687,6 @@ function ScriptList() {
};
// 替换排序列,使其可以拖拽
// eslint-disable-next-line react/destructuring-assignment
props.children[sortIndex + 1] = (
<td
className="arco-table-td"
@ -778,9 +749,7 @@ function ScriptList() {
<Select.Option value="disable">{t("disable")}</Select.Option>
<Select.Option value="export">{t("export")}</Select.Option>
<Select.Option value="delete">{t("delete")}</Select.Option>
<Select.Option value="check_update">
{t("check_update")}
</Select.Option>
<Select.Option value="check_update">{t("check_update")}</Select.Option>
</Select>
<Button
type="primary"
@ -853,9 +822,7 @@ function ScriptList() {
// 需要更新
Message.warning({
id: "checkupdate",
content: `${i18nName(item)} ${t(
"new_version_available"
)}`,
content: `${i18nName(item)} ${t("new_version_available")}`,
});
}
if (index === array.length - 1) {
@ -869,9 +836,7 @@ function ScriptList() {
.catch((e) => {
Message.error({
id: "checkupdate",
content: `${t("update_check_failed")}: ${
e.message
}`,
content: `${t("update_check_failed")}: ${e.message}`,
});
});
});
@ -944,12 +909,7 @@ function ScriptList() {
position="bl"
>
<Input
type={
newColumns[selectColumn].width === 0 ||
newColumns[selectColumn].width === -1
? ""
: "number"
}
type={newColumns[selectColumn].width === 0 || newColumns[selectColumn].width === -1 ? "" : "number"}
style={{ width: "80px" }}
size="mini"
value={
@ -1031,11 +991,7 @@ function ScriptList() {
}}
/>
{userConfig && (
<UserConfigPanel
script={userConfig.script}
userConfig={userConfig.userConfig}
values={userConfig.values}
/>
<UserConfigPanel script={userConfig.script} userConfig={userConfig.userConfig} values={userConfig.values} />
)}
<CloudScriptPlan
script={cloudScript}

View File

@ -8,15 +8,9 @@ import {
Select,
Space,
} from "@arco-design/web-react";
import FileSystemParams from "@App/pages/components/FileSystemParams";
import { SystemConfig } from "@App/pkg/config/config";
import IoC from "@App/app/ioc";
import FileSystemFactory, { FileSystemType } from "@Pkg/filesystem/factory";
import Title from "@arco-design/web-react/es/Typography/title";
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports
import { format } from "prettier";
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-import-module-exports
import babel from "prettier/parser-babel";
import GMApiSetting from "@App/pages/components/GMApiSetting";
import i18n from "@App/locales/locales";

View File

@ -18,8 +18,6 @@ import {
SubscribeDAO,
} from "@App/app/repo/subscribe";
import { ColumnProps } from "@arco-design/web-react/es/Table";
import IoC from "@App/app/ioc";
import SubscribeController from "@App/app/service/subscribe/controller";
import { IconSearch, IconUserAdd } from "@arco-design/web-react/icon";
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { semTime } from "@App/pkg/utils/utils";

View File

@ -12,17 +12,11 @@ import {
Space,
} from "@arco-design/web-react";
import Title from "@arco-design/web-react/es/Typography/title";
import IoC from "@App/app/ioc";
import SynchronizeController from "@App/app/service/synchronize/controller";
import FileSystemFactory, { FileSystemType } from "@Pkg/filesystem/factory";
import { SystemConfig } from "@App/pkg/config/config";
import { File, FileReader } from "@Pkg/filesystem/filesystem";
import { formatUnixTime } from "@App/pkg/utils/utils";
import FileSystemParams from "@App/pages/components/FileSystemParams";
import { IconQuestionCircleFill } from "@arco-design/web-react/icon";
import { RefInputType } from "@arco-design/web-react/es/Input/interface";
import { useTranslation } from "react-i18next";
import SystemController from "@App/app/service/system/controller";
function Tools() {
const [loading, setLoading] = useState<{ [key: string]: boolean }>({});

View File

@ -3,27 +3,16 @@ import CodeEditor from "@App/pages/components/CodeEditor";
import React, { useEffect, useRef, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { editor, KeyCode, KeyMod } from "monaco-editor";
import {
Button,
Dropdown,
Grid,
Menu,
Message,
Tabs,
Tooltip,
} from "@arco-design/web-react";
import { Button, Dropdown, Grid, Menu, Message, Tabs, Tooltip } from "@arco-design/web-react";
import TabPane from "@arco-design/web-react/es/Tabs/tab-pane";
import ScriptController from "@App/app/service/script/controller";
import normalTpl from "@App/template/normal.tpl";
import crontabTpl from "@App/template/crontab.tpl";
import backgroundTpl from "@App/template/background.tpl";
import { v4 as uuidv4 } from "uuid";
import "./index.css";
import IoC from "@App/app/ioc";
import LoggerCore from "@App/app/logger/core";
import Logger from "@App/app/logger/logger";
import { prepareScriptByCode } from "@App/pkg/utils/script";
import RuntimeController from "@App/runtime/content/runtime";
import ScriptStorage from "@App/pages/components/ScriptStorage";
import ScriptResource from "@App/pages/components/ScriptResource";
import ScriptSetting from "@App/pages/components/ScriptSetting";
@ -49,7 +38,7 @@ const Editor: React.FC<{
const [init, setInit] = useState(false);
const codeEditor = useRef<{ editor: editor.IStandaloneCodeEditor }>(null);
// Script.uuid为keyScript为value储存Script
ScriptMap.has(script.uuid) || ScriptMap.set(script.uuid, script);
ScriptMap.set(script.uuid, script);
useEffect(() => {
if (!codeEditor.current || !codeEditor.current.editor) {
setTimeout(() => {
@ -69,13 +58,13 @@ const Editor: React.FC<{
const activeEditor = editor
.getEditors()
// @ts-ignore
// eslint-disable-next-line no-underscore-dangle
.find((i) => i._focusTracker._hasFocus);
// 仅在获取到激活的editor时通过editor上绑定的uuid获取Script并指定激活的editor执行快捷键action
activeEditor &&
if (activeEditor) {
// @ts-ignore
item.action(ScriptMap.get(activeEditor.uuid), activeEditor);
}
});
});
codeEditor.current.editor.onKeyUp(() => {
@ -85,15 +74,7 @@ const Editor: React.FC<{
return () => {};
}, [init]);
return (
<CodeEditor
id={id}
ref={codeEditor}
code={script.code}
diffCode=""
editable
/>
);
return <CodeEditor id={id} ref={codeEditor} code={script.code} diffCode="" editable />;
};
type EditorMenu = {
@ -180,8 +161,7 @@ function ScriptEditor() {
>([]);
const [scriptList, setScriptList] = useState<Script[]>([]);
const [currentScript, setCurrentScript] = useState<Script>();
const [selectSciptButtonAndTab, setSelectSciptButtonAndTab] =
useState<string>("");
const [selectSciptButtonAndTab, setSelectSciptButtonAndTab] = useState<string>("");
const [rightOperationTab, setRightOperationTab] = useState<{
key: string;
uuid: string;
@ -196,10 +176,7 @@ function ScriptEditor() {
};
const { id } = useParams();
const save = (
script: Script,
e: editor.IStandaloneCodeEditor
): Promise<Script> => {
const save = (script: Script, e: editor.IStandaloneCodeEditor): Promise<Script> => {
// 解析code生成新的script并更新
return new Promise((resolve) => {
prepareScriptByCode(e.getValue(), script.origin || "", script.uuid)
@ -257,9 +234,7 @@ function ScriptEditor() {
return new Promise<void>((resolve) => {
chrome.downloads.download(
{
url: URL.createObjectURL(
new Blob([e.getValue()], { type: "text/javascript" })
),
url: URL.createObjectURL(new Blob([e.getValue()], { type: "text/javascript" })),
saveAs: true, // true直接弹出对话框false弹出下载选项
filename: `${script.name}.user.js`,
},
@ -306,8 +281,7 @@ function ScriptEditor() {
title: "调试",
hotKey: KeyMod.CtrlCmd | KeyCode.F5,
hotKeyString: "Ctrl+F5",
tooltip:
"只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
tooltip: "只有后台脚本/定时脚本才能调试, 且调试模式下不对进行权限校验(例如@connect)",
action: async (script, e) => {
// 保存更新代码之后再调试
const newScript = await save(script, e);
@ -457,44 +431,31 @@ function ScriptEditor() {
// eslint-disable-next-line default-case
switch (rightOperationTab.key) {
case "1":
newEditors = editors.filter(
(item) => item.script.uuid !== rightOperationTab.uuid
);
newEditors = editors.filter((item) => item.script.uuid !== rightOperationTab.uuid);
if (newEditors.length > 0) {
// 还有的话,如果之前有选中的,那么我们还是选中之前的,如果没有选中的我们就选中第一个
if (
rightOperationTab.selectSciptButtonAndTab ===
rightOperationTab.uuid
) {
if (rightOperationTab.selectSciptButtonAndTab === rightOperationTab.uuid) {
if (newEditors.length > 0) {
newEditors[0].active = true;
setSelectSciptButtonAndTab(newEditors[0].script.uuid);
}
} else {
setSelectSciptButtonAndTab(
rightOperationTab.selectSciptButtonAndTab
);
setSelectSciptButtonAndTab(rightOperationTab.selectSciptButtonAndTab);
// 之前选中的tab
editors.filter((item) => {
if (
item.script.uuid === rightOperationTab.selectSciptButtonAndTab
) {
if (item.script.uuid === rightOperationTab.selectSciptButtonAndTab) {
item.active = true;
} else {
item.active = false;
}
return (
item.script.uuid === rightOperationTab.selectSciptButtonAndTab
);
return item.script.uuid === rightOperationTab.selectSciptButtonAndTab;
});
}
}
setEditors([...newEditors]);
break;
case "2":
newEditors = editors.filter(
(item) => item.script.uuid === rightOperationTab.uuid
);
newEditors = editors.filter((item) => item.script.uuid === rightOperationTab.uuid);
setSelectSciptButtonAndTab(rightOperationTab.uuid);
setEditors([...newEditors]);
break;
@ -657,11 +618,7 @@ function ScriptEditor() {
}}
>
{menuItem.tooltip ? (
<Tooltip
key={`m${i.toString()}`}
position="right"
content={menuItem.tooltip}
>
<Tooltip key={`m${i.toString()}`} position="right" content={menuItem.tooltip}>
{btn}
</Tooltip>
) : (
@ -722,8 +679,7 @@ function ScriptEditor() {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
backgroundColor:
selectSciptButtonAndTab === script.uuid ? "gray" : "",
backgroundColor: selectSciptButtonAndTab === script.uuid ? "gray" : "",
}}
onClick={() => {
setSelectSciptButtonAndTab(script.uuid);

View File

@ -1,15 +1,7 @@
/* eslint-disable import/prefer-default-export */
import React from "react";
import IoC from "@App/app/ioc";
import { Metadata, Script, ScriptDAO } from "@App/app/repo/scripts";
import ValueManager from "@App/app/service/value/manager";
import { Avatar, Button, Space, Tooltip } from "@arco-design/web-react";
import {
IconBug,
IconCode,
IconGithub,
IconHome,
} from "@arco-design/web-react/icon";
import { IconBug, IconCode, IconGithub, IconHome } from "@arco-design/web-react/icon";
import { useTranslation } from "react-i18next";
// 较对脚本排序位置
@ -17,7 +9,7 @@ export function scriptListSort(result: Script[]) {
const dao = new ScriptDAO();
for (let i = 0; i < result.length; i += 1) {
if (result[i].sort !== i) {
dao.update(result[i].id, { sort: i });
dao.update(result[i].uuid, { sort: i });
result[i].sort = i;
}
}
@ -30,13 +22,7 @@ export function installUrlToHome(installUrl: string) {
if (installUrl.indexOf("scriptcat.org") !== -1) {
const id = installUrl.split("/")[5];
return (
<Button
type="text"
iconOnly
size="small"
target="_blank"
href={`https://scriptcat.org/script-show-page/${id}`}
>
<Button type="text" iconOnly size="small" target="_blank" href={`https://scriptcat.org/script-show-page/${id}`}>
<img width={16} height={16} src="/assets/logo.png" alt="" />
</Button>
);
@ -44,13 +30,7 @@ export function installUrlToHome(installUrl: string) {
if (installUrl.indexOf("greasyfork.org") !== -1) {
const id = installUrl.split("/")[4];
return (
<Button
type="text"
iconOnly
size="small"
target="_blank"
href={`https://greasyfork.org/scripts/${id}`}
>
<Button type="text" iconOnly size="small" target="_blank" href={`https://greasyfork.org/scripts/${id}`}>
<img width={16} height={16} src="/assets/logo/gf.png" alt="" />
</Button>
);
@ -89,6 +69,7 @@ export function installUrlToHome(installUrl: string) {
}
} catch (e) {
// ignore error
console.error(e);
}
return undefined;
}
@ -167,28 +148,7 @@ export function ListHomeRender({ script }: { script: Script }) {
}
export function getValues(script: Script) {
const { config } = script;
return (IoC.instance(ValueManager) as ValueManager)
.getValues(script)
.then((data) => {
const newValues: { [key: string]: any } = {};
Object.keys(config!).forEach((tabKey) => {
const tab = config![tabKey];
Object.keys(tab).forEach((key) => {
// 动态变量
if (tab[key].bind) {
const bindKey = tab[key].bind!.substring(1);
newValues[bindKey] =
data[bindKey] === undefined ? undefined : data[bindKey].value;
}
newValues[`${tabKey}.${key}`] =
data[`${tabKey}.${key}`] === undefined
? config![tabKey][key].default
: data[`${tabKey}.${key}`].value;
});
});
return newValues;
});
return {};
}
export type ScriptIconsProps = {
@ -218,6 +178,5 @@ export function ScriptIcons({ script, size = 32, style }: ScriptIconsProps) {
</Avatar>
);
}
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

View File

@ -0,0 +1,31 @@
import { createAsyncThunk } from "@reduxjs/toolkit";
import { createAppSlice } from "../hooks";
import { Script, ScriptDAO } from "@App/app/repo/scripts";
export const fetchScriptList = createAsyncThunk("script/fetchScriptList", () => {
return new ScriptDAO().all();
});
export const scriptSlice = createAppSlice({
name: "script",
initialState: {
scripts: [] as Script[],
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchScriptList.fulfilled, (state, action) => {
const newScripts: Script[] = [];
action.payload.forEach((item) => {
newScripts.push(item);
});
state.scripts = newScripts;
});
},
selectors: {
selectScripts: (state) => state.scripts,
},
});
// export const {} = scriptSlice.actions;
export const { selectScripts } = scriptSlice.selectors;

View File

@ -10,6 +10,7 @@ export const settingSlice = createAppSlice({
enable: true,
config: "",
},
scriptListColumnWidth: {} as { [key: string]: number },
},
reducers: (create) => {
// 初始化黑夜模式
@ -45,9 +46,10 @@ export const settingSlice = createAppSlice({
},
selectors: {
selectThemeMode: (state) => state.lightMode,
selectScriptListColumnWidth: (state) => state.scriptListColumnWidth,
},
});
export const { setDarkMode } = settingSlice.actions;
export const { selectThemeMode } = settingSlice.selectors;
export const { selectThemeMode, selectScriptListColumnWidth } = settingSlice.selectors;

View File

@ -2,10 +2,11 @@ import type { Action, ThunkAction } from "@reduxjs/toolkit";
import { combineSlices, configureStore } from "@reduxjs/toolkit";
import { setupListeners } from "@reduxjs/toolkit/query";
import { settingSlice } from "./features/setting";
import { scriptSlice } from "./features/script";
// `combineSlices` automatically combines the reducers using
// their `reducerPath`s, therefore we no longer need to call `combineReducers`.
const rootReducer = combineSlices(settingSlice);
const rootReducer = combineSlices(settingSlice, scriptSlice);
// Infer the `RootState` type from the root reducer
export type RootState = ReturnType<typeof rootReducer>;

View File

@ -0,0 +1,13 @@
// ==UserScript==
// @name New Userscript
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description try to take over the world!
// @author You
// @background
// ==/UserScript==
return new Promise((resolve, reject) => {
// Your code here...
resolve();
});

View File

@ -0,0 +1,3 @@
const utils = require('./utils');
utils.run();

View File

@ -0,0 +1,15 @@
{
"name": "cloudcat-package",
"version": "1.0.0",
"description": "scriptcat后台脚本打包项目",
"main": "index.js",
"scripts": {
"run": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "CodFrm",
"license": "MIT",
"dependencies": {
"scriptcat-nodejs": "^0.1.7"
}
}

View File

@ -0,0 +1,17 @@
const fs = require('fs');
const { ScriptCat } = require("scriptcat-nodejs/dist/src/scriptcat");
const { ModelValues } = require("scriptcat-nodejs/dist/src/storage/values");
const { cookies } = require('./cookies');
const { values } = require('./values');
exports.run = function () {
const code = fs.readFileSync('userScript.js', 'utf8');
const run = new ScriptCat();
run.RunOnce(code, {
cookies: cookies,
values: new ModelValues(values),
}).then((res) => {
console.log(res);
});
}

13
src/template/crontab.tpl Normal file
View File

@ -0,0 +1,13 @@
// ==UserScript==
// @name New Userscript
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description try to take over the world!
// @author You
// @crontab * * once * *
// ==/UserScript==
return new Promise((resolve, reject) => {
// Your code here...
resolve();
});

14
src/template/normal.tpl Normal file
View File

@ -0,0 +1,14 @@
// ==UserScript==
// @name New Userscript
// @namespace https://bbs.tampermonkey.net.cn/
// @version 0.1.0
// @description try to take over the world!
// @author You
// @match {{match}}
// ==/UserScript==
(function() {
'use strict';
// Your code here...
})();