chore: merge feat/external-and-native-plugins to main (#10)
This PR introduces native and external plugins support. The list of changes here are NOT comprehensive and only mentions the most important changes. Check commits from `00047da` to `d2c7f47` (and diff) for full list of changes.
BREAKING CHANGES:
- lib/modules!: Renamed `callBridgeMethod*` to `callNativeMethod*` and interface `Methods` to `NativeMethods`. This is to achieve naming consistency with the native API and be more clear.
- lib/storage!: Renamed to `@revenge-mod/json-storage`. A storage can be in any arbitrary format. While there's little potential for non-JSON storage formats, but the naming should allow for it regardless. This is not a drop-in replacements. There are breaking changes in export names and new APIs.
- plugins/revenge.api.storage!: Renamed to `revenge.api.jsonStorage`. Plugin options are renamed and its structure has changed as well.
- Native now holds the path for official storage APIs. This update uses new paths without any migration!
- lib/plugins!: `Plugin.errors` is now read-only. Use `Plugin.reportError(e)` instead.
- lib/plugins!: `Plugin.flags` has been completely removed in favor of these methods: `Plugin.requireReload()`, `Plugin.disable()`.
- New versioning system: Allowing any amount of number segments, with prerelease support.
- Any number of segments is supported: 1, 1.0, 1.0.0, 1.0.0.0, ...
- Prerelease labels are compared "digit-run", similar to Debian.
- Plugins must specify a version.
- User-facing plugins should use CalVer. API plugins should use SemVer.
Revenge APIs use SemVer while the new `discord` dependency parses the Discord version string directly.
- Dependency is now a Map, with support for version ranges and optional. Suggested URL is removed. (See new repo system)
- Plugins will now have to declare a dependency on `revenge.api` (Revenge API version) and `discord` (Discord version).
- Manifest now requires a format version (currently `1`)
New APIs:
- lib/plugins/utils: Utilities to format versioning
- lib/discord/design: `FloatingActionButton` types
Features:
- New repo system: Repository design is inspired by Linux package repositories.
- Repos host manifest, versions, channels, download URL/size, and hash for multiple plugins.
- New dependency system: Plugins are versioned, and shouldn't break when updates come.
Plugins can now declare dependency on supported Discord/Revenge API versions, so they won't break with auto-updates.
Dependencies can be optional, but dependency on `revenge.api` and `discord` must be required.
Dependents are soft-disabled when dependencies don't satisfy. Once satisfied, the plugin can start again.
- All updates for running plugins REQUIRE a reload to prevent stale linking issues. Both native and JS-side.
- Update checking is handled entirely by JS. But the native-side still handles installation, indexing, and verification.
- Local indexes are updated against remote indexes. Dependency checking always uses local indexes.
- Users can add custom repositories and custom their priority order. URL is the identity.
- Plugins can now have custom data URI icons instead of being restricted to React Native app assets.
- External plugin installs require UI confirmation before installing.
- Pending Update and Pending Reload filters in Plugins page.
- Ability to browse external plugins in repositories. Official repository will be added soon.
Fixes/Refactor:
- lib/modules/native/fs: `rmSync` not actually deleting anything.
- Refactored Plugin-related settings to a separate "Plugin Settings" plugin.
- Masonry layout fixes in landscape.
- Removed dead code.
- Fixed sorting for 'Enabled first'.
- Updated native method names to be more consistent.
- Make methods more 'safe' by removing non-null assertion casts.
This commit is contained in:
commit
f6a796f02f
89 changed files with 4286 additions and 1186 deletions
224
biome.json
224
biome.json
|
|
@ -1,113 +1,113 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
{
|
||||
"type": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"includes": [
|
||||
"**/*.js",
|
||||
"**/*.json",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.jsx",
|
||||
"**/*.mjs"
|
||||
],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"formatter": {
|
||||
"useEditorconfig": true
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"arrowParentheses": "asNeeded",
|
||||
"enabled": true,
|
||||
"quoteProperties": "asNeeded",
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"parser": {
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": true
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"useKeyWithClickEvents": {
|
||||
"level": "off"
|
||||
}
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": {
|
||||
"level": "warn"
|
||||
},
|
||||
"noCommaOperator": {
|
||||
"level": "off"
|
||||
},
|
||||
"useArrowFunction": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noNonNullAssertion": {
|
||||
"level": "off"
|
||||
},
|
||||
"useImportType": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"style": "separatedType"
|
||||
}
|
||||
},
|
||||
"useNodejsImportProtocol": {
|
||||
"level": "off"
|
||||
},
|
||||
"useTemplate": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"suspicious": {
|
||||
"noAssignInExpressions": {
|
||||
"level": "off"
|
||||
},
|
||||
"noConfusingVoidType": {
|
||||
"level": "off"
|
||||
},
|
||||
"noExplicitAny": {
|
||||
"level": "off"
|
||||
},
|
||||
"useIterableCallbackReturn": {
|
||||
"level": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vcs": {
|
||||
"clientKind": "git",
|
||||
"enabled": true,
|
||||
"useIgnoreFile": true
|
||||
}
|
||||
}
|
||||
"$schema": "https://biomejs.dev/schemas/2.5.4/schema.json",
|
||||
"assist": {
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": {
|
||||
"level": "on",
|
||||
"options": {
|
||||
"groups": [
|
||||
{
|
||||
"type": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"includes": [
|
||||
"**/*.js",
|
||||
"**/*.json",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"**/*.jsx",
|
||||
"**/*.mjs"
|
||||
],
|
||||
"ignoreUnknown": true
|
||||
},
|
||||
"formatter": {
|
||||
"useEditorconfig": true
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"arrowParentheses": "asNeeded",
|
||||
"enabled": true,
|
||||
"quoteProperties": "asNeeded",
|
||||
"quoteStyle": "single",
|
||||
"semicolons": "asNeeded",
|
||||
"trailingCommas": "all"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"parser": {
|
||||
"allowComments": true,
|
||||
"allowTrailingCommas": true
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"preset": "recommended",
|
||||
"a11y": {
|
||||
"useKeyWithClickEvents": {
|
||||
"level": "off"
|
||||
}
|
||||
},
|
||||
"complexity": {
|
||||
"noBannedTypes": {
|
||||
"level": "warn"
|
||||
},
|
||||
"noCommaOperator": {
|
||||
"level": "off"
|
||||
},
|
||||
"useArrowFunction": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": {
|
||||
"level": "off"
|
||||
},
|
||||
"useImportType": {
|
||||
"level": "warn",
|
||||
"options": {
|
||||
"style": "separatedType"
|
||||
}
|
||||
},
|
||||
"useNodejsImportProtocol": {
|
||||
"level": "off"
|
||||
},
|
||||
"useTemplate": {
|
||||
"level": "warn"
|
||||
}
|
||||
},
|
||||
"suspicious": {
|
||||
"noAssignInExpressions": {
|
||||
"level": "off"
|
||||
},
|
||||
"noConfusingVoidType": {
|
||||
"level": "off"
|
||||
},
|
||||
"noExplicitAny": {
|
||||
"level": "off"
|
||||
},
|
||||
"useIterableCallbackReturn": {
|
||||
"level": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vcs": {
|
||||
"clientKind": "git",
|
||||
"enabled": true,
|
||||
"useIgnoreFile": true
|
||||
}
|
||||
}
|
||||
382
bun.lock
382
bun.lock
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"lockfileVersion": 2,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
|
|
@ -11,37 +11,37 @@
|
|||
"@revenge-mod/devtools-shared": "^1.3.0",
|
||||
"@revenge-mod/discord": "workspace:*",
|
||||
"@revenge-mod/externals": "workspace:*",
|
||||
"@revenge-mod/json-storage": "workspace:*",
|
||||
"@revenge-mod/modules": "workspace:*",
|
||||
"@revenge-mod/patcher": "workspace:*",
|
||||
"@revenge-mod/plugins": "workspace:*",
|
||||
"@revenge-mod/react": "workspace:*",
|
||||
"@revenge-mod/storage": "workspace:*",
|
||||
"@revenge-mod/utils": "workspace:*",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/core": "^1.15.43",
|
||||
"chalk": "^5.6.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"magic-string": "^1.1.0",
|
||||
"react-native-click-outside": "^0.1.1",
|
||||
"rolldown": "1.1.0",
|
||||
"rolldown": "1.2.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.16",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@biomejs/biome": "^2.5.5",
|
||||
"@parcel/watcher": "^2.6.0",
|
||||
"@react-native-clipboard/clipboard": "^1.16.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"react-native-safe-area-context": "^5.7.0",
|
||||
"rolldown-plugin-dts": "^0.25.2",
|
||||
"react-native-safe-area-context": "^5.8.0",
|
||||
"rolldown-plugin-dts": "^0.27.14",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/core": "^7.14.0",
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@react-navigation/stack": "^7.6.15",
|
||||
"@shopify/flash-list": "1.7.2",
|
||||
"@types/node": "^25.9.2",
|
||||
"@react-navigation/stack": "^7.7.2",
|
||||
"@shopify/flash-list": "^2.3.2",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"buffer": "^6.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript": "^7.0.2",
|
||||
},
|
||||
},
|
||||
"lib/assets": {
|
||||
|
|
@ -56,6 +56,9 @@
|
|||
"lib/externals": {
|
||||
"name": "@revenge-mod/externals",
|
||||
},
|
||||
"lib/json-storage": {
|
||||
"name": "@revenge-mod/json-storage",
|
||||
},
|
||||
"lib/modules": {
|
||||
"name": "@revenge-mod/modules",
|
||||
},
|
||||
|
|
@ -68,17 +71,14 @@
|
|||
"lib/react": {
|
||||
"name": "@revenge-mod/react",
|
||||
},
|
||||
"lib/storage": {
|
||||
"name": "@revenge-mod/storage",
|
||||
},
|
||||
"lib/utils": {
|
||||
"name": "@revenge-mod/utils",
|
||||
},
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome",
|
||||
"@swc/core",
|
||||
"@parcel/watcher",
|
||||
"@swc/core",
|
||||
"@biomejs/biome",
|
||||
],
|
||||
"patchedDependencies": {
|
||||
"react-native-click-outside@0.1.1": "patches/react-native-click-outside@0.1.1.patch",
|
||||
|
|
@ -95,49 +95,49 @@
|
|||
"recyclerlistview": "npm:dry-uninstall",
|
||||
},
|
||||
"packages": {
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@8.0.0-rc.6", "", { "dependencies": { "@babel/parser": "^8.0.0-rc.6", "@babel/types": "^8.0.0-rc.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "@types/jsesc": "^2.5.0", "jsesc": "^3.0.2" } }, "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ=="],
|
||||
"@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.6", "", {}, "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g=="],
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.6", "", {}, "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg=="],
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@8.0.0-rc.6", "", { "dependencies": { "@babel/types": "^8.0.0-rc.6" }, "bin": "./bin/babel-parser.js" }, "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
"@babel/runtime": ["@babel/runtime@8.0.0", "", {}, "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
|
||||
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
|
||||
"@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="],
|
||||
|
||||
"@babel/types": ["@babel/types@8.0.0-rc.6", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.6", "@babel/helper-validator-identifier": "^8.0.0-rc.6" } }, "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw=="],
|
||||
"@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/biome@2.4.16", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
|
||||
"@biomejs/biome": ["@biomejs/biome@2.5.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.5", "@biomejs/cli-darwin-x64": "2.5.5", "@biomejs/cli-linux-arm64": "2.5.5", "@biomejs/cli-linux-arm64-musl": "2.5.5", "@biomejs/cli-linux-x64": "2.5.5", "@biomejs/cli-linux-x64-musl": "2.5.5", "@biomejs/cli-win32-arm64": "2.5.5", "@biomejs/cli-win32-x64": "2.5.5" }, "bin": { "biome": "bin/biome" } }, "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.16", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw=="],
|
||||
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.16", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
|
||||
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.5", "", { "os": "win32", "cpu": "x64" }, "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="],
|
||||
|
||||
"@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="],
|
||||
|
||||
|
|
@ -157,37 +157,35 @@
|
|||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="],
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.134.0", "", {}, "sha512-T0xuRRKrQFmocH8y+jGfpmSkGcheaJExY9lEihmR1Gm2aH+75B8CzgU2rABRQSzzDxLjZ15Sc0bRVLj5lVeNXQ=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.140.0", "", {}, "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ=="],
|
||||
|
||||
"@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="],
|
||||
"@parcel/watcher": ["@parcel/watcher@2.6.0", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.4" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.6.0", "@parcel/watcher-darwin-arm64": "2.6.0", "@parcel/watcher-darwin-x64": "2.6.0", "@parcel/watcher-freebsd-x64": "2.6.0", "@parcel/watcher-linux-arm-glibc": "2.6.0", "@parcel/watcher-linux-arm-musl": "2.6.0", "@parcel/watcher-linux-arm64-glibc": "2.6.0", "@parcel/watcher-linux-arm64-musl": "2.6.0", "@parcel/watcher-linux-x64-glibc": "2.6.0", "@parcel/watcher-linux-x64-musl": "2.6.0", "@parcel/watcher-win32-arm64": "2.6.0", "@parcel/watcher-win32-x64": "2.6.0" } }, "sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w=="],
|
||||
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="],
|
||||
"@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.6.0", "", { "os": "android", "cpu": "arm64" }, "sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA=="],
|
||||
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="],
|
||||
"@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.6.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw=="],
|
||||
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="],
|
||||
"@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.6.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw=="],
|
||||
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="],
|
||||
"@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.6.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="],
|
||||
"@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg=="],
|
||||
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="],
|
||||
"@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.6.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="],
|
||||
"@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g=="],
|
||||
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="],
|
||||
"@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.6.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="],
|
||||
"@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A=="],
|
||||
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="],
|
||||
"@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.6.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw=="],
|
||||
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="],
|
||||
"@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.6.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ=="],
|
||||
|
||||
"@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="],
|
||||
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="],
|
||||
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.6.0", "", { "os": "win32", "cpu": "x64" }, "sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A=="],
|
||||
|
||||
"@react-native-clipboard/clipboard": ["@react-native-clipboard/clipboard@1.16.3", "", { "peerDependencies": { "react": ">= 16.9.0", "react-native": ">= 0.61.5", "react-native-macos": ">= 0.61.0", "react-native-windows": ">= 0.61.0" }, "optionalPeers": ["react-native-macos", "react-native-windows"] }, "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ=="],
|
||||
|
||||
|
|
@ -205,15 +203,15 @@
|
|||
|
||||
"@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.81.4", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "*", "react-native": "*" }, "optionalPeers": ["@types/react"] }, "sha512-hBM+rMyL6Wm1Q4f/WpqGsaCojKSNUBqAXLABNGoWm1vabZ7cSnARMxBvA/2vo3hLcoR4v7zDK8tkKm9+O0LjVA=="],
|
||||
|
||||
"@react-navigation/core": ["@react-navigation/core@7.14.0", "", { "dependencies": { "@react-navigation/routers": "^7.5.3", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g=="],
|
||||
"@react-navigation/core": ["@react-navigation/core@7.21.11", "", { "dependencies": { "@react-navigation/routers": "^7.6.4", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "query-string": "^7.1.3", "react-is": "^19.1.0", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": ">= 18.2.0" } }, "sha512-bCW1PsLA/eOXDOukcJFEzlcL3Zpy8DJuDCfkDDwAQlAgoSZ/J9+ZeDRUMmCUi6xbnFgvFEEIMertaLeErOFP0Q=="],
|
||||
|
||||
"@react-navigation/elements": ["@react-navigation/elements@2.9.5", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-iHZU8rRN1014Upz73AqNVXDvSMZDh5/ktQ1CMe21rdgnOY79RWtHHBp9qOS3VtqlUVYGkuX5GEw5mDt4tKdl0g=="],
|
||||
"@react-navigation/elements": ["@react-navigation/elements@2.9.36", "", { "dependencies": { "color": "^4.2.3", "use-latest-callback": "^0.2.4", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@react-native-masked-view/masked-view": ">= 0.2.0", "@react-navigation/native": "^7.3.14", "react": ">= 18.2.0", "react-native": "*", "react-native-safe-area-context": ">= 4.0.0" }, "optionalPeers": ["@react-native-masked-view/masked-view"] }, "sha512-+10x9s5v2Q7FwAYdSmPMgILtxZyC5e4hWJQu8g5o3u4p8DUToTBmGvys/UvmEr+h9xmm0Go42qw9Ff2ape53kQ=="],
|
||||
|
||||
"@react-navigation/native": ["@react-navigation/native@7.1.28", "", { "dependencies": { "@react-navigation/core": "^7.14.0", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ=="],
|
||||
"@react-navigation/native": ["@react-navigation/native@7.3.14", "", { "dependencies": { "@react-navigation/core": "^7.21.11", "escape-string-regexp": "^4.0.0", "fast-deep-equal": "^3.1.3", "nanoid": "^3.3.11", "standard-navigation": "^0.0.8", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*" } }, "sha512-hcKTDNBuuAA1/xW6QeKYmMPVhk5W9dKGQpPmn5dQeeePwMpu5OZ14NOgwKH0w9D3tg2jupojTcVL0tsx5DTFXg=="],
|
||||
|
||||
"@react-navigation/routers": ["@react-navigation/routers@7.5.3", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg=="],
|
||||
"@react-navigation/routers": ["@react-navigation/routers@7.6.4", "", { "dependencies": { "nanoid": "^3.3.11" } }, "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w=="],
|
||||
|
||||
"@react-navigation/stack": ["@react-navigation/stack@7.7.2", "", { "dependencies": { "@react-navigation/elements": "^2.9.5", "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "@react-navigation/native": "^7.1.28", "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-dBXj+YEqfLsLQmmNyZ9lgu11N0JfpFCjjP4iEtlC1dPNNau2Rv18LEejXNU/k/fIgcfzYXFdQeiyEFW5O3f5Fw=="],
|
||||
"@react-navigation/stack": ["@react-navigation/stack@7.10.17", "", { "dependencies": { "@react-navigation/elements": "^2.9.36", "color": "^4.2.3", "use-latest-callback": "^0.2.4" }, "peerDependencies": { "@react-navigation/native": "^7.3.14", "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-safe-area-context": ">= 4.0.0", "react-native-screens": ">= 4.0.0" } }, "sha512-2b/AC8YxfPkFsN8DZGjrMiX6G86unbEonE0E8dA5LugzfKF/s28VBM7yBwZKRsp3ys5aVCvt/Z3zviQA/uGAIA=="],
|
||||
|
||||
"@revenge-mod/assets": ["@revenge-mod/assets@workspace:lib/assets"],
|
||||
|
||||
|
|
@ -227,6 +225,8 @@
|
|||
|
||||
"@revenge-mod/externals": ["@revenge-mod/externals@workspace:lib/externals"],
|
||||
|
||||
"@revenge-mod/json-storage": ["@revenge-mod/json-storage@workspace:lib/json-storage"],
|
||||
|
||||
"@revenge-mod/modules": ["@revenge-mod/modules@workspace:lib/modules"],
|
||||
|
||||
"@revenge-mod/patcher": ["@revenge-mod/patcher@workspace:lib/patcher"],
|
||||
|
|
@ -235,97 +235,91 @@
|
|||
|
||||
"@revenge-mod/react": ["@revenge-mod/react@workspace:lib/react"],
|
||||
|
||||
"@revenge-mod/storage": ["@revenge-mod/storage@workspace:lib/storage"],
|
||||
|
||||
"@revenge-mod/utils": ["@revenge-mod/utils@workspace:lib/utils"],
|
||||
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gCYzGOSkYY6Z034suzd20euvds7lPzMEEla62DJGE/ZAlR4OMBnNbvnBSsIGUCAr52gaWMsloGxP4tVGtN5aCA=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw=="],
|
||||
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JQBD77MNgu+4Z6RAyg69acugdrhhVoWesr3l47zohYZ2YV2fwkWMArkN/2p4l6Ei+Sno7W5q+UsKdVWq5Ens0w=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg=="],
|
||||
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-p/8cXUTK4Sob604e+xxPhVSbDFf29E6J0l/xESM9rdCfn3aDai3nEs6TnMHUsdD5aNlFz0+gDbiGlozLKGa2YA=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw=="],
|
||||
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KbtOSlVv6fElujiZWMcC3aQYhEwLVVf073RcwlSmpGQvIsKZFUqc0ef4sjUuurRwfbiI6JJXji9DQn+86hawmQ=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-9fZ9i0o0/MQaw7om6Z6TsT7tfCk0jtbEFtC+aPqZL5RNsGWNcHvn6EHgL3dAprjq+AZzPTAQjg2JtpJaMt+6pg=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tog7T66i+yFyIuuAnjL6xmW182W/qTBOUt6BtQ6lBIM1Eikh/fSMz4HGgvuCp5uU0zuIVWng7kDYthjCMOHcg=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w=="],
|
||||
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4b7yruLIIj/oZ3GpcLOvxcLCLDMraohn3IhQfN2hBP4w9UekG0DTIajWguJosRGfySf/+h/NwRUiMKoCpxCrqQ=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ=="],
|
||||
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QRDOVZd0bhQ5jLsUsCC3dUxDWdTSVY9WMznowZgCGOrZfLLgctWpelhUASEiBwsXfat/JwYnVd1EaxMhqyT+UQ=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw=="],
|
||||
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ypxT+Hq76NFG7woFbNbySnGEajFuYuIXeKz/jfCU+lXUoxfi3zLE6OG/ZQNeK3RpZSYJlAe2bokpsQ046CaieQ=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-IdovCmfROFmpTLahdecTDFL74aLERVYN68F/mLZjfVh6LfoplPfI6deyHNMTcVujbokDV5k05XrFO22zfv+qjg=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ=="],
|
||||
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pcA8xlFp2tyk9T2R6Fi/rPe3bQ1MA+sSMDNUU5Ogu80GHOatkE4P8YCreGAvZErm5Ho2YRXnyvNrWiRncfVysQ=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw=="],
|
||||
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.0", "", { "os": "none", "cpu": "arm64" }, "sha512-4+fexHayrLCWpriPh4c6dNvL4an34DEZCG7zOM/FD5QNF6h8DT+bDXzyB/kfC8lDJbaFb7jKShtnjDQFXVQEjg=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.0", "", { "os": "none", "cpu": "arm64" }, "sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-SbL++MNmOw6QamrwIGDMSSfM4ceTzFr+RjbOExJSLLBinScU4WI5OdA413h1qwPw2yH7lVF1+H4svQ+6mSXKTQ=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.2.0", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g=="],
|
||||
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+xTE6XC7wBgk0VKRXGG+QAnyW5S9b8vfsFpiMjf0waQTmSQSU8onsH/beyZ8X4aXVveJnotiy7VDjLOaW8bTrg=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA=="],
|
||||
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Ogji1TQNqH3ACLnYr+1Ns1nyrJ0CO2P585u9Hsh02pXvtFiFpgtgT2b3P4PnCOU86VVCvqtAeCN4OftMT8KU4w=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@shopify/flash-list": ["@shopify/flash-list@1.7.2", "", { "dependencies": { "recyclerlistview": "4.2.1", "tslib": "2.6.3" }, "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-rnadpDht/mlJLDM02HBni49EJJQhc51zjJ3diGnTz3MV6U8vK9Hztou+2C5d6bNLb4oZvSG5f7NTWejkipyMLw=="],
|
||||
"@shopify/flash-list": ["@shopify/flash-list@2.3.2", "", { "peerDependencies": { "@babel/runtime": "*", "react": "*", "react-native": "*" } }, "sha512-vQnd0y0Ag11yzS30llaeCrtIU3xYTMe7KEOP3dveLImnVjQEUw6BEg1+Ztja6aODlVNz1BpvxtlQ5eZGxiLwKw=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.27.10", "", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.27.12", "", {}, "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g=="],
|
||||
|
||||
"@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="],
|
||||
|
||||
"@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="],
|
||||
|
||||
"@swc/core": ["@swc/core@1.15.41", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.41", "@swc/core-darwin-x64": "1.15.41", "@swc/core-linux-arm-gnueabihf": "1.15.41", "@swc/core-linux-arm64-gnu": "1.15.41", "@swc/core-linux-arm64-musl": "1.15.41", "@swc/core-linux-ppc64-gnu": "1.15.41", "@swc/core-linux-s390x-gnu": "1.15.41", "@swc/core-linux-x64-gnu": "1.15.41", "@swc/core-linux-x64-musl": "1.15.41", "@swc/core-win32-arm64-msvc": "1.15.41", "@swc/core-win32-ia32-msvc": "1.15.41", "@swc/core-win32-x64-msvc": "1.15.41" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ=="],
|
||||
"@swc/core": ["@swc/core@1.15.46", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.27" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.46", "@swc/core-darwin-x64": "1.15.46", "@swc/core-linux-arm-gnueabihf": "1.15.46", "@swc/core-linux-arm64-gnu": "1.15.46", "@swc/core-linux-arm64-musl": "1.15.46", "@swc/core-linux-ppc64-gnu": "1.15.46", "@swc/core-linux-s390x-gnu": "1.15.46", "@swc/core-linux-x64-gnu": "1.15.46", "@swc/core-linux-x64-musl": "1.15.46", "@swc/core-win32-arm64-msvc": "1.15.46", "@swc/core-win32-ia32-msvc": "1.15.46", "@swc/core-win32-x64-msvc": "1.15.46" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-Ri3em2mBpq3h2zSPliCYl63otDGqek8PPEfv2nWgRQEbZ/VBCNyypVTVQ6cEbTCXBhy+WE2T3fQb08moIyuYaw=="],
|
||||
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.41", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw=="],
|
||||
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.46", "", { "os": "darwin", "cpu": "arm64" }, "sha512-IsISIT22EfktVJrlvIpnAxG2u/A9aob9l99HMlx80x72WlFmFPk1V3UhkEzx86eJP8hw049KTFv/RISho2cq2Q=="],
|
||||
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.41", "", { "os": "darwin", "cpu": "x64" }, "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA=="],
|
||||
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.46", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Tj4ppVIPCmUMpmGFiGtyEriwLyJ+yi/US4WfBrP/ok8COGddDZXLEzQETnKyK46mjvr1v0jevrS23zjoff7vA=="],
|
||||
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.41", "", { "os": "linux", "cpu": "arm" }, "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg=="],
|
||||
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.46", "", { "os": "linux", "cpu": "arm" }, "sha512-i8tUGnNjyOgMmfmgFSg4aeJLQoFyfpIHK5FjpQAwpRyQIqEUB2w1e8zIDQzY1WhOxx8NoS1S5iUL813Un4Sf5A=="],
|
||||
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ=="],
|
||||
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.46", "", { "os": "linux", "cpu": "arm64" }, "sha512-c0OnhqzdhfOvv6qhNCcByepB+sNYOGZyhtr2Qa6ZCHvAWTYhSRw4j/u92Stue9PbZ/6q74b9nHzi76+kVzqQHQ=="],
|
||||
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.41", "", { "os": "linux", "cpu": "arm64" }, "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A=="],
|
||||
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.46", "", { "os": "linux", "cpu": "arm64" }, "sha512-imyRpNEcUzFQFV2LE4jL68ErvmKEuZCbvZru77iQREunJ+bR4i658cupTgtG1mLYM3F1Tzy3Sb9xYb02KghWTg=="],
|
||||
|
||||
"@swc/core-linux-ppc64-gnu": ["@swc/core-linux-ppc64-gnu@1.15.41", "", { "os": "linux", "cpu": "ppc64" }, "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ=="],
|
||||
"@swc/core-linux-ppc64-gnu": ["@swc/core-linux-ppc64-gnu@1.15.46", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ctEfcl/HcUeomK33cbySiHZm98GEDIxTm1EkpBsYCiHxElYBzvTXVeuQT2YwbUXn9XCrjiw4ipyUNk33k26qRg=="],
|
||||
|
||||
"@swc/core-linux-s390x-gnu": ["@swc/core-linux-s390x-gnu@1.15.41", "", { "os": "linux", "cpu": "s390x" }, "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA=="],
|
||||
"@swc/core-linux-s390x-gnu": ["@swc/core-linux-s390x-gnu@1.15.46", "", { "os": "linux", "cpu": "s390x" }, "sha512-DxlMdnt84TtRVTv7WL/thWyz9+QU8QZNNoAP9rrk0P68LziuhfePp8MjQ44zIprpTHTsEwyziIuGUUN5iSC1bQ=="],
|
||||
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.41", "", { "os": "linux", "cpu": "x64" }, "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA=="],
|
||||
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.46", "", { "os": "linux", "cpu": "x64" }, "sha512-SKxI7J6t90XPl8hRUqtJi9NfGdunN/E/vZMc7Bc0figeRdOPDBT+Tm8g7cx9xM0T0mewh2l+8dewa3Am27/P+A=="],
|
||||
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.41", "", { "os": "linux", "cpu": "x64" }, "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ=="],
|
||||
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.46", "", { "os": "linux", "cpu": "x64" }, "sha512-qj9T6B7bosI0VEsrWOVXZN1OXxS8Tp63ywyrLxNdOycnUtLdkgYcoBsN5y8ImnDDsnwrEWZOy1e+J4xSe7mA3Q=="],
|
||||
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.41", "", { "os": "win32", "cpu": "arm64" }, "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg=="],
|
||||
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.46", "", { "os": "win32", "cpu": "arm64" }, "sha512-8p7l4c3LU+eA5g9Et1JPhNeMC1oQwXTGU+uah8DPIBX7YXzqswvaBtyKVmXefVGi/DJU1x3YJsc3mbAp9aWzSQ=="],
|
||||
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.41", "", { "os": "win32", "cpu": "ia32" }, "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw=="],
|
||||
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.46", "", { "os": "win32", "cpu": "ia32" }, "sha512-tUEnfr3Bn9u6FOjUb3PN9p+09qZC2j+wNDLKHzXXZn22rqGcUqR/ohCRSS+nG9B9+X+U+3FewNEHJkTmdIvMjQ=="],
|
||||
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.41", "", { "os": "win32", "cpu": "x64" }, "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw=="],
|
||||
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.46", "", { "os": "win32", "cpu": "x64" }, "sha512-Vux7UDzBJYQggSuPfcl2w9iu+IJpgpRCxHzgCaVkELnAXAE4XZMOTX9HNcaNiwfeIDqdu2rkr69RuDm6wY8neA=="],
|
||||
|
||||
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
|
||||
|
||||
"@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="],
|
||||
"@swc/types": ["@swc/types@0.1.27", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-K6h3iUlqeM946U4sXFYeahefR1YBbXJvko+hv8WS8/0BNJ4OHiHRywMnQUJCqkR7Y9+hqQ1TvEpiKqUhz7NEFg=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="],
|
||||
|
||||
"@types/istanbul-lib-report": ["@types/istanbul-lib-report@3.0.3", "", { "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA=="],
|
||||
|
||||
"@types/istanbul-reports": ["@types/istanbul-reports@3.0.4", "", { "dependencies": { "@types/istanbul-lib-report": "*" } }, "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ=="],
|
||||
|
||||
"@types/jsesc": ["@types/jsesc@2.5.1", "", {}, "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw=="],
|
||||
"@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
|
||||
|
||||
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||
|
||||
"@types/react-reconciler": ["@types/react-reconciler@0.28.9", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg=="],
|
||||
|
||||
|
|
@ -335,6 +329,92 @@
|
|||
|
||||
"@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="],
|
||||
|
||||
"@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="],
|
||||
|
||||
"@typescript/typescript-darwin-arm64": ["@typescript/typescript-darwin-arm64@7.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA=="],
|
||||
|
||||
"@typescript/typescript-darwin-x64": ["@typescript/typescript-darwin-x64@7.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA=="],
|
||||
|
||||
"@typescript/typescript-freebsd-arm64": ["@typescript/typescript-freebsd-arm64@7.0.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ=="],
|
||||
|
||||
"@typescript/typescript-freebsd-x64": ["@typescript/typescript-freebsd-x64@7.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw=="],
|
||||
|
||||
"@typescript/typescript-linux-arm": ["@typescript/typescript-linux-arm@7.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ=="],
|
||||
|
||||
"@typescript/typescript-linux-arm64": ["@typescript/typescript-linux-arm64@7.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ=="],
|
||||
|
||||
"@typescript/typescript-linux-loong64": ["@typescript/typescript-linux-loong64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ=="],
|
||||
|
||||
"@typescript/typescript-linux-mips64el": ["@typescript/typescript-linux-mips64el@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA=="],
|
||||
|
||||
"@typescript/typescript-linux-ppc64": ["@typescript/typescript-linux-ppc64@7.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA=="],
|
||||
|
||||
"@typescript/typescript-linux-riscv64": ["@typescript/typescript-linux-riscv64@7.0.2", "", { "os": "linux", "cpu": "none" }, "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ=="],
|
||||
|
||||
"@typescript/typescript-linux-s390x": ["@typescript/typescript-linux-s390x@7.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw=="],
|
||||
|
||||
"@typescript/typescript-linux-x64": ["@typescript/typescript-linux-x64@7.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A=="],
|
||||
|
||||
"@typescript/typescript-netbsd-arm64": ["@typescript/typescript-netbsd-arm64@7.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA=="],
|
||||
|
||||
"@typescript/typescript-netbsd-x64": ["@typescript/typescript-netbsd-x64@7.0.2", "", { "os": "none", "cpu": "x64" }, "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA=="],
|
||||
|
||||
"@typescript/typescript-openbsd-arm64": ["@typescript/typescript-openbsd-arm64@7.0.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ=="],
|
||||
|
||||
"@typescript/typescript-openbsd-x64": ["@typescript/typescript-openbsd-x64@7.0.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg=="],
|
||||
|
||||
"@typescript/typescript-sunos-x64": ["@typescript/typescript-sunos-x64@7.0.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g=="],
|
||||
|
||||
"@typescript/typescript-win32-arm64": ["@typescript/typescript-win32-arm64@7.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ=="],
|
||||
|
||||
"@typescript/typescript-win32-x64": ["@typescript/typescript-win32-x64@7.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g=="],
|
||||
|
||||
"@yuku-codegen/binding-darwin-arm64": ["@yuku-codegen/binding-darwin-arm64@0.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7cSJH6PaKLRBdCfiB4pM6EukvgOk5xV4tyuLOIOEqrHsbnV7brtyff7CjhZbeGozdIHoOnKOi5R7rrmCWN3QSw=="],
|
||||
|
||||
"@yuku-codegen/binding-darwin-x64": ["@yuku-codegen/binding-darwin-x64@0.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mhooLL+L5ytMxgz4ueXCIirU796X2xj97d4KSQW1HxZGzX6h8wOk5bIAhGqcmOL2bqAmMZ+UkBTbPC9VpzKb/g=="],
|
||||
|
||||
"@yuku-codegen/binding-freebsd-x64": ["@yuku-codegen/binding-freebsd-x64@0.8.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MHLOAlgGhdOh0ZfnWWnno4ljlFB04/Lox/7MIGYIvISMKFvejfC9Atb1t9G7pTVBvy+l5uqxzHGBSJUsDOOkTA=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-arm-gnu": ["@yuku-codegen/binding-linux-arm-gnu@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Ur3Awo45Sc5/Fglr8WN5XIf4IwAsq8wLd917Du8ow6mStxsBTHqFiH+tT7d5jV0FqcJnwy8EHVczmgde0zTo1A=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-arm-musl": ["@yuku-codegen/binding-linux-arm-musl@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-FIy7Ttx8oeUCd/8Y6IjnOsu+lRc6En+V/H67BlVphOeCySZAo5LU8VWrb4tv0DvjaSOzdm3DmdmRzmzN7NCqWQ=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-arm64-gnu": ["@yuku-codegen/binding-linux-arm64-gnu@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-s+25wl1TLvf+7LzasPEi1RR1sDfVAU0i1QH601mn3vj+HudFYBYNZtUBKFaZvl645QR6vcaDaGHnOaMZhBR3ig=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-arm64-musl": ["@yuku-codegen/binding-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pRha3Cjm4AnA5wEuhpg+8XXoGfwz5X61/9a/VNxeau47+kH6xjJspkEloIy/HbHgUnvVRqVHoEHczdGZT2J9NA=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-x64-gnu": ["@yuku-codegen/binding-linux-x64-gnu@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-GySXmiw5Dw99Ba3GMV2ExQVckihuangnrIpSJagPx8RFUNtwfmzpr2ibf8k31eEAE4YUofV7mEfJN5tN6+POnQ=="],
|
||||
|
||||
"@yuku-codegen/binding-linux-x64-musl": ["@yuku-codegen/binding-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-8YFfcPZz44v8YyekWVKkNz/cD4t6DW62/dr03OVtMfwUtHfmo/8wpby0JKMleAOXbgWItSGw81LtwqM4I9oS0A=="],
|
||||
|
||||
"@yuku-codegen/binding-win32-arm64": ["@yuku-codegen/binding-win32-arm64@0.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-YhENbgkuzjsil+zDNV35oU3PQMDg2RXh8BPt915WCNAbIIHcqgYupHJF3564206+DbPkZtcDvcoa34Wb5B8tbQ=="],
|
||||
|
||||
"@yuku-codegen/binding-win32-x64": ["@yuku-codegen/binding-win32-x64@0.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-qvzSRABXe6/ndubx+RNwgbFVbs7Pqroz/q/UR6vm++xsmfcpkMF43B23jgNl2xi2IUrEt8C/L1bBslXp+LtgnA=="],
|
||||
|
||||
"@yuku-parser/binding-darwin-arm64": ["@yuku-parser/binding-darwin-arm64@0.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-04AakSJhI4mPrqhZzXdFyaEDh0YkfeqbnyYY3aCrmxeWfR/Xr8+kFn5sh+wZYN/5HatPniELKHixJuUCUyfBvg=="],
|
||||
|
||||
"@yuku-parser/binding-darwin-x64": ["@yuku-parser/binding-darwin-x64@0.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BQJGI9bDeyb/X2rwhtXoBTQ9EtbkJtteX6C7cZ9jow0pqqmoOufgHPP2+m65GLk2eVNCBcfl3xlTGpJ7RFcviw=="],
|
||||
|
||||
"@yuku-parser/binding-freebsd-x64": ["@yuku-parser/binding-freebsd-x64@0.8.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-04hmgnU152wya88raI+RQhxZPgwXcHbfdNZLu3x5ggKnJVHLD8xZZLcWIKSs59CiEXL6PKVX1/cx8GoTYlaCaQ=="],
|
||||
|
||||
"@yuku-parser/binding-linux-arm-gnu": ["@yuku-parser/binding-linux-arm-gnu@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-+cuJWUK13lwce721XGRjz7izSr2Q1U0RlHkDzdkohWpRWlttTqRdxNnaW13nDc47GBDrNsXMHx+KLcebeOeeGg=="],
|
||||
|
||||
"@yuku-parser/binding-linux-arm-musl": ["@yuku-parser/binding-linux-arm-musl@0.8.0", "", { "os": "linux", "cpu": "arm" }, "sha512-rOVPRqt9cm1YP/wPV+yyZh0FYr6UR/wm/BXslvLuge0LDewVvDrm/AxcDrwWuPAu61Nzdg2rMVfcNnrplCbC1w=="],
|
||||
|
||||
"@yuku-parser/binding-linux-arm64-gnu": ["@yuku-parser/binding-linux-arm64-gnu@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-pAoIozKr6E+ptpaQz4CZv1O7cay2f4m7kbd+DSQug5MKdUBTZZ18GdWSPvq0fwQYraH9hZlyLqrMaeP5W/ncrA=="],
|
||||
|
||||
"@yuku-parser/binding-linux-arm64-musl": ["@yuku-parser/binding-linux-arm64-musl@0.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-JCAg50ahXuYlrsIi1jmymX9X/9B0JYBRroAHnYttN44tAvCo1PFqukHrw1up6HvEOoLA0OjlM0Zwh60u3Gc/Zg=="],
|
||||
|
||||
"@yuku-parser/binding-linux-x64-gnu": ["@yuku-parser/binding-linux-x64-gnu@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-tEeVQ14etp7lpUqXzq+X5AlQzFH+m3TVDiCKIq17zxnxJ117DOVvoveWSkSMt/lj68z48SvZUMHe7xLvqtO1lg=="],
|
||||
|
||||
"@yuku-parser/binding-linux-x64-musl": ["@yuku-parser/binding-linux-x64-musl@0.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+UGRYnF37nnbZNMsMjSGDXKUTIxYUmbbk3Lzib88sLK7kg2y80vjchYYoYsDUK9kAgqPWyA9hKGURHy/Kk9Few=="],
|
||||
|
||||
"@yuku-parser/binding-win32-arm64": ["@yuku-parser/binding-win32-arm64@0.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-l+7Va9/sX1ccRjzjJj6MR0arKsvHB5b419+pKbwzn+/18A/xfccWqmxXrn0C0NyVLlbEb3AV9Is+AbF72O85yA=="],
|
||||
|
||||
"@yuku-parser/binding-win32-x64": ["@yuku-parser/binding-win32-x64@0.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dolKDTJv2xrWowDBEkvSaDvCsVFZOA7DCUuD+7DatglS68WbuxS4LudU5bzSeAOL5vgPpyGm82469cMLnL+6vQ=="],
|
||||
|
||||
"@yuku-toolchain/types": ["@yuku-toolchain/types@0.8.0", "", {}, "sha512-hL/raFM5V9UT2lVE/lIWDTWvANqSB8TvMVp+PgICehBa96KhTl/UpGm370JXaacukR+QnaHM2Yv6O/UcrzAFGg=="],
|
||||
|
||||
"abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="],
|
||||
|
||||
"anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],
|
||||
|
|
@ -345,8 +425,6 @@
|
|||
|
||||
"asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="],
|
||||
|
||||
"ast-kit": ["ast-kit@3.0.0-beta.1", "", { "dependencies": { "@babel/parser": "^8.0.0-beta.4", "estree-walker": "^3.0.3", "pathe": "^2.0.3" } }, "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw=="],
|
||||
|
||||
"async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="],
|
||||
|
||||
"babel-jest": ["dry-uninstall@0.3.0", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
|
||||
|
|
@ -359,9 +437,7 @@
|
|||
|
||||
"bippy": ["bippy@0.3.34", "", { "dependencies": { "@types/react-reconciler": "^0.28.9" }, "peerDependencies": { "react": ">=17.0.1" } }, "sha512-vmptmU/20UdIWHHhq7qCSHhHzK7Ro3YJ1utU0fBG7ujUc58LEfTtilKxcF0IOgSjT5XLcm7CBzDjbv4lcKApGQ=="],
|
||||
|
||||
"birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
"brace-expansion": ["brace-expansion@1.1.16", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
|
|
@ -395,7 +471,7 @@
|
|||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
||||
"devalue": ["devalue@5.8.2", "", {}, "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA=="],
|
||||
|
||||
"dts-resolver": ["dts-resolver@3.0.0", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q=="],
|
||||
|
||||
|
|
@ -405,8 +481,6 @@
|
|||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
|
@ -461,41 +535,39 @@
|
|||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
"magic-string": ["magic-string@1.1.0", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-kS3VHe0nEPST2saQV4Rbkchcd3UBRkVTQHo1D3h/ZTwFDhai/mfKkmtPAtD129EOI7K3HlHIsFOt0WrI2/oU9g=="],
|
||||
|
||||
"memoize-one": ["memoize-one@5.2.1", "", {}, "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="],
|
||||
|
||||
"metro-runtime": ["metro-runtime@0.83.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-sWj9KN311yG22Zv0kVbAp9dorB9HtTThvQKsAn6PLxrVrz+1UBsLrQSxjE/s4PtzDi1HABC648jo4K9Euz/5jw=="],
|
||||
"metro-runtime": ["metro-runtime@0.83.7", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ=="],
|
||||
|
||||
"metro-source-map": ["metro-source-map@0.83.4", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.4", "nullthrows": "^1.1.1", "ob1": "0.83.4", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-pPbmQwS0zgU+/0u5KPkuvlsQP0V+WYQ9qNshqupIL720QRH0vS3QR25IVVtbunofEDJchI11Q4QtIbmUyhpOBw=="],
|
||||
"metro-source-map": ["metro-source-map@0.83.7", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.7", "nullthrows": "^1.1.1", "ob1": "0.83.7", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw=="],
|
||||
|
||||
"metro-symbolicate": ["metro-symbolicate@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-clyWAXDgkDHPwvldl95pcLTrJIqUj9GbZayL8tfeUs69ilsIUBpVym2lRd/8l3/8PIHCInxL868NvD2Y7OqKXg=="],
|
||||
"metro-symbolicate": ["metro-symbolicate@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.7", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.4", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw=="],
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
|
||||
|
||||
"ob1": ["ob1@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9JiflaRKCkxKzH8uuZlax72cHzZ8iFLsNIORFOAKDgZUOfvfwYWOVS0ezGLzPp/yEhVktD+PTTImC0AAehSOBw=="],
|
||||
"ob1": ["ob1@0.83.7", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
"obug": ["obug@2.1.4", "", {}, "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||
|
||||
"pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
|
||||
|
||||
|
|
@ -507,7 +579,7 @@
|
|||
|
||||
"react-devtools-core": ["dry-uninstall@0.3.0", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
|
||||
|
||||
"react-is": ["react-is@19.2.4", "", {}, "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA=="],
|
||||
"react-is": ["react-is@19.2.8", "", {}, "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ=="],
|
||||
|
||||
"react-native": ["react-native@0.81.4", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.4", "@react-native/codegen": "0.81.4", "@react-native/community-cli-plugin": "0.81.4", "@react-native/gradle-plugin": "0.81.4", "@react-native/js-polyfills": "0.81.4", "@react-native/normalize-colors": "0.81.4", "@react-native/virtualized-lists": "0.81.4", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-bt5bz3A/+Cv46KcjV0VQa+fo7MKxs17RCcpzjftINlen4ZDUl0I6Ut+brQ2FToa5oD0IB0xvQHfmsg2EDqsZdQ=="],
|
||||
|
||||
|
|
@ -521,21 +593,19 @@
|
|||
|
||||
"react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="],
|
||||
|
||||
"recyclerlistview": ["dry-uninstall@0.3.0", "", {}, "sha512-b8h94RVpETWkVV59x62NsY++79bM7Si6Dxq7a4iVxRcJU3ZJJ4vaiC7wUZwM8WDK0ySRL+i+T/1SMAzbJLejYA=="],
|
||||
|
||||
"regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="],
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"rolldown": ["rolldown@1.1.0", "", { "dependencies": { "@oxc-project/types": "=0.134.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.0", "@rolldown/binding-darwin-arm64": "1.1.0", "@rolldown/binding-darwin-x64": "1.1.0", "@rolldown/binding-freebsd-x64": "1.1.0", "@rolldown/binding-linux-arm-gnueabihf": "1.1.0", "@rolldown/binding-linux-arm64-gnu": "1.1.0", "@rolldown/binding-linux-arm64-musl": "1.1.0", "@rolldown/binding-linux-ppc64-gnu": "1.1.0", "@rolldown/binding-linux-s390x-gnu": "1.1.0", "@rolldown/binding-linux-x64-gnu": "1.1.0", "@rolldown/binding-linux-x64-musl": "1.1.0", "@rolldown/binding-openharmony-arm64": "1.1.0", "@rolldown/binding-wasm32-wasi": "1.1.0", "@rolldown/binding-win32-arm64-msvc": "1.1.0", "@rolldown/binding-win32-x64-msvc": "1.1.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-zpMvlJhs5PkXRTtKc0CaLBVI9AR/VDiJFpM+kx//hgToEca7FgMlGjaRIisXBcb19T76LswgmKECSQ96hjWr5A=="],
|
||||
"rolldown": ["rolldown@1.2.0", "", { "dependencies": { "@oxc-project/types": "=0.140.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.2.0", "@rolldown/binding-darwin-arm64": "1.2.0", "@rolldown/binding-darwin-x64": "1.2.0", "@rolldown/binding-freebsd-x64": "1.2.0", "@rolldown/binding-linux-arm-gnueabihf": "1.2.0", "@rolldown/binding-linux-arm64-gnu": "1.2.0", "@rolldown/binding-linux-arm64-musl": "1.2.0", "@rolldown/binding-linux-ppc64-gnu": "1.2.0", "@rolldown/binding-linux-s390x-gnu": "1.2.0", "@rolldown/binding-linux-x64-gnu": "1.2.0", "@rolldown/binding-linux-x64-musl": "1.2.0", "@rolldown/binding-openharmony-arm64": "1.2.0", "@rolldown/binding-wasm32-wasi": "1.2.0", "@rolldown/binding-win32-arm64-msvc": "1.2.0", "@rolldown/binding-win32-x64-msvc": "1.2.0" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA=="],
|
||||
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.25.2", "", { "dependencies": { "@babel/generator": "8.0.0-rc.6", "@babel/helper-validator-identifier": "8.0.0-rc.6", "@babel/parser": "8.0.0-rc.6", "ast-kit": "^3.0.0-beta.1", "birpc": "^4.0.0", "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "^2.1.1" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20260325.1", "rolldown": "^1.0.0", "typescript": "^5.0.0 || ^6.0.0", "vue-tsc": "~3.2.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg=="],
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.27.14", "", { "dependencies": { "dts-resolver": "^3.0.0", "get-tsconfig": "5.0.0-beta.5", "obug": "^2.1.4", "yuku-ast": "^0.8.0", "yuku-codegen": "^0.8.0", "yuku-parser": "^0.8.0" }, "peerDependencies": { "@typescript/native-preview": "*", "@volar/typescript": "~2.4.0", "rolldown": "^1.0.0", "typescript": "^5.0.0 || ^6.0.0 || ~7.0.0", "vue-tsc": "~3.2.0 || ~3.3.0" }, "optionalPeers": ["@typescript/native-preview", "@volar/typescript", "typescript", "vue-tsc"] }, "sha512-ZvuDDwoIpRK9RPxDXratCpklFO9QZZWndf/sd0VBFb4LEj0jj07UcHK9OCh7V4XiFz2Z89ziyBC2K6tJiDjrbw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
|
||||
|
||||
|
|
@ -549,6 +619,8 @@
|
|||
|
||||
"stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="],
|
||||
|
||||
"standard-navigation": ["standard-navigation@0.0.8", "", { "peerDependencies": { "react": "*" } }, "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g=="],
|
||||
|
||||
"strict-uri-encode": ["strict-uri-encode@2.0.0", "", {}, "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="],
|
||||
|
||||
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
|
@ -559,15 +631,15 @@
|
|||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"tslib": ["tslib@2.6.3", "", {}, "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="],
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="],
|
||||
|
||||
"type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
"typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||
|
||||
"use-latest-callback": ["use-latest-callback@0.2.6", "", { "peerDependencies": { "react": ">=16.8" } }, "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg=="],
|
||||
|
||||
|
|
@ -581,47 +653,31 @@
|
|||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="],
|
||||
"ws": ["ws@6.2.6", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA=="],
|
||||
|
||||
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
"yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
||||
|
||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||
|
||||
"@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
"yuku-ast": ["yuku-ast@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0" } }, "sha512-trBzFsSa6k32vzNUCH6pFhAoTzWD/NifSYOIQ/6v14vXKh7TRhd2vDNIwRguGdXvcyfBNEEGcsfxFHrnJbS+FQ=="],
|
||||
|
||||
"@babel/template/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
"yuku-codegen": ["yuku-codegen@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0" }, "optionalDependencies": { "@yuku-codegen/binding-darwin-arm64": "0.8.0", "@yuku-codegen/binding-darwin-x64": "0.8.0", "@yuku-codegen/binding-freebsd-x64": "0.8.0", "@yuku-codegen/binding-linux-arm-gnu": "0.8.0", "@yuku-codegen/binding-linux-arm-musl": "0.8.0", "@yuku-codegen/binding-linux-arm64-gnu": "0.8.0", "@yuku-codegen/binding-linux-arm64-musl": "0.8.0", "@yuku-codegen/binding-linux-x64-gnu": "0.8.0", "@yuku-codegen/binding-linux-x64-musl": "0.8.0", "@yuku-codegen/binding-win32-arm64": "0.8.0", "@yuku-codegen/binding-win32-x64": "0.8.0" } }, "sha512-f82SDo8moLRymtdYN7/cz2yRbWE6Pmbmph+mLj24QkIR8ASG/c2nETdHzBhV/3rR/r8K+qmy7+JqQV3thHAm8g=="],
|
||||
|
||||
"@babel/template/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
|
||||
|
||||
"@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
|
||||
|
||||
"@babel/traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@emnapi/core/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@emnapi/runtime/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
"yuku-parser": ["yuku-parser@0.8.0", "", { "dependencies": { "@yuku-toolchain/types": "^0.8.0", "yuku-ast": "^0.8.0" }, "optionalDependencies": { "@yuku-parser/binding-darwin-arm64": "0.8.0", "@yuku-parser/binding-darwin-x64": "0.8.0", "@yuku-parser/binding-freebsd-x64": "0.8.0", "@yuku-parser/binding-linux-arm-gnu": "0.8.0", "@yuku-parser/binding-linux-arm-musl": "0.8.0", "@yuku-parser/binding-linux-arm64-gnu": "0.8.0", "@yuku-parser/binding-linux-arm64-musl": "0.8.0", "@yuku-parser/binding-linux-x64-gnu": "0.8.0", "@yuku-parser/binding-linux-x64-musl": "0.8.0", "@yuku-parser/binding-win32-arm64": "0.8.0", "@yuku-parser/binding-win32-x64": "0.8.0" } }, "sha512-obrazyE8Cyh79xTQS9wv44khnhvkXG1CDSSy0bg+Pjjla+iXkKXK2b6+LTYXSN3qvVfQxc0MN5qhNKYr9sl3Zw=="],
|
||||
|
||||
"@jest/types/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"ast-kit/@babel/parser": ["@babel/parser@8.0.0-rc.1", "", { "dependencies": { "@babel/types": "^8.0.0-rc.1" }, "bin": "./bin/babel-parser.js" }, "sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA=="],
|
||||
|
||||
"jest-message-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"jest-util/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"jest-util/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"metro-source-map/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
"metro-runtime/@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
|
|
@ -629,28 +685,10 @@
|
|||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@jest/types/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="],
|
||||
|
||||
"jest-message-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"jest-util/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"metro-source-map/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"metro-source-map/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"ast-kit/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0-rc.2", "", {}, "sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ=="],
|
||||
|
||||
"ast-kit/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.0-rc.1", "", {}, "sha512-I4YnARytXC2RzkLNVnf5qFNFMzp679qZpmtw/V3Jt2uGnWiIxyJtaukjG7R8pSx8nG2NamICpGfljQsogj+FbQ=="],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
callNativeMethod,
|
||||
callNativeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
|
@ -15,7 +15,7 @@ export const Uncached: Cache = {
|
|||
|
||||
// In-memory cache
|
||||
export let cache: Cache =
|
||||
callBridgeMethodSync('revenge.caches.assets.read', []) ?? Uncached
|
||||
callNativeMethodSync('revenge.caches.assets.read', []) ?? Uncached
|
||||
|
||||
if (cache.version !== ExpectedCacheVersion) {
|
||||
// TODO: Alert to user to update build
|
||||
|
|
@ -32,7 +32,7 @@ export interface Cache {
|
|||
}
|
||||
|
||||
const save = debounce(() => {
|
||||
callBridgeMethod('revenge.caches.assets.write', [cache.data])
|
||||
callNativeMethod('revenge.caches.assets.write', [cache.data])
|
||||
}, 1000)
|
||||
|
||||
export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) {
|
||||
|
|
@ -43,7 +43,7 @@ export function cacheAsset(asset: Asset, moduleId: Metro.ModuleID) {
|
|||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
interface NativeMethods {
|
||||
'revenge.caches.assets.read': [[], Cache | null]
|
||||
'revenge.caches.assets.write': [[data: Cache['data']], void]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ export interface Design {
|
|||
Card: DiscordModules.Components.Card
|
||||
ContextMenu: DiscordModules.Components.ContextMenu
|
||||
ContextMenuItem: DiscordModules.Components.ContextMenuItem
|
||||
FloatingActionButton: DiscordModules.Components.FloatingActionButton
|
||||
IconButton: DiscordModules.Components.IconButton
|
||||
ImageButton: DiscordModules.Components.ImageButton
|
||||
LayerScope: DiscordModules.Components.LayerScope
|
||||
|
|
|
|||
|
|
@ -84,15 +84,57 @@ export function registerSettingsItems(record: Record<string, SettingsItem>) {
|
|||
*
|
||||
* @param key The section to add the settings item to.
|
||||
* @param item The settings item to add.
|
||||
* @param index The index to insert the settings item at. Defaults to -1 (end of the section).
|
||||
* @returns A function to remove the settings item from the section.
|
||||
*/
|
||||
export function addSettingsItemToSection(key: string, item: string) {
|
||||
export function addSettingsItemToSection(
|
||||
key: string,
|
||||
item: string,
|
||||
index?: number,
|
||||
): () => void
|
||||
/**
|
||||
* Adds a settings item to an existing section.
|
||||
*
|
||||
* @param key The section to add the settings item to.
|
||||
* @param fn A function that takes the current settings items and returns the new settings items. Removing items will result in an error.
|
||||
* @returns A function to remove the settings items from the section.
|
||||
*/
|
||||
export function addSettingsItemToSection(
|
||||
key: string,
|
||||
fn: (items: string[]) => string[],
|
||||
): () => void
|
||||
export function addSettingsItemToSection(
|
||||
key: string,
|
||||
item: string | ((items: string[]) => string[]),
|
||||
index = -1,
|
||||
) {
|
||||
const section = sSections[key]
|
||||
if (!section) throw new Error(`Section "${key}" does not exist`)
|
||||
|
||||
const newLength = section.settings.push(item)
|
||||
return () => {
|
||||
delete section.settings[newLength - 1]
|
||||
if (typeof item === 'function') {
|
||||
const before = [...section.settings]
|
||||
const after = [...item(before)]
|
||||
const added = after.filter(i => !before.includes(i))
|
||||
const removed = before.filter(i => !after.includes(i))
|
||||
if (removed.length > 0)
|
||||
throw new Error(
|
||||
`Cannot remove settings items from section "${key}"`,
|
||||
)
|
||||
|
||||
section.settings = after
|
||||
|
||||
return () => {
|
||||
section.settings = section.settings.filter(i => !added.includes(i))
|
||||
}
|
||||
} else {
|
||||
const actualIndex =
|
||||
index < 0 ? section.settings.length + (index + 1) : index
|
||||
section.settings.splice(actualIndex, 0, item)
|
||||
|
||||
return () => {
|
||||
const idx = section.settings.indexOf(item)
|
||||
if (idx !== -1) section.settings.splice(idx, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -306,6 +306,15 @@ export namespace DiscordModules {
|
|||
|
||||
export type ImageButton = FC<ImageButtonProps>
|
||||
|
||||
export interface FloatingActionButtonProps {
|
||||
icon: number
|
||||
onPress: () => void
|
||||
positionBottom?: number
|
||||
accessibilityLabel?: string
|
||||
}
|
||||
|
||||
export type FloatingActionButton = FC<FloatingActionButtonProps>
|
||||
|
||||
export interface StackProps extends ViewProps {
|
||||
spacing?: number
|
||||
align?: ViewStyle['alignItems']
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "@revenge-mod/storage",
|
||||
"name": "@revenge-mod/json-storage",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
|
|
@ -1,14 +1,30 @@
|
|||
import { FileModule } from '@revenge-mod/discord/native'
|
||||
import {
|
||||
exists as fsExists,
|
||||
getConstants,
|
||||
readFile,
|
||||
rm,
|
||||
writeFile,
|
||||
} from '@revenge-mod/modules/native/fs'
|
||||
import { pluginStorageDirFor } from '@revenge-mod/plugins/constants'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { cloneDeep, mergeDeep } from '@revenge-mod/utils/object'
|
||||
import type { AnyObject, DeepPartial, If } from '@revenge-mod/utils/types'
|
||||
|
||||
export type StorageSubscription<T extends AnyObject = AnyObject> = (
|
||||
/**
|
||||
* Get the storage path for a plugin's JSON storage document.
|
||||
*
|
||||
* @param id The plugin ID.
|
||||
* @param file The file name (or relative path) of the storage document inside the plugin's storage directory.
|
||||
*/
|
||||
export const pluginStoragePathFor = (id: string, file = 'storage.json') =>
|
||||
`${pluginStorageDirFor(id)}/${file}`
|
||||
|
||||
export type JsonStorageSubscription<T extends AnyObject = AnyObject> = (
|
||||
update: DeepPartial<T>,
|
||||
mode: (typeof StorageUpdateMode)[keyof typeof StorageUpdateMode],
|
||||
mode: (typeof JsonStorageUpdateMode)[keyof typeof JsonStorageUpdateMode],
|
||||
) => void
|
||||
|
||||
export const StorageUpdateMode = {
|
||||
export const JsonStorageUpdateMode = {
|
||||
/**
|
||||
* The update will be merged into the existing storage.
|
||||
*/
|
||||
|
|
@ -18,29 +34,37 @@ export const StorageUpdateMode = {
|
|||
*/
|
||||
Replace: 1,
|
||||
/**
|
||||
* Same behavior as {@link StorageUpdateMode.Replace}, but for the intial load of the storage.
|
||||
* Same behavior as {@link JsonStorageUpdateMode.Replace}, but for the intial load of the storage.
|
||||
*/
|
||||
Load: 2,
|
||||
} as const
|
||||
|
||||
export function Storage<T extends AnyObject>(
|
||||
this: Storage<T>,
|
||||
/**
|
||||
* Create a new JSON storage object.
|
||||
*
|
||||
* @param path The path to the storage document.
|
||||
* - Relative paths resolve against the app data directory (`/data/data/<pkg>` on Android).
|
||||
* Use `files/...` for app data or `cache/...` for cache on Android.
|
||||
* - Absolute paths are used as-is.
|
||||
*
|
||||
* @param options Options for the storage.
|
||||
*/
|
||||
export function JsonStorage<T extends AnyObject>(
|
||||
this: JsonStorage<T>,
|
||||
path: string,
|
||||
options?: StorageOptions<T>,
|
||||
options?: JsonStorageOptions<T>,
|
||||
) {
|
||||
const { CacheDirPath, DocumentsDirPath } = FileModule.getConstants()
|
||||
const fullPath = path.startsWith('/')
|
||||
? path
|
||||
: `${getConstants().data}/${path}`
|
||||
|
||||
const directory = options?.directory ?? 'documents'
|
||||
const dirPath = directory === 'cache' ? CacheDirPath : DocumentsDirPath
|
||||
const fullPath = `${dirPath}/${path}`
|
||||
|
||||
const subscriptions = new Set<StorageSubscription<T>>()
|
||||
const subscriptions = new Set<JsonStorageSubscription<T>>()
|
||||
|
||||
this.loaded = false
|
||||
|
||||
this.exists = () => FileModule.fileExists(fullPath)
|
||||
this.exists = () => fsExists(fullPath)
|
||||
this.delete = async function () {
|
||||
await FileModule.removeFile(directory, path)
|
||||
await rm(fullPath)
|
||||
const success = !(await this.exists())
|
||||
if (this.loaded && success) await this.get()
|
||||
return success
|
||||
|
|
@ -51,13 +75,13 @@ export function Storage<T extends AnyObject>(
|
|||
return () => subscriptions.delete(callback)
|
||||
}
|
||||
|
||||
async function write(storage: Storage<T>) {
|
||||
async function write(storage: JsonStorage<T>) {
|
||||
try {
|
||||
const contents = JSON.stringify(storage.cache)
|
||||
await FileModule.writeFile(directory, path, contents, 'utf8')
|
||||
await writeFile(fullPath, contents)
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`Failed to write storage (<${directory}>/${path}): ${getErrorStack(e)}`,
|
||||
`Failed to write storage (${fullPath}): ${getErrorStack(e)}`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
|
|
@ -71,17 +95,17 @@ export function Storage<T extends AnyObject>(
|
|||
return this.cache
|
||||
}
|
||||
|
||||
const contents = await FileModule.readFile(fullPath, 'utf8')
|
||||
const contents = await readFile(fullPath)
|
||||
if (contents) {
|
||||
this.loaded = true
|
||||
try {
|
||||
const cache = (this.cache = JSON.parse(contents))
|
||||
for (const sub of subscriptions)
|
||||
sub(cache, StorageUpdateMode.Load)
|
||||
sub(cache, JsonStorageUpdateMode.Load)
|
||||
return cache
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`Failed to parse storage (<${directory}>/${path}): ${getErrorStack(e)}`,
|
||||
`Failed to parse storage (${fullPath}): ${getErrorStack(e)}`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
|
|
@ -98,7 +122,9 @@ export function Storage<T extends AnyObject>(
|
|||
for (const sub of subscriptions)
|
||||
sub(
|
||||
value,
|
||||
replace ? StorageUpdateMode.Replace : StorageUpdateMode.Merge,
|
||||
replace
|
||||
? JsonStorageUpdateMode.Replace
|
||||
: JsonStorageUpdateMode.Merge,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -106,34 +132,30 @@ export function Storage<T extends AnyObject>(
|
|||
}
|
||||
|
||||
// React is only initialized right before the init stage, so this is a dummy method
|
||||
// See init.ts in api.storage plugin for the actual implementation
|
||||
Storage.prototype.use = () => {
|
||||
throw new Error('Storage#use can only be called after the init stage!')
|
||||
// See init.ts in api.json-storage plugin for the actual implementation
|
||||
JsonStorage.prototype.use = () => {
|
||||
throw new Error('JsonStorage#use can only be called after the init stage!')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a storage object for a given path and directory.
|
||||
* Get a JSON storage object for a given path.
|
||||
*
|
||||
* @param path Path relative to the directory.
|
||||
* @param directory Directory to use. Can be either 'cache' or 'documents'.
|
||||
* @param path The path to the storage document.
|
||||
* - Relative paths resolve against the app data directory (`/data/data/<pkg>` on Android).
|
||||
* Use `files/...` for app data or `cache/...` for cache on Android.
|
||||
* - Absolute paths are used as-is.
|
||||
*/
|
||||
export function getStorage<T extends AnyObject = AnyObject>(
|
||||
export function getJsonStorage<T extends AnyObject = AnyObject>(
|
||||
path: string,
|
||||
options?: StorageOptions<T>,
|
||||
): Storage<T> {
|
||||
const storage: Storage<T> = Object.create(Storage.prototype)
|
||||
Storage.call(storage, path, options)
|
||||
options?: JsonStorageOptions<T>,
|
||||
): JsonStorage<T> {
|
||||
const storage: JsonStorage<T> = Object.create(JsonStorage.prototype)
|
||||
JsonStorage.call(storage, path, options)
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
export interface StorageOptions<T extends AnyObject = AnyObject> {
|
||||
/**
|
||||
* The directory of the storage file.
|
||||
*
|
||||
* @default 'documents'
|
||||
*/
|
||||
directory?: StorageDirectory
|
||||
export interface JsonStorageOptions<T extends AnyObject = AnyObject> {
|
||||
/**
|
||||
* The default value to use for the storage. This will also be used for cache.
|
||||
*
|
||||
|
|
@ -148,11 +170,11 @@ export interface StorageOptions<T extends AnyObject = AnyObject> {
|
|||
load?: boolean
|
||||
}
|
||||
|
||||
export type UseStorageFilter<T extends AnyObject = AnyObject> = (
|
||||
...params: Parameters<StorageSubscription<T>>
|
||||
export type UseJsonStorageFilter<T extends AnyObject = AnyObject> = (
|
||||
...params: Parameters<JsonStorageSubscription<T>>
|
||||
) => any
|
||||
|
||||
export interface Storage<T extends AnyObject> {
|
||||
export interface JsonStorage<T extends AnyObject> {
|
||||
/**
|
||||
* Whether the storage has been loaded. If the storage is not loaded, `storage.cache` may be `undefined`.
|
||||
* If you have `options.default` set, you can use this property to check if `storage.cache` is the default value or not.
|
||||
|
|
@ -172,7 +194,7 @@ export interface Storage<T extends AnyObject> {
|
|||
* ```tsx
|
||||
* type Settings = { key: boolean, nested: { key: boolean } }
|
||||
*
|
||||
* const SettingsStorage = getStorage<Settings>('settings.json')
|
||||
* const SettingsStorage = getJsonStorage<Settings>('settings.json')
|
||||
*
|
||||
* const MyComponent = () => {
|
||||
* // Re-renders every time any of the keys in the settings object change
|
||||
|
|
@ -190,14 +212,14 @@ export interface Storage<T extends AnyObject> {
|
|||
* // ...
|
||||
* }
|
||||
*/
|
||||
use(filter?: UseStorageFilter<T>): T | undefined
|
||||
use(filter?: UseJsonStorageFilter<T>): T | undefined
|
||||
/**
|
||||
* Subscribe to storage updates.
|
||||
*
|
||||
* @param callback The callback to call when the storage is updated.
|
||||
* @returns A function to unsubscribe.
|
||||
*/
|
||||
subscribe(callback: StorageSubscription<T>): () => void
|
||||
subscribe(callback: JsonStorageSubscription<T>): () => void
|
||||
/**
|
||||
* Get the storage.
|
||||
*/
|
||||
|
|
@ -222,5 +244,3 @@ export interface Storage<T extends AnyObject> {
|
|||
*/
|
||||
delete(): Promise<boolean>
|
||||
}
|
||||
|
||||
export type StorageDirectory = 'cache' | 'documents'
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { Storage } from '@revenge-mod/storage'
|
||||
import { JsonStorage } from '@revenge-mod/json-storage'
|
||||
import { useReRender } from '@revenge-mod/utils/react'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import type { StorageSubscription } from '@revenge-mod/storage'
|
||||
import type { JsonStorageSubscription } from '@revenge-mod/json-storage'
|
||||
|
||||
const proto = Storage.prototype as Storage<any>
|
||||
// Actual implementation of Storage#use
|
||||
const proto = JsonStorage.prototype as JsonStorage<any>
|
||||
// Actual implementation of JsonStorage#use
|
||||
proto.use = function (filter) {
|
||||
if (!this.cache) this.get()
|
||||
|
||||
const reRender = useReRender()
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const sub: StorageSubscription = filter
|
||||
const sub: JsonStorageSubscription = filter
|
||||
? (update, mode) => {
|
||||
if (filter(update, mode)) reRender()
|
||||
}
|
||||
31
lib/json-storage/src/types.ts
Normal file
31
lib/json-storage/src/types.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { AnyObject } from '@revenge-mod/utils/types'
|
||||
import type { JsonStorage, JsonStorageOptions } from '.'
|
||||
|
||||
declare module '@revenge-mod/plugins/types' {
|
||||
export interface UnscopedPreInitPluginApi {
|
||||
jsonStorage: typeof import('.')
|
||||
}
|
||||
|
||||
export interface PluginApiExtensionsOptions {
|
||||
jsonStorage?: AnyObject
|
||||
}
|
||||
|
||||
export interface PluginOptions<O extends PluginApiExtensionsOptions> {
|
||||
jsonStorage?: JsonStorageOptions<NonNullable<O['jsonStorage']>> & {
|
||||
/**
|
||||
* File name (or relative path) of the storage document inside the
|
||||
* plugin's storage directory.
|
||||
*
|
||||
* @default 'storage.json'
|
||||
*/
|
||||
file?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface InitPluginApi<O extends PluginApiExtensionsOptions> {
|
||||
/**
|
||||
* The plugin JSON storage.
|
||||
*/
|
||||
jsonStorage: JsonStorage<NonNullable<O['jsonStorage']>>
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
callNativeMethod,
|
||||
callNativeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
|
@ -17,7 +17,7 @@ export const Uncached: Cache = {
|
|||
|
||||
// In-memory cache
|
||||
export let cache: Cache =
|
||||
callBridgeMethodSync('revenge.caches.modules.read', []) ?? Uncached
|
||||
callNativeMethodSync('revenge.caches.modules.read', []) ?? Uncached
|
||||
|
||||
if (cache.version !== ExpectedCacheVersion) {
|
||||
// TODO: Alert to user to update build
|
||||
|
|
@ -37,7 +37,7 @@ export interface Cache {
|
|||
}
|
||||
|
||||
const save = debounce(() => {
|
||||
callBridgeMethod('revenge.caches.modules.write', [
|
||||
callNativeMethod('revenge.caches.modules.write', [
|
||||
cache.blacklist,
|
||||
cache.finds,
|
||||
])
|
||||
|
|
@ -74,7 +74,7 @@ export const getFilterMatches = (
|
|||
export const getBlacklistedModules = () => cache.blacklist
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
interface NativeMethods {
|
||||
'revenge.caches.modules.read': [[], Cache | null]
|
||||
'revenge.caches.modules.write': [
|
||||
[blacklist: Blacklist, finds: Finds],
|
||||
|
|
|
|||
|
|
@ -39,9 +39,10 @@ export type WaitForModulesOptions<ReturnNamespace extends boolean = boolean> =
|
|||
export type WaitForModulesResult<
|
||||
F extends Filter,
|
||||
O extends WaitForModulesOptions,
|
||||
> = O extends RunFilterReturnExportsOptions<true>
|
||||
? MaybeDefaultExportMatched<FilterResult<F>>
|
||||
: FilterResult<F>
|
||||
> =
|
||||
O extends RunFilterReturnExportsOptions<true>
|
||||
? MaybeDefaultExportMatched<FilterResult<F>>
|
||||
: FilterResult<F>
|
||||
|
||||
/**
|
||||
* Wait for modules to initialize. **Callback won't be called if the module is already initialized!**
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { callBridgeMethodSync } from '@revenge-mod/modules/native'
|
||||
import { callNativeMethodSync } from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { FullVersion } from '~constants'
|
||||
import { cache, cacheBlacklistedModule, Uncached } from '../caches'
|
||||
|
|
@ -191,7 +191,7 @@ function handleFactoryCall(
|
|||
const msg = `Module ${mInitializingId} failed to initialize:\n\n${getErrorStack(e)}`
|
||||
|
||||
if (__DEV__) {
|
||||
callBridgeMethodSync('revenge.alertError', [msg, FullVersion])
|
||||
callNativeMethodSync('revenge.alertError', [msg, FullVersion])
|
||||
} else {
|
||||
// So... it wasn't a great idea to throw, Discord has pushed a broken build that has some failing modules
|
||||
// Vanilla Metro would swallow the error and just return an empty object as the exports..., insanity
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export function getModuleDependencies(
|
|||
* @returns Non-zero number if the module is initialized, `0` if it is not initialized or does not exist.
|
||||
*/
|
||||
export function isModuleInitialized(id: Metro.ModuleID): number | undefined {
|
||||
// biome-ignore lint/suspicious/noNonNullAssertedOptionalChain: Hot path, undefined & <n> === 0
|
||||
return mList.get(id)?.flags! & Initialized
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,50 +1,50 @@
|
|||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
callNativeMethod,
|
||||
callNativeMethodSync,
|
||||
} from '@revenge-mod/modules/native'
|
||||
|
||||
export function readFile(path: string) {
|
||||
return callBridgeMethod('revenge.fs.read', [path])
|
||||
return callNativeMethod('revenge.fs.read', [path])
|
||||
}
|
||||
|
||||
export function writeFile(path: string, data: string) {
|
||||
return callBridgeMethod('revenge.fs.write', [path, data])
|
||||
return callNativeMethod('revenge.fs.write', [path, data])
|
||||
}
|
||||
|
||||
export function exists(path: string) {
|
||||
return callBridgeMethod('revenge.fs.exists', [path])
|
||||
return callNativeMethod('revenge.fs.exists', [path])
|
||||
}
|
||||
|
||||
export function rm(path: string) {
|
||||
return callBridgeMethod('revenge.fs.delete', [path])
|
||||
return callNativeMethod('revenge.fs.delete', [path])
|
||||
}
|
||||
|
||||
export function existsSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.exists', [path])
|
||||
return callNativeMethodSync('revenge.fs.exists', [path])
|
||||
}
|
||||
|
||||
export function readFileSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.read', [path])
|
||||
return callNativeMethodSync('revenge.fs.read', [path])
|
||||
}
|
||||
|
||||
export function writeFileSync(path: string, data: string) {
|
||||
return callBridgeMethodSync('revenge.fs.write', [path, data])
|
||||
return callNativeMethodSync('revenge.fs.write', [path, data])
|
||||
}
|
||||
|
||||
export function rmSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.exists', [path])
|
||||
return callNativeMethodSync('revenge.fs.delete', [path])
|
||||
}
|
||||
|
||||
export function deleteFileSync(path: string) {
|
||||
return callBridgeMethodSync('revenge.fs.delete', [path])
|
||||
return callNativeMethodSync('revenge.fs.delete', [path])
|
||||
}
|
||||
|
||||
export function getConstants() {
|
||||
return callBridgeMethodSync('revenge.fs.getConstants', [])
|
||||
return callNativeMethodSync('revenge.fs.getConstants', [])
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface Methods {
|
||||
export interface NativeMethods {
|
||||
'revenge.fs.getConstants': [
|
||||
[],
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { getCurrentStack, getErrorStack } from '@revenge-mod/utils/error'
|
||||
|
||||
const turboModuleProxy = globalThis.__turboModuleProxy
|
||||
|
||||
/**
|
||||
|
|
@ -45,7 +47,7 @@ function makePayload(name: string, args: any[]): object {
|
|||
* @param args The arguments to pass to the native method.
|
||||
* @returns A promise that resolves with the result of the native method call.
|
||||
*/
|
||||
export async function callBridgeMethod<N extends MethodName>(
|
||||
export async function callNativeMethod<N extends MethodName>(
|
||||
name: N,
|
||||
args: MethodArgs<N>,
|
||||
): Promise<MethodResult<N>> {
|
||||
|
|
@ -72,7 +74,7 @@ export async function callBridgeMethod<N extends MethodName>(
|
|||
* @param args The arguments to pass to the native method.
|
||||
* @returns The result of the native method call.
|
||||
*/
|
||||
export function callBridgeMethodSync<N extends MethodName>(
|
||||
export function callNativeMethodSync<N extends MethodName>(
|
||||
name: N,
|
||||
args: MethodArgs<N>,
|
||||
): MethodResult<N> {
|
||||
|
|
@ -93,7 +95,7 @@ export function callBridgeMethodSync<N extends MethodName>(
|
|||
*/
|
||||
export function getBridgeInfo(): BridgeInfo | null {
|
||||
try {
|
||||
return callBridgeMethodSync('revenge.info', [])
|
||||
return callNativeMethodSync('revenge.info', [])
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mFailed to get native bridge info: ${e}\u001b[0m`,
|
||||
|
|
@ -103,15 +105,76 @@ export function getBridgeInfo(): BridgeInfo | null {
|
|||
}
|
||||
}
|
||||
|
||||
type AnyFunction = (...args: any[]) => any
|
||||
|
||||
const CallableReturnNativeMethodName = 'revenge.__callableReturn' as const
|
||||
const CallableModuleName = 'RevengeBridge'
|
||||
const ExposedJSMethods: {
|
||||
[methodName: string]: AnyFunction
|
||||
} = {}
|
||||
|
||||
/**
|
||||
* Registers a JS method that can be called from native code.
|
||||
*
|
||||
* @param name The name of the method to register.
|
||||
* @param method The method implementation.
|
||||
*/
|
||||
export function registerJSMethod(name: string, method: AnyFunction) {
|
||||
if (__DEV__ && ExposedJSMethods[name])
|
||||
nativeLoggingHook(
|
||||
`\u001b[33mWarning: Overwriting existing registered JS method: ${name}\n${getCurrentStack()}\u001b[0m`,
|
||||
1,
|
||||
)
|
||||
|
||||
ExposedJSMethods[name] = _wrapJSMethod(method)
|
||||
}
|
||||
|
||||
function _wrapJSMethod(method: AnyFunction) {
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
const ret = method(...args)
|
||||
if (ret instanceof Promise)
|
||||
ret.then(
|
||||
result => _returnJSCall({ result: result ?? null }),
|
||||
error => _returnJSCall({ error: getErrorStack(error) }),
|
||||
)
|
||||
else _returnJSCall({ result: ret ?? null })
|
||||
} catch (error) {
|
||||
_returnJSCall({ error: getErrorStack(error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _returnJSCall(payload: object) {
|
||||
callNativeMethodSync(CallableReturnNativeMethodName, [payload])
|
||||
}
|
||||
|
||||
RN$registerCallableModule(
|
||||
CallableModuleName,
|
||||
() =>
|
||||
new Proxy(ExposedJSMethods, {
|
||||
get(target, prop: string) {
|
||||
const method = target[prop]
|
||||
if (!method)
|
||||
return () => {
|
||||
// Can't catch this native side, this will crash the app
|
||||
throw new Error(`JS method not found: ${prop}`)
|
||||
}
|
||||
return method
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
export interface BridgeInfo {
|
||||
name: string
|
||||
version: number
|
||||
}
|
||||
|
||||
export type MethodName = Extract<keyof Methods, string>
|
||||
export type MethodArgs<T extends MethodName> = Methods[T][0]
|
||||
export type MethodResult<T extends MethodName> = Methods[T][1]
|
||||
export type MethodName = Extract<keyof NativeMethods, string>
|
||||
export type MethodArgs<T extends MethodName> = NativeMethods[T][0]
|
||||
export type MethodResult<T extends MethodName> = NativeMethods[T][1]
|
||||
|
||||
export interface Methods {
|
||||
export interface NativeMethods {
|
||||
'revenge.info': [[], BridgeInfo]
|
||||
[CallableReturnNativeMethodName]: [[payload: object], void]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
"./constants": {
|
||||
"default": "./src/constants.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"default": "./src/utils.ts"
|
||||
},
|
||||
"./preinit": {
|
||||
"default": "./src/preinit.ts"
|
||||
},
|
||||
|
|
@ -20,6 +23,9 @@
|
|||
},
|
||||
"./_": {
|
||||
"default": "./src/_internal/index.ts"
|
||||
},
|
||||
"./_/repositories": {
|
||||
"default": "./src/_internal/repositories.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { getInternalPluginMeta, getPluginDependencies } from '.'
|
||||
import type { Plugin, PluginApiDecorator } from '../types'
|
||||
import type { PluginApiDecorator } from '../types'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '.'
|
||||
|
||||
export type PluginApiDecoratorStore<T extends 'PreInit' | 'Init' | 'Start'> =
|
||||
|
|
@ -29,7 +29,7 @@ export function addPluginApiDecorator(
|
|||
|
||||
export function decoratePluginApi(
|
||||
store: PluginApiDecoratorStore<any>,
|
||||
plugin: Plugin<any, any>,
|
||||
plugin: AnyPlugin,
|
||||
meta: InternalPluginMeta,
|
||||
) {
|
||||
// Don't decorate API plugins with API plugins...
|
||||
|
|
@ -51,7 +51,7 @@ export function decoratePluginApi(
|
|||
const decorators = store.get(dep)
|
||||
|
||||
if (decorators) {
|
||||
const { handleError } = getInternalPluginMeta(dep)!
|
||||
const { handleError } = getInternalPluginMeta(dep)
|
||||
|
||||
try {
|
||||
for (const decorator of decorators)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getInternalPluginMeta, getPluginDependencies } from '../_internal'
|
||||
import { getPluginDependencies, isPluginStartable } from '../_internal'
|
||||
import { pApis } from './decorators'
|
||||
import type { AnyPlugin } from '../_internal'
|
||||
|
||||
|
|
@ -23,8 +23,27 @@ export const pListOrdered: AnyPlugin[] = []
|
|||
// Pending plugins to be computed
|
||||
export const pPending = new Set<AnyPlugin>()
|
||||
|
||||
// Reserved dependencies verified by native
|
||||
export const ApiDependencyId = 'revenge.api'
|
||||
export const DiscordDependencyId = 'discord'
|
||||
|
||||
export function isReservedDependency(id: string) {
|
||||
return id === ApiDependencyId || id === DiscordDependencyId
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the plugin has start-order dependencies. Reserved dependencies don't count.
|
||||
*/
|
||||
function hasGraphableDependencies(plugin: AnyPlugin): boolean {
|
||||
const deps = plugin.manifest.dependencies
|
||||
if (!deps) return false
|
||||
for (const id in deps) if (!isReservedDependency(id)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
export function computePendingNodes() {
|
||||
for (const plugin of pPending) resolvePluginGraph(plugin)
|
||||
for (const plugin of pPending)
|
||||
if (isPluginStartable(plugin)) resolvePluginGraph(plugin)
|
||||
|
||||
const apis: AnyPlugin[] = []
|
||||
|
||||
|
|
@ -44,8 +63,8 @@ export function computePendingNodes() {
|
|||
continue
|
||||
}
|
||||
|
||||
if (plugin.manifest.dependencies?.length) {
|
||||
for (const dep of getPluginDependencies(plugin))
|
||||
if (hasGraphableDependencies(plugin)) {
|
||||
for (const dep of getPluginDependencies(plugin, false))
|
||||
if (!pLeafOrSingleNodes.has(dep)) stack.push(dep)
|
||||
|
||||
stack.push(plugin)
|
||||
|
|
@ -60,18 +79,12 @@ export function computePendingNodes() {
|
|||
}
|
||||
|
||||
export function resolvePluginGraph(plugin: AnyPlugin) {
|
||||
const { manifest } = plugin
|
||||
|
||||
if (manifest.dependencies?.length) {
|
||||
if (hasGraphableDependencies(plugin)) {
|
||||
// Optimisitically add to root nodes (if there are dependents, it will be removed later)
|
||||
pRootNodes.add(plugin)
|
||||
|
||||
for (const dep of getPluginDependencies(plugin)) {
|
||||
const depMeta = getInternalPluginMeta(dep)!
|
||||
depMeta.dependents.push(plugin)
|
||||
|
||||
// Not a root node if it has dependencies
|
||||
if (dep.manifest.dependencies?.length) pRootNodes.delete(dep)
|
||||
}
|
||||
// Not a root node if it has dependencies
|
||||
for (const dep of getPluginDependencies(plugin, false))
|
||||
if (hasGraphableDependencies(dep)) pRootNodes.delete(dep)
|
||||
} else pLeafOrSingleNodes.add(plugin)
|
||||
}
|
||||
|
|
|
|||
239
lib/plugins/src/_internal/external-plugins.ts
Normal file
239
lib/plugins/src/_internal/external-plugins.ts
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import {
|
||||
callNativeMethod,
|
||||
callNativeMethodSync,
|
||||
registerJSMethod,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { pUnscopedApi } from '../apis'
|
||||
import {
|
||||
disablePlugin,
|
||||
forgetInitialPluginState,
|
||||
getInternalPluginMeta,
|
||||
handlePluginError,
|
||||
InternalPluginFlags,
|
||||
isPluginEnabled,
|
||||
PluginFlags,
|
||||
pEmitter,
|
||||
pList,
|
||||
registerInternalPlugin,
|
||||
registerPlugin,
|
||||
toPluginError,
|
||||
} from '.'
|
||||
import { pPending } from './dependency-graph'
|
||||
import { registerRepositoryEvents } from './repositories'
|
||||
import type { PluginManifest, PluginOptionsFactory } from '../types'
|
||||
import type {
|
||||
AnyPlugin,
|
||||
PluginError,
|
||||
PluginInstallReadyEvent,
|
||||
PluginSource,
|
||||
} from '.'
|
||||
|
||||
interface ExternalPlugin {
|
||||
manifest: PluginManifest
|
||||
script?: string
|
||||
internal?: boolean
|
||||
essential?: boolean
|
||||
enabledByDefault?: boolean
|
||||
api?: boolean
|
||||
/**
|
||||
* The plugin failed to load at native boot (session-skip).
|
||||
* It is registered so the user sees it and the reasons, but it never runs this session.
|
||||
* - Dependency failures keep the enabled flag (auto-recovers next boot once resolved).
|
||||
* - Own-fault failures (bad code, bad manifest) arrive already disabled by native.
|
||||
*/
|
||||
failed?: boolean
|
||||
/** Where the plugin came from. Missing or `repo: null` means sideloaded. */
|
||||
source?: PluginSource | null
|
||||
unsatisfiedOptionalDependencies?: string[]
|
||||
/** Errors the native side already hit (eg. at boot before JS was up, after faulty update). */
|
||||
errors?: PluginError[]
|
||||
}
|
||||
|
||||
type PluginInstallResult =
|
||||
| { error: false; plugin: ExternalPlugin }
|
||||
| { error: PluginError }
|
||||
|
||||
export function registerExternalPlugins() {
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.pluginInstallResult',
|
||||
async function installedHandler(result: PluginInstallResult) {
|
||||
if (result.error !== false) {
|
||||
pEmitter.emit('install', { error: result.error })
|
||||
return
|
||||
}
|
||||
|
||||
// Native only fires this for fresh installs of new IDs
|
||||
// Updates go through `pluginUpdated` instead and apply at reload
|
||||
const { plugin } = result
|
||||
|
||||
try {
|
||||
// Drop the stale boot snapshot entry (from before a mid-session uninstall)
|
||||
// so the fresh install registers disabled
|
||||
forgetInitialPluginState(plugin.manifest.id)
|
||||
registerExternalPlugin(plugin)
|
||||
} catch (e) {
|
||||
pEmitter.emit('install', { error: toPluginError(e) })
|
||||
return
|
||||
}
|
||||
|
||||
pEmitter.emit('install', {
|
||||
error: false,
|
||||
manifest: plugin.manifest,
|
||||
updated: false,
|
||||
pending: false,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.pluginUpdated',
|
||||
async function updatePendingHandler({
|
||||
id,
|
||||
version,
|
||||
}: {
|
||||
id: string
|
||||
version: string
|
||||
}) {
|
||||
// The new version is on disk only, the running plugin keeps its old code until reload
|
||||
// PendingUpdate causes the reload alert to show up
|
||||
const plugin = pList.get(id)
|
||||
if (plugin) {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.flags |= PluginFlags.PendingUpdate
|
||||
}
|
||||
|
||||
pEmitter.emit('install', {
|
||||
error: false,
|
||||
pending: true,
|
||||
id,
|
||||
version,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.pluginInstallReady',
|
||||
(event: PluginInstallReadyEvent) => {
|
||||
pEmitter.emit('installReady', event)
|
||||
},
|
||||
)
|
||||
|
||||
registerRepositoryEvents()
|
||||
|
||||
const externals = callNativeMethodSync('revenge.plugins.list', [])
|
||||
if (!externals) return
|
||||
|
||||
for (const external of externals)
|
||||
try {
|
||||
// Skip plugins whose JS counterpart is already registered
|
||||
if (pList.has(external.manifest.id)) continue
|
||||
registerExternalPlugin(external)
|
||||
} catch (e) {
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mFailed to register external plugin "${external.manifest.id}": ${getErrorStack(e)}\u001b[0m`,
|
||||
2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function registerExternalPlugin(external: ExternalPlugin) {
|
||||
const {
|
||||
manifest,
|
||||
script,
|
||||
internal,
|
||||
essential,
|
||||
enabledByDefault,
|
||||
api,
|
||||
failed,
|
||||
source,
|
||||
unsatisfiedOptionalDependencies,
|
||||
errors,
|
||||
} = external
|
||||
|
||||
const id = internal
|
||||
? registerInternalPlugin(
|
||||
manifest,
|
||||
createOptionsFactory(script),
|
||||
essential || enabledByDefault ? PluginFlags.Enabled : 0,
|
||||
InternalPluginFlags.Internal |
|
||||
(essential ? InternalPluginFlags.Essential : 0) |
|
||||
(api ? InternalPluginFlags.API : 0),
|
||||
)
|
||||
: registerPlugin(
|
||||
manifest,
|
||||
createOptionsFactory(script),
|
||||
enabledByDefault ? PluginFlags.Enabled : 0,
|
||||
)
|
||||
|
||||
const plugin = pList.get(id)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
|
||||
meta.source = source
|
||||
if (unsatisfiedOptionalDependencies?.length)
|
||||
meta.unsatisfiedOptionalDependencies = Object.freeze(
|
||||
unsatisfiedOptionalDependencies,
|
||||
)
|
||||
|
||||
/** @see {@link PluginFlags.Failed} */
|
||||
if (failed) {
|
||||
meta.flags |= PluginFlags.Failed
|
||||
pPending.delete(plugin)
|
||||
}
|
||||
|
||||
// Sync errors the native side already caught
|
||||
if (errors?.length)
|
||||
for (const error of errors) handlePluginError(error, plugin)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export async function uninstallExternalPlugin(plugin: AnyPlugin) {
|
||||
if (isPluginEnabled(plugin)) await disablePlugin(plugin)
|
||||
|
||||
await callNativeMethod('revenge.plugins.uninstall', [plugin.manifest.id])
|
||||
|
||||
// Native cleared its persisted flags, drop our boot snapshot entry too
|
||||
forgetInitialPluginState(plugin.manifest.id)
|
||||
|
||||
pList.delete(plugin.manifest.id)
|
||||
pEmitter.emit('unregister', plugin)
|
||||
}
|
||||
|
||||
export async function resyncPluginSources() {
|
||||
const externals = await callNativeMethod('revenge.plugins.list', [])
|
||||
if (!externals) return
|
||||
|
||||
for (const external of externals) {
|
||||
const plugin = pList.get(external.manifest.id)
|
||||
if (!plugin) continue
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.source = external.source
|
||||
pEmitter.emit('flagUpdate', plugin)
|
||||
}
|
||||
}
|
||||
|
||||
export function confirmInstall(
|
||||
token: string,
|
||||
accepted: boolean,
|
||||
): Promise<{ result: 'installed' | 'pending' | 'cancelled' }> {
|
||||
return callNativeMethod('revenge.plugins.confirmInstall', [token, accepted])
|
||||
}
|
||||
|
||||
function createOptionsFactory(script?: string): PluginOptionsFactory {
|
||||
if (!script) return () => ({})
|
||||
return () => new Function('revenge', `return ${script}`)(pUnscopedApi)
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface NativeMethods {
|
||||
'revenge.plugins.list': [[], ExternalPlugin[] | null]
|
||||
'revenge.plugins.installFile': [[], null]
|
||||
'revenge.plugins.uninstall': [[string], null]
|
||||
'revenge.plugins.confirmInstall': [
|
||||
[token: string, accepted: boolean],
|
||||
{ result: 'installed' | 'pending' | 'cancelled' },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
|
||||
import {
|
||||
callBridgeMethod,
|
||||
callBridgeMethodSync,
|
||||
callNativeMethod,
|
||||
callNativeMethodSync,
|
||||
registerJSMethod,
|
||||
} from '@revenge-mod/modules/native'
|
||||
import { exists, rm } from '@revenge-mod/modules/native/fs'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { sleepReject } from '@revenge-mod/utils/promise'
|
||||
import { pUnscopedApi as uapi } from '../apis'
|
||||
import {
|
||||
PluginFlags as Flag,
|
||||
PersistentPluginFlags,
|
||||
PluginStatus as Status,
|
||||
} from '../constants'
|
||||
import { pluginStorageDirFor, PluginStatus as Status } from '../constants'
|
||||
import {
|
||||
addPluginApiDecorator,
|
||||
decoratePluginApi,
|
||||
|
|
@ -20,7 +18,9 @@ import {
|
|||
pDecoratorsStart,
|
||||
} from './decorators'
|
||||
import {
|
||||
ApiDependencyId,
|
||||
computePendingNodes,
|
||||
DiscordDependencyId,
|
||||
pLeafOrSingleNodes,
|
||||
pListOrdered,
|
||||
pPending,
|
||||
|
|
@ -31,16 +31,49 @@ import type {
|
|||
PluginApi,
|
||||
PluginApiExtensionsOptions,
|
||||
PluginCleanup,
|
||||
PluginDependency,
|
||||
PluginManifest,
|
||||
PluginOptions,
|
||||
PluginOptionsFactory,
|
||||
PluginVersion,
|
||||
PreInitPluginApi,
|
||||
} from '../types'
|
||||
|
||||
export type AnyPlugin = Plugin<any, any>
|
||||
|
||||
const InternalPluginVersion: PluginVersion = (() => {
|
||||
const [segments, label] = __BUILD_VERSION__.split('-')
|
||||
const nums = segments.split('.').map(Number)
|
||||
if (!label) return { nums }
|
||||
return { nums, label }
|
||||
})()
|
||||
|
||||
const MaxWaitTime = 5000
|
||||
|
||||
export const PluginFlags = {
|
||||
Enabled: 1 << 0,
|
||||
PendingReload: 1 << 1,
|
||||
EnabledLate: 1 << 2,
|
||||
/**
|
||||
* A newer version of the plugin is on disk, but the running version is still active.
|
||||
* The new plugin version will be reloaded on next reload.
|
||||
*
|
||||
* This flag is JS-side.
|
||||
*/
|
||||
PendingUpdate: 1 << 3,
|
||||
/**
|
||||
* The plugin failed to load this session (aka. session-skip), but is registered so the user sees it and the reason.
|
||||
* It can never run in this session, and it isn't disabled however.
|
||||
*
|
||||
* This is usually because some dependencies failed to start, but could be caused by other misc errors too,
|
||||
* such as missing dependencies, bad code, etc.
|
||||
*
|
||||
* This flag is JS-side.
|
||||
*/
|
||||
Failed: 1 << 4,
|
||||
}
|
||||
|
||||
const Flag = PluginFlags
|
||||
|
||||
const PluginApiLevel = {
|
||||
None: 0,
|
||||
PreInit: 1,
|
||||
|
|
@ -70,15 +103,28 @@ export interface InternalPluginMeta {
|
|||
cleanups: PluginCleanup[]
|
||||
iflags: number
|
||||
apiLevel: number
|
||||
dependents: AnyPlugin[]
|
||||
dependencies?: AnyPlugin[]
|
||||
unsatisfiedOptionalDependencies: readonly string[]
|
||||
options: PluginOptions<any>
|
||||
optionsFactory?: PluginOptionsFactory<any>
|
||||
flags: number
|
||||
nativeErrors: readonly PluginError[]
|
||||
/**
|
||||
* Where the plugin came from. `repo: null` or missing means sideloaded.
|
||||
* Internal plugins never have one.
|
||||
*/
|
||||
source?: PluginSource | null
|
||||
}
|
||||
|
||||
export interface PluginSource {
|
||||
// url
|
||||
repo: string | null
|
||||
channel: string
|
||||
}
|
||||
|
||||
export const pUnscopedApi = uapi
|
||||
export const pEmitter = new TypedEventEmitter<{
|
||||
register: [AnyPlugin, PluginOptions<any>, update?: true]
|
||||
unregister: [AnyPlugin]
|
||||
disabled: [AnyPlugin]
|
||||
enabled: [AnyPlugin]
|
||||
preInited: [AnyPlugin]
|
||||
|
|
@ -87,74 +133,261 @@ export const pEmitter = new TypedEventEmitter<{
|
|||
stopped: [AnyPlugin]
|
||||
errored: [AnyPlugin, unknown]
|
||||
flagUpdate: [AnyPlugin]
|
||||
install: [PluginInstallEvent]
|
||||
installReady: [PluginInstallReadyEvent]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* A sideloaded plugin was staged and validated.
|
||||
* The user can confirm (or declines) through `confirmInstall(token, accepted)`.
|
||||
*/
|
||||
export interface PluginInstallReadyEvent {
|
||||
/** Single-use confirmation token. */
|
||||
token: string
|
||||
manifest: {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
author: string
|
||||
version: string
|
||||
icon?: string | null
|
||||
}
|
||||
/** The installed version this replaces, or null for a fresh install. */
|
||||
replaces: string | null
|
||||
}
|
||||
|
||||
export type PluginInstallEvent =
|
||||
| {
|
||||
error: false
|
||||
manifest: PluginManifest
|
||||
updated: boolean
|
||||
pending: false
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The plugin was applied on disk only. The running version, if any, is
|
||||
* untouched and the new one loads at next reload.
|
||||
*/
|
||||
error: false
|
||||
pending: true
|
||||
id: string
|
||||
version: string
|
||||
}
|
||||
| { error: PluginError }
|
||||
|
||||
/// PLUGIN ERRORS
|
||||
|
||||
export interface PluginError {
|
||||
/** One of {@link PluginErrorCodes}, or something unknown. */
|
||||
code: string
|
||||
message: string
|
||||
stack?: string | null
|
||||
}
|
||||
|
||||
export const PluginErrorCodes = {
|
||||
// Discovery / boot
|
||||
ManifestInvalid: 'MANIFEST_INVALID',
|
||||
DependencyMissing: 'DEPENDENCY_MISSING',
|
||||
DependencyUnsatisfied: 'DEPENDENCY_UNSATISFIED',
|
||||
DependencyFailed: 'DEPENDENCY_FAILED',
|
||||
DependencyCycle: 'DEPENDENCY_CYCLE',
|
||||
LoadFailed: 'LOAD_FAILED',
|
||||
|
||||
/* The plugin's own code threw */
|
||||
PluginError: 'PLUGIN_ERROR',
|
||||
|
||||
InstallInvalidZip: 'INSTALL_INVALID_ZIP',
|
||||
InstallVerifyFailed: 'INSTALL_VERIFY_FAILED',
|
||||
InstallMismatch: 'INSTALL_MISMATCH',
|
||||
InstallFailed: 'INSTALL_FAILED',
|
||||
} as const
|
||||
|
||||
export function isPluginError(e: unknown): e is PluginError {
|
||||
return (
|
||||
typeof e === 'object' &&
|
||||
e !== null &&
|
||||
typeof (e as PluginError).code === 'string' &&
|
||||
typeof (e as PluginError).message === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export function toPluginError(e: unknown): PluginError {
|
||||
if (isPluginError(e)) return e
|
||||
if (e instanceof Error)
|
||||
return {
|
||||
code: PluginErrorCodes.PluginError,
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
}
|
||||
return { code: PluginErrorCodes.PluginError, message: String(e) }
|
||||
}
|
||||
|
||||
export function formatPluginError(e: unknown): string {
|
||||
const err = toPluginError(e)
|
||||
return `[${err.code}] ${err.message}${err.stack ? `\n${err.stack}` : ''}`
|
||||
}
|
||||
|
||||
export const pList = new Map<PluginManifest['id'], AnyPlugin>()
|
||||
const pMetadata = new WeakMap<AnyPlugin, InternalPluginMeta>()
|
||||
|
||||
const { flags: PersistedFlags }: PersistedPluginStates = callBridgeMethodSync(
|
||||
'revenge.plugins.states.read',
|
||||
[],
|
||||
) ?? {
|
||||
flags: {},
|
||||
/// STATE-SYNC
|
||||
|
||||
const { states: InitialPersistedStates }: PersistedPluginStates =
|
||||
callNativeMethodSync('revenge.plugins.states.read', []) ?? {
|
||||
states: {},
|
||||
}
|
||||
|
||||
export function forgetInitialPluginState(id: PluginManifest['id']) {
|
||||
delete InitialPersistedStates[id]
|
||||
}
|
||||
|
||||
pEmitter.on('flagUpdate', plugin => {
|
||||
PersistedFlags[plugin.manifest.id] = plugin.flags & PersistentPluginFlags
|
||||
callBridgeMethod('revenge.plugins.states.write', [PersistedFlags])
|
||||
// Native is the source of truth, we're expecting dispatches from native to sync states
|
||||
registerJSMethod('revenge.plugins.states.update', (id, state) => {
|
||||
applyPluginFlags(
|
||||
id as PluginManifest['id'],
|
||||
pluginStateToFlags(state as PluginStateObject),
|
||||
)
|
||||
})
|
||||
|
||||
function pluginStateToFlags(state: PluginStateObject): number {
|
||||
return (
|
||||
(state.enabled ? Flag.Enabled : 0) |
|
||||
(state.pendingReload ? Flag.PendingReload : 0) |
|
||||
(state.enabledLate ? Flag.EnabledLate : 0)
|
||||
)
|
||||
}
|
||||
|
||||
async function applyPluginFlags(id: PluginManifest['id'], flags: number) {
|
||||
const plugin = pList.get(id)
|
||||
if (!plugin) return
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
// Repersist JS-side flags
|
||||
flags |= meta.flags & (Flag.PendingUpdate | Flag.Failed)
|
||||
if (meta.flags === flags) return
|
||||
|
||||
const wasEnabled = meta.flags & Flag.Enabled
|
||||
const nowEnabled = flags & Flag.Enabled
|
||||
|
||||
if (wasEnabled && !nowEnabled) {
|
||||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
meta.flags = flags
|
||||
pEmitter.emit('disabled', plugin)
|
||||
} else {
|
||||
meta.flags = flags
|
||||
if (!wasEnabled && nowEnabled) pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
}
|
||||
|
||||
/// NATIVE-ERROR SYNC
|
||||
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.pluginErrored',
|
||||
(id: string, errors: PluginError[]) => {
|
||||
const plugin = pList.get(id)
|
||||
if (!plugin) return
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.nativeErrors = Object.freeze(errors)
|
||||
// TODO: Do we need an errored event specifically?
|
||||
// Nudge open UI to re-render the errors row
|
||||
pEmitter.emit('flagUpdate', plugin)
|
||||
},
|
||||
)
|
||||
|
||||
/// REST OF THE SYSTEM
|
||||
|
||||
/**
|
||||
* Registers a new plugin with the system.
|
||||
*
|
||||
* @param manifest The manifest of the plugin.
|
||||
* @param options The options for the plugin.
|
||||
* @param defflags The default flags for the plugin.
|
||||
* @param iflags The internal flags for the plugin.
|
||||
*/
|
||||
export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
options: PluginOptions<O>,
|
||||
options: PluginOptions<O> | PluginOptionsFactory<O>,
|
||||
defflags: number,
|
||||
) {
|
||||
return register(manifest, options, defflags, 0)
|
||||
}
|
||||
|
||||
export type InternalPluginManifest = Omit<
|
||||
PluginManifest,
|
||||
'version' | 'format' | 'dependencies'
|
||||
> &
|
||||
Partial<Pick<PluginManifest, 'version' | 'format' | 'dependencies'>>
|
||||
|
||||
export function registerInternalPlugin<O extends PluginApiExtensionsOptions>(
|
||||
manifest: InternalPluginManifest,
|
||||
options: PluginOptions<O> | PluginOptionsFactory<O>,
|
||||
defflags: number,
|
||||
iflags = 0,
|
||||
) {
|
||||
manifest.version ??= InternalPluginVersion
|
||||
manifest.format ??= 1
|
||||
manifest.dependencies ??= {}
|
||||
manifest.dependencies[ApiDependencyId] ??= { version: '*' }
|
||||
manifest.dependencies[DiscordDependencyId] ??= { version: '*' }
|
||||
|
||||
return register(manifest as PluginManifest, options, defflags, iflags)
|
||||
}
|
||||
|
||||
function register<O extends PluginApiExtensionsOptions>(
|
||||
manifest: PluginManifest,
|
||||
options: PluginOptions<O> | PluginOptionsFactory<O>,
|
||||
defflags: number,
|
||||
iflags: number,
|
||||
) {
|
||||
// TODO(plugins): verify plugin manifest
|
||||
if (pList.has(manifest.id))
|
||||
throw new Error(`Plugin with ID "${manifest.id}" already registered`)
|
||||
|
||||
const factory = typeof options === 'function' ? options : undefined
|
||||
const resolved = typeof options === 'function' ? undefined : options
|
||||
|
||||
const plugin = {
|
||||
errors: [],
|
||||
manifest,
|
||||
lifecycles: {
|
||||
preInit: options.preInit,
|
||||
init: options.init,
|
||||
start: options.start,
|
||||
stop: options.stop,
|
||||
preInit: resolved?.preInit,
|
||||
init: resolved?.init,
|
||||
start: resolved?.start,
|
||||
stop: resolved?.stop,
|
||||
},
|
||||
SettingsComponent: options.SettingsComponent,
|
||||
SettingsComponent: resolved?.SettingsComponent,
|
||||
status: 0,
|
||||
disable: () => disablePlugin(plugin),
|
||||
stop: () => stopPlugin(plugin),
|
||||
reportError: (e: unknown) => handlePluginError(e, plugin),
|
||||
requireReload: () => {
|
||||
meta.flags |= Flag.PendingReload
|
||||
},
|
||||
api: undefined,
|
||||
set flags(flags: number) {
|
||||
if (meta.flags === flags) return
|
||||
meta.flags = flags
|
||||
pEmitter.emit('flagUpdate', this)
|
||||
},
|
||||
get flags() {
|
||||
return meta.flags
|
||||
},
|
||||
}
|
||||
|
||||
let flags = InitialPersistedStates[manifest.id]
|
||||
? pluginStateToFlags(InitialPersistedStates[manifest.id])
|
||||
: defflags
|
||||
|
||||
const meta: InternalPluginMeta = {
|
||||
cleanups: [],
|
||||
nativeErrors: Object.freeze([]),
|
||||
promises: [],
|
||||
iflags,
|
||||
apiLevel: PluginApiLevel.None,
|
||||
dependents: [],
|
||||
unsatisfiedOptionalDependencies: Object.freeze([]),
|
||||
handleError: e => handlePluginError(e, plugin),
|
||||
options,
|
||||
flags: PersistedFlags[manifest.id] ?? defflags,
|
||||
options: resolved ?? {},
|
||||
optionsFactory: factory,
|
||||
set flags(newFlags: number) {
|
||||
if (newFlags === flags) return
|
||||
flags = newFlags
|
||||
pEmitter.emit('flagUpdate', plugin)
|
||||
},
|
||||
get flags() {
|
||||
return flags
|
||||
},
|
||||
}
|
||||
|
||||
pMetadata.set(plugin, meta)
|
||||
|
|
@ -167,47 +400,87 @@ export function registerPlugin<O extends PluginApiExtensionsOptions>(
|
|||
// Only add to pending if the plugin is enabled
|
||||
else if (isPluginEnabled(plugin)) pPending.add(plugin)
|
||||
|
||||
pEmitter.emit('register', plugin, options)
|
||||
pEmitter.emit('register', plugin, meta.options)
|
||||
|
||||
return { id: manifest.id } satisfies PluginDependency
|
||||
return manifest.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets dependencies for a plugin.
|
||||
*/
|
||||
export function getPluginDependencies(plugin: AnyPlugin): AnyPlugin[] {
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
if (meta.dependencies) return meta.dependencies
|
||||
|
||||
export function getPluginDependencies(
|
||||
plugin: AnyPlugin,
|
||||
throwOnMissing = true,
|
||||
): AnyPlugin[] {
|
||||
const { dependencies, id } = plugin.manifest
|
||||
const deps: AnyPlugin[] = []
|
||||
const { unsatisfiedOptionalDependencies } = getInternalPluginMeta(plugin)
|
||||
|
||||
if (dependencies?.length)
|
||||
for (const { id: depId } of dependencies) {
|
||||
if (dependencies)
|
||||
for (const [depId, spec] of Object.entries(dependencies)) {
|
||||
const dep = pList.get(depId)
|
||||
|
||||
if (dep) {
|
||||
if (isPluginEnabled(dep)) deps.push(dep)
|
||||
else
|
||||
throw new Error(
|
||||
`Plugin "${id}" depends on disabled plugin "${depId}"`,
|
||||
)
|
||||
} else {
|
||||
// TODO: Once external plugins are implemented, we will have to check the external plugin registry here as well
|
||||
// External plugin registry should ideally be Record<PluginManifest['id'], [PluginManifest, Flags: number, PluginCode: string]>
|
||||
// Then we register the plugin here and do dep = pList.get(id) again
|
||||
|
||||
throw new Error(
|
||||
`Plugin "${id}" depends on unregistered plugin "${depId}"`,
|
||||
if (
|
||||
!spec.optional ||
|
||||
(isPluginEnabled(dep) &&
|
||||
!unsatisfiedOptionalDependencies.includes(depId))
|
||||
)
|
||||
deps.push(dep)
|
||||
} else if (!spec.optional) {
|
||||
if (throwOnMissing) {
|
||||
throw new Error(
|
||||
`Plugin "${id}" depends on unregistered plugin "${depId}"`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (meta.dependencies = deps)
|
||||
return deps
|
||||
}
|
||||
|
||||
export function isPluginEnabled({ flags }: AnyPlugin): boolean {
|
||||
return Boolean(flags & Flag.Enabled)
|
||||
export function getMissingPluginDependencies(plugin: AnyPlugin): string[] {
|
||||
const { dependencies } = plugin.manifest
|
||||
if (!dependencies) return []
|
||||
|
||||
const missing: string[] = []
|
||||
for (const [depId, spec] of Object.entries(dependencies)) {
|
||||
if (!spec.optional && !pList.has(depId)) missing.push(depId)
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
export function getPluginDependents(
|
||||
plugin: AnyPlugin,
|
||||
includeLinkedOptionals = false,
|
||||
): AnyPlugin[] {
|
||||
const { id } = plugin.manifest
|
||||
const dependents: AnyPlugin[] = []
|
||||
const enabled = isPluginEnabled(plugin)
|
||||
|
||||
for (const p of pList.values()) {
|
||||
const spec = p.manifest.dependencies?.[id]
|
||||
if (!spec) continue
|
||||
|
||||
if (!spec.optional) dependents.push(p)
|
||||
else if (
|
||||
includeLinkedOptionals &&
|
||||
enabled &&
|
||||
!getInternalPluginMeta(p).unsatisfiedOptionalDependencies.includes(
|
||||
id,
|
||||
)
|
||||
)
|
||||
dependents.push(p)
|
||||
}
|
||||
|
||||
return dependents
|
||||
}
|
||||
|
||||
export function isPluginEnabled(plugin: AnyPlugin): boolean {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
return Boolean(meta && meta.flags & Flag.Enabled)
|
||||
}
|
||||
|
||||
export function isPluginEnabledLate(plugin: AnyPlugin): boolean {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
return Boolean(meta && meta.flags & Flag.EnabledLate)
|
||||
}
|
||||
|
||||
export function isPluginEssential({ iflags }: InternalPluginMeta): boolean {
|
||||
|
|
@ -218,44 +491,121 @@ export function isPluginInternal({ iflags }: InternalPluginMeta): boolean {
|
|||
return Boolean(iflags & InternalPluginFlags.Internal)
|
||||
}
|
||||
|
||||
export function isPluginErrored(plugin: AnyPlugin) {
|
||||
return Boolean(plugin.flags & Flag.Errored)
|
||||
export function isPluginErrored(plugin: AnyPlugin): boolean {
|
||||
return plugin.errors.length > 0
|
||||
}
|
||||
|
||||
function guardPluginEnabled(plugin: AnyPlugin) {
|
||||
export function isPluginPendingReload(plugin: AnyPlugin): boolean {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
return Boolean(meta && meta.flags & Flag.PendingReload)
|
||||
}
|
||||
|
||||
export function isPluginPendingUpdate(plugin: AnyPlugin): boolean {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
return Boolean(meta && meta.flags & Flag.PendingUpdate)
|
||||
}
|
||||
|
||||
/** @see {@link Flag.Failed} */
|
||||
export function isPluginFailed(plugin: AnyPlugin): boolean {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
return Boolean(meta && meta.flags & Flag.Failed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires that the plugin can be started: enabled, not pending a reload, and not failed at discovery.
|
||||
* PendingUpdate doesn't block, it only means a newer version sits on disk for next reload.
|
||||
*/
|
||||
function requirePluginStartableState(plugin: AnyPlugin) {
|
||||
if (!isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
|
||||
if (isPluginPendingReload(plugin))
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" requires a reload before it can be started again`,
|
||||
)
|
||||
|
||||
if (isPluginFailed(plugin))
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" failed to load this session (reload to retry)`,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a plugin is startable.
|
||||
*
|
||||
* @see {@link requirePluginStartableState}.
|
||||
*/
|
||||
export function isPluginStartable(plugin: AnyPlugin): boolean {
|
||||
try {
|
||||
requirePluginStartableState(plugin)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors that occur in plugins.
|
||||
*/
|
||||
async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
||||
plugin.errors.push(e)
|
||||
plugin.flags |= Flag.Errored
|
||||
export async function handlePluginError(e: unknown, plugin: AnyPlugin) {
|
||||
;(plugin.errors as unknown[]).push(e)
|
||||
|
||||
// TODO: Emit errored event so UI can update?
|
||||
// Update: errored event removed (but status changes are still emitted), so UI should update fine
|
||||
|
||||
nativeLoggingHook(
|
||||
`\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${getErrorStack(e)}\u001b[0m`,
|
||||
`\u001b[31mPlugin "${plugin.manifest.id}" encountered an error: ${
|
||||
isPluginError(e) ? formatPluginError(e) : getErrorStack(e)
|
||||
}\u001b[0m`,
|
||||
2,
|
||||
)
|
||||
|
||||
plugin.api?.logger?.error('Plugin encountered an error', e)
|
||||
pEmitter.emit('errored', plugin, e)
|
||||
|
||||
if (!isPluginEssential(getInternalPluginMeta(plugin)!))
|
||||
if (
|
||||
!isPluginEssential(getInternalPluginMeta(plugin)) &&
|
||||
// Failed plugins keep their persisted enabled state untouched, recovering on a later boot
|
||||
!isPluginFailed(plugin) &&
|
||||
// Multiple errors may surface, but we only want to disable the plugin once
|
||||
isPluginEnabled(plugin)
|
||||
)
|
||||
await plugin.disable()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a deferred options factory, populating the plugin's lifecycles and options.
|
||||
*/
|
||||
function resolvePluginOptions(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
||||
const { optionsFactory } = meta
|
||||
if (!optionsFactory) return
|
||||
|
||||
meta.optionsFactory = undefined
|
||||
|
||||
try {
|
||||
const options = optionsFactory()
|
||||
|
||||
meta.options = options
|
||||
plugin.lifecycles.preInit = options.preInit
|
||||
plugin.lifecycles.init = options.init
|
||||
plugin.lifecycles.start = options.start
|
||||
plugin.lifecycles.stop = options.stop
|
||||
plugin.SettingsComponent = options.SettingsComponent
|
||||
} catch (e) {
|
||||
meta.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the plugin API for the preInit lifecycle.
|
||||
*/
|
||||
function tryPreparePluginPreInit(plugin: AnyPlugin) {
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
if (meta.apiLevel >= PluginApiLevel.PreInit) return
|
||||
|
||||
// Clear errors from previous runs
|
||||
plugin.errors = []
|
||||
plugin.flags &= ~Flag.Errored
|
||||
|
||||
resolvePluginOptions(plugin, meta)
|
||||
|
||||
plugin.api = {
|
||||
cleanup: (...items) => {
|
||||
|
|
@ -276,7 +626,7 @@ function tryPreparePluginPreInit(plugin: AnyPlugin) {
|
|||
* Prepares the plugin API for the init lifecycle.
|
||||
*/
|
||||
function tryPreparePluginInit(plugin: AnyPlugin) {
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
if (meta.apiLevel >= PluginApiLevel.Init) return
|
||||
|
||||
const api = plugin.api as InitPluginApi
|
||||
|
|
@ -293,7 +643,7 @@ function tryPreparePluginInit(plugin: AnyPlugin) {
|
|||
* Prepares the plugin API for the start lifecycle.
|
||||
*/
|
||||
function tryPreparePluginStart(plugin: AnyPlugin) {
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
if (meta.apiLevel >= PluginApiLevel.Start) return
|
||||
|
||||
const api = plugin.api as PluginApi
|
||||
|
|
@ -310,20 +660,22 @@ function tryPreparePluginStart(plugin: AnyPlugin) {
|
|||
* Disables a plugin, as well as all its dependents.
|
||||
*/
|
||||
export async function disablePlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
// PendingReload/PendingUpdate never block disabling
|
||||
if (!isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
|
||||
if (isPluginEssential(meta))
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" is essential and cannot be disabled`,
|
||||
)
|
||||
|
||||
const { dependents } = meta
|
||||
|
||||
// Disable dependents
|
||||
await Promise.all(
|
||||
dependents.map(dep => {
|
||||
if (dep.flags & Flag.Enabled) return disablePlugin(dep)
|
||||
getPluginDependents(plugin).map(dep => {
|
||||
if (getInternalPluginMeta(dep)!.flags & Flag.Enabled)
|
||||
return disablePlugin(dep)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -331,7 +683,12 @@ export async function disablePlugin(plugin: AnyPlugin) {
|
|||
if (plugin.status && !(plugin.status & Status.Stopping))
|
||||
await stopPlugin(plugin)
|
||||
|
||||
plugin.flags &= ~Flag.Enabled
|
||||
await callNativeMethod('revenge.plugins.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
false,
|
||||
])
|
||||
|
||||
meta.flags &= ~Flag.Enabled
|
||||
pEmitter.emit('disabled', plugin)
|
||||
}
|
||||
|
||||
|
|
@ -348,12 +705,32 @@ export async function enablePlugin(plugin: AnyPlugin) {
|
|||
}),
|
||||
)
|
||||
|
||||
plugin.flags |= Flag.Enabled
|
||||
const result = await callNativeMethod('revenge.plugins.setEnabled', [
|
||||
plugin.manifest.id,
|
||||
true,
|
||||
])
|
||||
|
||||
if (result?.code === 'DEPENDENCIES_UNSATISFIED') {
|
||||
const details = result.problems
|
||||
.map(
|
||||
p =>
|
||||
`"${p.id}" (requires ${p.required}, installed: ${p.installed ?? 'none'}${p.installed && !p.enabled ? ', disabled' : ''})`,
|
||||
)
|
||||
.join(', ')
|
||||
|
||||
throw new Error(
|
||||
`Cannot enable plugin "${plugin.manifest.id}": unsatisfied dependencies: ${details}`,
|
||||
)
|
||||
}
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.flags |= Flag.Enabled
|
||||
|
||||
pEmitter.emit('enabled', plugin)
|
||||
}
|
||||
|
||||
export async function runPluginLate(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
if (plugin.status & Status.Started)
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is already started`)
|
||||
|
|
@ -368,7 +745,14 @@ export async function runPluginLate(plugin: AnyPlugin) {
|
|||
pListOrdered
|
||||
.filter(plugin => !plugin.status)
|
||||
.map(async function runLate(plugin) {
|
||||
plugin.flags |= Flag.EnabledLate
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
meta.flags |= Flag.EnabledLate
|
||||
|
||||
// Plugin would already be started native-side if it was not started late
|
||||
// But on late starts (enable, fresh install), we must start the native side too
|
||||
await callNativeMethod('revenge.plugins.startNative', [
|
||||
plugin.manifest.id,
|
||||
])
|
||||
|
||||
// Prepare the plugin API
|
||||
await preInitPlugin(plugin)
|
||||
|
|
@ -382,7 +766,7 @@ export async function runPluginLate(plugin: AnyPlugin) {
|
|||
* Runs the preInit lifecycle of a plugin.
|
||||
*/
|
||||
export async function preInitPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -396,7 +780,7 @@ export async function preInitPlugin(plugin: AnyPlugin) {
|
|||
tryPreparePluginPreInit(plugin)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = getInternalPluginMeta(plugin)!
|
||||
const { promises, handleError } = getInternalPluginMeta(plugin)
|
||||
|
||||
try {
|
||||
if (!lifecycles.preInit) return
|
||||
|
|
@ -426,7 +810,7 @@ export async function preInitPlugin(plugin: AnyPlugin) {
|
|||
* Runs the init lifecycle of a plugin.
|
||||
*/
|
||||
export async function initPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -471,7 +855,7 @@ export async function initPlugin(plugin: AnyPlugin) {
|
|||
* Starts a plugin by running its start lifecycle.
|
||||
*/
|
||||
export async function startPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
requirePluginStartableState(plugin)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
|
|
@ -485,7 +869,7 @@ export async function startPlugin(plugin: AnyPlugin) {
|
|||
tryPreparePluginStart(plugin)
|
||||
|
||||
const { lifecycles } = plugin
|
||||
const { promises, handleError } = getInternalPluginMeta(plugin)!
|
||||
const { promises, handleError } = getInternalPluginMeta(plugin)
|
||||
|
||||
try {
|
||||
if (!lifecycles.start) return
|
||||
|
|
@ -515,13 +899,15 @@ export async function startPlugin(plugin: AnyPlugin) {
|
|||
* Stops a plugin by running its stop lifecycle and cleanup functions.
|
||||
*/
|
||||
export async function stopPlugin(plugin: AnyPlugin) {
|
||||
guardPluginEnabled(plugin)
|
||||
// PendingReload/PendingUpdate shouldn't block stopping
|
||||
if (!isPluginEnabled(plugin))
|
||||
throw new Error(`Plugin "${plugin.manifest.id}" is not enabled`)
|
||||
|
||||
const {
|
||||
manifest: { id },
|
||||
} = plugin
|
||||
|
||||
const meta = getInternalPluginMeta(plugin)!
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
|
||||
if (isPluginEssential(meta))
|
||||
throw new Error(`Plugin "${id}" is essential and cannot be stopped`)
|
||||
|
|
@ -541,7 +927,7 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
'Plugin lifecycles timed out, force stopping',
|
||||
),
|
||||
]).catch(e => {
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
meta.flags |= Flag.PendingReload
|
||||
return handlePluginError(e, plugin)
|
||||
})
|
||||
else if (
|
||||
|
|
@ -549,6 +935,15 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
)
|
||||
throw new Error(`Plugin "${id}" is not running`)
|
||||
|
||||
// Stop dependents
|
||||
await Promise.all(
|
||||
getPluginDependents(plugin, true).map(dep =>
|
||||
dep.status && !(dep.status & Status.Stopping)
|
||||
? stopPlugin(dep)
|
||||
: undefined,
|
||||
),
|
||||
)
|
||||
|
||||
plugin.status |= Status.Stopping
|
||||
|
||||
try {
|
||||
|
|
@ -583,7 +978,7 @@ export async function stopPlugin(plugin: AnyPlugin) {
|
|||
async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
||||
async function handleStopError(e: unknown) {
|
||||
// Some cleanup was unsuccessful, so we need to reload the app
|
||||
plugin.flags |= Flag.ReloadRequired
|
||||
meta.flags |= Flag.PendingReload
|
||||
return handlePluginError(e, plugin)
|
||||
}
|
||||
|
||||
|
|
@ -600,20 +995,59 @@ async function cleanupPlugin(plugin: AnyPlugin, meta: InternalPluginMeta) {
|
|||
}
|
||||
|
||||
export function getInternalPluginMeta(plugin: AnyPlugin): InternalPluginMeta {
|
||||
return pMetadata.get(plugin)!
|
||||
const meta = pMetadata.get(plugin)
|
||||
if (!meta)
|
||||
throw new Error(
|
||||
`Plugin "${plugin.manifest.id}" has no internal metadata, is it registered?`,
|
||||
)
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
export async function deleteStorageForPlugin(plugin: Plugin<any, any>) {
|
||||
const dir = pluginStorageDirFor(plugin.manifest.id)
|
||||
|
||||
if (await exists(dir)) await rm(dir)
|
||||
}
|
||||
|
||||
export {
|
||||
confirmInstall,
|
||||
resyncPluginSources,
|
||||
uninstallExternalPlugin,
|
||||
} from './external-plugins'
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface Methods {
|
||||
interface NativeMethods {
|
||||
'revenge.plugins.startNative': [[id: PluginManifest['id']], null]
|
||||
'revenge.plugins.states.read': [[], PersistedPluginStates | null]
|
||||
'revenge.plugins.states.write': [[statuses: PersistedPluginFlags], void]
|
||||
'revenge.plugins.setEnabled': [
|
||||
[id: PluginManifest['id'], enabled: boolean],
|
||||
SetEnabledError | null,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
interface PersistedPluginStates {
|
||||
flags: PersistedPluginFlags
|
||||
type SetEnabledError = {
|
||||
code: 'DEPENDENCIES_UNSATISFIED'
|
||||
problems: Array<{
|
||||
id: PluginManifest['id']
|
||||
/** The declared range (`*` for any). */
|
||||
required: string
|
||||
/** Installed version, or `null` when missing entirely. */
|
||||
installed: string | null
|
||||
enabled: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
interface PersistedPluginFlags {
|
||||
[id: PluginManifest['id']]: number
|
||||
interface PluginStateObject {
|
||||
enabled?: boolean
|
||||
pendingReload?: boolean
|
||||
errored?: boolean
|
||||
enabledLate?: boolean
|
||||
}
|
||||
|
||||
interface PersistedPluginStates {
|
||||
states: {
|
||||
[id: PluginManifest['id']]: PluginStateObject
|
||||
}
|
||||
}
|
||||
|
|
|
|||
300
lib/plugins/src/_internal/repositories.ts
Normal file
300
lib/plugins/src/_internal/repositories.ts
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
import { TypedEventEmitter } from '@revenge-mod/discord/common/utils'
|
||||
import { callNativeMethod, registerJSMethod } from '@revenge-mod/modules/native'
|
||||
|
||||
export interface DownloadProgressEvent {
|
||||
id: string
|
||||
version: string
|
||||
/** The repository the artifact downloads from. */
|
||||
repo: string
|
||||
/** Bytes received so far. */
|
||||
received: number
|
||||
/** Total bytes, from the plan's size field. */
|
||||
total: number
|
||||
/** 1-based position of this artifact in the plan. */
|
||||
index: number
|
||||
/** Number of artifacts in the plan. */
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface RepoStateEvent {
|
||||
url: string
|
||||
state: 'refreshing' | 'ready' | 'error'
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const repoEvents = new TypedEventEmitter<{
|
||||
downloadProgress: [DownloadProgressEvent]
|
||||
repoState: [RepoStateEvent]
|
||||
}>()
|
||||
|
||||
export function registerRepositoryEvents() {
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.downloadProgress',
|
||||
(event: DownloadProgressEvent) => {
|
||||
repoEvents.emit('downloadProgress', event)
|
||||
},
|
||||
)
|
||||
|
||||
registerJSMethod(
|
||||
'revenge.plugins.events.repoStateUpdate',
|
||||
(event: RepoStateEvent) => {
|
||||
repoEvents.emit('repoState', event)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A plugin repository as reported by native.
|
||||
*
|
||||
* The repository's URL is its identity.
|
||||
* The hidden internal repository (serving internal plugins) is always first and cannot be modified or removed.
|
||||
*/
|
||||
export interface Repo {
|
||||
/** Absolute URL of the repository root; also its identity. */
|
||||
url: string
|
||||
enabled: boolean
|
||||
internal: boolean
|
||||
/** Display metadata from the cached index, if any. */
|
||||
name: string | null
|
||||
description: string | null
|
||||
/** A Discord-packaged asset name or a `data:` URL. Never a remote URL. */
|
||||
icon: string | null
|
||||
}
|
||||
|
||||
export interface RepoConfigEntry {
|
||||
url: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function listRepos(): Promise<Repo[]> {
|
||||
return callNativeMethod('revenge.plugins.repos.list', [])
|
||||
}
|
||||
|
||||
export function setRepos(config: RepoConfigEntry[]): Promise<null> {
|
||||
return callNativeMethod('revenge.plugins.repos.set', [config])
|
||||
}
|
||||
|
||||
export function refreshRepo(url: string): Promise<Repo> {
|
||||
return callNativeMethod('revenge.plugins.repos.refresh', [url])
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes every enabled user repository in parallel.
|
||||
* Per-repo failures are collected instead.
|
||||
*/
|
||||
export async function refreshAllRepos(): Promise<{
|
||||
refreshed: Repo[]
|
||||
errors: { url: string; error: unknown }[]
|
||||
}> {
|
||||
const repos = await listRepos()
|
||||
const refreshed: Repo[] = []
|
||||
const errors: { url: string; error: unknown }[] = []
|
||||
|
||||
await Promise.all(
|
||||
repos
|
||||
.filter(repo => !repo.internal && repo.enabled)
|
||||
.map(repo =>
|
||||
refreshRepo(repo.url).then(
|
||||
result => {
|
||||
refreshed.push(result)
|
||||
},
|
||||
error => {
|
||||
errors.push({ url: repo.url, error })
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return { refreshed, errors }
|
||||
}
|
||||
|
||||
export interface RepoPluginListing {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
author: string
|
||||
/** A Discord-packaged asset name or a `data:` URL. Never a remote URL. */
|
||||
icon: string | null
|
||||
/** Channel pointers (eg. `latest`), each naming a key of {@link versions}. */
|
||||
channels: Record<string, string>
|
||||
versions: Record<
|
||||
string,
|
||||
{
|
||||
/** Absolute artifact URL. `null` for the internal repository (nothing downloadable). */
|
||||
url: string | null
|
||||
sha256: string | null
|
||||
size: number
|
||||
dependencies: Record<
|
||||
string,
|
||||
{
|
||||
version: string
|
||||
optional: boolean
|
||||
}
|
||||
>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export interface InstallPlanAction {
|
||||
id: string
|
||||
version: string
|
||||
url: string
|
||||
sha256: string
|
||||
size: number
|
||||
/** The repository this action installs from (recorded as provenance). */
|
||||
repo: string
|
||||
/** The channel followed for future update checks. */
|
||||
channel: string
|
||||
/** The installed version being replaced, or `null` for a fresh install. */
|
||||
replaces: string | null
|
||||
}
|
||||
|
||||
export interface InstallPlan {
|
||||
actions: InstallPlanAction[]
|
||||
/** Non-blocking problems (skipped optionals, dependent-range conflicts). */
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export interface RepoUpdate {
|
||||
id: string
|
||||
installed: string
|
||||
available: string
|
||||
channel: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists one repository's plugins from its cached index.
|
||||
*/
|
||||
export function listRepoPlugins(url: string): Promise<RepoPluginListing[]> {
|
||||
return callNativeMethod('revenge.plugins.repos.listPlugins', [url])
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves an install of one plugin (+ unsatisfied dependencies) against cached indexes.
|
||||
*/
|
||||
export function planInstall(
|
||||
id: string,
|
||||
version?: string,
|
||||
channel?: string,
|
||||
filteredRepos?: string[],
|
||||
): Promise<InstallPlan> {
|
||||
return callNativeMethod('revenge.plugins.planInstall', [
|
||||
id,
|
||||
version ?? null,
|
||||
channel ?? null,
|
||||
filteredRepos ?? null,
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes one confirmed install plan: download all, verify all, then apply on disk.
|
||||
*
|
||||
* - Fresh plugins (new IDs with live dependencies) load immediately (`installed`).
|
||||
* - Updates, and fresh IDs depending on them, only land on disk and load at next reload (`pending`).
|
||||
* - `skipped` lists actions an overlapping plan already satisfied.
|
||||
*/
|
||||
export function installFromRepo(plan: InstallPlan): Promise<{
|
||||
installed: string[]
|
||||
pending: string[]
|
||||
skipped: string[]
|
||||
}> {
|
||||
return callNativeMethod('revenge.plugins.install', [plan])
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists available updates for plugins pinned to one repository, from its **cached index**.
|
||||
* Call {@link refreshRepo} first to ensure the index is up-to-date.
|
||||
*/
|
||||
export function listUpdates(url: string): Promise<RepoUpdate[]> {
|
||||
return callNativeMethod('revenge.plugins.repos.listUpdates', [url])
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists updates across every enabled user repository in parallel.
|
||||
* Per-repo failures (eg. no cached index yet) are returned.
|
||||
*/
|
||||
export async function listAllUpdates(): Promise<{
|
||||
updates: RepoUpdate[]
|
||||
errors: { url: string; error: unknown }[]
|
||||
}> {
|
||||
const repos = await listRepos()
|
||||
const updates: RepoUpdate[] = []
|
||||
const errors: { url: string; error: unknown }[] = []
|
||||
|
||||
await Promise.all(
|
||||
repos
|
||||
.filter(repo => !repo.internal && repo.enabled)
|
||||
.map(repo =>
|
||||
listUpdates(repo.url).then(
|
||||
result => {
|
||||
updates.push(...result)
|
||||
},
|
||||
error => {
|
||||
errors.push({ url: repo.url, error })
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return { updates, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves and installs updates for every entry of {@link listAllUpdates}.
|
||||
* Failures are collected per plugin. Updates land on disk only (`pending`), a reload applies them.
|
||||
*/
|
||||
export async function updateAllPlugins(): Promise<{
|
||||
installed: string[]
|
||||
pending: string[]
|
||||
errors: { id: string; error: unknown }[]
|
||||
}> {
|
||||
const { updates } = await listAllUpdates()
|
||||
const installed: string[] = []
|
||||
const pending: string[] = []
|
||||
const errors: { id: string; error: unknown }[] = []
|
||||
|
||||
await Promise.all(
|
||||
updates.map(async update => {
|
||||
try {
|
||||
const plan = await planInstall(
|
||||
update.id,
|
||||
undefined,
|
||||
update.channel,
|
||||
)
|
||||
const result = await installFromRepo(plan)
|
||||
installed.push(...result.installed)
|
||||
pending.push(...result.pending)
|
||||
} catch (error) {
|
||||
errors.push({ id: update.id, error })
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return { installed, pending, errors }
|
||||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
interface NativeMethods {
|
||||
'revenge.plugins.repos.list': [[], Repo[]]
|
||||
'revenge.plugins.repos.set': [[config: RepoConfigEntry[]], null]
|
||||
'revenge.plugins.repos.refresh': [[url: string], Repo]
|
||||
'revenge.plugins.repos.listPlugins': [
|
||||
[url: string],
|
||||
RepoPluginListing[],
|
||||
]
|
||||
'revenge.plugins.repos.listUpdates': [[url: string], RepoUpdate[]]
|
||||
'revenge.plugins.planInstall': [
|
||||
[
|
||||
id: string,
|
||||
version: string | null,
|
||||
channel: string | null,
|
||||
filteredRepos: string[] | null,
|
||||
],
|
||||
InstallPlan,
|
||||
]
|
||||
'revenge.plugins.install': [
|
||||
[plan: InstallPlan],
|
||||
{ installed: string[]; pending: string[]; skipped: string[] },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
import * as PluginsApiConstants from '@revenge-mod/plugins/constants'
|
||||
import * as PluginsApiUtils from '@revenge-mod/plugins/utils'
|
||||
|
||||
export interface PluginApiPlugins {
|
||||
utils: typeof import('@revenge-mod/plugins/utils')
|
||||
constants: typeof import('@revenge-mod/plugins/constants')
|
||||
}
|
||||
|
||||
export const plugins: PluginApiPlugins = {
|
||||
constants: PluginsApiConstants,
|
||||
utils: PluginsApiUtils,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,4 @@
|
|||
/**
|
||||
* The plugin flags.
|
||||
*/
|
||||
export const PluginFlags = {
|
||||
/**
|
||||
* The plugin is enabled.
|
||||
*/
|
||||
Enabled: 1 << 0,
|
||||
/**
|
||||
* The plugin requires a reload to apply changes.
|
||||
*/
|
||||
ReloadRequired: 1 << 1,
|
||||
/**
|
||||
* The plugin has errors.
|
||||
*/
|
||||
Errored: 1 << 2,
|
||||
/**
|
||||
* The plugin was enabled after the app was started.
|
||||
* This is usually caused by a newly installed plugin, or a plugin that was re-enabled.
|
||||
*/
|
||||
EnabledLate: 1 << 3,
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitmask of {@link PluginFlags} that are persisted to storage.
|
||||
*/
|
||||
export const PersistentPluginFlags = PluginFlags.Enabled
|
||||
import { callNativeMethodSync } from '@revenge-mod/modules/native'
|
||||
|
||||
/**
|
||||
* The plugin status.
|
||||
|
|
@ -39,4 +13,13 @@ export const PluginStatus = {
|
|||
Stopping: 1 << 6,
|
||||
}
|
||||
|
||||
export const PluginsStorageDirectory = 'revenge/plugins/storage'
|
||||
const { storageRootPath } = callNativeMethodSync(
|
||||
'revenge.plugins.getConstants',
|
||||
[],
|
||||
)
|
||||
|
||||
/**
|
||||
* Absolute path to per-plugin storage directory.
|
||||
*/
|
||||
// TODO: is it best to dupe this logic with the native side???
|
||||
export const pluginStorageDirFor = (id: string) => `${storageRootPath}/${id}`
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { initPlugin, isPluginEnabled } from './_internal'
|
||||
import { initPlugin, isPluginStartable } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
|
||||
computePendingNodes()
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (isPluginEnabled(plugin)) initPlugin(plugin)
|
||||
if (isPluginStartable(plugin)) initPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { isPluginEnabled, preInitPlugin } from './_internal'
|
||||
import { isPluginStartable, preInitPlugin } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
import { registerExternalPlugins } from './_internal/external-plugins'
|
||||
|
||||
registerExternalPlugins()
|
||||
computePendingNodes()
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (isPluginEnabled(plugin)) preInitPlugin(plugin)
|
||||
if (isPluginStartable(plugin)) preInitPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { isPluginEnabled, startPlugin } from './_internal'
|
||||
import { isPluginStartable, startPlugin } from './_internal'
|
||||
import { computePendingNodes, pListOrdered } from './_internal/dependency-graph'
|
||||
|
||||
computePendingNodes()
|
||||
|
|
@ -10,4 +10,4 @@ if (__DEV__)
|
|||
)
|
||||
|
||||
for (const plugin of pListOrdered)
|
||||
if (isPluginEnabled(plugin)) startPlugin(plugin)
|
||||
if (isPluginStartable(plugin)) startPlugin(plugin)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import type { PluginApiExternals } from './apis/externals'
|
|||
import type { PluginApiModules } from './apis/modules'
|
||||
import type { PluginApiPlugins } from './apis/plugins'
|
||||
import type { PluginApiReact } from './apis/react'
|
||||
import type { PluginFlags, PluginStatus } from './constants'
|
||||
import type { PluginStatus } from './constants'
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyInterface: To be extended by actual extensions
|
||||
export interface PluginApiExtensionsOptions {}
|
||||
|
|
@ -136,26 +136,14 @@ export interface PluginApi<
|
|||
plugin: Plugin<O, 'Start'>
|
||||
}
|
||||
|
||||
// TODO(plugins): support plugin bundles
|
||||
// export interface PluginBundle {
|
||||
// /**
|
||||
// * The unique identifier for the plugin bundle.
|
||||
// */
|
||||
// id: string
|
||||
// /**
|
||||
// * The author of the plugin bundle.
|
||||
// */
|
||||
// author: string
|
||||
// /**
|
||||
// * The URL of the plugin bundle.
|
||||
// */
|
||||
// url: string
|
||||
// }
|
||||
|
||||
/**
|
||||
* The plugin manifest.
|
||||
*/
|
||||
export interface PluginManifest {
|
||||
/**
|
||||
* The manifest format version.
|
||||
*/
|
||||
format: number
|
||||
/**
|
||||
* The unique identifier for the plugin.
|
||||
*/
|
||||
|
|
@ -173,25 +161,36 @@ export interface PluginManifest {
|
|||
*/
|
||||
description: string
|
||||
/**
|
||||
* The icon of the plugin.
|
||||
* The icon of the plugin. An asset name, or a `data:` URL.
|
||||
*/
|
||||
icon?: string
|
||||
/**
|
||||
* The dependencies of the plugin.
|
||||
* The dependencies of the plugin, keyed by plugin ID.
|
||||
*/
|
||||
dependencies?: PluginDependency[]
|
||||
dependencies?: Record<string, PluginDependency>
|
||||
/**
|
||||
* The plugin's version.
|
||||
*/
|
||||
version: PluginVersion
|
||||
}
|
||||
|
||||
export interface PluginVersion {
|
||||
nums: number[]
|
||||
label?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A dependency specification. The dependency's plugin ID is the key in {@link PluginManifest.dependencies}.
|
||||
*/
|
||||
export interface PluginDependency {
|
||||
/**
|
||||
* The ID of this dependency.
|
||||
* Version range the dependency must satisfy.
|
||||
*/
|
||||
id: string
|
||||
// TODO(plugins): support plugin bundles
|
||||
// /**
|
||||
// * The bundle of this dependency.
|
||||
// */
|
||||
// bundle: PluginBundle
|
||||
version?: string
|
||||
/**
|
||||
* Whether this dependency can be optionally linked.
|
||||
*/
|
||||
optional?: boolean
|
||||
}
|
||||
|
||||
export interface PluginOptions<
|
||||
|
|
@ -200,6 +199,16 @@ export interface PluginOptions<
|
|||
SettingsComponent?: PluginSettingsComponent<O>
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that lazily creates the plugin options.
|
||||
*
|
||||
* Only passed when the options (and lifecycles) must only be created right before the plugin runs,
|
||||
* for example to avoid evaluating external plugin code until it is actually used.
|
||||
*/
|
||||
export type PluginOptionsFactory<
|
||||
O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions,
|
||||
> = () => PluginOptions<O>
|
||||
|
||||
/**
|
||||
* The plugin lifecycles.
|
||||
*/
|
||||
|
|
@ -239,18 +248,9 @@ export interface Plugin<
|
|||
S extends
|
||||
keyof PluginApiInLifecycleMap<O> = keyof PluginApiInLifecycleMap<O>,
|
||||
> {
|
||||
// TODO(plugins): support plugin bundles
|
||||
// /**
|
||||
// * The plugin bundle this plugin belongs to.
|
||||
// */
|
||||
// bundle: PluginBundle
|
||||
manifest: PluginManifest
|
||||
lifecycles: PluginLifecycles<O>
|
||||
|
||||
/**
|
||||
* @see {@link PluginFlags}
|
||||
*/
|
||||
flags: number
|
||||
/**
|
||||
* @see {@link PluginStatus}
|
||||
*/
|
||||
|
|
@ -258,7 +258,11 @@ export interface Plugin<
|
|||
/**
|
||||
* Errors encountered during the plugin lifecycles.
|
||||
*/
|
||||
errors: unknown[]
|
||||
errors: readonly unknown[]
|
||||
/**
|
||||
* Reports an error encountered during the plugin's execution.
|
||||
*/
|
||||
reportError(e: unknown): void
|
||||
|
||||
SettingsComponent?: PluginSettingsComponent<O>
|
||||
|
||||
|
|
@ -271,6 +275,10 @@ export interface Plugin<
|
|||
* Stop the plugin.
|
||||
*/
|
||||
stop(this: Plugin<O, S>): Promise<void>
|
||||
/**
|
||||
* Marks this plugin as requiring a reload to apply changes.
|
||||
*/
|
||||
requireReload(this: Plugin<O, S>): void
|
||||
|
||||
/**
|
||||
* The plugin API.
|
||||
|
|
@ -298,3 +306,9 @@ export type PluginApiInLifecycleMap<
|
|||
export interface PluginSettingsComponent<
|
||||
O extends PluginApiExtensionsOptions = PluginApiExtensionsOptions,
|
||||
> extends FunctionComponent<{ api: PluginApi<O> }> {}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface NativeMethods {
|
||||
'revenge.plugins.getConstants': [[], { storageRootPath: string }]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
7
lib/plugins/src/utils.ts
Normal file
7
lib/plugins/src/utils.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { PluginVersion } from '@revenge-mod/plugins/types'
|
||||
|
||||
/**
|
||||
* Formats a plugin version for display.
|
||||
*/
|
||||
export const formatVersion = (version: PluginVersion) =>
|
||||
version.nums.join('.') + (version.label ? `-${version.label}` : '')
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import type { AnyObject } from '@revenge-mod/utils/types'
|
||||
import type { Storage, StorageOptions } from '.'
|
||||
|
||||
declare module '@revenge-mod/plugins/types' {
|
||||
export interface UnscopedPreInitPluginApi {
|
||||
storage: typeof import('.')
|
||||
}
|
||||
|
||||
export interface PluginApiExtensionsOptions {
|
||||
storage?: AnyObject
|
||||
}
|
||||
|
||||
export interface PluginOptions<O extends PluginApiExtensionsOptions> {
|
||||
storage?: Omit<StorageOptions<NonNullable<O['storage']>>, 'directory'>
|
||||
}
|
||||
|
||||
export interface InitPluginApi<O extends PluginApiExtensionsOptions> {
|
||||
/**
|
||||
* The plugin storage.
|
||||
*/
|
||||
storage: Storage<NonNullable<O['storage']>>
|
||||
}
|
||||
}
|
||||
26
package.json
26
package.json
|
|
@ -3,24 +3,24 @@
|
|||
"version": "0.1.0",
|
||||
"module": "src/index.ts",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.16",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
"@biomejs/biome": "^2.5.5",
|
||||
"@parcel/watcher": "^2.6.0",
|
||||
"@react-native-clipboard/clipboard": "^1.16.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"react-native-safe-area-context": "^5.7.0",
|
||||
"rolldown-plugin-dts": "^0.25.2"
|
||||
"react-native-safe-area-context": "^5.8.0",
|
||||
"rolldown-plugin-dts": "^0.27.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-navigation/core": "^7.14.0",
|
||||
"@react-navigation/native": "^7.1.28",
|
||||
"@react-navigation/stack": "^7.6.15",
|
||||
"@shopify/flash-list": "1.7.2",
|
||||
"@types/node": "^25.9.2",
|
||||
"@react-navigation/stack": "^7.7.2",
|
||||
"@shopify/flash-list": "^2.3.2",
|
||||
"@types/node": "^25.3.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"buffer": "^6.0.3",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.4",
|
||||
"typescript": "^6.0.3"
|
||||
"typescript": "^7.0.2"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
@ -45,17 +45,17 @@
|
|||
"@revenge-mod/devtools-shared": "^1.3.0",
|
||||
"@revenge-mod/discord": "workspace:*",
|
||||
"@revenge-mod/externals": "workspace:*",
|
||||
"@revenge-mod/json-storage": "workspace:*",
|
||||
"@revenge-mod/modules": "workspace:*",
|
||||
"@revenge-mod/patcher": "workspace:*",
|
||||
"@revenge-mod/plugins": "workspace:*",
|
||||
"@revenge-mod/react": "workspace:*",
|
||||
"@revenge-mod/storage": "workspace:*",
|
||||
"@revenge-mod/utils": "workspace:*",
|
||||
"@swc/core": "^1.15.41",
|
||||
"@swc/core": "^1.15.43",
|
||||
"chalk": "^5.6.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"magic-string": "^1.1.0",
|
||||
"react-native-click-outside": "^0.1.1",
|
||||
"rolldown": "1.1.0"
|
||||
"rolldown": "1.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@react-native/community-cli-plugin": "npm:dry-uninstall",
|
||||
|
|
@ -71,4 +71,4 @@
|
|||
"patchedDependencies": {
|
||||
"react-native-click-outside@0.1.1": "patches/react-native-click-outside@0.1.1.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { callBridgeMethod } from '@revenge-mod/modules/native'
|
||||
import { callNativeMethod } from '@revenge-mod/modules/native'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { BuildEnvironment, FullVersion } from '~constants'
|
||||
import type { Metro } from '@revenge-mod/modules/types'
|
||||
|
|
@ -27,7 +27,7 @@ Object.defineProperty(globalThis, '__registerSegment', {
|
|||
export function onError(error: unknown) {
|
||||
const stack = getErrorStack(error)
|
||||
|
||||
callBridgeMethod('revenge.alertError', [
|
||||
callNativeMethod('revenge.alertError', [
|
||||
stack,
|
||||
`${FullVersion} (${BuildEnvironment})`,
|
||||
])
|
||||
|
|
@ -36,7 +36,7 @@ export function onError(error: unknown) {
|
|||
}
|
||||
|
||||
declare module '@revenge-mod/modules/native' {
|
||||
export interface Methods {
|
||||
export interface NativeMethods {
|
||||
'revenge.alertError': [[error: string, version: string], void]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Initialize init libraries
|
||||
import '@revenge-mod/react/init'
|
||||
import '@revenge-mod/storage/init'
|
||||
import '@revenge-mod/json-storage/init'
|
||||
|
||||
import { onRunApplication } from '@revenge-mod/react/native'
|
||||
import { onError } from '~index'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/object'
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.api.discord',
|
||||
name: 'Discord API',
|
||||
|
|
|
|||
85
src/plugins/preinit/api.json-storage/index.ts
Normal file
85
src/plugins/preinit/api.json-storage/index.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import * as jsonStorage from '@revenge-mod/json-storage'
|
||||
import { getJsonStorage } from '@revenge-mod/json-storage'
|
||||
import { exists, rm } from '@revenge-mod/modules/native/fs'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { pluginStorageDirFor } from '@revenge-mod/plugins/constants'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/object'
|
||||
import type { JsonStorage, JsonStorageOptions } from '@revenge-mod/json-storage'
|
||||
import type { InitPluginApi, Plugin } from '@revenge-mod/plugins/types'
|
||||
import type { AnyObject } from '@revenge-mod/utils/types'
|
||||
|
||||
type PluginJsonStorageOptions = JsonStorageOptions & {
|
||||
file?: string
|
||||
}
|
||||
|
||||
const storageOptions = new WeakMap<Plugin<any, any>, PluginJsonStorageOptions>()
|
||||
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.api.json-storage',
|
||||
name: 'JSON Storage API',
|
||||
description: '@revenge-mod/json-storage API for plugins.',
|
||||
author: 'Revenge',
|
||||
icon: 'PollsIcon',
|
||||
},
|
||||
{
|
||||
preInit({ decorate, unscoped }) {
|
||||
unscoped.jsonStorage = jsonStorage
|
||||
|
||||
decorate((plugin, { jsonStorage }) => {
|
||||
if (jsonStorage) storageOptions.set(plugin, jsonStorage)
|
||||
})
|
||||
},
|
||||
init({ decorate }) {
|
||||
const makePluginStorage = (
|
||||
plugin: Plugin,
|
||||
opts?: PluginJsonStorageOptions,
|
||||
) =>
|
||||
getJsonStorage(
|
||||
jsonStorage.pluginStoragePathFor(
|
||||
plugin.manifest.id,
|
||||
opts?.file,
|
||||
),
|
||||
opts,
|
||||
)
|
||||
|
||||
decorate(plugin => {
|
||||
const opts = storageOptions.get(plugin)
|
||||
|
||||
if (opts?.load)
|
||||
plugin.api.jsonStorage = makePluginStorage(plugin, opts)
|
||||
else
|
||||
defineLazyProperty(plugin.api, 'jsonStorage', () =>
|
||||
makePluginStorage(plugin, opts),
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
// biome-ignore format: Don't format this
|
||||
InternalPluginFlags.Internal |
|
||||
InternalPluginFlags.Essential |
|
||||
InternalPluginFlags.API,
|
||||
)
|
||||
|
||||
export async function deleteJsonStorageForPlugin(plugin: Plugin<any, any>) {
|
||||
const path = pluginStorageDirFor(plugin.manifest.id)
|
||||
if (await exists(path)) await rm(path)
|
||||
|
||||
const api = plugin.api as
|
||||
| InitPluginApi<{ jsonStorage: AnyObject }>
|
||||
| undefined
|
||||
|
||||
// Plugin may have never initialized, so there is no API and nothing to refresh
|
||||
if (!api) return
|
||||
|
||||
// Only update already initialized storages
|
||||
const storage = Object.getOwnPropertyDescriptor(api, 'jsonStorage')
|
||||
?.value as JsonStorage<AnyObject> | undefined
|
||||
|
||||
await storage?.get()
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
PluginFlags,
|
||||
PluginsStorageDirectory,
|
||||
} from '@revenge-mod/plugins/constants'
|
||||
import * as storage from '@revenge-mod/storage'
|
||||
import { getStorage } from '@revenge-mod/storage'
|
||||
import { defineLazyProperty } from '@revenge-mod/utils/object'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { InitPluginApi, Plugin } from '@revenge-mod/plugins/types'
|
||||
import type { StorageOptions } from '@revenge-mod/storage'
|
||||
import type { AnyObject } from '@revenge-mod/utils/types'
|
||||
|
||||
const storageOptions = new WeakMap<AnyPlugin, StorageOptions>()
|
||||
|
||||
registerPlugin(
|
||||
{
|
||||
id: 'revenge.api.storage',
|
||||
name: 'Storage API',
|
||||
description: '@revenge-mod/storage API for plugins.',
|
||||
author: 'Revenge',
|
||||
icon: 'PollsIcon',
|
||||
},
|
||||
{
|
||||
preInit({ decorate, unscoped }) {
|
||||
unscoped.storage = storage
|
||||
|
||||
decorate((plugin, { storage }) => {
|
||||
if (storage) storageOptions.set(plugin, storage)
|
||||
})
|
||||
},
|
||||
init({ decorate }) {
|
||||
const makePluginStorage = (plugin: Plugin, opts?: StorageOptions) =>
|
||||
getStorage(getStoragePathForPlugin(plugin), {
|
||||
...opts,
|
||||
directory: 'documents',
|
||||
})
|
||||
|
||||
decorate(plugin => {
|
||||
const opts = storageOptions.get(plugin)
|
||||
|
||||
if (opts?.load)
|
||||
plugin.api.storage = makePluginStorage(plugin, opts)
|
||||
else
|
||||
defineLazyProperty(plugin.api, 'storage', () =>
|
||||
makePluginStorage(plugin, opts),
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
// biome-ignore format: Don't format this
|
||||
InternalPluginFlags.Internal |
|
||||
InternalPluginFlags.Essential |
|
||||
InternalPluginFlags.API,
|
||||
)
|
||||
|
||||
function getStoragePathForPlugin(plugin: AnyPlugin) {
|
||||
return `${PluginsStorageDirectory}/${plugin.manifest.id}.json`
|
||||
}
|
||||
|
||||
export async function deleteStorageForPlugin(plugin: AnyPlugin) {
|
||||
const api = plugin.api as InitPluginApi<{ storage: AnyObject }> | undefined
|
||||
const storage = api?.storage ?? getStorage(getStoragePathForPlugin(plugin))
|
||||
console.log(api?.storage, storage, getStoragePathForPlugin(plugin))
|
||||
if (await storage.exists()) await storage.delete()
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import defer * as UtilsCallback from '@revenge-mod/utils/callback'
|
||||
import defer * as UtilsDiscord from '@revenge-mod/utils/discord'
|
||||
import defer * as UtilsError from '@revenge-mod/utils/error'
|
||||
|
|
@ -9,7 +12,7 @@ import defer * as UtilsProxy from '@revenge-mod/utils/proxy'
|
|||
import defer * as UtilsReact from '@revenge-mod/utils/react'
|
||||
import defer * as UtilsTree from '@revenge-mod/utils/tree'
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.api.utils',
|
||||
name: 'Utils API',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { AppStartPerformance } from '@revenge-mod/discord/preinit'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { BuildEnvironment, FullVersion } from '~/constants'
|
||||
|
||||
if (__DEV__) {
|
||||
|
|
@ -10,7 +13,7 @@ if (__DEV__) {
|
|||
let tsPreInit: number
|
||||
let tsInit: number
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.logging',
|
||||
name: 'Logging',
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@ import { waitForModules } from '@revenge-mod/modules/finders'
|
|||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { getModuleDependencies } from '@revenge-mod/modules/metro/utils'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isPluginEnabledLate,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { noop } from '@revenge-mod/utils/callback'
|
||||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
|
||||
|
|
@ -35,7 +39,7 @@ const fakeSentryCarrier = new Proxy(
|
|||
|
||||
const getFakeCarrier = () => fakeSentryCarrier
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.no-track',
|
||||
name: 'No Track',
|
||||
|
|
@ -45,8 +49,7 @@ registerPlugin(
|
|||
},
|
||||
{
|
||||
preInit({ cleanup, plugin }) {
|
||||
if (plugin.flags & PluginFlags.EnabledLate)
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
if (isPluginEnabledLate(plugin)) plugin.requireReload()
|
||||
|
||||
// utils/SentryUtils.native.tsx
|
||||
const unsubSU = waitForModules(
|
||||
|
|
@ -206,7 +209,7 @@ registerPlugin(
|
|||
cleanup(unsubTI, unsubATAC, unsubHTTPUtils)
|
||||
},
|
||||
stop({ plugin }) {
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
plugin.requireReload()
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ const DTAutoConnectSetting: SettingsItem = {
|
|||
useTitle: () => 'Auto-connect to DevTools',
|
||||
useDescription: () => 'Automatically connect to DevTools during startup.',
|
||||
useValue: () =>
|
||||
api.storage.use(s => s.devTools?.autoConnect !== undefined)!.devTools
|
||||
.autoConnect,
|
||||
api.jsonStorage.use(s => s.devTools?.autoConnect !== undefined)!
|
||||
.devTools.autoConnect,
|
||||
onValueChange: v => {
|
||||
api.storage.set({ devTools: { autoConnect: v } })
|
||||
api.jsonStorage.set({ devTools: { autoConnect: v } })
|
||||
},
|
||||
type: 'toggle',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ const RDTAutoConnectSetting: SettingsItem = {
|
|||
'Automatically connect to React DevTools during startup.',
|
||||
usePredicate: () => RDTContext.active,
|
||||
useValue: () =>
|
||||
api.storage.use(s => s.reactDevTools?.autoConnect !== undefined)!
|
||||
api.jsonStorage.use(s => s.reactDevTools?.autoConnect !== undefined)!
|
||||
.reactDevTools.autoConnect,
|
||||
onValueChange: v => {
|
||||
api.storage.set({ reactDevTools: { autoConnect: v } })
|
||||
api.jsonStorage.set({ reactDevTools: { autoConnect: v } })
|
||||
},
|
||||
type: 'toggle',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,12 @@ import {
|
|||
refreshSettingsNavigator,
|
||||
refreshSettingsOverviewScreen,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isPluginEnabledLate,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import pluginSettings from '../settings'
|
||||
import * as dt from './devtools'
|
||||
import defer * as rdt from './react-devtools'
|
||||
|
|
@ -33,17 +37,17 @@ const defaultStorage: Storage = {
|
|||
}
|
||||
|
||||
// TODO(PalmDevs): only register in development builds once updates can be made automatic
|
||||
registerPlugin<{ storage: Storage }>(
|
||||
registerInternalPlugin<{ jsonStorage: Storage }>(
|
||||
{
|
||||
id: 'revenge.developer-kit',
|
||||
name: 'Developer Kit',
|
||||
description: 'Tools assisting Revenge developers.',
|
||||
author: 'Revenge',
|
||||
icon: 'WrenchIcon',
|
||||
dependencies: [pluginSettings],
|
||||
dependencies: { [pluginSettings]: {} },
|
||||
},
|
||||
{
|
||||
storage: {
|
||||
jsonStorage: {
|
||||
load: true,
|
||||
default: defaultStorage,
|
||||
},
|
||||
|
|
@ -52,12 +56,12 @@ registerPlugin<{ storage: Storage }>(
|
|||
|
||||
onSettingsModulesLoaded(utils.register)
|
||||
|
||||
if (api_.plugin.flags & PluginFlags.EnabledLate) {
|
||||
if (isPluginEnabledLate(api_.plugin)) {
|
||||
refreshSettingsOverviewScreen()
|
||||
refreshSettingsNavigator()
|
||||
}
|
||||
|
||||
const settings = await api.storage.get()
|
||||
const settings = await api.jsonStorage.get()
|
||||
|
||||
dt.DTContext.addr = settings.devTools.address
|
||||
dt.DTContext.alias = settings.devTools.alias ?? ''
|
||||
|
|
@ -75,4 +79,4 @@ registerPlugin<{ storage: Storage }>(
|
|||
)
|
||||
|
||||
// Expose to EvalJSSetting
|
||||
export let api: PluginApi<{ storage: Storage }>
|
||||
export let api: PluginApi<{ jsonStorage: Storage }>
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export default function RevengeDeveloperSettingScreen() {
|
|||
|
||||
export function DTConnectionSetting() {
|
||||
const open = useIsDTConnected()
|
||||
const settings = api.storage.use(
|
||||
const settings = api.jsonStorage.use(
|
||||
s =>
|
||||
s.devTools?.address !== undefined ||
|
||||
s.devTools?.alias !== undefined,
|
||||
|
|
@ -100,7 +100,7 @@ export function DTConnectionSetting() {
|
|||
label="DevTools"
|
||||
leadingText="ws://"
|
||||
onBlur={() =>
|
||||
api.storage
|
||||
api.jsonStorage
|
||||
.set({
|
||||
devTools: {
|
||||
address: DTContext.addr,
|
||||
|
|
@ -125,7 +125,7 @@ export function DTConnectionSetting() {
|
|||
isDisabled={open}
|
||||
label="DevTools Alias"
|
||||
onBlur={() =>
|
||||
api.storage
|
||||
api.jsonStorage
|
||||
.set({
|
||||
devTools: {
|
||||
alias: DTContext.alias,
|
||||
|
|
@ -150,7 +150,7 @@ export function DTConnectionSetting() {
|
|||
|
||||
export function RDTAddrSetting() {
|
||||
const open = useIsRDTConnected()
|
||||
const settings = api.storage.use(s => s.reactDevTools?.address)
|
||||
const settings = api.jsonStorage.use(s => s.reactDevTools?.address)
|
||||
|
||||
return (
|
||||
<Design.TextInput
|
||||
|
|
@ -160,7 +160,7 @@ export function RDTAddrSetting() {
|
|||
label="React DevTools"
|
||||
leadingText="ws://"
|
||||
onBlur={() =>
|
||||
api.storage
|
||||
api.jsonStorage
|
||||
.set({
|
||||
reactDevTools: {
|
||||
address: RDTContext.addr,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ export function register() {
|
|||
[Setting.AssetBrowser]: AssetBrowserSetting,
|
||||
[Setting.TestErrorBoundary]: TestErrorBoundarySetting,
|
||||
}),
|
||||
addSettingsItemToSection('REVENGE', Setting.RevengeDeveloper),
|
||||
addSettingsItemToSection('REVENGE', Setting.RevengeDeveloper, -1),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
import { waitForModules } from '@revenge-mod/modules/finders'
|
||||
import { withName } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import ErrorBoundaryScreen from './components/ErrorBoundaryScreen'
|
||||
import type { Component, ReactNode } from 'react'
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.error-boundary',
|
||||
name: 'Error Boundary',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { useState } from 'react'
|
||||
import type { AssetId } from '@revenge-mod/assets/types'
|
||||
|
||||
const {
|
||||
ActionSheet,
|
||||
BottomSheetTitleHeader,
|
||||
TableCheckboxRow,
|
||||
TableRadioGroup,
|
||||
TableRadioRow,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
} = Design
|
||||
|
||||
export const BrowseSortLabels = {
|
||||
name: 'Name (A-Z)',
|
||||
size: 'Size (smallest first)',
|
||||
} as const
|
||||
|
||||
export type BrowseSortKey = keyof typeof BrowseSortLabels
|
||||
|
||||
const SortIcons: Record<BrowseSortKey, AssetId> = {
|
||||
name: getAssetIdByName('IdIcon')!,
|
||||
size: getAssetIdByName('DownloadIcon')!,
|
||||
}
|
||||
|
||||
export interface BrowseFilterAndSortActionSheetProps {
|
||||
/** Repositories that currently have entries to browse. */
|
||||
repos: Array<{ url: string; name: string | null }>
|
||||
/** Checked repository URLs. */
|
||||
checked: string[]
|
||||
setChecked: (urls: string[]) => void
|
||||
sort: BrowseSortKey
|
||||
setSort: (sort: BrowseSortKey) => void
|
||||
reverse: boolean
|
||||
setReverse: (reverse: boolean) => void
|
||||
}
|
||||
|
||||
export default function BrowseFilterAndSortActionSheet({
|
||||
repos,
|
||||
checked,
|
||||
setChecked,
|
||||
sort,
|
||||
setSort,
|
||||
reverse,
|
||||
setReverse,
|
||||
}: BrowseFilterAndSortActionSheetProps) {
|
||||
const [checked_, setChecked_] = useState(checked)
|
||||
const [reverse_, setReverse_] = useState(reverse)
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<BottomSheetTitleHeader title="Filter & Sort" />
|
||||
<TableRowGroup title="Filter by Repositories">
|
||||
{repos.map(repo => {
|
||||
const isChecked = checked_.includes(repo.url)
|
||||
|
||||
return (
|
||||
<TableCheckboxRow
|
||||
key={repo.url}
|
||||
label={repo.name ?? repo.url}
|
||||
subLabel={repo.name ? repo.url : undefined}
|
||||
value={repo.url}
|
||||
checked={isChecked}
|
||||
onPress={() => {
|
||||
const v = isChecked
|
||||
? checked_.filter(url => url !== repo.url)
|
||||
: [...checked_, repo.url]
|
||||
|
||||
setChecked(v)
|
||||
setChecked_(v)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TableRowGroup>
|
||||
<TableRadioGroup
|
||||
title="Sort by"
|
||||
defaultValue={sort}
|
||||
onChange={v => setSort(v as BrowseSortKey)}
|
||||
>
|
||||
{Object.entries(BrowseSortLabels).map(([key, label]) => (
|
||||
<TableRadioRow
|
||||
key={key}
|
||||
icon={
|
||||
<TableRowAssetIcon
|
||||
id={SortIcons[key as BrowseSortKey]}
|
||||
/>
|
||||
}
|
||||
label={label}
|
||||
value={key}
|
||||
/>
|
||||
))}
|
||||
</TableRadioGroup>
|
||||
<TableRowGroup>
|
||||
<TableSwitchRow
|
||||
label="Reverse results"
|
||||
value={reverse_}
|
||||
onValueChange={v => {
|
||||
setReverse(v)
|
||||
setReverse_(v)
|
||||
}}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { PluginInfo } from './PluginCard'
|
||||
import { IdRow, RepositoryRow } from './PluginOptionsActionSheet'
|
||||
|
||||
const { ActionSheet, Button, Stack, TableRowGroup } = Design
|
||||
|
||||
const DownloadIcon = getAssetIdByName('DownloadIcon', 'png')!
|
||||
|
||||
export interface BrowsePluginActionSheetProps {
|
||||
name: string
|
||||
author: string
|
||||
description: string
|
||||
version: string
|
||||
icon?: string
|
||||
id: string
|
||||
/** Display text for the Repository row, eg. `Name (url)`. */
|
||||
repositoryText: string
|
||||
onInstall: () => void
|
||||
sheetKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Sheet for a plugin that isn't installed yet, opened from the Browse screen.
|
||||
*/
|
||||
export default function BrowsePluginActionSheet({
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
version,
|
||||
icon,
|
||||
id,
|
||||
repositoryText,
|
||||
onInstall,
|
||||
sheetKey,
|
||||
}: BrowsePluginActionSheetProps) {
|
||||
return (
|
||||
<ActionSheet>
|
||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
author={author}
|
||||
version={version}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
text="Install"
|
||||
icon={DownloadIcon}
|
||||
onPress={() => {
|
||||
ActionSheetActionCreators.hideActionSheet(
|
||||
sheetKey,
|
||||
)
|
||||
onInstall()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TableRowGroup title="Advanced">
|
||||
<IdRow id={id} />
|
||||
<RepositoryRow text={repositoryText} copyable />
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,14 +1,22 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { styles } from '@revenge-mod/components/_'
|
||||
import FormSwitch from '@revenge-mod/components/FormSwitch'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { isPluginEssential } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
isPluginEssential,
|
||||
isPluginPendingUpdate,
|
||||
isPluginStartable,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { formatVersion } from '@revenge-mod/plugins/utils'
|
||||
import { memo } from 'react'
|
||||
import { Image, Pressable } from 'react-native'
|
||||
import { Pressable } from 'react-native'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
import { openPluginSettings } from '../utils/alerts'
|
||||
import { showPluginOptionsActionSheet } from '../utils/sheets'
|
||||
import {
|
||||
showBrowsePluginActionSheet,
|
||||
showPluginOptionsActionSheet,
|
||||
} from '../utils/sheets'
|
||||
import { PluginIcon } from './PluginIcon'
|
||||
import { usePluginEnabled } from './PluginStateProvider'
|
||||
import {
|
||||
useClickOutsideTooltip,
|
||||
|
|
@ -17,48 +25,42 @@ import {
|
|||
} from './TooltipProvider'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { Card, Text, Stack, IconButton, createStyles } = Design
|
||||
const { Card, Text, Stack, IconButton, Button, createStyles } = Design
|
||||
|
||||
const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
const MoreVerticalIcon = getAssetIdByName('MoreVerticalIcon', 'png')!
|
||||
const DownloadIcon = getAssetIdByName('DownloadIcon', 'png')!
|
||||
|
||||
export const PLUGIN_CARD_ESTIMATED_SIZE = 116
|
||||
|
||||
export const PLUGIN_CARD_HALF_GUTTER = 6
|
||||
|
||||
export const PluginCard = memo(function PluginCard({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
author,
|
||||
icon,
|
||||
actions,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
author: string
|
||||
icon?: string
|
||||
actions?: React.ReactNode
|
||||
rowEnd?: boolean
|
||||
columnEnd?: boolean
|
||||
}) {
|
||||
const styles_ = usePluginCardStyles()
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={[
|
||||
styles_.card,
|
||||
styles.grow,
|
||||
columnEnd && styles_.columnEnd,
|
||||
rowEnd && styles_.rowEnd,
|
||||
]}
|
||||
>
|
||||
<Card style={[styles_.card, styles.grow]}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
description={description}
|
||||
author={author}
|
||||
icon={icon}
|
||||
actions={actions}
|
||||
version={version}
|
||||
aligned
|
||||
/>
|
||||
</Card>
|
||||
|
|
@ -69,6 +71,7 @@ export const PluginInfo = memo(function PluginInfo({
|
|||
name,
|
||||
description,
|
||||
author,
|
||||
version,
|
||||
icon,
|
||||
actions,
|
||||
aligned,
|
||||
|
|
@ -76,6 +79,7 @@ export const PluginInfo = memo(function PluginInfo({
|
|||
name: string
|
||||
description: string
|
||||
author: string
|
||||
version: string
|
||||
icon?: string
|
||||
actions?: React.ReactNode
|
||||
aligned?: boolean
|
||||
|
|
@ -93,10 +97,7 @@ export const PluginInfo = memo(function PluginInfo({
|
|||
spacing={8}
|
||||
style={[styles_.topContainer, styles.flex]}
|
||||
>
|
||||
<Image
|
||||
source={icon ? getAssetIdByName(icon) : PuzzlePieceIcon}
|
||||
style={styles_.icon}
|
||||
/>
|
||||
<PluginIcon icon={icon} />
|
||||
<Text
|
||||
variant="heading-lg/semibold"
|
||||
textBreakStrategy="balanced"
|
||||
|
|
@ -117,6 +118,7 @@ export const PluginInfo = memo(function PluginInfo({
|
|||
variant="heading-md/medium"
|
||||
>
|
||||
by {author}
|
||||
{version ? ` \u2022 ${version}` : ''}
|
||||
</Text>
|
||||
<Text style={styles.grow} variant="text-md/medium">
|
||||
{description}
|
||||
|
|
@ -129,21 +131,21 @@ export const PluginInfo = memo(function PluginInfo({
|
|||
export const InstalledPluginCard = memo(function InstalledPluginCard({
|
||||
plugin,
|
||||
meta,
|
||||
rowEnd,
|
||||
columnEnd,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
meta: InternalPluginMeta
|
||||
rowEnd?: boolean
|
||||
columnEnd?: boolean
|
||||
}) {
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
|
||||
const {
|
||||
manifest: { name, description, author, icon },
|
||||
manifest: { name, description, version, author, icon },
|
||||
} = plugin
|
||||
|
||||
const essential = isPluginEssential(meta)
|
||||
const startable = isPluginStartable(plugin)
|
||||
const pendingUpdate = isPluginPendingUpdate(plugin)
|
||||
|
||||
const toggleDisabled = essential || pendingUpdate
|
||||
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const essentialTooltip = useEssentialPluginTooltip()
|
||||
|
|
@ -159,10 +161,9 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
version={formatVersion(version)}
|
||||
author={author}
|
||||
icon={icon}
|
||||
rowEnd={rowEnd}
|
||||
columnEnd={columnEnd}
|
||||
actions={
|
||||
<>
|
||||
<IconButton
|
||||
|
|
@ -176,7 +177,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!enabled)
|
||||
if (!startable)
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
|
|
@ -189,16 +190,16 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
size="sm"
|
||||
variant="secondary"
|
||||
icon={SettingsIcon}
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
openPluginSettings(plugin)
|
||||
}}
|
||||
disabled={!enabled}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (essential)
|
||||
if (toggleDisabled)
|
||||
requestAnimationFrame(() => {
|
||||
essentialTooltip.targetRef.current =
|
||||
switchRef.current
|
||||
|
|
@ -209,7 +210,7 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
>
|
||||
<FormSwitch
|
||||
key={plugin.manifest.id}
|
||||
disabled={essential}
|
||||
disabled={toggleDisabled}
|
||||
onValueChange={enabled => {
|
||||
if (enabled) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
|
|
@ -223,24 +224,73 @@ export const InstalledPluginCard = memo(function InstalledPluginCard({
|
|||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Card for a plugin that isn't installed yet, shown on the Browse screen.
|
||||
* No switch or settings button, just a more menu and a small Install button.
|
||||
*/
|
||||
export const BrowsePluginCard = memo(function BrowsePluginCard({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
author,
|
||||
icon,
|
||||
id,
|
||||
repositoryText,
|
||||
onInstall,
|
||||
}: {
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
author: string
|
||||
icon?: string
|
||||
id: string
|
||||
repositoryText: string
|
||||
onInstall: () => void
|
||||
}) {
|
||||
return (
|
||||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
version={version}
|
||||
author={author}
|
||||
icon={icon}
|
||||
actions={
|
||||
<>
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
icon={MoreVerticalIcon}
|
||||
onPress={() => {
|
||||
showBrowsePluginActionSheet({
|
||||
name,
|
||||
author,
|
||||
description,
|
||||
version,
|
||||
icon,
|
||||
id,
|
||||
repositoryText,
|
||||
onInstall,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
text="Install"
|
||||
icon={DownloadIcon}
|
||||
onPress={onInstall}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const usePluginCardStyles = createStyles({
|
||||
icon: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
tintColor: Tokens.default.colors.TEXT_DEFAULT,
|
||||
},
|
||||
card: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
gap: 4,
|
||||
marginBottom: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
rowEnd: {
|
||||
marginBottom: 0,
|
||||
},
|
||||
columnEnd: {
|
||||
marginRight: 0,
|
||||
margin: PLUGIN_CARD_HALF_GUTTER,
|
||||
},
|
||||
topContainer: {
|
||||
alignItems: 'center',
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { useState } from 'react'
|
||||
import { PixelRatio, useWindowDimensions, View } from 'react-native'
|
||||
import { PLUGIN_CARD_ESTIMATED_SIZE } from './PluginCard'
|
||||
import { PluginFlashList } from './PluginList'
|
||||
|
|
@ -17,6 +18,9 @@ export default function PluginHasDependenciesAlert({
|
|||
}) {
|
||||
const { height: windowHeight } = useWindowDimensions()
|
||||
const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64
|
||||
const [height, setHeight] = useState(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependencies.length,
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
|
|
@ -35,14 +39,14 @@ export default function PluginHasDependenciesAlert({
|
|||
extraContent={
|
||||
<View
|
||||
style={{
|
||||
height: Math.min(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependencies.length,
|
||||
maxHeight,
|
||||
),
|
||||
height,
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<PluginFlashList plugins={dependencies} />
|
||||
<PluginFlashList
|
||||
plugins={dependencies}
|
||||
onContentSizeChange={(_, h) => setHeight(h)}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
actions={
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { useState } from 'react'
|
||||
import { PixelRatio, useWindowDimensions, View } from 'react-native'
|
||||
import { PLUGIN_CARD_ESTIMATED_SIZE } from './PluginCard'
|
||||
import { PluginFlashList } from './PluginList'
|
||||
|
|
@ -17,6 +18,9 @@ export default function PluginHasDependentsAlert({
|
|||
}) {
|
||||
const { height: windowHeight } = useWindowDimensions()
|
||||
const maxHeight = PixelRatio.get() * windowHeight * 0.35 - 64
|
||||
const [height, setHeight] = useState(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependents.length,
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
|
|
@ -35,14 +39,14 @@ export default function PluginHasDependentsAlert({
|
|||
extraContent={
|
||||
<View
|
||||
style={{
|
||||
height: Math.min(
|
||||
PLUGIN_CARD_ESTIMATED_SIZE * dependents.length,
|
||||
maxHeight,
|
||||
),
|
||||
height,
|
||||
maxHeight,
|
||||
}}
|
||||
>
|
||||
<PluginFlashList plugins={dependents} />
|
||||
<PluginFlashList
|
||||
plugins={dependents}
|
||||
onContentSizeChange={(_, h) => setHeight(h)}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
actions={
|
||||
38
src/plugins/start/settings.plugins/components/PluginIcon.tsx
Normal file
38
src/plugins/start/settings.plugins/components/PluginIcon.tsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { Tokens } from '@revenge-mod/discord/common'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Image } from 'react-native'
|
||||
|
||||
const PuzzlePieceIcon = getAssetIdByName('PuzzlePieceIcon', 'png')!
|
||||
|
||||
export function PluginIcon({
|
||||
icon,
|
||||
size = 20,
|
||||
}: {
|
||||
icon?: string
|
||||
size?: number
|
||||
}) {
|
||||
const styles = usePluginIconStyles()
|
||||
// Icons are either a packaged asset name or an inline data: URL, never fetched.
|
||||
// Asset icons are monochrome and get tinted, data: URLs render as-is.
|
||||
const dataUrl = icon?.startsWith('data:')
|
||||
|
||||
return (
|
||||
<Image
|
||||
source={
|
||||
dataUrl
|
||||
? { uri: icon }
|
||||
: icon
|
||||
? (getAssetIdByName(icon) ?? PuzzlePieceIcon)
|
||||
: PuzzlePieceIcon
|
||||
}
|
||||
style={[!dataUrl && styles.icon, { width: size, height: size }]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const usePluginIconStyles = Design.createStyles({
|
||||
icon: {
|
||||
tintColor: Tokens.default.colors.TEXT_DEFAULT,
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { confirmInstall } from '@revenge-mod/plugins/_'
|
||||
import type { PluginInstallReadyEvent } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
||||
export default function PluginInstallConfirmAlert({
|
||||
prompt,
|
||||
}: {
|
||||
prompt: PluginInstallReadyEvent
|
||||
}) {
|
||||
const { manifest } = prompt
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
title={`Install ${manifest.name}?`}
|
||||
content={`${manifest.id} v${manifest.version}${manifest.author ? ` by ${manifest.author}` : ''}`}
|
||||
extraContent={
|
||||
prompt.replaces != null ? (
|
||||
<Text
|
||||
variant="text-md/medium"
|
||||
color="text-feedback-critical"
|
||||
>
|
||||
This replaces the installed version {prompt.replaces}.
|
||||
The update applies after a reload.
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
text="Install"
|
||||
variant="primary"
|
||||
onPress={() => confirmInstall(prompt.token, true)}
|
||||
/>
|
||||
<AlertActionButton
|
||||
text="Cancel"
|
||||
variant="secondary"
|
||||
onPress={() => confirmInstall(prompt.token, false)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
|
||||
import { formatPluginError } from '@revenge-mod/plugins/_'
|
||||
import type { PluginError } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
||||
export default function PluginInstallFailedAlert({
|
||||
error,
|
||||
}: {
|
||||
error: PluginError
|
||||
}) {
|
||||
return (
|
||||
<AlertModal
|
||||
title="Plugin install failed"
|
||||
content={
|
||||
<Text variant="text-md/medium" color="text-feedback-critical">
|
||||
{error.message}
|
||||
</Text>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
text="Copy details"
|
||||
variant="secondary"
|
||||
onPress={() => {
|
||||
Clipboard.setString(formatPluginError(error))
|
||||
}}
|
||||
/>
|
||||
<AlertActionButton text="Got it" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
167
src/plugins/start/settings.plugins/components/PluginList.tsx
Normal file
167
src/plugins/start/settings.plugins/components/PluginList.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { styles } from '@revenge-mod/components/_'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { formatVersion } from '@revenge-mod/plugins/utils'
|
||||
import { FlashList } from '@shopify/flash-list'
|
||||
import { useWindowDimensions } from 'react-native'
|
||||
import {
|
||||
BrowsePluginCard,
|
||||
InstalledPluginCard,
|
||||
PLUGIN_CARD_HALF_GUTTER,
|
||||
PluginCard,
|
||||
} from './PluginCard'
|
||||
import { useResetTooltips } from './TooltipProvider'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
||||
import type { FlashListProps } from '@shopify/flash-list'
|
||||
|
||||
const { Text } = Design
|
||||
|
||||
const gutterCompensation = { margin: -PLUGIN_CARD_HALF_GUTTER }
|
||||
|
||||
export function PluginFlashList({
|
||||
plugins,
|
||||
onContentSizeChange,
|
||||
}: { plugins: AnyPlugin[] } & Pick<
|
||||
FlashListProps<AnyPlugin>,
|
||||
'onContentSizeChange'
|
||||
>) {
|
||||
const resetTooltips = useResetTooltips()
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
style={gutterCompensation}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
||||
keyExtractor={plugin => plugin.manifest.id}
|
||||
renderItem={({
|
||||
item: {
|
||||
manifest: { name, description, version, author, icon },
|
||||
},
|
||||
}) => (
|
||||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
version={formatVersion(version)}
|
||||
author={author}
|
||||
icon={icon}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function InstalledPluginMasonryFlashList({
|
||||
plugins,
|
||||
}: {
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
const numColumns = useNumColumns()
|
||||
const resetTooltips = useResetTooltips()
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
masonry
|
||||
style={gutterCompensation}
|
||||
// FAB is 56px tall, plus 16px spacing on top and bottom
|
||||
contentContainerStyle={{ paddingBottom: 56 + 2 * 16 }}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={([plugin]) => plugin.manifest.id}
|
||||
numColumns={numColumns}
|
||||
ListEmptyComponent={NoPlugins}
|
||||
renderItem={({ item: [plugin, meta] }) => (
|
||||
<InstalledPluginCard plugin={plugin} meta={meta} />
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NoPlugins() {
|
||||
return (
|
||||
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
|
||||
No plugins found. Try changing your query or filters.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
function useNumColumns() {
|
||||
const { width } = useWindowDimensions()
|
||||
const actualWidth = width - styles.pagePadding.paddingHorizontal * 2
|
||||
return Math.floor(actualWidth / 448) || 1
|
||||
}
|
||||
|
||||
/**
|
||||
* One plugin offered by one repository on the Browse screen.
|
||||
* The same ID can appear as separate entries from different repositories.
|
||||
*/
|
||||
export interface BrowseEntry {
|
||||
/** `repoUrl#id` */
|
||||
key: string
|
||||
listing: RepoPluginListing
|
||||
/** For repository filtering. */
|
||||
repoUrl: string
|
||||
repoName: string | null
|
||||
/** Display text for the Repository row/sheet, eg. `Name (url)`. */
|
||||
repositoryText: string
|
||||
version: string
|
||||
/** The channel the displayed version comes from, if the listing has any. */
|
||||
channel?: string
|
||||
size: number
|
||||
installed?: readonly [AnyPlugin, InternalPluginMeta]
|
||||
}
|
||||
|
||||
export function BrowsePluginMasonryFlashList({
|
||||
entries,
|
||||
onInstall,
|
||||
}: {
|
||||
entries: BrowseEntry[]
|
||||
onInstall: (entry: BrowseEntry) => void
|
||||
}) {
|
||||
const numColumns = useNumColumns()
|
||||
const resetTooltips = useResetTooltips()
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
masonry
|
||||
style={gutterCompensation}
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
data={entries}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={entry => entry.key}
|
||||
numColumns={numColumns}
|
||||
ListEmptyComponent={NoBrowsePlugins}
|
||||
renderItem={({ item: entry }) => {
|
||||
if (entry.installed) {
|
||||
const [plugin, meta] = entry.installed
|
||||
return <InstalledPluginCard plugin={plugin} meta={meta} />
|
||||
}
|
||||
|
||||
const { listing } = entry
|
||||
return (
|
||||
<BrowsePluginCard
|
||||
name={listing.name}
|
||||
description={listing.description}
|
||||
version={entry.version}
|
||||
author={listing.author}
|
||||
icon={listing.icon ?? undefined}
|
||||
id={listing.id}
|
||||
repositoryText={entry.repositoryText}
|
||||
onInstall={() => onInstall(entry)}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NoBrowsePlugins() {
|
||||
return (
|
||||
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
|
||||
No plugins available. Add repositories to browse plugins.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text, TableRowGroup, TableRow } = Design
|
||||
|
||||
export default function PluginMissingDependenciesAlert({
|
||||
plugin,
|
||||
dependencies,
|
||||
action,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
/** The missing required dependencies: id + the declared version range. */
|
||||
dependencies: { id: string; range: string }[]
|
||||
action: () => unknown
|
||||
}) {
|
||||
return (
|
||||
<AlertModal
|
||||
title="Plugin has missing dependencies"
|
||||
content={
|
||||
<Text color="text-default">
|
||||
Plugin{' '}
|
||||
<Text variant="text-md/semibold" color="text-default">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
requires plugins that are not installed. Install them from
|
||||
your repositories to enable it.
|
||||
</Text>
|
||||
}
|
||||
extraContent={
|
||||
<TableRowGroup>
|
||||
{dependencies.map(dep => (
|
||||
<TableRow
|
||||
key={dep.id}
|
||||
label={dep.id}
|
||||
subLabel={`Requires version ${dep.range}`}
|
||||
/>
|
||||
))}
|
||||
</TableRowGroup>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
text="Install"
|
||||
variant="primary"
|
||||
onPress={action}
|
||||
/>
|
||||
<AlertActionButton text="Cancel" variant="secondary" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { FormSwitch, TableRowAssetIcon } from '@revenge-mod/components'
|
||||
import {
|
||||
ActionSheetActionCreators,
|
||||
ToastActionCreators,
|
||||
} from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
|
||||
import {
|
||||
formatPluginError,
|
||||
getInternalPluginMeta,
|
||||
getPluginDependencies,
|
||||
getPluginDependents,
|
||||
InternalPluginFlags,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
isPluginPendingUpdate,
|
||||
isPluginStartable,
|
||||
PluginFlags,
|
||||
runPluginLate,
|
||||
stopPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { listRepos } from '@revenge-mod/plugins/_/repositories'
|
||||
import { PluginStatus } from '@revenge-mod/plugins/constants'
|
||||
import { formatVersion } from '@revenge-mod/plugins/utils'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
import {
|
||||
openPluginSettings,
|
||||
showPluginClearDataConfirmation,
|
||||
showPluginUninstallConfirmation,
|
||||
} from '../utils/alerts'
|
||||
import { messageOf, showErrorToast } from '../utils/repos'
|
||||
import { PluginInfo } from './PluginCard'
|
||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
useClickOutsideTooltip,
|
||||
useEnablePluginTooltip,
|
||||
} from './TooltipProvider'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export interface PluginOptionsActionSheetProps {
|
||||
plugin: AnyPlugin
|
||||
sheetKey: string
|
||||
}
|
||||
|
||||
const { ActionSheet, IconButton, TableRowGroup, TableRow, Stack } = Design
|
||||
|
||||
const FileWarningIcon = getAssetIdByName('FileWarningIcon', 'png')!
|
||||
const PlayIcon = getAssetIdByName('PlayIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
const StopIcon = getAssetIdByName('StopIcon', 'png')!
|
||||
const TrashIcon = getAssetIdByName('TrashIcon', 'png')!
|
||||
|
||||
export default function PluginOptionsActionSheet({
|
||||
plugin,
|
||||
sheetKey,
|
||||
}: PluginOptionsActionSheetProps) {
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const essential = isPluginEssential(meta)
|
||||
const pendingUpdate = isPluginPendingUpdate(plugin)
|
||||
const { name, author, description, icon, version } = plugin.manifest
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
author={author}
|
||||
version={formatVersion(version)}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={
|
||||
!essential && (
|
||||
<FormSwitch
|
||||
value={enabled}
|
||||
disabled={pendingUpdate}
|
||||
onValueChange={v => {
|
||||
if (v) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ClickOutsideProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<PluginActions
|
||||
plugin={plugin}
|
||||
closeSheet={() => {
|
||||
ActionSheetActionCreators.hideActionSheet(
|
||||
sheetKey,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</EnablePluginTooltipProvider>
|
||||
</ClickOutsideProvider>
|
||||
<StatusSection plugin={plugin} />
|
||||
<AdvancedSection plugin={plugin} />
|
||||
</Stack>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusSection({ plugin }: { plugin: AnyPlugin }) {
|
||||
const status = usePluginStatus(plugin)
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const errors = [...plugin.errors, ...meta.nativeErrors]
|
||||
|
||||
return (
|
||||
<TableRowGroup title="Status">
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="CircleInformationIcon" />}
|
||||
label="Status (TODO)"
|
||||
subLabel={bitFieldToString(PluginStatus, status)}
|
||||
/>
|
||||
{errors.length > 0 && (
|
||||
<TableRow
|
||||
variant="danger"
|
||||
label="Errors"
|
||||
icon={
|
||||
<TableRowAssetIcon
|
||||
variant="danger"
|
||||
name="CircleErrorIcon"
|
||||
/>
|
||||
}
|
||||
subLabel={`${errors.length} errors. Tap to copy.`}
|
||||
onPress={() => {
|
||||
Clipboard.setString(
|
||||
errors.map(formatPluginError).join('\n\n'),
|
||||
)
|
||||
showCopiedToClipboardToast()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableRowGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function AdvancedSection({ plugin }: { plugin: AnyPlugin }) {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const dependents = getPluginDependents(plugin, true)
|
||||
const dependencies = getPluginDependencies(plugin, false)
|
||||
const repositoryText = usePluginRepositoryText(plugin)
|
||||
const { id, name } = plugin.manifest
|
||||
|
||||
return (
|
||||
<TableRowGroup title="Advanced">
|
||||
<IdRow id={id} />
|
||||
<RepositoryRow
|
||||
text={repositoryText}
|
||||
copyable={!!meta.source?.repo}
|
||||
/>
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="FlagIcon" />}
|
||||
label="Flags"
|
||||
subLabel={bitFieldToString(PluginFlags, meta.flags)}
|
||||
/>
|
||||
{meta.iflags > 0 && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="FlagIcon" />}
|
||||
label="Internal Flags"
|
||||
subLabel={bitFieldToString(
|
||||
InternalPluginFlags,
|
||||
meta.iflags,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{dependencies.length > 0 && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependencies (TODO)"
|
||||
subLabel={`${name} depends on ${dependencies.length} other plugins`}
|
||||
/>
|
||||
)}
|
||||
{dependents.length > 0 && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependents (TODO)"
|
||||
subLabel={`${dependents.length} other plugins depend on ${name}`}
|
||||
/>
|
||||
)}
|
||||
</TableRowGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function bitFieldToString(map: Record<string, number>, bitField: number) {
|
||||
return (
|
||||
Object.entries(map)
|
||||
.filter(([, value]) => bitField & value)
|
||||
.map(([key]) => key)
|
||||
.join(', ') || '-'
|
||||
)
|
||||
}
|
||||
|
||||
export function IdRow({ id }: { id: string }) {
|
||||
return (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="IdIcon" />}
|
||||
label="ID"
|
||||
subLabel={id}
|
||||
onPress={() => {
|
||||
Clipboard.setString(id)
|
||||
showCopiedToClipboardToast()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function RepositoryRow({
|
||||
text,
|
||||
copyable,
|
||||
}: {
|
||||
text: string
|
||||
copyable?: boolean
|
||||
}) {
|
||||
return (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="GlobeEarthIcon" />}
|
||||
label="Repository"
|
||||
subLabel={text}
|
||||
onPress={
|
||||
copyable
|
||||
? () => {
|
||||
Clipboard.setString(text)
|
||||
showCopiedToClipboardToast()
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function usePluginRepositoryText(plugin: AnyPlugin) {
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const internal = isPluginInternal(meta)
|
||||
const repoUrl = meta.source?.repo ?? null
|
||||
const hasUrl = !internal && repoUrl
|
||||
|
||||
const [repoName, setRepoName] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasUrl) return
|
||||
listRepos().then(
|
||||
repos => {
|
||||
const repo = repos.find(r => r.url === repoUrl)
|
||||
if (repo?.name) setRepoName(repo.name)
|
||||
},
|
||||
() => {},
|
||||
)
|
||||
}, [repoUrl, hasUrl])
|
||||
|
||||
return hasUrl
|
||||
? repoName
|
||||
? `${repoName} (${repoUrl})`
|
||||
: repoUrl
|
||||
: internal
|
||||
? 'Built-in'
|
||||
: 'Sideloaded'
|
||||
}
|
||||
|
||||
function PluginActions({
|
||||
plugin,
|
||||
closeSheet,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
closeSheet: () => void
|
||||
}) {
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const startable = isPluginStartable(plugin)
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
// Any lifecycle progress counts as running, stop waits for in-flight lifecycles
|
||||
const running = Boolean(usePluginStatus(plugin))
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
justify="space-around"
|
||||
style={{ paddingHorizontal: 8, paddingVertical: 16 }}
|
||||
>
|
||||
{enabled && !isPluginEssential(meta) && (
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={running ? StopIcon : PlayIcon}
|
||||
label={running ? 'Stop' : 'Start'}
|
||||
disabled={!running && !startable}
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (running) await stopPlugin(plugin)
|
||||
else await runPluginLate(plugin)
|
||||
} catch (e) {
|
||||
showErrorToast(messageOf(e))
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={FileWarningIcon}
|
||||
label="Clear Data"
|
||||
onPress={() => {
|
||||
showPluginClearDataConfirmation(plugin, closeSheet)
|
||||
}}
|
||||
/>
|
||||
{!isPluginInternal(meta) && (
|
||||
<IconButton
|
||||
variant="destructive"
|
||||
size="lg"
|
||||
icon={TrashIcon}
|
||||
label="Uninstall"
|
||||
onPress={() => {
|
||||
showPluginUninstallConfirmation(plugin, closeSheet)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (!startable)
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
ref={settingsRef}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={SettingsIcon}
|
||||
label="Settings"
|
||||
disabled={!startable}
|
||||
onPress={() => {
|
||||
openPluginSettings(plugin)
|
||||
closeSheet()
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const CopyIcon = lookupGeneratedIconComponent('CopyIcon')!
|
||||
|
||||
export function showCopiedToClipboardToast() {
|
||||
ToastActionCreators.open({
|
||||
key: 'REVENGE_PLUGIN_SETTINGS_COPIED',
|
||||
content: 'Copied to clipboard',
|
||||
IconComponent: CopyIcon,
|
||||
})
|
||||
}
|
||||
|
|
@ -64,6 +64,11 @@ export default function PluginStatesProvider({
|
|||
pEmitter.on('disabled', handleDisabled)
|
||||
|
||||
return () => {
|
||||
pEmitter.off('preInited', handleStatusChange)
|
||||
pEmitter.off('inited', handleStatusChange)
|
||||
pEmitter.off('started', handleStatusChange)
|
||||
pEmitter.off('stopped', handleStatusChange)
|
||||
|
||||
pEmitter.off('enabled', handleEnabled)
|
||||
pEmitter.off('disabled', handleDisabled)
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import {
|
||||
isPluginPendingReload,
|
||||
isPluginPendingUpdate,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { AlertModal, AlertActionButton, Text } = Design
|
||||
|
||||
export default function PluginUninstallConfirmationAlert({
|
||||
plugin,
|
||||
action,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
action: () => Promise<void>
|
||||
}) {
|
||||
const reloadPending =
|
||||
isPluginPendingReload(plugin) || isPluginPendingUpdate(plugin)
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
title="Uninstall plugin?"
|
||||
content={
|
||||
<Text color="text-default">
|
||||
<Text variant="text-md/semibold" color="text-default">
|
||||
{plugin.manifest.name}
|
||||
</Text>{' '}
|
||||
and all of its data will be removed. This cannot be undone.
|
||||
</Text>
|
||||
}
|
||||
extraContent={
|
||||
reloadPending && (
|
||||
<Text
|
||||
variant="text-md/semibold"
|
||||
color="text-feedback-critical"
|
||||
>
|
||||
This plugin is pending a reload. Uninstalling now may
|
||||
leave unintended side effects.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
onPress={action}
|
||||
text="Uninstall"
|
||||
variant="destructive"
|
||||
/>
|
||||
<AlertActionButton text="Cancel" variant="secondary" />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
11
src/plugins/start/settings.plugins/constants.ts
Normal file
11
src/plugins/start/settings.plugins/constants.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
export const Setting = {
|
||||
RevengePlugins: 'RevengePlugins',
|
||||
RevengePluginsAdvanced: 'RevengePluginsAdvanced',
|
||||
RevengePluginsBrowse: 'RevengePluginsBrowse',
|
||||
} as const
|
||||
|
||||
export const RouteNames = {
|
||||
[Setting.RevengePlugins]: 'Revenge Plugins',
|
||||
[Setting.RevengePluginsAdvanced]: 'Revenge Plugins Advanced',
|
||||
[Setting.RevengePluginsBrowse]: 'Browse Plugins',
|
||||
} as const
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { RouteNames, Setting } from '../constants'
|
||||
import defer * as RevengePluginsAdvancedSettingScreen from '../screens/RevengePluginsAdvancedSettingScreen'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
const RevengePluginsAdvancedSetting: SettingsItem = {
|
||||
parent: null,
|
||||
type: 'route',
|
||||
IconComponent: () => <TableRowAssetIcon name="SettingsIcon" />,
|
||||
useTitle: () => 'Advanced',
|
||||
screen: {
|
||||
route: RouteNames[Setting.RevengePluginsAdvanced],
|
||||
getComponent: () => RevengePluginsAdvancedSettingScreen.default,
|
||||
},
|
||||
}
|
||||
|
||||
export default RevengePluginsAdvancedSetting
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { RouteNames, Setting } from '../constants'
|
||||
import defer * as RevengePluginsBrowseSettingScreen from '../screens/RevengePluginsBrowseSettingScreen'
|
||||
import type { SettingsItem } from '@revenge-mod/discord/modules/settings'
|
||||
|
||||
const RevengePluginsBrowseSetting: SettingsItem = {
|
||||
parent: null,
|
||||
type: 'route',
|
||||
IconComponent: () => <TableRowAssetIcon name="PlusLargeIcon" />,
|
||||
useTitle: () => 'Browse Plugins',
|
||||
screen: {
|
||||
route: RouteNames[Setting.RevengePluginsBrowse],
|
||||
getComponent: () => RevengePluginsBrowseSettingScreen.default,
|
||||
},
|
||||
}
|
||||
|
||||
export default RevengePluginsBrowseSetting
|
||||
104
src/plugins/start/settings.plugins/index.ts
Normal file
104
src/plugins/start/settings.plugins/index.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { ToastActionCreators } from '@revenge-mod/discord/actions'
|
||||
import {
|
||||
onSettingsModulesLoaded,
|
||||
refreshSettingsNavigator,
|
||||
refreshSettingsOverviewScreen,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
isPluginEnabledLate,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
refreshAllRepos,
|
||||
updateAllPlugins,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import pluginSettings from '../settings'
|
||||
import type { PluginApi } from '@revenge-mod/plugins/types'
|
||||
|
||||
export interface Storage {
|
||||
autoUpdate: boolean
|
||||
lastUpdateCheck?: number
|
||||
}
|
||||
|
||||
const OUTDATED_THRESHOLD = 24 * 60 * 60 * 1000 // 1 day
|
||||
const AUTO_UPDATE_CHECK_DELAY = 10_000
|
||||
|
||||
const CircleXIconComponent = lookupGeneratedIconComponent(
|
||||
'CircleXIcon',
|
||||
'CircleXIcon-primary',
|
||||
'CircleXIcon-secondary',
|
||||
)!
|
||||
|
||||
registerInternalPlugin<{ jsonStorage: Storage }>(
|
||||
{
|
||||
id: 'revenge.settings.plugins',
|
||||
name: 'Plugin Settings',
|
||||
description: 'Plugin management UI for Revenge.',
|
||||
author: 'Revenge',
|
||||
icon: 'PuzzlePieceIcon',
|
||||
dependencies: { [pluginSettings]: {} },
|
||||
},
|
||||
{
|
||||
jsonStorage: {
|
||||
load: true,
|
||||
default: {
|
||||
autoUpdate: true,
|
||||
},
|
||||
},
|
||||
async start(api_) {
|
||||
api = api_
|
||||
|
||||
// @as-require
|
||||
import('./plugins')
|
||||
|
||||
onSettingsModulesLoaded(() => {
|
||||
// @as-require
|
||||
import('./register')
|
||||
})
|
||||
|
||||
if (isPluginEnabledLate(api_.plugin)) {
|
||||
refreshSettingsOverviewScreen()
|
||||
refreshSettingsNavigator()
|
||||
}
|
||||
|
||||
const settings = await api.jsonStorage.get()
|
||||
if (!settings.autoUpdate) return
|
||||
if (
|
||||
settings.lastUpdateCheck !== undefined &&
|
||||
Date.now() - settings.lastUpdateCheck <= OUTDATED_THRESHOLD
|
||||
)
|
||||
return
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const { errors } = await refreshAllRepos()
|
||||
await updateAllPlugins()
|
||||
|
||||
if (!errors.length)
|
||||
await api.jsonStorage.set({
|
||||
lastUpdateCheck: Date.now(),
|
||||
})
|
||||
} catch (e) {
|
||||
ToastActionCreators.open({
|
||||
key: 'PLUGIN_UPDATE_CHECK_FAILED',
|
||||
content: 'Failed to check for plugin updates',
|
||||
IconComponent: CircleXIconComponent,
|
||||
})
|
||||
|
||||
api.logger.warn('Failed to check for plugin updates', e)
|
||||
}
|
||||
}, AUTO_UPDATE_CHECK_DELAY)
|
||||
},
|
||||
stop({ cleanup }) {
|
||||
cleanup(refreshSettingsOverviewScreen, refreshSettingsNavigator)
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
|
||||
)
|
||||
|
||||
// Exposed for the Advanced settings screen (auto-update toggle)
|
||||
export let api: PluginApi<{ jsonStorage: Storage }>
|
||||
|
|
@ -1,16 +1,27 @@
|
|||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import {
|
||||
AlertActionCreators,
|
||||
ToastActionCreators,
|
||||
} from '@revenge-mod/discord/actions'
|
||||
import {
|
||||
refreshSettingsNavigator,
|
||||
registerSettingsItem,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
isPluginErrored,
|
||||
isPluginPendingReload,
|
||||
isPluginPendingUpdate,
|
||||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { Setting } from '../settings/constants'
|
||||
import defer * as NavigatorHeaderWithIcon from './components/NavigatorHeaderWithIcon'
|
||||
import PluginInstallConfirmAlert from './components/PluginInstallConfirmAlert'
|
||||
import PluginInstallFailedAlert from './components/PluginInstallFailedAlert'
|
||||
import PluginsFailedToStartAlert from './components/PluginsFailedToStartAlert'
|
||||
import PluginsRequireReloadAlert from './components/PluginsRequireReloadAlert'
|
||||
import { Setting } from './constants'
|
||||
import type { StackScreenProps } from '@react-navigation/stack'
|
||||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||
import type { PluginApi } from '@revenge-mod/plugins/types'
|
||||
|
|
@ -72,9 +83,10 @@ pEmitter.on('started', plugin => {
|
|||
|
||||
/// RELOAD REQUIRED ALERT
|
||||
|
||||
function showReloadRequiredAlertIfNeeded() {
|
||||
function showPendingReloadAlertIfNeeded() {
|
||||
const plugins = [...pList.values()].filter(
|
||||
plugin => plugin.flags & PluginFlags.ReloadRequired,
|
||||
plugin =>
|
||||
isPluginPendingReload(plugin) || isPluginPendingUpdate(plugin),
|
||||
)
|
||||
|
||||
if (!plugins.length) return
|
||||
|
|
@ -87,8 +99,8 @@ function showReloadRequiredAlertIfNeeded() {
|
|||
}
|
||||
|
||||
function showErrorAlertIfNeeded() {
|
||||
const plugins = [...pList.values()].filter(
|
||||
plugin => plugin.flags & PluginFlags.Errored,
|
||||
const plugins = [...pList.values()].filter(plugin =>
|
||||
isPluginErrored(plugin),
|
||||
)
|
||||
|
||||
if (!plugins.length) return
|
||||
|
|
@ -101,13 +113,68 @@ function showErrorAlertIfNeeded() {
|
|||
}
|
||||
|
||||
pEmitter.on('flagUpdate', plugin => {
|
||||
if (plugin.flags & PluginFlags.ReloadRequired)
|
||||
showReloadRequiredAlertIfNeeded()
|
||||
if (isPluginPendingReload(plugin) || isPluginPendingUpdate(plugin))
|
||||
showPendingReloadAlertIfNeeded()
|
||||
})
|
||||
|
||||
pEmitter.on('stopped', plugin => {
|
||||
if (plugin.flags & PluginFlags.Errored) showErrorAlertIfNeeded()
|
||||
if (isPluginErrored(plugin)) showErrorAlertIfNeeded()
|
||||
})
|
||||
|
||||
/// PLUGIN INSTALL FEEDBACK
|
||||
|
||||
const pluginInstallToastKeyFor = (id: string) => `REVENGE_PLUGIN_INSTALL:${id}`
|
||||
const PluginInstallFailedAlertKey = 'plugin-install-failed'
|
||||
const PluginInstallConfirmAlertKey = 'plugin-install-confirm'
|
||||
|
||||
pEmitter.on('installReady', prompt => {
|
||||
AlertActionCreators.dismissAlert(PluginInstallConfirmAlertKey)
|
||||
AlertActionCreators.openAlert(
|
||||
PluginInstallConfirmAlertKey,
|
||||
<PluginInstallConfirmAlert prompt={prompt} />,
|
||||
)
|
||||
})
|
||||
|
||||
pEmitter.on('install', result => {
|
||||
if (result.error !== false) {
|
||||
AlertActionCreators.dismissAlert(PluginInstallFailedAlertKey)
|
||||
AlertActionCreators.openAlert(
|
||||
PluginInstallFailedAlertKey,
|
||||
<PluginInstallFailedAlert error={result.error} />,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (result.pending) {
|
||||
// Applied on disk only, the new version loads at next reload
|
||||
showInstallToast(
|
||||
`Downloaded ${pList.get(result.id)?.manifest.name || result.id} ${result.version}, reload to apply`,
|
||||
result.id,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
showInstallToast(
|
||||
result.updated
|
||||
? `Updated ${result.manifest.name}`
|
||||
: `Installed ${result.manifest.name}`,
|
||||
result.manifest.id,
|
||||
)
|
||||
})
|
||||
|
||||
const CircleCheckIcon = lookupGeneratedIconComponent(
|
||||
'CircleCheckIcon',
|
||||
'CircleCheckIcon-secondary',
|
||||
'CircleCheckIcon-primary',
|
||||
)
|
||||
|
||||
function showInstallToast(content: string, id: string) {
|
||||
ToastActionCreators.open({
|
||||
key: pluginInstallToastKeyFor(id),
|
||||
content,
|
||||
IconComponent: CircleCheckIcon,
|
||||
})
|
||||
}
|
||||
|
||||
showErrorAlertIfNeeded()
|
||||
showReloadRequiredAlertIfNeeded()
|
||||
showPendingReloadAlertIfNeeded()
|
||||
17
src/plugins/start/settings.plugins/register.tsx
Normal file
17
src/plugins/start/settings.plugins/register.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import {
|
||||
addSettingsItemToSection,
|
||||
registerSettingsItems,
|
||||
} from '@revenge-mod/discord/modules/settings'
|
||||
import { Setting } from './constants'
|
||||
import RevengePluginsAdvancedSetting from './definitions/RevengePluginsAdvancedSetting'
|
||||
import RevengePluginsBrowseSetting from './definitions/RevengePluginsBrowseSetting'
|
||||
import RevengePluginsSetting from './definitions/RevengePluginsSetting'
|
||||
|
||||
registerSettingsItems({
|
||||
[Setting.RevengePlugins]: RevengePluginsSetting,
|
||||
[Setting.RevengePluginsAdvanced]: RevengePluginsAdvancedSetting,
|
||||
[Setting.RevengePluginsBrowse]: RevengePluginsBrowseSetting,
|
||||
})
|
||||
|
||||
// The settings plugin registers this section, and it always starts before us (we depend on it).
|
||||
addSettingsItemToSection('REVENGE', Setting.RevengePlugins, 1)
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { FormSwitch } from '@revenge-mod/components'
|
||||
import { styles } from '@revenge-mod/components/_'
|
||||
import Page from '@revenge-mod/components/Page'
|
||||
import TableRowAssetIcon from '@revenge-mod/components/TableRowAssetIcon'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { callNativeMethod } from '@revenge-mod/modules/native'
|
||||
import { resyncPluginSources } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
listAllUpdates,
|
||||
listRepos,
|
||||
refreshAllRepos,
|
||||
refreshRepo,
|
||||
repoEvents,
|
||||
setRepos,
|
||||
updateAllPlugins,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ScrollView, View } from 'react-native'
|
||||
import { api } from '..'
|
||||
import { formatBytes, messageOf, showErrorToast } from '../utils/repos'
|
||||
import type {
|
||||
DownloadProgressEvent,
|
||||
Repo,
|
||||
RepoConfigEntry,
|
||||
RepoStateEvent,
|
||||
RepoUpdate,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
|
||||
const {
|
||||
Button,
|
||||
ContextMenu,
|
||||
IconButton,
|
||||
Stack,
|
||||
TableRow,
|
||||
TableRowGroup,
|
||||
TableSwitchRow,
|
||||
TextInput,
|
||||
} = Design
|
||||
|
||||
const MoreIcon = getAssetIdByName('MoreVerticalIcon')!
|
||||
const UpIconComponent = lookupGeneratedIconComponent('ArrowSmallUpIcon')!
|
||||
const DownIconComponent = lookupGeneratedIconComponent('ArrowSmallDownIcon')!
|
||||
const TrashIconComponent = lookupGeneratedIconComponent('TrashIcon')!
|
||||
|
||||
function toConfig(list: Repo[]): RepoConfigEntry[] {
|
||||
return list.map(({ url, enabled }) => ({ url, enabled }))
|
||||
}
|
||||
|
||||
function repoSubLabel(repo: Repo, state?: RepoStateEvent['state']) {
|
||||
if (state === 'refreshing') return `${repo.url} (refreshing...)`
|
||||
if (state === 'error') return `${repo.url} (refresh failed)`
|
||||
return repo.url
|
||||
}
|
||||
|
||||
interface UserRepoRowProps {
|
||||
repo: Repo
|
||||
state?: RepoStateEvent['state']
|
||||
onMove: (repo: Repo, delta: number) => void
|
||||
onRemove: (repo: Repo) => void
|
||||
onToggle: (repo: Repo, enabled: boolean) => void
|
||||
}
|
||||
|
||||
function UserRepoRow({
|
||||
repo,
|
||||
state,
|
||||
onMove,
|
||||
onRemove,
|
||||
onToggle,
|
||||
}: UserRepoRowProps) {
|
||||
const menuItems = [
|
||||
[
|
||||
{
|
||||
label: 'Move up',
|
||||
IconComponent: UpIconComponent,
|
||||
action: () => onMove(repo, -1),
|
||||
},
|
||||
{
|
||||
label: 'Move down',
|
||||
IconComponent: DownIconComponent,
|
||||
action: () => onMove(repo, 1),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: 'Refresh',
|
||||
IconComponent: lookupGeneratedIconComponent('RefreshIcon')!,
|
||||
action: () =>
|
||||
refreshRepo(repo.url).catch(e => {
|
||||
showErrorToast(messageOf(e))
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
IconComponent: TrashIconComponent,
|
||||
variant: 'destructive' as const,
|
||||
action: () => onRemove(repo),
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
label={repo.name ?? repo.url}
|
||||
subLabel={repoSubLabel(repo, state)}
|
||||
trailing={
|
||||
<Stack direction="horizontal" spacing={8}>
|
||||
<ContextMenu
|
||||
items={menuItems}
|
||||
title={repo.name ?? repo.url}
|
||||
>
|
||||
{props => (
|
||||
<IconButton
|
||||
{...props}
|
||||
icon={MoreIcon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
/>
|
||||
)}
|
||||
</ContextMenu>
|
||||
<FormSwitch
|
||||
value={repo.enabled}
|
||||
onValueChange={enabled => onToggle(repo, enabled)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function RevengePluginsAdvancedSettingScreen() {
|
||||
// Internal repositories are native-managed and not part of the config.
|
||||
const [repos, setReposState] = useState<Repo[]>([])
|
||||
const [url, setUrl] = useState('')
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [updates, setUpdates] = useState<RepoUpdate[] | null>(null)
|
||||
const [repoStates, setRepoStates] = useState<
|
||||
Record<string, RepoStateEvent['state']>
|
||||
>({})
|
||||
const [progress, setProgress] = useState<DownloadProgressEvent | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const onRepoState = (event: RepoStateEvent) => {
|
||||
setRepoStates(states => ({ ...states, [event.url]: event.state }))
|
||||
}
|
||||
const onProgress = (event: DownloadProgressEvent) => {
|
||||
setProgress(event.received >= event.total ? null : event)
|
||||
}
|
||||
|
||||
repoEvents.on('repoState', onRepoState)
|
||||
repoEvents.on('downloadProgress', onProgress)
|
||||
return () => {
|
||||
repoEvents.off('repoState', onRepoState)
|
||||
repoEvents.off('downloadProgress', onProgress)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const settings = api.jsonStorage.use()
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
listRepos().then(setReposState, e => showErrorToast(messageOf(e)))
|
||||
}, [])
|
||||
|
||||
useEffect(refresh, [refresh])
|
||||
|
||||
const commit = useCallback(
|
||||
async (config: RepoConfigEntry[]) => {
|
||||
try {
|
||||
await setRepos(config)
|
||||
} catch (e) {
|
||||
showErrorToast(messageOf(e))
|
||||
}
|
||||
refresh()
|
||||
},
|
||||
[refresh],
|
||||
)
|
||||
|
||||
const userRepos = repos.filter(repo => !repo.internal)
|
||||
|
||||
const move = useCallback(
|
||||
(repo: Repo, delta: number) => {
|
||||
const config = toConfig(userRepos)
|
||||
const from = config.findIndex(entry => entry.url === repo.url)
|
||||
const to = from + delta
|
||||
if (from < 0 || to < 0 || to >= config.length) return
|
||||
;[config[from], config[to]] = [config[to], config[from]]
|
||||
commit(config)
|
||||
},
|
||||
[commit, userRepos],
|
||||
)
|
||||
|
||||
const addRepo = useCallback(async () => {
|
||||
const added = url.trim()
|
||||
await commit([...toConfig(userRepos), { url: added, enabled: true }])
|
||||
setUrl('')
|
||||
|
||||
try {
|
||||
// Refresh the new repository right away so its plugins show up
|
||||
await refreshRepo(added)
|
||||
} catch (e) {
|
||||
showErrorToast(messageOf(e))
|
||||
}
|
||||
refresh()
|
||||
}, [commit, refresh, url, userRepos])
|
||||
|
||||
const removeRepo = useCallback(
|
||||
async (repo: Repo) => {
|
||||
await commit(toConfig(userRepos.filter(r => r.url !== repo.url)))
|
||||
|
||||
// Plugins installed from the removed repository turn Sideloaded
|
||||
resyncPluginSources().catch(() => {})
|
||||
},
|
||||
[commit, userRepos],
|
||||
)
|
||||
|
||||
const toggleRepo = useCallback(
|
||||
(repo: Repo, enabled: boolean) => {
|
||||
commit(
|
||||
toConfig(userRepos).map(entry =>
|
||||
entry.url === repo.url ? { ...entry, enabled } : entry,
|
||||
),
|
||||
)
|
||||
},
|
||||
[commit, userRepos],
|
||||
)
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const { errors } = await refreshAllRepos()
|
||||
if (errors.length)
|
||||
showErrorToast(
|
||||
errors.map(e => `${e.url}: ${e.error}`).join('\n'),
|
||||
)
|
||||
else await api.jsonStorage.set({ lastUpdateCheck: Date.now() })
|
||||
|
||||
const result = await listAllUpdates()
|
||||
setUpdates(result.updates)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
refresh()
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
const updateAll = useCallback(async () => {
|
||||
setBusy(true)
|
||||
try {
|
||||
const { errors } = await updateAllPlugins()
|
||||
if (errors.length)
|
||||
showErrorToast(
|
||||
errors.map(e => `${e.id}: ${e.error}`).join('\n'),
|
||||
)
|
||||
setUpdates(null)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
refresh()
|
||||
}
|
||||
}, [refresh])
|
||||
|
||||
const lastCheckedSubLabel =
|
||||
updates !== null && !updates.length
|
||||
? 'All plugins up to date!'
|
||||
: settings?.lastUpdateCheck !== undefined &&
|
||||
`Last checked: ${new Date(settings.lastUpdateCheck).toLocaleString()}`
|
||||
|
||||
return (
|
||||
<Page spacing={0}>
|
||||
<ScrollView keyboardShouldPersistTaps="handled" style={styles.flex}>
|
||||
<Stack spacing={16} style={{ paddingBottom: 32 }}>
|
||||
<Stack direction="horizontal" spacing={8}>
|
||||
<View style={styles.grow}>
|
||||
<TextInput
|
||||
label="Repository URL"
|
||||
onChange={setUrl}
|
||||
placeholder="https://example.com/plugins"
|
||||
size="md"
|
||||
value={url}
|
||||
/>
|
||||
</View>
|
||||
</Stack>
|
||||
<Button
|
||||
disabled={!url.trim()}
|
||||
onPress={addRepo}
|
||||
text="Add repository"
|
||||
variant="primary"
|
||||
/>
|
||||
<TableRowGroup title="Repositories">
|
||||
{repos.map(repo =>
|
||||
repo.internal ? (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="LockIcon" />}
|
||||
key={repo.url}
|
||||
label={repo.name ?? repo.url}
|
||||
subLabel={repo.description ?? repo.url}
|
||||
/>
|
||||
) : (
|
||||
<UserRepoRow
|
||||
key={repo.url}
|
||||
repo={repo}
|
||||
state={repoStates[repo.url]}
|
||||
onMove={move}
|
||||
onRemove={removeRepo}
|
||||
onToggle={toggleRepo}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</TableRowGroup>
|
||||
<TableRowGroup title="Updates">
|
||||
<TableSwitchRow
|
||||
label="Update plugins automatically"
|
||||
subLabel="Check repositories and apply plugin updates after startup."
|
||||
onValueChange={autoUpdate => {
|
||||
api.jsonStorage.set({ autoUpdate })
|
||||
}}
|
||||
value={settings?.autoUpdate ?? true}
|
||||
/>
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="RefreshIcon" />}
|
||||
label="Check for updates"
|
||||
subLabel={lastCheckedSubLabel}
|
||||
disabled={busy || !userRepos.length}
|
||||
onPress={checkForUpdates}
|
||||
/>
|
||||
{progress ? (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="<EMPTY>" />}
|
||||
label={`Downloading ${progress.id} ${progress.version}`}
|
||||
subLabel={`${formatBytes(progress.received)} / ${formatBytes(progress.total)} (${progress.index} of ${progress.count})`}
|
||||
/>
|
||||
) : null}
|
||||
{updates?.length ? (
|
||||
<>
|
||||
{updates.map(update => (
|
||||
<TableRow
|
||||
icon={
|
||||
<TableRowAssetIcon name="DownloadIcon" />
|
||||
}
|
||||
key={update.id}
|
||||
label={update.id}
|
||||
subLabel={`${update.installed} → ${update.available} (${update.channel})`}
|
||||
/>
|
||||
))}
|
||||
<TableRow
|
||||
icon={
|
||||
<TableRowAssetIcon name="DownloadIcon" />
|
||||
}
|
||||
label="Update all"
|
||||
onPress={updateAll}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</TableRowGroup>
|
||||
<TableRowGroup title="Advanced">
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="DownloadIcon" />}
|
||||
label="Install from file"
|
||||
onPress={() =>
|
||||
callNativeMethod(
|
||||
'revenge.plugins.installFile',
|
||||
[],
|
||||
)
|
||||
}
|
||||
/>
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { styles } from '@revenge-mod/components/_'
|
||||
import Page from '@revenge-mod/components/Page'
|
||||
import SearchInput from '@revenge-mod/components/SearchInput'
|
||||
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { getInternalPluginMeta, pEmitter, pList } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
listRepoPlugins,
|
||||
listRepos,
|
||||
refreshAllRepos,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import { BrowsePluginMasonryFlashList } from '../components/PluginList'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
EssentialPluginTooltipProvider,
|
||||
} from '../components/TooltipProvider'
|
||||
import { runInstallFlow } from '../utils/repos'
|
||||
import type { RepoPluginListing } from '@revenge-mod/plugins/_/repositories'
|
||||
import type { BrowseSortKey } from '../components/BrowseFilterAndSortActionSheet'
|
||||
import type { BrowseEntry } from '../components/PluginList'
|
||||
|
||||
const { Stack, IconButton, LayerScope } = Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
|
||||
const SearchDebounceTime = 100
|
||||
|
||||
export default function RevengePluginsBrowseSettingScreen() {
|
||||
return (
|
||||
<LayerScope>
|
||||
<ClickOutsideProvider>
|
||||
<PluginStatesProvider>
|
||||
<Page spacing={16}>
|
||||
<EssentialPluginTooltipProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<Screen />
|
||||
</EnablePluginTooltipProvider>
|
||||
</EssentialPluginTooltipProvider>
|
||||
</Page>
|
||||
</PluginStatesProvider>
|
||||
</ClickOutsideProvider>
|
||||
</LayerScope>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Let the user pick a release channel
|
||||
/** The channel a listing displays: `latest`, else its first channel. */
|
||||
function displayChannelOf(listing: RepoPluginListing): string | undefined {
|
||||
if (listing.channels.latest) return 'latest'
|
||||
return Object.keys(listing.channels)[0]
|
||||
}
|
||||
|
||||
function compareNames(a: BrowseEntry, b: BrowseEntry) {
|
||||
return a.listing.name.localeCompare(b.listing.name, undefined, {
|
||||
sensitivity: 'base',
|
||||
})
|
||||
}
|
||||
|
||||
const Sorts: Record<BrowseSortKey, (a: BrowseEntry, b: BrowseEntry) => number> =
|
||||
{
|
||||
name: compareNames,
|
||||
size: (a, b) => a.size - b.size || compareNames(a, b),
|
||||
}
|
||||
|
||||
function Screen() {
|
||||
const [entries, setEntries] = useState<BrowseEntry[]>([])
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const debouncedSetSearch = useCallback(
|
||||
debounce(setSearch, SearchDebounceTime),
|
||||
[],
|
||||
)
|
||||
|
||||
// Unchecked repository URLs, so repos that appear later default to checked
|
||||
const [excluded, setExcluded] = useState<string[]>([])
|
||||
const [sort, setSort] = useState<BrowseSortKey>('name')
|
||||
const [reverse, setReverse] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const repos = await listRepos()
|
||||
const all: BrowseEntry[] = []
|
||||
|
||||
await Promise.all(
|
||||
repos
|
||||
.filter(repo => !repo.internal && repo.enabled)
|
||||
.map(repo =>
|
||||
listRepoPlugins(repo.url).then(
|
||||
listings => {
|
||||
const repositoryText = repo.name
|
||||
? `${repo.name} (${repo.url})`
|
||||
: repo.url
|
||||
|
||||
for (const listing of listings) {
|
||||
const plugin = pList.get(listing.id)
|
||||
const displayChannel = displayChannelOf(listing)
|
||||
const displayVersion = displayChannel
|
||||
? (listing.channels[displayChannel] ?? '')
|
||||
: ''
|
||||
all.push({
|
||||
key: `${repo.url}#${listing.id}`,
|
||||
listing,
|
||||
repoUrl: repo.url,
|
||||
repoName: repo.name ?? null,
|
||||
repositoryText,
|
||||
version: displayVersion,
|
||||
// TODO: Let the user pick a release channel
|
||||
channel: displayChannel,
|
||||
size:
|
||||
listing.versions[displayVersion]
|
||||
?.size ?? 0,
|
||||
installed: plugin
|
||||
? ([
|
||||
plugin,
|
||||
getInternalPluginMeta(plugin),
|
||||
] as const)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
},
|
||||
// No cached index yet is normal, ignore
|
||||
() => {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
setEntries(all)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Show cached indexes right away, then refresh everything
|
||||
load()
|
||||
refreshAllRepos().then(load, () => {})
|
||||
}, [load])
|
||||
|
||||
// Fresh installs register live, re-mark entries as installed when they do
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => load()
|
||||
|
||||
pEmitter.on('register', handleUpdate)
|
||||
pEmitter.on('unregister', handleUpdate)
|
||||
|
||||
return () => {
|
||||
pEmitter.off('register', handleUpdate)
|
||||
pEmitter.off('unregister', handleUpdate)
|
||||
}
|
||||
}, [load])
|
||||
|
||||
const install = useCallback(
|
||||
async (entry: BrowseEntry) => {
|
||||
// Pin the displayed version, channel, and repository, so the plan matches the card
|
||||
await runInstallFlow(
|
||||
entry.listing.id,
|
||||
entry.version || undefined,
|
||||
entry.channel,
|
||||
[entry.repoUrl],
|
||||
)
|
||||
load()
|
||||
},
|
||||
[load],
|
||||
)
|
||||
|
||||
// Repositories that currently have entries, for the filter sheet
|
||||
const repos = useMemo(() => {
|
||||
const seen = new Map<string, string | null>()
|
||||
for (const entry of entries)
|
||||
if (!seen.has(entry.repoUrl))
|
||||
seen.set(entry.repoUrl, entry.repoName)
|
||||
|
||||
return [...seen].map(([url, name]) => ({ url, name }))
|
||||
}, [entries])
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const query = search.toLowerCase()
|
||||
|
||||
const result = entries.filter(entry => {
|
||||
if (excluded.includes(entry.repoUrl)) return false
|
||||
if (!query) return true
|
||||
|
||||
const { name, description, author, id } = entry.listing
|
||||
return (
|
||||
name.toLowerCase().includes(query) ||
|
||||
description.toLowerCase().includes(query) ||
|
||||
author.toLowerCase().includes(query) ||
|
||||
id.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
result.sort(Sorts[sort])
|
||||
if (reverse) result.reverse()
|
||||
|
||||
return result
|
||||
}, [entries, excluded, search, sort, reverse])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="horizontal">
|
||||
<View style={styles.grow}>
|
||||
<SearchInput onChange={debouncedSetSearch} size="md" />
|
||||
</View>
|
||||
<IconButton
|
||||
icon={FiltersHorizontalIcon}
|
||||
variant="tertiary"
|
||||
onPress={() =>
|
||||
ActionSheetActionCreators.openLazy(
|
||||
import(
|
||||
'../components/BrowseFilterAndSortActionSheet'
|
||||
),
|
||||
'browse-filter-and-sort',
|
||||
{
|
||||
repos,
|
||||
checked: repos
|
||||
.map(repo => repo.url)
|
||||
.filter(url => !excluded.includes(url)),
|
||||
setChecked: (urls: string[]) =>
|
||||
setExcluded(
|
||||
repos
|
||||
.map(repo => repo.url)
|
||||
.filter(url => !urls.includes(url)),
|
||||
),
|
||||
sort,
|
||||
setSort,
|
||||
reverse,
|
||||
setReverse,
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
<BrowsePluginMasonryFlashList
|
||||
entries={visible}
|
||||
onInstall={install}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -11,11 +11,19 @@ import {
|
|||
isPluginEnabled,
|
||||
isPluginEssential,
|
||||
isPluginInternal,
|
||||
isPluginPendingReload,
|
||||
isPluginPendingUpdate,
|
||||
pEmitter,
|
||||
pList,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import { debounce } from '@revenge-mod/utils/callback'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import RevengeIcon from '~assets/RevengeIcon'
|
||||
|
|
@ -25,14 +33,16 @@ import {
|
|||
EnablePluginTooltipProvider,
|
||||
EssentialPluginTooltipProvider,
|
||||
} from '../components/TooltipProvider'
|
||||
import type { RouteProp } from '@react-navigation/core'
|
||||
import { RouteNames, Setting } from '../constants'
|
||||
import type { NavigationProp, RouteProp } from '@react-navigation/core'
|
||||
import type { ReactNavigationParamList } from '@revenge-mod/externals/react-navigation'
|
||||
import type { FilterAndSortActionSheetProps } from '../components/FilterAndSortActionSheet'
|
||||
import type { RouteNames, Setting } from '../constants'
|
||||
|
||||
const { Stack, IconButton, LayerScope } = Design
|
||||
const { Stack, IconButton, FloatingActionButton, LayerScope } = Design
|
||||
|
||||
const FiltersHorizontalIcon = getAssetIdByName('FiltersHorizontalIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon')!
|
||||
const PlusLargeIcon = getAssetIdByName('PlusLargeIcon')!
|
||||
|
||||
export default function RevengePluginsSettingScreen() {
|
||||
return (
|
||||
|
|
@ -67,6 +77,14 @@ const Filters: FilterAndSortActionSheetProps['filters'] = {
|
|||
icon: getAssetIdByName('CircleErrorIcon')!,
|
||||
filter: plugin => plugin.errors.length > 0,
|
||||
},
|
||||
'Pending Reload': {
|
||||
icon: getAssetIdByName('RetryIcon')!,
|
||||
filter: plugin => isPluginPendingReload(plugin),
|
||||
},
|
||||
'Pending Update': {
|
||||
icon: getAssetIdByName('RefreshIcon')!,
|
||||
filter: plugin => isPluginPendingUpdate(plugin),
|
||||
},
|
||||
Internal: {
|
||||
icon: RevengeIcon,
|
||||
desc: 'Included with Revenge.',
|
||||
|
|
@ -94,10 +112,48 @@ const Sorts = {
|
|||
'Enabled first': [
|
||||
getAssetIdByName('CircleCheckIcon')!,
|
||||
(a, b) =>
|
||||
(b.flags & PluginFlags.Enabled) - (a.flags & PluginFlags.Enabled),
|
||||
isPluginEnabled(a) === isPluginEnabled(b)
|
||||
? a.manifest.name.localeCompare(b.manifest.name)
|
||||
: isPluginEnabled(a)
|
||||
? -1
|
||||
: 1,
|
||||
],
|
||||
} satisfies FilterAndSortActionSheetProps['sorts']
|
||||
|
||||
function HeaderButton() {
|
||||
const navigation = useNavigation<NavigationProp<any>>()
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={SettingsIcon}
|
||||
onPress={() =>
|
||||
navigation.navigate(RouteNames[Setting.RevengePluginsAdvanced])
|
||||
}
|
||||
variant="tertiary"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function BrowseFloatingActionButton() {
|
||||
const navigation = useNavigation<NavigationProp<any>>()
|
||||
|
||||
return (
|
||||
<FloatingActionButton
|
||||
icon={PlusLargeIcon}
|
||||
accessibilityLabel="Browse plugins"
|
||||
onPress={() =>
|
||||
navigation.navigate(RouteNames[Setting.RevengePluginsBrowse])
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function snapshotPlugins() {
|
||||
return [...pList.values()].map(
|
||||
plugin => [plugin, getInternalPluginMeta(plugin)] as const,
|
||||
)
|
||||
}
|
||||
|
||||
function Screen() {
|
||||
const navigation = useNavigation()
|
||||
const route =
|
||||
|
|
@ -114,18 +170,30 @@ function Screen() {
|
|||
[],
|
||||
)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => <HeaderButton />,
|
||||
})
|
||||
}, [navigation])
|
||||
|
||||
const filter = route.params?.filter ?? DefaultFilters
|
||||
const matchAll = route.params?.matchAll ?? true
|
||||
const reverse = route.params?.reverse ?? false
|
||||
const sort = route.params?.sort ?? DefaultSort
|
||||
|
||||
const allPlugins = useMemo(
|
||||
() =>
|
||||
[...pList.values()].map(
|
||||
plugin => [plugin, getInternalPluginMeta(plugin)!] as const,
|
||||
),
|
||||
[],
|
||||
)
|
||||
const [allPlugins, setAllPlugins] = useState(snapshotPlugins)
|
||||
|
||||
useEffect(() => {
|
||||
const handleUpdate = () => setAllPlugins(snapshotPlugins())
|
||||
|
||||
pEmitter.on('register', handleUpdate)
|
||||
pEmitter.on('unregister', handleUpdate)
|
||||
|
||||
return () => {
|
||||
pEmitter.off('register', handleUpdate)
|
||||
pEmitter.off('unregister', handleUpdate)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const plugins = useMemo(
|
||||
() =>
|
||||
|
|
@ -188,6 +256,7 @@ function Screen() {
|
|||
/>
|
||||
</Stack>
|
||||
<InstalledPluginMasonryFlashList plugins={plugins} />
|
||||
<BrowseFloatingActionButton />
|
||||
</>
|
||||
)
|
||||
}
|
||||
16
src/plugins/start/settings.plugins/types.ts
Normal file
16
src/plugins/start/settings.plugins/types.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import type { RouteNames, Setting } from './constants'
|
||||
|
||||
declare module '@revenge-mod/externals/react-navigation' {
|
||||
interface ReactNavigationParamList extends PluginsSettingsParamList {}
|
||||
}
|
||||
|
||||
type PluginsSettingsParamList = {
|
||||
[K in (typeof RouteNames)[keyof typeof RouteNames]]: object
|
||||
} & {
|
||||
[K in (typeof RouteNames)[(typeof Setting)['RevengePlugins']]]: {
|
||||
sort?: string
|
||||
filter?: string[]
|
||||
matchAll?: boolean
|
||||
reverse?: boolean
|
||||
}
|
||||
}
|
||||
89
src/plugins/start/settings.plugins/utils/actions.tsx
Normal file
89
src/plugins/start/settings.plugins/utils/actions.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
disablePlugin,
|
||||
enablePlugin,
|
||||
getMissingPluginDependencies,
|
||||
getPluginDependencies,
|
||||
getPluginDependents,
|
||||
isPluginEnabled,
|
||||
runPluginLate,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
installFromRepo,
|
||||
planInstall,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { noop } from '@revenge-mod/utils/callback'
|
||||
import {
|
||||
showPluginHasDependenciesAlert,
|
||||
showPluginHasDependentsAlert,
|
||||
showPluginMissingDependenciesAlert,
|
||||
} from './alerts'
|
||||
import { confirmPlan, messageOf, showErrorToast } from './repos'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { InstallPlan } from '@revenge-mod/plugins/_/repositories'
|
||||
|
||||
export async function handleEnablePlugin(plugin: AnyPlugin) {
|
||||
const missing = getMissingPluginDependencies(plugin)
|
||||
if (missing.length) {
|
||||
const deps = missing.map(id => ({
|
||||
id,
|
||||
range: plugin.manifest.dependencies?.[id]?.version ?? '*',
|
||||
}))
|
||||
|
||||
// Required deps can be uninstalled. Offer installing them back.
|
||||
showPluginMissingDependenciesAlert(plugin, deps, () => {
|
||||
// Non-async because we can't show 2 dialogs at the same time, so we need for the PluginMissing to close first
|
||||
// Then the ConfirmPlan dialog will open, and then the install will run after that.
|
||||
Promise.all(missing.map(id => planInstall(id)))
|
||||
.then(
|
||||
it =>
|
||||
it.reduce(
|
||||
(acc, cur) => ({
|
||||
actions: [...acc.actions, ...cur.actions],
|
||||
warnings: [...acc.warnings, ...cur.warnings],
|
||||
}),
|
||||
{ actions: [], warnings: [] },
|
||||
) satisfies InstallPlan,
|
||||
)
|
||||
.then(async plan => {
|
||||
const accepted = await confirmPlan(plan)
|
||||
if (!accepted) return false
|
||||
|
||||
await installFromRepo(plan)
|
||||
|
||||
await handleEnablePlugin(plugin)
|
||||
})
|
||||
.catch(e => showErrorToast(messageOf(e)))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const dependencies = getPluginDependencies(plugin)
|
||||
const disabledDeps = dependencies.filter(dep => !isPluginEnabled(dep))
|
||||
|
||||
async function action() {
|
||||
try {
|
||||
await enablePlugin(plugin)
|
||||
} catch (e) {
|
||||
// Requirements not satisfied by native, don't try to start
|
||||
showErrorToast(messageOf(e))
|
||||
return
|
||||
}
|
||||
|
||||
await runPluginLate(plugin).catch(noop)
|
||||
}
|
||||
|
||||
if (disabledDeps.length)
|
||||
showPluginHasDependenciesAlert(plugin, disabledDeps, action)
|
||||
else await action()
|
||||
}
|
||||
|
||||
export async function handleDisablePlugin(plugin: AnyPlugin) {
|
||||
const dependents = getPluginDependents(plugin, true)
|
||||
const action = () => disablePlugin(plugin)
|
||||
|
||||
const enabledDeps = dependents.filter(isPluginEnabled)
|
||||
|
||||
if (enabledDeps.length)
|
||||
showPluginHasDependentsAlert(plugin, enabledDeps, action)
|
||||
else await action()
|
||||
}
|
||||
|
|
@ -1,10 +1,18 @@
|
|||
import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
||||
import { RootNavigationRef } from '@revenge-mod/discord/modules/main_tabs_v2'
|
||||
import { deleteStorageForPlugin } from '~plugins/preinit/api.storage'
|
||||
import {
|
||||
deleteStorageForPlugin,
|
||||
isPluginStartable,
|
||||
uninstallExternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { deleteJsonStorageForPlugin } from '~plugins/preinit/api.json-storage'
|
||||
import PluginClearDataConfirmationAlert from '../components/PluginClearDataConfirmationAlert'
|
||||
import PluginHasDependenciesAlert from '../components/PluginHasDependenciesAlert'
|
||||
import PluginHasDependentsAlert from '../components/PluginHasDependentsAlert'
|
||||
import PluginMissingDependenciesAlert from '../components/PluginMissingDependenciesAlert'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import PluginUninstallConfirmationAlert from '../components/PluginUninstallConfirmationAlert'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export function showPluginClearDataConfirmation(
|
||||
|
|
@ -14,7 +22,13 @@ export function showPluginClearDataConfirmation(
|
|||
const KEY = 'plugin-clear-data-confirmation'
|
||||
|
||||
async function action() {
|
||||
await deleteStorageForPlugin(plugin)
|
||||
try {
|
||||
await deleteStorageForPlugin(plugin)
|
||||
// Trigger update for JSON storage
|
||||
await deleteJsonStorageForPlugin(plugin)
|
||||
} catch (e) {
|
||||
alert(getErrorStack(e))
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
|
|
@ -24,8 +38,26 @@ export function showPluginClearDataConfirmation(
|
|||
)
|
||||
}
|
||||
|
||||
export function showPluginUninstallConfirmation(
|
||||
plugin: AnyPlugin,
|
||||
callback: () => void,
|
||||
) {
|
||||
const KEY = 'plugin-uninstall-confirmation'
|
||||
|
||||
async function action() {
|
||||
// Uninstalling also deletes data
|
||||
await uninstallExternalPlugin(plugin)
|
||||
callback()
|
||||
}
|
||||
|
||||
AlertActionCreators.openAlert(
|
||||
KEY,
|
||||
<PluginUninstallConfirmationAlert plugin={plugin} action={action} />,
|
||||
)
|
||||
}
|
||||
|
||||
export function openPluginSettings(plugin: AnyPlugin) {
|
||||
if (!plugin.SettingsComponent) return
|
||||
if (!plugin.SettingsComponent || !isPluginStartable(plugin)) return
|
||||
|
||||
const navigation = RootNavigationRef.getRootNavigationRef()
|
||||
if (navigation.isReady()) navigation.navigate(plugin.manifest.id)
|
||||
|
|
@ -53,6 +85,21 @@ export function showPluginHasDependenciesAlert(
|
|||
)
|
||||
}
|
||||
|
||||
export function showPluginMissingDependenciesAlert(
|
||||
plugin: AnyPlugin,
|
||||
dependencies: { id: string; range: string }[],
|
||||
action: () => unknown,
|
||||
) {
|
||||
AlertActionCreators.openAlert(
|
||||
'plugin-missing-dependencies',
|
||||
<PluginMissingDependenciesAlert
|
||||
plugin={plugin}
|
||||
dependencies={dependencies}
|
||||
action={action}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
export function showPluginHasDependentsAlert(
|
||||
plugin: AnyPlugin,
|
||||
dependents: AnyPlugin[],
|
||||
172
src/plugins/start/settings.plugins/utils/repos.tsx
Normal file
172
src/plugins/start/settings.plugins/utils/repos.tsx
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import {
|
||||
AlertActionCreators,
|
||||
ToastActionCreators,
|
||||
} from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
|
||||
import { isPluginError } from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
installFromRepo,
|
||||
listRepoPlugins,
|
||||
listRepos,
|
||||
planInstall,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { PluginIcon } from '../components/PluginIcon'
|
||||
import type {
|
||||
InstallPlan,
|
||||
RepoPluginListing,
|
||||
} from '@revenge-mod/plugins/_/repositories'
|
||||
|
||||
const { AlertActionButton, AlertModal } = Design
|
||||
|
||||
const CircleXIconComponent = lookupGeneratedIconComponent(
|
||||
'CircleXIcon',
|
||||
'CircleXIcon-primary',
|
||||
'CircleXIcon-secondary',
|
||||
)!
|
||||
|
||||
export function showErrorToast(message: string) {
|
||||
ToastActionCreators.open({
|
||||
key: 'REVENGE_REPOSITORIES_ERROR',
|
||||
content: message,
|
||||
IconComponent: CircleXIconComponent,
|
||||
})
|
||||
}
|
||||
|
||||
export function messageOf(e: unknown) {
|
||||
if (e instanceof Error) return e.message
|
||||
if (isPluginError(e)) return e.message
|
||||
return String(e)
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const PlanConfirmAlertKey = 'repo-install-plan-confirm'
|
||||
|
||||
/**
|
||||
* Shows the resolved plan and asks before anything downloads.
|
||||
* Resolves true only when the user presses Install.
|
||||
*/
|
||||
export async function confirmPlan(plan: InstallPlan): Promise<boolean> {
|
||||
const repos = await listRepos()
|
||||
|
||||
let resolve!: (value: boolean) => void
|
||||
const promise = new Promise<boolean>(r => (resolve = r))
|
||||
const listingCache = new Map<string, RepoPluginListing[]>()
|
||||
|
||||
const summary = await Promise.all(
|
||||
plan.actions.map(async action => {
|
||||
const row = (
|
||||
label: string,
|
||||
repo: string,
|
||||
author?: string,
|
||||
icon?: string | null,
|
||||
) => (
|
||||
<Design.TableRow
|
||||
icon={<PluginIcon icon={icon ?? undefined} size={24} />}
|
||||
onPress={() => {
|
||||
Clipboard.setString(
|
||||
`${label} (${action.id}) v${action.version} (${action.channel})${author ? ` by ${author}` : ''} from ${repo} (${action.repo})\n\n` +
|
||||
`Replaces: ${action.replaces ?? '(None)'}\n` +
|
||||
`Download: ${action.url} (size: ${action.size})\n` +
|
||||
`SHA256: ${action.sha256}`,
|
||||
)
|
||||
}}
|
||||
label={
|
||||
action.replaces
|
||||
? `${label} • ${action.replaces} → ${action.version}`
|
||||
: `${label} • ${action.version}`
|
||||
}
|
||||
subLabel={`${author ? `${author} • ` : ''}${repo} • ${formatBytes(action.size)}`}
|
||||
/>
|
||||
)
|
||||
|
||||
const repo = repos.find(r => r.url === action.repo)
|
||||
if (!repo) return row(action.id, action.repo)
|
||||
|
||||
const plugins =
|
||||
listingCache.get(repo.url) ?? (await listRepoPlugins(repo.url))
|
||||
|
||||
const plugin = plugins.find(p => p.id === action.id)
|
||||
if (!plugin) return row(action.id, repo.name || action.repo)
|
||||
|
||||
return row(
|
||||
plugin.name || action.id,
|
||||
repo.name || action.repo,
|
||||
plugin.author,
|
||||
plugin.icon,
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
AlertActionCreators.openAlert(
|
||||
PlanConfirmAlertKey,
|
||||
<AlertModal
|
||||
title={
|
||||
plan.actions.length === 1
|
||||
? 'Install plugin?'
|
||||
: `Install ${plan.actions.length} plugins?`
|
||||
}
|
||||
content={
|
||||
plan.actions.length > 1
|
||||
? 'Some plugins require other plugins to be installed first. The following will be installed:'
|
||||
: undefined
|
||||
}
|
||||
extraContent={
|
||||
<Design.TableRowGroup>{summary}</Design.TableRowGroup>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<AlertActionButton
|
||||
text="Install"
|
||||
variant="primary"
|
||||
onPress={() => resolve(true)}
|
||||
/>
|
||||
<AlertActionButton
|
||||
text="Cancel"
|
||||
variant="secondary"
|
||||
onPress={() => resolve(false)}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>,
|
||||
)
|
||||
|
||||
return await promise
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the install flow for one plugin: resolve, show warnings, confirm the plan, and execute.
|
||||
* Errors show as toasts.
|
||||
*
|
||||
* Pass `version`, `channel`, and `filteredRepos` to pin what the user was shown, so the plan
|
||||
* matches the card instead of resolving to whatever a higher-priority repo serves.
|
||||
*
|
||||
* Resolves true only when the plan was confirmed and installed.
|
||||
*/
|
||||
export async function runInstallFlow(
|
||||
id: string,
|
||||
version?: string,
|
||||
channel?: string,
|
||||
filteredRepos?: string[],
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const plan = await planInstall(id, version, channel, filteredRepos)
|
||||
if (plan.warnings.length) showErrorToast(plan.warnings.join('\n'))
|
||||
|
||||
const accepted = await confirmPlan(plan)
|
||||
AlertActionCreators.dismissAlert(PlanConfirmAlertKey)
|
||||
if (!accepted) return false
|
||||
|
||||
await installFromRepo(plan)
|
||||
return true
|
||||
} catch (e) {
|
||||
showErrorToast(messageOf(e))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { ActionSheetActionCreators } from '@revenge-mod/discord/actions'
|
||||
import PluginStatesProvider from '../components/PluginStateProvider'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
import type { BrowsePluginActionSheetProps } from '../components/BrowsePluginActionSheet'
|
||||
import type { PluginOptionsActionSheetProps } from '../components/PluginOptionsActionSheet'
|
||||
|
||||
export function showPluginOptionsActionSheet(plugin: AnyPlugin) {
|
||||
|
|
@ -18,3 +19,15 @@ export function showPluginOptionsActionSheet(plugin: AnyPlugin) {
|
|||
{ plugin, sheetKey: KEY },
|
||||
)
|
||||
}
|
||||
|
||||
export function showBrowsePluginActionSheet(
|
||||
props: Omit<BrowsePluginActionSheetProps, 'sheetKey'>,
|
||||
) {
|
||||
const KEY = 'browse-plugin-action-sheet'
|
||||
|
||||
ActionSheetActionCreators.openLazy(
|
||||
import('../components/BrowsePluginActionSheet'),
|
||||
KEY,
|
||||
{ ...props, sheetKey: KEY },
|
||||
)
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import { styles } from '@revenge-mod/components/_'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { FlashList, MasonryFlashList } from '@shopify/flash-list'
|
||||
import { useWindowDimensions } from 'react-native'
|
||||
import {
|
||||
InstalledPluginCard,
|
||||
PLUGIN_CARD_ESTIMATED_SIZE,
|
||||
PluginCard,
|
||||
} from './PluginCard'
|
||||
import { useResetTooltips } from './TooltipProvider'
|
||||
import type { AnyPlugin, InternalPluginMeta } from '@revenge-mod/plugins/_'
|
||||
|
||||
const { Text } = Design
|
||||
|
||||
export function PluginFlashList({ plugins }: { plugins: AnyPlugin[] }) {
|
||||
const { width, height } = useWindowDimensions()
|
||||
const actualWidth = width - styles.pagePadding.paddingHorizontal * 2
|
||||
const resetTooltips = useResetTooltips()
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={plugins.length === 1 ? 0 : 16}
|
||||
keyExtractor={plugin => plugin.manifest.id}
|
||||
estimatedItemSize={PLUGIN_CARD_ESTIMATED_SIZE}
|
||||
estimatedListSize={{ width: actualWidth, height }}
|
||||
renderItem={({
|
||||
item: {
|
||||
manifest: { name, description, author, icon },
|
||||
},
|
||||
index,
|
||||
}) => (
|
||||
<PluginCard
|
||||
name={name}
|
||||
description={description}
|
||||
author={author}
|
||||
icon={icon}
|
||||
rowEnd={index === plugins.length - 1}
|
||||
columnEnd
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function InstalledPluginMasonryFlashList({
|
||||
plugins,
|
||||
}: {
|
||||
plugins: (readonly [AnyPlugin, InternalPluginMeta])[]
|
||||
}) {
|
||||
const { width, height } = useWindowDimensions()
|
||||
const actualWidth = width - styles.pagePadding.paddingHorizontal * 2
|
||||
const numColumns = Math.floor(actualWidth / 448) || 1
|
||||
const resetTooltips = useResetTooltips()
|
||||
|
||||
return (
|
||||
<MasonryFlashList
|
||||
contentContainerStyle={{ paddingBottom: 16 }}
|
||||
data={plugins}
|
||||
onScrollBeginDrag={resetTooltips}
|
||||
fadingEdgeLength={16}
|
||||
keyExtractor={([plugin]) => plugin.manifest.id}
|
||||
estimatedListSize={{ width: actualWidth, height }}
|
||||
estimatedItemSize={PLUGIN_CARD_ESTIMATED_SIZE}
|
||||
numColumns={numColumns}
|
||||
ListEmptyComponent={NoPlugins}
|
||||
renderItem={({ item: [plugin, meta], columnIndex, index }) => (
|
||||
<InstalledPluginCard
|
||||
plugin={plugin}
|
||||
meta={meta}
|
||||
rowEnd={
|
||||
index > plugins.length - numColumns &&
|
||||
columnIndex <= plugins.length % numColumns
|
||||
}
|
||||
columnEnd={columnIndex === numColumns - 1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NoPlugins() {
|
||||
return (
|
||||
<Text variant="heading-md/medium" style={{ textAlign: 'center' }}>
|
||||
No plugins found. Try changing your query or filters.
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
import { getAssetIdByName } from '@revenge-mod/assets'
|
||||
import { FormSwitch, TableRowAssetIcon } from '@revenge-mod/components'
|
||||
import {
|
||||
ActionSheetActionCreators,
|
||||
ToastActionCreators,
|
||||
} from '@revenge-mod/discord/actions'
|
||||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { Clipboard } from '@revenge-mod/externals/react-native-clipboard'
|
||||
import {
|
||||
getInternalPluginMeta,
|
||||
InternalPluginFlags,
|
||||
isPluginEssential,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags, PluginStatus } from '@revenge-mod/plugins/constants'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import { getErrorStack } from '@revenge-mod/utils/error'
|
||||
import { Pressable } from 'react-native'
|
||||
import { ClickOutsideProvider } from 'react-native-click-outside'
|
||||
import { handleDisablePlugin, handleEnablePlugin } from '../utils/actions'
|
||||
import {
|
||||
openPluginSettings,
|
||||
showPluginClearDataConfirmation,
|
||||
} from '../utils/alerts'
|
||||
import { PluginInfo } from './PluginCard'
|
||||
import { usePluginEnabled, usePluginStatus } from './PluginStateProvider'
|
||||
import {
|
||||
EnablePluginTooltipProvider,
|
||||
useClickOutsideTooltip,
|
||||
useEnablePluginTooltip,
|
||||
} from './TooltipProvider'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export interface PluginOptionsActionSheetProps {
|
||||
plugin: AnyPlugin
|
||||
sheetKey: string
|
||||
}
|
||||
|
||||
const { ActionSheet, IconButton, TableRowGroup, TableRow, Stack } = Design
|
||||
|
||||
const FileWarningIcon = getAssetIdByName('FileWarningIcon', 'png')!
|
||||
const SettingsIcon = getAssetIdByName('SettingsIcon', 'png')!
|
||||
|
||||
export default function PluginOptionsActionSheet({
|
||||
plugin,
|
||||
sheetKey,
|
||||
}: PluginOptionsActionSheetProps) {
|
||||
const status = usePluginStatus(plugin)
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
const meta = getInternalPluginMeta(plugin)
|
||||
const { dependencies, dependents } = meta!
|
||||
const essential = isPluginEssential(meta)
|
||||
const {
|
||||
manifest: { id, name, author, description, icon },
|
||||
errors,
|
||||
flags,
|
||||
} = plugin
|
||||
|
||||
return (
|
||||
<ActionSheet>
|
||||
<Stack spacing={24} style={{ paddingTop: 8 }}>
|
||||
<PluginInfo
|
||||
name={name}
|
||||
author={author}
|
||||
description={description}
|
||||
icon={icon}
|
||||
actions={
|
||||
!essential && (
|
||||
<FormSwitch
|
||||
value={enabled}
|
||||
onValueChange={v => {
|
||||
if (v) handleEnablePlugin(plugin)
|
||||
else handleDisablePlugin(plugin)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ClickOutsideProvider>
|
||||
<EnablePluginTooltipProvider>
|
||||
<PluginActions
|
||||
plugin={plugin}
|
||||
closeSheet={() => {
|
||||
ActionSheetActionCreators.hideActionSheet(
|
||||
sheetKey,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</EnablePluginTooltipProvider>
|
||||
</ClickOutsideProvider>
|
||||
<TableRowGroup title="Status">
|
||||
<TableRow
|
||||
icon={
|
||||
<TableRowAssetIcon name="CircleInformationIcon" />
|
||||
}
|
||||
label="Status (TODO)"
|
||||
subLabel={bitFieldToString(PluginStatus, status)}
|
||||
/>
|
||||
{errors.length && (
|
||||
<TableRow
|
||||
variant="danger"
|
||||
label="Errors"
|
||||
icon={
|
||||
<TableRowAssetIcon
|
||||
variant="danger"
|
||||
name="CircleErrorIcon"
|
||||
/>
|
||||
}
|
||||
subLabel={`${errors.length} errors. Tap to copy.`}
|
||||
onPress={() => {
|
||||
Clipboard.setString(
|
||||
errors.map(getErrorStack).join('\n'),
|
||||
)
|
||||
showCopiedToClipboardToast()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</TableRowGroup>
|
||||
<TableRowGroup title="Advanced">
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="IdIcon" />}
|
||||
label="ID"
|
||||
subLabel={id}
|
||||
onPress={() => {
|
||||
Clipboard.setString(id)
|
||||
showCopiedToClipboardToast()
|
||||
}}
|
||||
/>
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="FlagIcon" />}
|
||||
label="Flags"
|
||||
subLabel={bitFieldToString(PluginFlags, flags)}
|
||||
/>
|
||||
{meta.iflags && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="FlagIcon" />}
|
||||
label="Internal Flags"
|
||||
subLabel={bitFieldToString(
|
||||
InternalPluginFlags,
|
||||
meta.iflags,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{dependencies?.length && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependencies (TODO)"
|
||||
subLabel={`${name} depends on ${dependencies.length} other plugins`}
|
||||
/>
|
||||
)}
|
||||
{dependents?.length && (
|
||||
<TableRow
|
||||
icon={<TableRowAssetIcon name="ListBulletsIcon" />}
|
||||
label="Dependents (TODO)"
|
||||
subLabel={`${dependents.length} other plugins depend on ${name}`}
|
||||
/>
|
||||
)}
|
||||
</TableRowGroup>
|
||||
</Stack>
|
||||
</ActionSheet>
|
||||
)
|
||||
}
|
||||
|
||||
function bitFieldToString(map: Record<string, number>, bitField: number) {
|
||||
return (
|
||||
Object.entries(map)
|
||||
.filter(([, value]) => bitField & value)
|
||||
.map(([key]) => key)
|
||||
.join(', ') || '-'
|
||||
)
|
||||
}
|
||||
|
||||
function PluginActions({
|
||||
plugin,
|
||||
closeSheet,
|
||||
}: {
|
||||
plugin: AnyPlugin
|
||||
closeSheet: () => void
|
||||
}) {
|
||||
const enabled = usePluginEnabled(plugin)
|
||||
const enableTooltip = useEnablePluginTooltip()
|
||||
const settingsRef = useClickOutsideTooltip(useEnablePluginTooltip, () => {})
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
justify="space-around"
|
||||
style={{ paddingHorizontal: 8, paddingVertical: 16 }}
|
||||
>
|
||||
<IconButton
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={FileWarningIcon}
|
||||
label="Clear Data"
|
||||
onPress={() => {
|
||||
showPluginClearDataConfirmation(plugin, closeSheet)
|
||||
}}
|
||||
/>
|
||||
{plugin.SettingsComponent && (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (enabled) return
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
enableTooltip.targetRef.current =
|
||||
settingsRef.current
|
||||
enableTooltip.setVisible(true)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<Design.IconButton
|
||||
ref={settingsRef}
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
icon={SettingsIcon}
|
||||
label="Settings"
|
||||
disabled={!enabled}
|
||||
onPress={() => {
|
||||
openPluginSettings(plugin)
|
||||
closeSheet()
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const CopyIcon = lookupGeneratedIconComponent('CopyIcon')!
|
||||
|
||||
function showCopiedToClipboardToast() {
|
||||
ToastActionCreators.open({
|
||||
key: 'REVENGE_PLUGIN_SETTINGS_COPIED',
|
||||
content: 'Copied to clipboard',
|
||||
IconComponent: CopyIcon,
|
||||
})
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ export const Setting = {
|
|||
// MAIN SETTINGS
|
||||
|
||||
Revenge: 'Revenge',
|
||||
RevengePlugins: 'RevengePlugins',
|
||||
|
||||
// SUBSETTINGS
|
||||
|
||||
|
|
@ -20,5 +19,4 @@ export const Setting = {
|
|||
|
||||
export const RouteNames = {
|
||||
[Setting.Revenge]: 'Revenge',
|
||||
[Setting.RevengePlugins]: 'Revenge Plugins',
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -4,8 +4,11 @@ import { waitForModuleWithImportedPath } from '@revenge-mod/discord/utils/module
|
|||
import { waitForModules } from '@revenge-mod/modules/finders'
|
||||
import { withName } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { React } from '@revenge-mod/react'
|
||||
import { asap, noop } from '@revenge-mod/utils/callback'
|
||||
import { getCurrentStack } from '@revenge-mod/utils/error'
|
||||
|
|
@ -15,7 +18,7 @@ import type { FC } from 'react'
|
|||
|
||||
let DEBUG_patchedNavigator = false
|
||||
|
||||
const pluginSettings = registerPlugin(
|
||||
const pluginSettings = registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.settings',
|
||||
name: 'Settings',
|
||||
|
|
@ -25,9 +28,6 @@ const pluginSettings = registerPlugin(
|
|||
},
|
||||
{
|
||||
start() {
|
||||
// @as-require
|
||||
import('./plugins')
|
||||
|
||||
onSettingsModulesLoaded(() => {
|
||||
// @as-require
|
||||
import('./register')
|
||||
|
|
@ -40,7 +40,7 @@ const pluginSettings = registerPlugin(
|
|||
})
|
||||
|
||||
waitForModuleWithImportedPath(
|
||||
'modules/user_settings/native/core/SettingsNavigator.tsx',
|
||||
'modules/user_settings/core/native/SettingsNavigator.tsx',
|
||||
exports => {
|
||||
patchSettingsNavigator(exports)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ import ReactVersionSetting from './definitions/ReactVersionSetting'
|
|||
import ReloadSetting from './definitions/ReloadSetting'
|
||||
import RevengeDiscordSetting from './definitions/RevengeDiscordSetting'
|
||||
import RevengeLicenseSetting from './definitions/RevengeLicenseSetting'
|
||||
import RevengePluginsSetting from './definitions/RevengePluginsSetting'
|
||||
import RevengeSetting from './definitions/RevengeSetting'
|
||||
import RevengeSourceRepositorySetting from './definitions/RevengeSourceRepositorySetting'
|
||||
import RevengeVersionSetting from './definitions/RevengeVersionSetting'
|
||||
|
||||
registerSettingsItems({
|
||||
[Setting.Revenge]: RevengeSetting,
|
||||
[Setting.RevengePlugins]: RevengePluginsSetting,
|
||||
[Setting.RevengeSourceRepository]: RevengeSourceRepositorySetting,
|
||||
[Setting.RevengeLicense]: RevengeLicenseSetting,
|
||||
[Setting.RevengeDiscord]: RevengeDiscordSetting,
|
||||
|
|
@ -31,5 +29,5 @@ registerSettingsItems({
|
|||
|
||||
registerSettingsSection('REVENGE', {
|
||||
label: 'Revenge',
|
||||
settings: [Setting.Revenge, Setting.RevengePlugins],
|
||||
settings: [Setting.Revenge],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { RouteNames, Setting } from './constants'
|
||||
import type { RouteNames } from './constants'
|
||||
|
||||
declare module '@revenge-mod/externals/react-navigation' {
|
||||
interface ReactNavigationParamList extends RevengeSettingsParamList {}
|
||||
|
|
@ -6,11 +6,4 @@ declare module '@revenge-mod/externals/react-navigation' {
|
|||
|
||||
type RevengeSettingsParamList = {
|
||||
[K in (typeof RouteNames)[keyof typeof RouteNames]]: object
|
||||
} & {
|
||||
[K in (typeof RouteNames)[(typeof Setting)['RevengePlugins']]]: {
|
||||
sort?: string
|
||||
filter?: string[]
|
||||
matchAll?: boolean
|
||||
reverse?: boolean
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import {
|
||||
disablePlugin,
|
||||
enablePlugin,
|
||||
getInternalPluginMeta,
|
||||
getPluginDependencies,
|
||||
isPluginEnabled,
|
||||
runPluginLate,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import {
|
||||
showPluginHasDependenciesAlert,
|
||||
showPluginHasDependentsAlert,
|
||||
} from './alerts'
|
||||
import type { AnyPlugin } from '@revenge-mod/plugins/_'
|
||||
|
||||
export async function handleEnablePlugin(plugin: AnyPlugin) {
|
||||
const dependencies = getPluginDependencies(plugin)!
|
||||
const disabledDeps = dependencies?.filter(dep => !isPluginEnabled(dep))
|
||||
|
||||
async function action() {
|
||||
await enablePlugin(plugin)
|
||||
await runPluginLate(plugin)
|
||||
}
|
||||
|
||||
if (disabledDeps?.length)
|
||||
showPluginHasDependenciesAlert(plugin, disabledDeps, action)
|
||||
else await action()
|
||||
}
|
||||
|
||||
export async function handleDisablePlugin(plugin: AnyPlugin) {
|
||||
const { dependents } = getInternalPluginMeta(plugin)!
|
||||
const action = () => disablePlugin(plugin)
|
||||
|
||||
const enabledDeps = dependents.filter(isPluginEnabled)
|
||||
|
||||
if (enabledDeps.length)
|
||||
showPluginHasDependentsAlert(plugin, enabledDeps, action)
|
||||
else await action()
|
||||
}
|
||||
|
|
@ -3,12 +3,15 @@ import { getStore, Stores } from '@revenge-mod/discord/flux'
|
|||
import { getModules } from '@revenge-mod/modules/finders'
|
||||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { instead } from '@revenge-mod/patcher'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { lookupGeneratedIconComponent } from '@revenge-mod/utils/discord'
|
||||
import type { DiscordModules } from '@revenge-mod/discord/types'
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.staff-settings',
|
||||
name: 'Staff Settings',
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import { AlertActionCreators } from '@revenge-mod/discord/actions'
|
|||
import { Design } from '@revenge-mod/discord/design'
|
||||
import { getModules } from '@revenge-mod/modules/finders'
|
||||
import { withProps } from '@revenge-mod/modules/finders/filters'
|
||||
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
|
||||
import { PluginFlags } from '@revenge-mod/plugins/constants'
|
||||
import {
|
||||
InternalPluginFlags,
|
||||
PluginFlags,
|
||||
registerInternalPlugin,
|
||||
} from '@revenge-mod/plugins/_'
|
||||
import { afterJSX, beforeJSX } from '@revenge-mod/react/jsx-runtime'
|
||||
import { findInReactFiber } from '@revenge-mod/utils/react'
|
||||
import { Image } from 'react-native'
|
||||
|
|
@ -47,7 +50,7 @@ type ViewWithProfileBadges = ReactElement<{
|
|||
children: Array<ReactElement<ProfileBadgeProps, FC<ProfileBadgeProps>>>
|
||||
}>
|
||||
|
||||
registerPlugin(
|
||||
registerInternalPlugin(
|
||||
{
|
||||
id: 'revenge.user-badges',
|
||||
name: 'User Badges',
|
||||
|
|
@ -92,7 +95,7 @@ registerPlugin(
|
|||
cleanup(unsub)
|
||||
},
|
||||
stop({ plugin }) {
|
||||
plugin.flags |= PluginFlags.ReloadRequired
|
||||
plugin.requireReload()
|
||||
},
|
||||
},
|
||||
PluginFlags.Enabled,
|
||||
|
|
|
|||
4
types/globals.d.ts
vendored
4
types/globals.d.ts
vendored
|
|
@ -109,6 +109,10 @@ declare global {
|
|||
var nativeModuleProxy: Record<string, unknown>
|
||||
var __turboModuleProxy: ((name: string) => unknown) | undefined
|
||||
function nativeLoggingHook(str: string, level: number): void
|
||||
function RN$registerCallableModule(
|
||||
name: string,
|
||||
moduleProvider: () => object,
|
||||
): void
|
||||
function alert(message: unknown): void
|
||||
function queueMicrotask(cb: () => void): void
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue