8 Commits

Author SHA1 Message Date
鲁树人
d514a87198 0.6.0 2025-10-15 22:47:59 +09:00
鲁树人
5c537ab8d9 ci: simplify release 2025-10-15 22:47:59 +09:00
鲁树人
48f10e8e30 fix: layout issue with android instruction in mobile screen 2025-10-15 22:08:51 +09:00
鲁树人
dfac382cbd Merge pull request '添加 Mac QQ 音乐 v10 的说明' (#3) from feat/mac-qqmusic-v10 into main
Reviewed-on: https://git.um-react.app/um/um-react/pulls/3
2025-10-15 12:59:19 +00:00
鲁树人
7b2558c585 feat: update instructions and scripts for QQMusic Mac v10.7 and v8.8.0 2025-10-15 02:08:32 +09:00
鲁树人
fb52b0197c feat: add insturctions on how to dump keys for v10 2025-10-15 00:57:56 +09:00
鲁树人
f49f629917 build: allow vite to access 'index.html' 2025-10-14 07:54:21 +09:00
鲁树人
8093d30579 chore: remove npm package lock file 2025-10-14 07:53:54 +09:00
22 changed files with 652 additions and 11988 deletions

View File

@@ -26,6 +26,35 @@ jobs:
with: with:
name: site name: site
path: dist/ path: dist/
- name: Create Release Package
if: startsWith(github.ref, 'refs/tags/')
run: |
VERSION=${GITHUB_REF#refs/tags/v}
mkdir release/
python3 -m zipfile -c "release/um-react-${VERSION}.zip" dist/.
cp win64/dist/*.zip "release/um-react-win64-${VERSION}.zip"
- name: Create Draft Release
if: startsWith(github.ref, 'refs/tags/')
uses: akkuman/gitea-release-action@v1
with:
draft: true
files: |
release/um-react-*.zip
release/um-react-win64-*.zip
body: |
上个版本:[v0.0.0](https://git.um-react.app/um/um-react/releases/tag/v0.0.0)
## 🐛 修正
- 修正内容
## ✨ 新增
- 新增内容
## 🔧 维护
- 维护内容
- name: Prepare for deployment - name: Prepare for deployment
run: | run: |
cp um-react.zip dist/"release-${GITHUB_SHA}.zip" cp um-react.zip dist/"release-${GITHUB_SHA}.zip"

11870
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "um-react", "name": "um-react",
"private": true, "private": true,
"version": "0.5.2", "version": "0.6.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@@ -44,6 +44,7 @@
"@types/react-dom": "^19.1.9", "@types/react-dom": "^19.1.9",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/sql.js": "^1.4.9", "@types/sql.js": "^1.4.9",
"@types/tar-stream": "^3.1.4",
"@types/wicg-file-system-access": "^2023.10.6", "@types/wicg-file-system-access": "^2023.10.6",
"@typescript-eslint/eslint-plugin": "^8.42.0", "@typescript-eslint/eslint-plugin": "^8.42.0",
"@typescript-eslint/parser": "^8.42.0", "@typescript-eslint/parser": "^8.42.0",
@@ -64,6 +65,7 @@
"sass": "^1.92.1", "sass": "^1.92.1",
"simple-git-hooks": "^2.13.1", "simple-git-hooks": "^2.13.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"tar-stream": "^3.1.7",
"terser": "^5.44.0", "terser": "^5.44.0",
"typescript": "^5.9.2", "typescript": "^5.9.2",
"typescript-eslint": "^8.42.0", "typescript-eslint": "^8.42.0",

104
pnpm-lock.yaml generated
View File

@@ -92,6 +92,9 @@ importers:
'@types/sql.js': '@types/sql.js':
specifier: ^1.4.9 specifier: ^1.4.9
version: 1.4.9 version: 1.4.9
'@types/tar-stream':
specifier: ^3.1.4
version: 3.1.4
'@types/wicg-file-system-access': '@types/wicg-file-system-access':
specifier: ^2023.10.6 specifier: ^2023.10.6
version: 2023.10.6 version: 2023.10.6
@@ -152,6 +155,9 @@ importers:
tailwindcss: tailwindcss:
specifier: ^4.1.13 specifier: ^4.1.13
version: 4.1.13 version: 4.1.13
tar-stream:
specifier: ^3.1.7
version: 3.1.7
terser: terser:
specifier: ^5.44.0 specifier: ^5.44.0
version: 5.44.0 version: 5.44.0
@@ -1533,6 +1539,9 @@ packages:
'@types/sql.js@1.4.9': '@types/sql.js@1.4.9':
resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==} resolution: {integrity: sha512-ep8b36RKHlgWPqjNG9ToUrPiwkhwh0AEzy883mO5Xnd+cL6VBH1EvSjBAAuxLUFF2Vn/moE3Me6v9E1Lo+48GQ==}
'@types/tar-stream@3.1.4':
resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==}
'@types/trusted-types@2.0.7': '@types/trusted-types@2.0.7':
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
@@ -1744,6 +1753,14 @@ packages:
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
b4a@1.7.3:
resolution: {integrity: sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==}
peerDependencies:
react-native-b4a: '*'
peerDependenciesMeta:
react-native-b4a:
optional: true
babel-plugin-polyfill-corejs2@0.4.14: babel-plugin-polyfill-corejs2@0.4.14:
resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==}
peerDependencies: peerDependencies:
@@ -1762,6 +1779,14 @@ packages:
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bare-events@2.8.0:
resolution: {integrity: sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==}
peerDependencies:
bare-abort-controller: '*'
peerDependenciesMeta:
bare-abort-controller:
optional: true
brace-expansion@1.1.12: brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
@@ -2122,6 +2147,9 @@ packages:
eventemitter3@5.0.1: eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
events-universal@1.0.1:
resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==}
expect-type@1.2.2: expect-type@1.2.2:
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
@@ -2129,6 +2157,9 @@ packages:
fast-deep-equal@3.1.3: fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
fast-glob@3.3.3: fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'} engines: {node: '>=8.6.0'}
@@ -2792,15 +2823,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'} engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.0.2: minizlib@3.1.0:
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
hasBin: true
mrmime@2.0.1: mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -3275,6 +3301,9 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
streamx@2.23.0:
resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==}
string-argv@0.3.2: string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'} engines: {node: '>=0.6.19'}
@@ -3352,8 +3381,11 @@ packages:
resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==}
engines: {node: '>=6'} engines: {node: '>=6'}
tar@7.4.3: tar-stream@3.1.7:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tar@7.5.1:
resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==}
engines: {node: '>=18'} engines: {node: '>=18'}
temp-dir@2.0.0: temp-dir@2.0.0:
@@ -3373,6 +3405,9 @@ packages:
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
engines: {node: '>=18'} engines: {node: '>=18'}
text-decoder@1.2.3:
resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==}
tinybench@2.9.0: tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
@@ -4995,7 +5030,7 @@ snapshots:
'@tailwindcss/oxide@4.1.13': '@tailwindcss/oxide@4.1.13':
dependencies: dependencies:
detect-libc: 2.0.4 detect-libc: 2.0.4
tar: 7.4.3 tar: 7.5.1
optionalDependencies: optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.13 '@tailwindcss/oxide-android-arm64': 4.1.13
'@tailwindcss/oxide-darwin-arm64': 4.1.13 '@tailwindcss/oxide-darwin-arm64': 4.1.13
@@ -5115,6 +5150,10 @@ snapshots:
'@types/emscripten': 1.41.1 '@types/emscripten': 1.41.1
'@types/node': 24.3.1 '@types/node': 24.3.1
'@types/tar-stream@3.1.4':
dependencies:
'@types/node': 24.3.1
'@types/trusted-types@2.0.7': {} '@types/trusted-types@2.0.7': {}
'@types/unist@2.0.11': {} '@types/unist@2.0.11': {}
@@ -5383,6 +5422,8 @@ snapshots:
dependencies: dependencies:
possible-typed-array-names: 1.1.0 possible-typed-array-names: 1.1.0
b4a@1.7.3: {}
babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4):
dependencies: dependencies:
'@babel/compat-data': 7.28.4 '@babel/compat-data': 7.28.4
@@ -5409,6 +5450,8 @@ snapshots:
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
bare-events@2.8.0: {}
brace-expansion@1.1.12: brace-expansion@1.1.12:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -5832,10 +5875,18 @@ snapshots:
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
events-universal@1.0.1:
dependencies:
bare-events: 2.8.0
transitivePeerDependencies:
- bare-abort-controller
expect-type@1.2.2: {} expect-type@1.2.2: {}
fast-deep-equal@3.1.3: {} fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
fast-glob@3.3.3: fast-glob@3.3.3:
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -6493,12 +6544,10 @@ snapshots:
minipass@7.1.2: {} minipass@7.1.2: {}
minizlib@3.0.2: minizlib@3.1.0:
dependencies: dependencies:
minipass: 7.1.2 minipass: 7.1.2
mkdirp@3.0.1: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
ms@2.1.3: {} ms@2.1.3: {}
@@ -6982,6 +7031,15 @@ snapshots:
es-errors: 1.3.0 es-errors: 1.3.0
internal-slot: 1.1.0 internal-slot: 1.1.0
streamx@2.23.0:
dependencies:
events-universal: 1.0.1
fast-fifo: 1.3.2
text-decoder: 1.2.3
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
string-argv@0.3.2: {} string-argv@0.3.2: {}
string-width@4.2.3: string-width@4.2.3:
@@ -7079,13 +7137,21 @@ snapshots:
tapable@2.2.3: {} tapable@2.2.3: {}
tar@7.4.3: tar-stream@3.1.7:
dependencies:
b4a: 1.7.3
fast-fifo: 1.3.2
streamx: 2.23.0
transitivePeerDependencies:
- bare-abort-controller
- react-native-b4a
tar@7.5.1:
dependencies: dependencies:
'@isaacs/fs-minipass': 4.0.1 '@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0 chownr: 3.0.0
minipass: 7.1.2 minipass: 7.1.2
minizlib: 3.0.2 minizlib: 3.1.0
mkdirp: 3.0.1
yallist: 5.0.0 yallist: 5.0.0
temp-dir@2.0.0: {} temp-dir@2.0.0: {}
@@ -7110,6 +7176,12 @@ snapshots:
glob: 10.4.5 glob: 10.4.5
minimatch: 9.0.5 minimatch: 9.0.5
text-decoder@1.2.3:
dependencies:
b4a: 1.7.3
transitivePeerDependencies:
- react-native-b4a
tinybench@2.9.0: {} tinybench@2.9.0: {}
tinyexec@0.3.2: {} tinyexec@0.3.2: {}

View File

@@ -9,10 +9,10 @@ export function RootExplorerGuide() {
<div className="flex flex-col items-start gap-4 @md:flex-row"> <div className="flex flex-col items-start gap-4 @md:flex-row">
<div> <div>
<Header5 className="[&]:mt-0 [&]:pt-0">Amaze </Header5> <Header5 className="[&]:mt-0 [&]:pt-0">Amaze </Header5>
<ul className="ml-2 list-disc list-inside"> <ul className="ml-2 list-disc">
<li> <li>
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center flex-wrap">
<FiMenu /> <FiMenu className="m-1" />
</div> </div>
</li> </li>
<li> <li>
@@ -41,16 +41,16 @@ export function RootExplorerGuide() {
</div> </div>
<div> <div>
<Header5 className="[&]:mt-0 [&]:pt-0">MT </Header5> <Header5 className="[&]:mt-0 [&]:pt-0">MT </Header5>
<ul className="ml-2 list-disc list-inside"> <ul className="ml-2 list-disc">
<li> <li>
<div className="inline-flex items-center gap-1"> <div className="inline-flex items-center flex-wrap">
<FiMenu /> <FiMenu className="m-1" />
</div> </div>
</li> </li>
<li> <li>
<div className="inline-flex items-center"> <div className="inline-flex items-center flex-wrap">
<FiMoreVertical className="ml-1" /> <FiMoreVertical className="ml-1" />
<VQuote></VQuote> <VQuote></VQuote>
</div> </div>
</li> </li>
<li> <li>

View File

@@ -0,0 +1,32 @@
import type { ReactNode } from 'react';
import { ExtLink } from './ExtLink';
import { IoMdArchive } from 'react-icons/io';
export type DownloadBase64Props = {
data: string;
filename: string;
mimetype?: string;
className?: string;
icon?: boolean | ReactNode;
children?: ReactNode;
};
export function DownloadBase64({
className,
children,
data,
filename,
icon,
mimetype = 'application/octet-stream',
}: DownloadBase64Props) {
return (
<ExtLink
icon={icon ?? <IoMdArchive className="inline size-sm ml-1" />}
className={className ?? 'link-info mx-1'}
download={filename}
href={`data:${mimetype};base64,${data}`}
>
{children ?? <code>{filename}</code>}
</ExtLink>
);
}

View File

@@ -1,15 +1,15 @@
import type { AnchorHTMLAttributes } from 'react'; import type { AnchorHTMLAttributes, ReactNode } from 'react';
import { FiExternalLink } from 'react-icons/fi'; import { FiExternalLink } from 'react-icons/fi';
export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & { export type ExtLinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
icon?: boolean; icon?: boolean | ReactNode;
}; };
export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) { export function ExtLink({ className, icon = true, children, ...props }: ExtLinkProps) {
return ( return (
<a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}> <a rel="noreferrer noopener nofollow" target="_blank" className={`link ${className}`} {...props}>
{children} {children}
{icon && <FiExternalLink className="inline size-sm ml-1" />} {icon === true ? <FiExternalLink className="inline size-sm ml-1" /> : icon}
</a> </a>
); );
} }

View File

@@ -5,7 +5,7 @@ export function Footer() {
const appVersionShort = '__APP_VERSION_SHORT__'; const appVersionShort = '__APP_VERSION_SHORT__';
return ( return (
<footer className="flex flex-col text-center p-4 bg-base-200"> <footer className="flex flex-col text-center p-4 bg-base-200">
<p className="flex flex-row justify-center items-center h-[1em]"> <div className="flex flex-row justify-center items-center h-[1em]">
<a className="link link-info mr-1" href="https://git.um-react.app/um/um-react"> <a className="link link-info mr-1" href="https://git.um-react.app/um/um-react">
</a> </a>
@@ -19,7 +19,7 @@ export function Footer() {
</a> </a>
, v{appVersionShort} , v{appVersionShort}
<SDKVersion />) <SDKVersion />)
</p> </div>
<p> <p>
{'© 2019 - '} {'© 2019 - '}
<CurrentYear /> <CurrentYear />

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { FileInput } from '~/components/FileInput'; import { FileInput } from '~/components/FileInput';
import { InSecretImportModalContext } from '~/context/InSecretImportModal';
export interface ImportSecretModalProps { export interface ImportSecretModalProps {
clientName?: React.ReactNode; clientName?: React.ReactNode;
@@ -31,7 +32,7 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
}, [show]); }, [show]);
return ( return (
<dialog ref={refModel} className="modal"> <dialog ref={refModel} className="modal" onClose={onClose}>
<div className="modal-box"> <div className="modal-box">
<form method="dialog" onSubmit={() => onClose()}> <form method="dialog" onSubmit={() => onClose()}>
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
@@ -41,7 +42,9 @@ export function ImportSecretModal({ clientName, children, show, onClose, onImpor
<FileInput onReceiveFiles={handleFileReceived}></FileInput> <FileInput onReceiveFiles={handleFileReceived}></FileInput>
<div className="mt-2">{clientName && <>{clientName}</>}</div> <div className="mt-2">{clientName && <>{clientName}</>}</div>
<div>{children}</div> <InSecretImportModalContext.Provider value={true}>
<div>{children}</div>
</InSecretImportModalContext.Provider>
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@@ -0,0 +1,3 @@
import { createContext } from 'react';
export const InSecretImportModalContext = createContext<boolean>(false);

View File

@@ -16,7 +16,7 @@ export function OtherFAQ() {
<div className="flex flex-col md:flex-row gap-2 md:gap-8"> <div className="flex flex-col md:flex-row gap-2 md:gap-8">
<div> <div>
<Header4></Header4> <Header4></Header4>
<ul className="list-disc list-inside pl-2"> <ul className="list-disc pl-8">
<li>Via </li> <li>Via </li>
<li></li> <li></li>
<li>UC </li> <li>UC </li>
@@ -25,7 +25,7 @@ export function OtherFAQ() {
<div> <div>
<Header4></Header4> <Header4></Header4>
<ul className="list-disc list-inside pl-2"> <ul className="list-disc pl-8">
<li></li> <li></li>
<li></li> <li></li>
<li></li> <li></li>

View File

@@ -1,94 +1,31 @@
import { RiFileCopyLine } from 'react-icons/ri'; import { useId } from 'react';
import { toast } from 'react-toastify'; import { InstructionsMacV8 } from './InstructionsMacV8';
import { ExtLink } from '~/components/ExtLink'; import { InstructionsMacV10 } from './InstructionsMacV10';
import { FilePathBlock } from '~/components/FilePathBlock';
import { VQuote } from '~/components/HelpText/VQuote';
import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey';
import BlockUpdateScript from './assets/QQ 音乐 Mac 屏蔽升级.tar.gz?base64';
const MAC_CLIENT_URL =
'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
const DB_PATH =
'~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
export function InstructionsMac() { export function InstructionsMac() {
const copyDbPathToClipboard = () => { const macInstructionId = useId();
navigator.clipboard
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return ( return (
<> <>
<p>Mac 使 mmkv </p> <p>Mac 使 mmkv </p>
<p> v8.8.0 </p> <p>使 v8.8.0 v10.7 v8.8.0 </p>
<p className="mt-4"> QQ Mac 8.8.0:</p> <div className="join join-vertical bg-base-100 mt-2 max-w-full">
<ul className="list-disc pl-6"> <div className="collapse collapse-arrow join-item border-base-300 border">
<li> <input type="radio" name={macInstructionId} />
<ExtLink className="link-info" href={MAC_CLIENT_URL}> <div className="collapse-title font-semibold">使 QQ Mac v8.8.0</div>
<code>Archive.org</code> <div className="collapse-content text-sm min-w-0">
</ExtLink> <InstructionsMacV8 />
</li> </div>
<li> </div>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}> <div className="collapse collapse-arrow join-item border-base-300 border">
Telegram <input type="radio" name={macInstructionId} />
</ExtLink> <div className="collapse-title font-semibold">使 QQ Mac v10.7.1</div>
</li> <div className="collapse-content text-sm min-w-0">
</ul> <InstructionsMacV10 />
</div>
<p className="mt-4"> </div>
</div>
<ExtLink
className="link-info mx-1"
download="QQ 音乐 Mac 屏蔽升级.tar.gz"
href={`data:application/gzip;base64,${BlockUpdateScript}`}
>
QQ Mac .tar.gz
</ExtLink>
<code>QQ Mac .command</code> QQ
</p>
<p className="mt-4"></p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
<RiFileCopyLine className="text-xl" />
<span></span>
</button>
<code>MMKVStreamEncryptId</code>
</li>
<li>
<VQuote></VQuote><VQuote></VQuote>
</li>
<li>
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
<VQuote></VQuote>
</li>
<li>
<code>MMKVStreamEncryptId</code>
</li>
<li></li>
</ol>
</> </>
); );
} }

View File

@@ -0,0 +1,65 @@
import { ExtLink } from '~/components/ExtLink';
import {
commandName as DUMP_COMMAND_NAME,
tarName as DUMP_COMMAND_TARBALL_NAME,
tarball as DUMP_COMMAND_BASE64,
} from './assets/qqmusic_v10.7_dump.command?&name=QQ 音乐 Mac v10 密钥提取.command&mac-command';
import { DownloadBase64 } from '~/components/DownloadBase64';
import { VQuote } from '~/components/HelpText/VQuote';
import { InSecretImportModalContext } from '~/context/InSecretImportModal';
import { useContext } from 'react';
const MAC_CLIENT_URL =
'https://c.y.qq.com/cgi-bin/file_redirect.fcg?bid=dldir&file=ecosfile%2Fmusic_clntupate%2Fmac%2Fother%2FQQMusicMac10.7.1Build00.dmg&sign=1-0cb9ee4c40e7447e2113cfdee2dc11c88487b0e31fe37cfe1c59e12c20956dce-689e9373';
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/30';
export function InstructionsMacV10() {
const inSecretImportModal = useContext(InSecretImportModalContext);
return (
<>
<p className="mt-4"> QQ Mac 10.7.1:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
QQ
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
Telegram
</ExtLink>
</li>
</ul>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<DownloadBase64 data={DUMP_COMMAND_BASE64} filename={DUMP_COMMAND_TARBALL_NAME}></DownloadBase64>
<code>{DUMP_COMMAND_NAME}</code>
</li>
<li>
<p>
<code>{DUMP_COMMAND_NAME}</code>
</p>
<p>
<VQuote></VQuote><VQuote></VQuote>
</p>
</li>
<li>
<code>qqmusic-mac-*.mmkv</code> <code>*</code>
</li>
{inSecretImportModal ? (
<li>
<code>qqmusic-mac-*.mmkv</code> <VQuote></VQuote>
</li>
) : (
<li>
<code>qqmusic-mac-*.mmkv</code>
</li>
)}
</ol>
</>
);
}

View File

@@ -0,0 +1,89 @@
import { RiFileCopyLine } from 'react-icons/ri';
import { ExtLink } from '~/components/ExtLink';
import { FilePathBlock } from '~/components/FilePathBlock';
import { VQuote } from '~/components/HelpText/VQuote';
import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey';
import { copyToClipboard } from '~/util/clipboard';
import {
commandName as BLOCK_UPDATE_COMAND,
tarName as BLOCK_UPDATE_TAR_NAME,
tarball as BLOCK_UPDATE_BASE64,
} from './assets/qqmusic_v8.8.0_patch_update.command?&name=QQ 音乐 Mac v8.8.0 屏蔽更新.command&mac-command';
import { DownloadBase64 } from '~/components/DownloadBase64';
import { useContext } from 'react';
import { InSecretImportModalContext } from '~/context/InSecretImportModal';
const MAC_CLIENT_URL =
'https://web.archive.org/web/20230903/https://dldir1.qq.com/music/clntupate/mac/QQMusicMac_Mgr.dmg';
const MAC_CLIENT_TG_URL = 'https://t.me/um_lsr_ch/21';
const DB_PATH =
'~/Library/Containers/com.tencent.QQMusicMac/Data/Library/Application Support/QQMusicMac/mmkv/MMKVStreamEncryptId';
export function InstructionsMacV8() {
const inSecretImportModal = useContext(InSecretImportModalContext);
return (
<>
<p className="mt-4"> QQ Mac 8.8.0:</p>
<ul className="list-disc pl-6">
<li>
<ExtLink className="link-info" href={MAC_CLIENT_URL}>
<code>Archive.org</code>
</ExtLink>
</li>
<li>
<ExtLink className="link-info" href={MAC_CLIENT_TG_URL}>
Telegram
</ExtLink>
</li>
</ul>
<p className="mt-4">
<DownloadBase64 filename={BLOCK_UPDATE_TAR_NAME} data={BLOCK_UPDATE_BASE64}></DownloadBase64>
<code>{BLOCK_UPDATE_COMAND}</code>
<span> QQ </span>
</p>
<p>
<VQuote></VQuote><VQuote></VQuote>
</p>
<p className="mt-4"></p>
<FilePathBlock>{DB_PATH}</FilePathBlock>
<h4 className="font-bold text-lg mt-4"></h4>
<ol className="list-decimal pl-6">
<li>
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={() => copyToClipboard(DB_PATH)}>
<RiFileCopyLine className="text-xl" />
<span></span>
</button>
<code>MMKVStreamEncryptId</code>
</li>
<li>
{inSecretImportModal ? (
<p>
<VQuote></VQuote><VQuote></VQuote>
</p>
) : (
<p></p>
)}
<p>
<VQuote>
<ShiftKey className="mx-1" />
{'+'}
<MacCommandKey className="mx-1" />
{'+'}
<kbd className="kbd mx-1">G</kbd>
</VQuote>
<VQuote></VQuote>
</p>
</li>
</ol>
</>
);
}

View File

@@ -0,0 +1,3 @@
com.tencent.QQMusicMac.plist
iData/
qqmusic-mac-*.mmkv

View File

@@ -0,0 +1,213 @@
#!/usr/bin/env python3
# QQMusic Mac MMKV Decryptor by LSR@Unlock Music
import hashlib
import re
import sys
from argparse import ArgumentParser
from dataclasses import dataclass
from os import PathLike
from os.path import dirname
from pathlib import Path
from struct import pack, unpack
@dataclass
class MMKVDecryptionData:
udid: str
mmkv_path: Path
mmkv_key: str
data: bytes
@property
def mmkv_name(self) -> str:
return self.mmkv_path.name
def _aes_128_cfb_decrypt(key: bytes, iv: bytes, ciphertext: bytes) -> bytes:
"""Decrypt using `Crypto.Cipher.AES` _or_ fallback to `OpenSSL` otherwise"""
try:
from Crypto.Cipher import AES # pyright: ignore[reportMissingImports]
aes = AES.new(key[:16], AES.MODE_CFB, iv=iv, segment_size=128)
return aes.decrypt(ciphertext)
except ImportError:
from subprocess import PIPE, Popen
process = Popen(
["openssl", "enc", "-aes-128-cfb", "-d", "-K", key.hex(), "-iv", iv.hex()],
stdin=PIPE,
stdout=PIPE,
stderr=PIPE,
text=False,
)
stdout, stderr = process.communicate(input=ciphertext)
if process.returncode != 0:
raise RuntimeError(
f"OpenSSL error (install PyCryptodome instead): {stderr.decode()}"
)
return stdout
def _caesar(text: str, shift: int) -> str:
"""A simple Caesar cipher implementation for alphanumeric characters"""
result = ""
for char in text:
if char.isalpha():
base = ord("A") if char.isupper() else ord("a")
result += chr((ord(char) - base + shift) % 26 + base)
elif char.isdigit():
result += chr((ord(char) - ord("0") + shift) % 10 + ord("0"))
else:
result += char
return result
__MMKV_TYPE_STREAM_KEY = 1
def _derive_mmkv_config(udid: str, mmkv_type: int):
"""Derive MMKV name and key from UDID, return (name, key)"""
str1 = _caesar(udid, mmkv_type + 3)
int1 = int(udid[5:7], 16)
int2 = 5 + (int1 + mmkv_type) % 4
mmkv_name = str1[0:int2]
int3 = mmkv_type + 0xA546
str3 = f"{udid}{int3:04x}"
mmkv_key = hashlib.md5(str3.encode()).hexdigest()
return mmkv_name, mmkv_key
def _decrypt_mmkv(path: PathLike, key: bytes):
"""Decrypt MMKV file using the given key, return decrypted data"""
with open(path, "rb") as mmkv, open(str(path) + ".crc", "rb") as crc:
crc.seek(12)
iv = crc.read(16)
(real_size,) = unpack("<I", crc.read(4))
(mmkv_payload_size,) = unpack("<I", mmkv.read(4))
if mmkv_payload_size != real_size:
raise ValueError("MMKV file size mismatch")
decrypted_data = pack("<I", real_size)
decrypted_data += _aes_128_cfb_decrypt(key, iv, mmkv.read(real_size))
return decrypted_data
def _dump_udid(plist_file: PathLike):
"""Extract UDIDs from the given plist file"""
with open(plist_file, "rb") as f:
plist = f.read()
for m in re.finditer(rb"_\x10\(([0-9a-f]{40})_", plist):
yield m.group(1).decode()
def _dump_mmkv(plist_file: PathLike, data_dir: PathLike):
"""Dump all MMKV files from the given plist file and iData directory"""
for udid in _dump_udid(plist_file):
mmkv_name, mmkv_key = _derive_mmkv_config(udid, __MMKV_TYPE_STREAM_KEY)
mmkv_path = Path(data_dir) / mmkv_name
if not mmkv_path.exists() or not mmkv_path.is_file():
print(f"MMKV file not found, skipping (path={mmkv_path})", file=sys.stderr)
continue
try:
decrypted_mmkv = _decrypt_mmkv(mmkv_path, mmkv_key.encode())
except Exception as e:
print(
"Error decrypting mmkv, skipping"
f" (path={mmkv_path}, key={mmkv_key}, error={e})",
file=sys.stderr,
)
continue
yield MMKVDecryptionData(
udid=udid,
mmkv_path=mmkv_path,
mmkv_key=mmkv_key,
data=decrypted_mmkv,
)
def main():
parser = ArgumentParser(
description="QQMusic Mac MMKV Decryptor by LSR@Unlock Music"
)
parser.add_argument(
"-p",
"--plist",
type=str,
nargs="+",
help="Path to com.tencent.QQMusicMac.plist file or files",
default=[],
)
parser.add_argument(
"-i",
"--idata",
type=str,
help="Path to iData directory",
default="",
)
parser.add_argument("-f", "--force", action="store_true", help="Force overwrite")
parser.add_argument(
"-o",
"--output",
type=str,
help="Output directory for decrypted MMKV files (default: script directory)",
default=dirname(__file__),
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="Enable verbose output"
)
args = parser.parse_args()
home_dir = Path.home()
app_sandbox_dir = home_dir / "Library/Containers/com.tencent.QQMusicMac/Data"
idata_dir = app_sandbox_dir / "Library/Application Support/QQMusicMac/iData"
if args.idata:
idata_dir = Path(args.idata)
plists = []
if args.plist:
plists = [Path(p) for p in args.plist]
else:
for base_dir in (home_dir, app_sandbox_dir):
plists.append(base_dir / "Library/Preferences/com.tencent.QQMusicMac.plist")
output_dir = Path(args.output)
output_dir.mkdir(parents=True, exist_ok=True)
force = args.force
verbose = args.verbose
for plist_file in plists:
if plist_file.exists() and plist_file.is_file():
for dump in _dump_mmkv(plist_file, idata_dir):
out_path = output_dir / f"qqmusic-mac-{dump.mmkv_path.name}.mmkv"
if out_path.exists() and not force:
print(f"output exists, skipping (name={out_path.name})")
continue
if verbose:
print("*** MMKV DUMP ENTRY START ***")
print(f"UDID: {dump.udid}")
print(f"MMKV Name: {dump.mmkv_path.name}")
print(f"MMKV Key: {dump.mmkv_key}")
print(f"Output: {out_path.name}")
print("**** MMKV DUMP ENTRY END ****")
else:
print(f"Dumping mmkv: {out_path.name}...")
try:
with open(out_path, "wb") as f:
f.write(dump.data)
except Exception as e:
print(f"Error writing decrypted mmkv: {e}", file=sys.stderr)
continue
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,28 @@
#!/bin/sh
echo '补丁中…'
patch_count=0
patch_qqmusic() {
SUDO="$1"
APP="$2"
if [ ! -d "$APP" ]; then
echo "路径不存在,跳过 $APP..."
return
fi
echo "修补 $APP..."
$SUDO sed -i.bak 's#<string>8.8.0</string>#<string>88.8.0</string>#' \
"$APP/Contents/Info.plist"
$SUDO codesign --force --deep --sign - "$APP"
$SUDO xattr -d com.apple.quarantine "$APP"
patch_count=$((patch_count + 1))
}
patch_qqmusic sudo "/Applications/QQMusic.app"
patch_qqmusic "" "$HOME/Applications/QQMusic.app"
echo "完成,已修补 $patch_count 个 QQ 音乐安装"

12
src/util/clipboard.ts Normal file
View File

@@ -0,0 +1,12 @@
import { toast } from 'react-toastify';
export const copyToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};

6
src/vite-env.d.ts vendored
View File

@@ -11,3 +11,9 @@ declare module '*?base64' {
const content: string; const content: string;
export default content; export default content;
} }
declare module '*&mac-command' {
export const tarball: string;
export const commandName: string;
export const tarName: string;
}

View File

@@ -0,0 +1,37 @@
import { basename } from 'node:path';
import { readFile } from 'node:fs/promises';
import { Plugin } from 'vite';
import tar from 'tar-stream';
export const macCommandLoader: Plugin = {
name: 'mac-command-loader',
async transform(_: unknown, id: string) {
const [path, query] = id.split('?');
if (!query || !query.includes('mac-command')) return null;
const params = new URLSearchParams(query);
// Mac .command packer.
// - Create a tarball with the given file (a+x)
// - Encode to base64
const tarball = tar.pack();
const name = params.get('name') || `${basename(path)}.command`;
const data = await readFile(path);
tarball.entry({ name, mode: 0o755 }, data);
tarball.finalize();
const chunks: Buffer[] = [];
for await (const chunk of tarball) {
chunks.push(chunk as Buffer);
}
const dataBuffer = Buffer.concat(chunks);
const base64 = dataBuffer.toString('base64');
return `
export const tarball = ${JSON.stringify(base64)};
export const commandName = ${JSON.stringify(name)};
export const tarName = ${JSON.stringify(`${name}.tar`)};
`;
},
};

View File

@@ -12,6 +12,7 @@ import tailwindcss from '@tailwindcss/vite';
import { tryCommand } from './support/command'; import { tryCommand } from './support/command';
import { base64Loader } from './support/b64-loader'; import { base64Loader } from './support/b64-loader';
import { macCommandLoader } from './support/mac-command-loader';
const projectRoot = url.fileURLToPath(new URL('.', import.meta.url)); const projectRoot = url.fileURLToPath(new URL('.', import.meta.url));
const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8')); const pkg = JSON.parse(fs.readFileSync(projectRoot + '/package.json', 'utf-8'));
@@ -32,6 +33,7 @@ export default defineConfig({
// strict: false, // strict: false,
allow: [ allow: [
'index.html',
'src', 'src',
'node_modules', 'node_modules',
@@ -46,6 +48,7 @@ export default defineConfig({
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
base64Loader, base64Loader,
macCommandLoader,
replace({ replace({
preventAssignment: true, preventAssignment: true,
values: { values: {