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:
Palm 2026-07-25 23:48:15 +04:00 • committed by GitHub
commit f6a796f02f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 4286 additions and 1186 deletions

View file

@ -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
View file

@ -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=="],
}
}

View file

@ -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]
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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']

View file

@ -1,5 +1,5 @@
{
"name": "@revenge-mod/storage",
"name": "@revenge-mod/json-storage",
"private": true,
"type": "module",
"exports": {

View file

@ -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'

View file

@ -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()
}

View 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']>>
}
}

View file

@ -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],

View file

@ -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!**

View file

@ -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

View file

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

View file

@ -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': [
[],
{

View file

@ -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]
}

View file

@ -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"
}
}
}

View file

@ -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)

View file

@ -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)
}

View 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' },
]
}
}

View file

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

View 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[] },
]
}
}

View file

@ -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,
}

View file

@ -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}`

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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
View 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}` : '')

View file

@ -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']>>
}
}

View file

@ -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"
}
}
}

View file

@ -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]
}
}

View file

@ -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'

View file

@ -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',

View 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()
}

View file

@ -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()
}

View file

@ -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',

View file

@ -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',

View file

@ -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,

View file

@ -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',
}

View file

@ -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',
}

View file

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

View file

@ -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,

View file

@ -29,6 +29,6 @@ export function register() {
[Setting.AssetBrowser]: AssetBrowserSetting,
[Setting.TestErrorBoundary]: TestErrorBoundarySetting,
}),
addSettingsItemToSection('REVENGE', Setting.RevengeDeveloper),
addSettingsItemToSection('REVENGE', Setting.RevengeDeveloper, -1),
)
}

View file

@ -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',

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -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',

View file

@ -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={

View file

@ -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={

View 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,
},
})

View file

@ -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)}
/>
</>
}
/>
)
}

View file

@ -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" />
</>
}
/>
)
}

View 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>
)
}

View file

@ -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" />
</>
}
/>
)
}

View file

@ -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,
})
}

View file

@ -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)
}

View file

@ -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" />
</>
}
/>
)
}

View 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

View file

@ -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

View file

@ -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

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

View file

@ -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()

View 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)

View file

@ -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>
)
}

View file

@ -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}
/>
</>
)
}

View file

@ -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 />
</>
)
}

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

View 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()
}

View file

@ -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[],

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

View file

@ -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 },
)
}

View file

@ -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>
)
}

View file

@ -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,
})
}

View file

@ -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

View file

@ -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)
},

View file

@ -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],
})

View file

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

View file

@ -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()
}

View file

@ -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',

View file

@ -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
View file

@ -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