From bde81e44219439abca99514a3ebf8252d103ff5c Mon Sep 17 00:00:00 2001 From: WakelessSloth56 Date: Tue, 16 Jul 2024 18:45:35 +0800 Subject: [PATCH] refactor: build Co-authored-by: lainio24 --- .editorconfig | 15 + .github/workflows/pages.yml | 28 +- .gitignore | 8 +- .prettierignore | 7 +- .vscode/settings.json | 2 +- README.md | 28 +- build.sh | 2 + package.json | 61 + pnpm-lock.yaml | 1125 ++++++++++++++++++ rollup.config.js | 39 + scripts/build-html.cjs | 38 + scripts/build-style.cjs | 16 + scripts/const.cjs | 47 + scripts/jsconfig.json | 7 + scripts/terser.config.cjs | 4 + scripts/utils.cjs | 27 + src/core/data.ts | 564 +++++++++ src/core/index.ts | 14 + src/{player.metadata.ts => core/metadata.ts} | 9 +- src/{player.main.ts => core/player.ts} | 20 +- src/{ => core}/utils.ts | 65 +- src/html/index.html | 124 ++ src/lib/CommentCoreLibrary.d.ts | 78 ++ src/lib/assjs.d.ts | 14 + src/player.player.ts | 556 --------- src/style/player.css | 226 ++++ tsconfig.json | 10 +- 27 files changed, 2490 insertions(+), 644 deletions(-) create mode 100644 .editorconfig create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 rollup.config.js create mode 100644 scripts/build-html.cjs create mode 100644 scripts/build-style.cjs create mode 100644 scripts/const.cjs create mode 100644 scripts/jsconfig.json create mode 100644 scripts/terser.config.cjs create mode 100644 scripts/utils.cjs create mode 100644 src/core/data.ts create mode 100644 src/core/index.ts rename src/{player.metadata.ts => core/metadata.ts} (92%) rename src/{player.main.ts => core/player.ts} (94%) rename src/{ => core}/utils.ts (62%) create mode 100644 src/html/index.html create mode 100644 src/lib/CommentCoreLibrary.d.ts create mode 100644 src/lib/assjs.d.ts delete mode 100644 src/player.player.ts create mode 100644 src/style/player.css diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ce873b1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.yml] +indent_size = 2 diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index c08225f..9f2bf50 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -19,27 +19,39 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - - name: Build + - name: Setup PNPM + uses: pnpm/action-setup@v4 + with: + version: 9 + run_install: false + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'pnpm' + + - name: Install and Build run: | - chmod +x ./build.sh - ./build.sh + pnpm install + pnpm run build - name: Archive Artifact shell: sh run: | tar \ --dereference --hard-dereference \ - --directory "./public" \ + --directory "./build" \ -cvf "$RUNNER_TEMP/artifact.tar" \ . - name: Upload Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: github-pages path: ${{ runner.temp }}/artifact.tar @@ -55,4 +67,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index c02ccbe..73464db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ temp node_modules -public/lib -src/lib +src/lib/* +!src/lib/*.d.ts -public/*.js -public/*.js.map +public +build diff --git a/.prettierignore b/.prettierignore index e750ce1..8e4880c 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,4 @@ -src/lib/* -assets/lib/* -assets/dist/* +src/lib/*.js +src/lib/*.css +public/* +build/* diff --git a/.vscode/settings.json b/.vscode/settings.json index f23cea0..0d4b7f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "cSpell.words": ["danmaku", "danmakuload", "stime"], + "cSpell.words": ["assjs", "danmaku", "danmakuload", "stime"], "conventionalCommits.scopes": ["repo", "utils", "player", "pages", "start"], "prettier.printWidth": 120, "markdownlint.config": { diff --git a/README.md b/README.md index 4a1e244..4c0731b 100644 --- a/README.md +++ b/README.md @@ -11,28 +11,32 @@ ## 关于 使用 TypeScript 重构并开源了很早之前 PCC-Studio 编写与内部使用的项目。 -> ~~LainIO24:早期的雪山代码并没有得到重构,反而堆得更高力(悲~~ -自提交 `9eddaae4` 起加入了更多的新功能。 +
-## 使用 +🏔 + +早期的雪山代码并没有得到重构,反而堆得更高力(悲 -- [网站版](https://hi.auioc.org/BilibiliLocalPlayerH5) -- [单文件版](https://hi.auioc.org/BilibiliLocalPlayerH5/aio.html):使用“另存为……”可保存为单个 HTML 文件到本地离线使用。 +
-两版本功能完全相同。 +## 使用 + +- ## 开发 1. 克隆项目到本地 -2. 下载 [`CommentCoreLibrary.js`](https://github.com/jabbany/CommentCoreLibrary/raw/19db2962ed0ce637a2b99facdf8634d51bb1b503/dist/CommentCoreLibrary.js) 和 [`ass.js`](https://github.com/weizhenye/ASS/raw/e6a3605a2343655d9ef80bdd7e9fe92f92edca22/dist/ass.js) 到 `src/lib` -3. 运行 `tsc` 指令构建 `public/player.js` -4. 浏览器打开 `public/index.html` 即可使用或调试 +2. `pnpm install` +3. 下载 `CommentCoreLibrary` 到 `src/lib`[^1]: + + - [`CommentCoreLibrary.js`](https://unpkg.com/comment-core-library@0.11.1/dist/CommentCoreLibrary.js) + - [`CommentCoreLibrary.css`](https://unpkg.com/comment-core-library@0.11.1/dist/css/style.css) -注意事项: +4. 运行 `pnpm run build:dev` +5. 浏览器打开 `public/index.html` -- 本项目构建不需要项目级别的 NPM 包,需要的只有用于编译 TypeScript 的全局包 `typescript`。 -- `build.sh` 脚本用于持续集成自动构建,开发时一般不需要使用,在本地使用可能会破坏项目结构! +[^1]: ## 致谢 diff --git a/build.sh b/build.sh index a6d871a..b7c193a 100644 --- a/build.sh +++ b/build.sh @@ -1,6 +1,8 @@ #!/bin/sh mkdir temp +mkdir public +mkdir build # NPM npm install -g typescript diff --git a/package.json b/package.json new file mode 100644 index 0000000..9138ded --- /dev/null +++ b/package.json @@ -0,0 +1,61 @@ +{ + "name": "bilibili-local-player-h5", + "version": "2.0.0", + "description": "bilibili-local-player-h5", + "main": "index.js", + "type": "module", + "scripts": { + "dev:html": "cross-env NODE_ENV=development node scripts/build-html.cjs", + "dev:script": "cross-env NODE_ENV=development rollup -c --bundleConfigAsCjs", + "dev:style": "cross-env NODE_ENV=development node scripts/build-style.cjs", + "prod:html": "cross-env NODE_ENV=production node scripts/build-html.cjs", + "prod:script": "cross-env NODE_ENV=production rollup -c --bundleConfigAsCjs", + "prod:style": "cross-env NODE_ENV=production node scripts/build-style.cjs", + "watch:html": "onchange -i \"src/html/*\" -- pnpm run dev:html", + "watch:script": "rollup -c rollup.config.js --bundleConfigAsCjs --watch", + "watch:style": "onchange -i \"src/style/*\" -- pnpm run dev:style", + "build:dev": "pnpm run dev:html && pnpm run dev:style && pnpm run dev:script", + "build": "pnpm run prod:html && pnpm run prod:style && pnpm run prod:script" + }, + "keywords": [], + "homepage": "https://hi.auioc.org/BilibiliLocalPlayerH5/", + "author": { + "name": "PCC-Studio", + "url": "https://www.pccstudio.com" + }, + "contributors": [ + { + "name": "LainIO24", + "email": "lainio24@outlook.com", + "url": "https://github.com/lainio24" + }, + { + "name": "WakelessSloth56", + "url": "https://github.com/WakelessSloth56" + }, + { + "name": "AUIOC", + "url": "https://www.auioc.org" + } + ], + "license": "AGPL-3.0-or-later", + "dependencies": { + "assjs": "^0.0.11" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-legacy": "^3.0.2", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^11.1.6", + "@types/node": "^20.14.10", + "cross-env": "^7.0.3", + "html-minifier-terser": "^7.2.0", + "node-html-parser": "^6.1.13", + "onchange": "^7.1.0", + "rollup": "^4.18.1", + "terser": "^5.31.2", + "tslib": "^2.6.3", + "typescript": "^5.5.3" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..6b1901c --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1125 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + assjs: + specifier: ^0.0.11 + version: 0.0.11 + devDependencies: + '@rollup/plugin-commonjs': + specifier: ^26.0.1 + version: 26.0.1(rollup@4.18.1) + '@rollup/plugin-legacy': + specifier: ^3.0.2 + version: 3.0.2(rollup@4.18.1) + '@rollup/plugin-replace': + specifier: ^5.0.7 + version: 5.0.7(rollup@4.18.1) + '@rollup/plugin-terser': + specifier: ^0.4.4 + version: 0.4.4(rollup@4.18.1) + '@rollup/plugin-typescript': + specifier: ^11.1.6 + version: 11.1.6(rollup@4.18.1)(tslib@2.6.3)(typescript@5.5.3) + '@types/node': + specifier: ^20.14.10 + version: 20.14.10 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + html-minifier-terser: + specifier: ^7.2.0 + version: 7.2.0 + node-html-parser: + specifier: ^6.1.13 + version: 6.1.13 + onchange: + specifier: ^7.1.0 + version: 7.1.0 + rollup: + specifier: ^4.18.1 + version: 4.18.1 + terser: + specifier: ^5.31.2 + version: 5.31.2 + tslib: + specifier: ^2.6.3 + version: 2.6.3 + typescript: + specifier: ^5.5.3 + version: 5.5.3 + +packages: + + '@blakeembrey/deque@1.0.5': + resolution: {integrity: sha512-6xnwtvp9DY1EINIKdTfvfeAtCYw4OqBZJhtiqkT3ivjnEfa25VQ3TsKvaFfKm8MyGIEfE95qLe+bNEt3nB0Ylg==} + + '@blakeembrey/template@1.1.0': + resolution: {integrity: sha512-iZf+UWfL+DogJVpd/xMQyP6X6McYd6ArdYoPMiv/zlOTzeXXfQbYxBNJJBF6tThvsjLMbA8tLjkCdm9RWMFCCw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.6': + resolution: {integrity: sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@rollup/plugin-commonjs@26.0.1': + resolution: {integrity: sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-legacy@3.0.2': + resolution: {integrity: sha512-4VEUHi8HfAyZpj6HXRPxDW9y3dS9FNAigK6l/S54jfRCEb2gtDGr/wGeiqOtXNgaRj385vYeFtmX7wwS9XkgPg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-replace@5.0.7': + resolution: {integrity: sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-terser@0.4.4': + resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-typescript@11.1.6': + resolution: {integrity: sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.14.0||^3.0.0||^4.0.0 + tslib: '*' + typescript: '>=3.7.0' + peerDependenciesMeta: + rollup: + optional: true + tslib: + optional: true + + '@rollup/pluginutils@5.1.0': + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.18.1': + resolution: {integrity: sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.18.1': + resolution: {integrity: sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.18.1': + resolution: {integrity: sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.18.1': + resolution: {integrity: sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-linux-arm-gnueabihf@4.18.1': + resolution: {integrity: sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.18.1': + resolution: {integrity: sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.18.1': + resolution: {integrity: sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.18.1': + resolution: {integrity: sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.1': + resolution: {integrity: sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.18.1': + resolution: {integrity: sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.18.1': + resolution: {integrity: sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.18.1': + resolution: {integrity: sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.18.1': + resolution: {integrity: sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.18.1': + resolution: {integrity: sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.18.1': + resolution: {integrity: sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.18.1': + resolution: {integrity: sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.5': + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + '@types/node@20.14.10': + resolution: {integrity: sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==} + + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + assjs@0.0.11: + resolution: {integrity: sha512-4h55j9RzOeDSWz93NPNb/+r9oycXHthaTk/LOV2eENIRK+WbaixEn2bjuMsYYCYTxab03F/8fMkwU6jfrbvKCg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + clean-css@5.3.3: + resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==} + engines: {node: '>= 10.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + foreground-child@3.2.1: + resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} + engines: {node: '>=14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + html-minifier-terser@7.2.0: + resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} + engines: {node: ^14.13.1 || >=16.0.0} + hasBin: true + + ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.14.0: + resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + magic-string@0.30.10: + resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + + node-html-parser@6.1.13: + resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + onchange@7.1.0: + resolution: {integrity: sha512-ZJcqsPiWUAUpvmnJri5TPBooqJOPmC0ttN65juhN15Q8xA+Nbg3BaxBHXQ45EistKKlKElb0edmbPWnKSBkvMg==} + hasBin: true + + package-json-from-dist@1.0.0: + resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + + param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + + pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + rollup@4.18.1: + resolution: {integrity: sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + smob@1.5.0: + resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + terser@5.31.2: + resolution: {integrity: sha512-LGyRZVFm/QElZHy/CPr/O4eNZOZIzsrQ92y4v9UJe/pFJjypje2yI3C2FmPtvUEnhadlSbmG2nXtdcjHOjCfxw==} + engines: {node: '>=10'} + hasBin: true + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + tslib@2.6.3: + resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==} + + typescript@5.5.3: + resolution: {integrity: sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + +snapshots: + + '@blakeembrey/deque@1.0.5': {} + + '@blakeembrey/template@1.1.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/source-map@0.3.6': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@rollup/plugin-commonjs@26.0.1(rollup@4.18.1)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.1) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 10.4.5 + is-reference: 1.2.1 + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.18.1 + + '@rollup/plugin-legacy@3.0.2(rollup@4.18.1)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.1) + optionalDependencies: + rollup: 4.18.1 + + '@rollup/plugin-replace@5.0.7(rollup@4.18.1)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.1) + magic-string: 0.30.10 + optionalDependencies: + rollup: 4.18.1 + + '@rollup/plugin-terser@0.4.4(rollup@4.18.1)': + dependencies: + serialize-javascript: 6.0.2 + smob: 1.5.0 + terser: 5.31.2 + optionalDependencies: + rollup: 4.18.1 + + '@rollup/plugin-typescript@11.1.6(rollup@4.18.1)(tslib@2.6.3)(typescript@5.5.3)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.1) + resolve: 1.22.8 + typescript: 5.5.3 + optionalDependencies: + rollup: 4.18.1 + tslib: 2.6.3 + + '@rollup/pluginutils@5.1.0(rollup@4.18.1)': + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + optionalDependencies: + rollup: 4.18.1 + + '@rollup/rollup-android-arm-eabi@4.18.1': + optional: true + + '@rollup/rollup-android-arm64@4.18.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.18.1': + optional: true + + '@rollup/rollup-darwin-x64@4.18.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.18.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.18.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.18.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.18.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.18.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.18.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.18.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.18.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.18.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.18.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.18.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.18.1': + optional: true + + '@types/estree@1.0.5': {} + + '@types/node@20.14.10': + dependencies: + undici-types: 5.26.5 + + acorn@8.12.1: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + assjs@0.0.11: {} + + balanced-match@1.0.2: {} + + binary-extensions@2.3.0: {} + + boolbase@1.0.0: {} + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + camel-case@4.1.2: + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.3 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + clean-css@5.3.3: + dependencies: + source-map: 0.6.1 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@10.0.1: {} + + commander@2.20.3: {} + + commondir@1.0.1: {} + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.3 + + cross-spawn@7.0.3: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + + css-what@6.1.0: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.3 + + eastasianwidth@0.2.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + entities@4.5.0: {} + + estree-walker@2.0.2: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + foreground-child@3.2.1: + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@10.4.5: + dependencies: + foreground-child: 3.2.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 1.11.1 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + html-minifier-terser@7.2.0: + dependencies: + camel-case: 4.1.2 + clean-css: 5.3.3 + commander: 10.0.1 + entities: 4.5.0 + param-case: 3.0.4 + relateurl: 0.2.7 + terser: 5.31.2 + + ignore@5.3.1: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.14.0: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.5 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + lower-case@2.0.2: + dependencies: + tslib: 2.6.3 + + lru-cache@10.4.3: {} + + magic-string@0.30.10: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minipass@7.1.2: {} + + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.6.3 + + node-html-parser@6.1.13: + dependencies: + css-select: 5.1.0 + he: 1.2.0 + + normalize-path@3.0.0: {} + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + onchange@7.1.0: + dependencies: + '@blakeembrey/deque': 1.0.5 + '@blakeembrey/template': 1.1.0 + arg: 4.1.3 + chokidar: 3.6.0 + cross-spawn: 7.0.3 + ignore: 5.3.1 + tree-kill: 1.2.2 + + package-json-from-dist@1.0.0: {} + + param-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.3 + + pascal-case@3.1.2: + dependencies: + no-case: 3.0.4 + tslib: 2.6.3 + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + picomatch@2.3.1: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + relateurl@0.2.7: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.14.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.18.1: + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.18.1 + '@rollup/rollup-android-arm64': 4.18.1 + '@rollup/rollup-darwin-arm64': 4.18.1 + '@rollup/rollup-darwin-x64': 4.18.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.18.1 + '@rollup/rollup-linux-arm-musleabihf': 4.18.1 + '@rollup/rollup-linux-arm64-gnu': 4.18.1 + '@rollup/rollup-linux-arm64-musl': 4.18.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.18.1 + '@rollup/rollup-linux-riscv64-gnu': 4.18.1 + '@rollup/rollup-linux-s390x-gnu': 4.18.1 + '@rollup/rollup-linux-x64-gnu': 4.18.1 + '@rollup/rollup-linux-x64-musl': 4.18.1 + '@rollup/rollup-win32-arm64-msvc': 4.18.1 + '@rollup/rollup-win32-ia32-msvc': 4.18.1 + '@rollup/rollup-win32-x64-msvc': 4.18.1 + fsevents: 2.3.3 + + safe-buffer@5.2.1: {} + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + smob@1.5.0: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.0.1 + + supports-preserve-symlinks-flag@1.0.0: {} + + terser@5.31.2: + dependencies: + '@jridgewell/source-map': 0.3.6 + acorn: 8.12.1 + commander: 2.20.3 + source-map-support: 0.5.21 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tree-kill@1.2.2: {} + + tslib@2.6.3: {} + + typescript@5.5.3: {} + + undici-types@5.26.5: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..191d30a --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,39 @@ +import typescript from '@rollup/plugin-typescript'; +import replace from '@rollup/plugin-replace'; +import terser from '@rollup/plugin-terser'; +const { version, srcPath } = require('./scripts/utils.cjs'); +const { javascript: terserOptions } = require('./scripts/terser.config.cjs'); + +const dev = process.env.NODE_ENV !== 'production'; + +const ccl = '../lib/CommentCoreLibrary.js'; + +export default [ + { + input: 'src/core/index.ts', + external: [ccl, 'assjs'], + output: [ + { + file: dev ? 'public/player.js' : 'build/assets/player.min.js', + format: 'iife', + name: '__player__', + sourcemap: dev, + globals: { + ['assjs']: 'ASS', + [srcPath('core', ccl)]: 'window', + }, + }, + ], + context: 'window', + plugins: [ + typescript(), + replace({ + include: 'src/core/index.ts', + // (!) [plugin replace] @rollup/plugin-replace: 'preventAssignment' currently defaults to false. It is recommended to set this option to `true`, as the next major version will default this option to `true`. + preventAssignment: true, + values: { _version_: version() }, + }), + ...[dev ? [] : [terser(terserOptions)]], + ], + }, +]; diff --git a/scripts/build-html.cjs b/scripts/build-html.cjs new file mode 100644 index 0000000..78fe4b7 --- /dev/null +++ b/scripts/build-html.cjs @@ -0,0 +1,38 @@ +const { DEV, inputs, outputs, styles, scripts } = require('./const.cjs'); +const HTMLParser = require('node-html-parser'); +const { minify } = require('html-minifier-terser'); +const { html: terserOptions } = require('./terser.config.cjs'); +const { readFile, writeFile, version } = require('./utils.cjs'); + +const html = HTMLParser.parse( + readFile(inputs.html), // + { + comment: true, + voidTag: { closingSlash: true }, + } +); + +const i = DEV ? 0 : 1; +const setCss = (id, /** @type {keyof styles} */ type) => + html.getElementById(id)?.setAttribute('href', styles[type][i]).removeAttribute('id'); +const setScript = (id, /** @type {keyof scripts} */ type) => + html.getElementById(id)?.setAttribute('src', scripts[type][i]).removeAttribute('id'); +const setVersion = (version) => html.getElementById('version')?.set_content(version).removeAttribute('id'); + +setVersion(version(true)); + +setCss('css-player', 'player'); +setCss('css-ccl', 'ccl'); + +setScript('script-player', 'player'); +setScript('script-ccl', 'ccl'); +setScript('script-ass', 'ass'); + +(async () => { + let output = html.toString(); + if (!DEV) { + output = await minify(output, terserOptions); + } + + writeFile(outputs.html[DEV ? 'dev' : 'prod'], output); +})(); diff --git a/scripts/build-style.cjs b/scripts/build-style.cjs new file mode 100644 index 0000000..32e47e1 --- /dev/null +++ b/scripts/build-style.cjs @@ -0,0 +1,16 @@ +const { DEV, inputs, outputs } = require('./const.cjs'); +const fs = require('fs'); +const { minify } = require('html-minifier-terser'); +const { html: terserOptions } = require('./terser.config.cjs'); +const { readFile, writeFile } = require('./utils.cjs'); + +if (DEV) { + fs.copyFileSync(inputs.style, outputs.style.dev); +} else { + const css = readFile(inputs.style); + (async () => { + const minCss = await minify(css, terserOptions); + + writeFile(outputs.style.prod, minCss); + })(); +} diff --git a/scripts/const.cjs b/scripts/const.cjs new file mode 100644 index 0000000..bc8910c --- /dev/null +++ b/scripts/const.cjs @@ -0,0 +1,47 @@ +const { srcPath: src, buildPath: build, publicPath: pub } = require('./utils.cjs'); + +const DEV = process.env.NODE_ENV !== 'production'; + +const inputs = { + html: src('html/index.html'), + style: src('style/player.css'), +}; + +const outputs = { + html: { + dev: pub('index.html'), + prod: build('index.html'), + }, + style: { + dev: pub('player.css'), + prod: build('assets/player.min.css'), + }, +}; + +const styles = { + player: [ + '../src/style/player.css', // + 'assets/player.min.css', + ], + ccl: [ + '../src/lib/CommentCoreLibrary.css', // + 'https://unpkg.com/comment-core-library@0.11.1/dist/css/style.min.css', + ], +}; + +const scripts = { + player: [ + 'player.js', // + 'assets/player.min.js', + ], + ccl: [ + '../src/lib/CommentCoreLibrary.js', + 'https://unpkg.com/comment-core-library@0.11.1/dist/CommentCoreLibrary.min.js', + ], + ass: [ + '../node_modules/assjs/dist/ass.js', // + 'https://unpkg.com/assjs@0.0.11/dist/ass.min.js', + ], +}; + +module.exports = { DEV, inputs, outputs, styles, scripts }; diff --git a/scripts/jsconfig.json b/scripts/jsconfig.json new file mode 100644 index 0000000..64c6831 --- /dev/null +++ b/scripts/jsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "checkJs": true, + "module": "CommonJS" + }, + "include": ["./*"] +} diff --git a/scripts/terser.config.cjs b/scripts/terser.config.cjs new file mode 100644 index 0000000..1acc53f --- /dev/null +++ b/scripts/terser.config.cjs @@ -0,0 +1,4 @@ +const html = { collapseWhitespace: true, minifyJS: true }; +const javascript = { compress: { pure_funcs: ['console.debug'] } }; + +module.exports = { html, javascript }; diff --git a/scripts/utils.cjs b/scripts/utils.cjs new file mode 100644 index 0000000..288360e --- /dev/null +++ b/scripts/utils.cjs @@ -0,0 +1,27 @@ +const path = require('path'); +const fs = require('fs'); +const _execSync = require('child_process').execSync; + +const execSync = (cmd) => _execSync(cmd).toString().trim(); +const commitHash = (short = false) => { + const hash = execSync('git rev-parse --verify HEAD'); + return short ? hash.slice(0, 8) : hash; +}; +const branch = () => execSync('git branch --show-current'); +const isDirty = () => execSync('git status --short').length !== 0; +const version = (short = false) => `${branch()}@${commitHash(short)}${isDirty ? '(dirty)' : ''}`; + +const srcPath = (...p) => path.resolve(__dirname, '../src', ...p); +const buildPath = (...p) => path.resolve(__dirname, '../build', ...p); +const publicPath = (...p) => path.resolve(__dirname, '../public', ...p); + +function writeFile(file, text) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, text, 'utf8'); +} + +function readFile(file) { + return fs.readFileSync(file, 'utf-8'); +} + +module.exports = { version, srcPath, buildPath, publicPath, readFile, writeFile }; diff --git a/src/core/data.ts b/src/core/data.ts new file mode 100644 index 0000000..629af88 --- /dev/null +++ b/src/core/data.ts @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2022-2024 AUIOC.ORG + * Copyright (C) 2018-2022 PCC-Studio + * + * This file is part of BilibiliLocalPlayerH5. + * + * BilibiliLocalPlayerH5 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + * details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import ASS from 'assjs'; +import { BilibiliFormat, CommentManager, CommentProvider } from '../lib/CommentCoreLibrary.js'; +import { EDC, PlayerMetadata } from './metadata'; +import Player from './player'; +import { + fTime, + opacityInvisible, + opacityVisible, + randomStr, + spans, + timeToSeconds, + toggleDisplay, + toggleDisplayBi, +} from './utils'; + +function toggleDisplayByData(dataName: string, clazz: string) { + let r = ''; + for (let i = 0; i < 4; i++) { + r += `.player[data-${dataName}='${i < 2 ? 'true' : 'false'}'] `; + r += `.${clazz}>span:${i % 2 === 0 ? 'first' : 'last'}-child`; + r += `{display: ${i > 0 && i - 3 < 0 ? 'none' : 'unset'};}\n`; + } + return r; +} + +function toggleComponent(P: Player, key: string, on: Function, onMsg: string, off: Function, offMsg: string) { + if (!P.temp[key]) { + P.temp[key] = true; + on(); + P.setContainerData(key, true); + P.toast(onMsg); + } else { + P.temp[key] = false; + off(); + P.setContainerData(key, false); + P.toast(offMsg); + } +} + +function option(value: string, text = value, selected = false) { + const option = document.createElement('option'); + option.value = value; + option.innerHTML = text; + if (selected) { + option.selected = true; + } + return option; +} + +const icon_danmaku_off = + ''; +const icon_subtitle_on = + ''; + +const icons = { + play: '', + pause: '', + volume: '', + mute: '', + danmakuOff: icon_danmaku_off, + danmakuOn: + icon_danmaku_off + + '', + subtitleOn: icon_subtitle_on, + subtitleOff: icon_subtitle_on.replace('d=', 'fill="#7e7e7e" d='), + exitFullscreen: + '', + fullscreen: + '', +} as const; + +function icon(p: K) { + return ( + '' + + icons[p] + + '' + ); +} + +// ====================================================================== // + +function initDanmaku(stage: HTMLElement, url: string, onload: () => void) { + const provider = new CommentProvider(); + provider.addStaticSource(CommentProvider.XMLProvider('GET', url, null, null), CommentProvider.SOURCE_XML); + provider.addParser(new BilibiliFormat.XMLParser(), CommentProvider.SOURCE_XML); + const commentManager = new CommentManager(stage); + provider.addTarget(commentManager); + commentManager.init('css'); + provider + .load() + .then(() => { + commentManager.start(); + onload(); + }) + .catch((e) => alert('DanmakuError: ' + e)); + return commentManager; +} + +function hasDanmaku(p: Player) { + return p.danmakuUrl ? true : false; +} + +function initSubtitle(stage: HTMLElement, video: HTMLVideoElement, url: string) { + const req = new XMLHttpRequest(); + req.open('GET', url, false); + req.send(); + if (req.status === 200) { + return new ASS(req.responseText, video, { container: stage, resampling: 'video_height' }); + } + return null; +} + +function hasSubtitle(p: Player) { + return p.subtitleUrl ? true : false; +} + +// ====================================================================== // + +const toastBox = new EDC('div') // + .class('toast box visibility-transition invisible') + .playerEvents({ + toast: (P, E, T: CustomEvent) => { + E.innerHTML = T.detail.content; + opacityVisible(E); + clearTimeout(P.temp.toastTimer); + P.temp.toastTimer = setTimeout(() => opacityInvisible(E), 800); + }, + }); + +const playToggle = new EDC('button', 'playToggle') // + .class('play-toggle') + .title('Play/Pause') + .css(() => toggleDisplayByData('paused', 'play-toggle')) + .selfEvents({ + click: (P) => P.togglePlay(), + }) + .children(...spans(icon('play'), icon('pause'))); + +const muteToggle = new EDC('button', 'muteToggle') + .class('mute-toggle') + .title('Mute/Unmute') + .css(() => toggleDisplayByData('muted', 'mute-toggle')) + .selfEvents({ + click: (P) => P.toggleMute(), + }) + .children(...spans(icon('mute'), icon('volume'))); + +const volumeInput = new EDC('input', 'volume') + .class('volume') + .title('Volume') + .attrs({ type: 'number', min: '0', max: '100', step: '5', value: '100' }) + .selfEvents({ + input: (P, E) => P.setVolume(E.valueAsNumber / 100), + }) + .playerEvents({ + mute: (_, E) => (E.disabled = true), + unmute: (_, E) => (E.disabled = false), + }) + .videoEvents({ + volumechange: (_, E, V) => (E.valueAsNumber = Math.round(V.volume * 100)), + }); + +const progressBar = new EDC('input', 'progress') + .class('progress') + .attrs({ + type: 'range', + min: '0', + max: '1', + step: '0.0001', + 'default-value': '0', + 'data-seeking': 'false', + }) + .selfEvents({ + create: (_, E) => (E.valueAsNumber = 0), + change: (P, E) => { + P.seekPercent(E.valueAsNumber); + opacityInvisible(P.elements.progressPopup); + P.temp.progressInputting = false; + }, + input: (P, E) => { + P.temp.progressInputting = true; + const value = E.valueAsNumber; + const popup = P.elements.progressPopup; + popup.textContent = fTime(P.video.duration * value); + popup.style.left = `calc(${value * 100}% + (${8 - value * 100 * 0.15}px))`; + popup.style.transform = 'translateX(' + -popup.offsetWidth / 2 + 'px)'; + opacityVisible(popup); + }, + }) + .videoEvents({ + timeupdate: (P, E, V) => { + if (!P.temp.progressInputting) { + const v = V.currentTime / V.duration; + E.valueAsNumber = v ? v : 0; + } + }, + }); + +const progressPopup = new EDC('div', 'progressPopup') // + .class('progress-popup box visibility-transition invisible'); + +const timeInput = new EDC('input', 'timeInput') + .class('time-input hide') + .attrs({ type: 'time', step: '1' }) + .selfEvents({ + change: (P, E) => { + if (E.validity.valid) { + P.seek(timeToSeconds(E.value)); + } else { + E.value = fTime(P.video.currentTime, true); + } + }, + }) + .videoEvents({ + canplay: (P, E, V) => { + E.value = P.fCurrentTime(true); + E.max = fTime(V.duration, true); + }, + timeupdate: (P, E, V) => { + E.value = P.fCurrentTime(true); + }, + }); + +const timeCurrent = new EDC('span', 'timeCurrent') // + .html('--:--') + .videoEvents({ + canplay: (P, E, V) => (E.textContent = P.fCurrentTime()), + timeupdate: (P, E, V) => (E.textContent = P.fCurrentTime()), + }); + +const timeTotal = new EDC('span') + .html('--:--') + .selfEvents({ + click: (P) => toggleDisplay(P.elements.timeInput, P.elements.timeCurrent), + }) + .videoEvents({ + canplay: (_, E, V) => (E.textContent = fTime(V.duration)), + }); + +const playbackRate = new EDC('select', 'playbackRate') // + .title('Playback Rate') + .class('playback-rate') + .videoEvents({ + canplay: (_, E, V) => (V.playbackRate = parseFloat(E.value)), + }) + .selfEvents({ + change: (P, E) => { + P.video.playbackRate = parseFloat(E.value); + P.toast('Playback Rate: ' + E.value); + }, + }) + .children( + option('2.0'), // + option('1.5'), + option('1.0', '1.0', true), + option('0.5') + ); + +const subtitleToggle = new EDC('button', 'subtitleToggle') + .condition(hasSubtitle) + .class('subtitle-toggle') + .title('Subtitle') + .css(() => toggleDisplayByData('subtitle-on', 'subtitle-toggle')) + .selfEvents({ + click: (P) => + toggleComponent( + P, + 'subtitleOn', + () => P.subtitleManager.show(), + 'Subtitle On', + () => P.subtitleManager.hide(), + 'Subtitle Off' + ), + }) + .children(...spans(icon('subtitleOn'), icon('subtitleOff'))); + +const danmakuToggle = new EDC('button', 'danmakuToggle') + .class('danmaku-toggle') + .title('Danmaku') + .css(() => toggleDisplayByData('danmaku-on', 'danmaku-toggle')) + .selfEvents({ + click: (P) => + toggleComponent( + P, + 'danmakuOn', + () => P.commentManager.start(), + 'Danmaku On', + () => { + P.commentManager.clear(); + P.commentManager.stop(); + }, + 'Danmaku Off' + ), + }) + .children(...spans(icon('danmakuOn'), icon('danmakuOff'))); + +const danmakuListToggle = new EDC('button', 'danmakuListToggle') // + .html('?') + .title('Danmaku list') + .selfEvents({ + click: (P) => toggleDisplay(P.elements.danmakuList), + }) + .playerEvents({ + danmakuload: (P, E) => (E.innerHTML = `(${P.commentManager.timeline.length})`), + }); + +const danmakuTimeOffset = new EDC('input') + .class('danmaku-time-offset') + .title('Danmaku time offset') + .attrs({ type: 'number', step: '1', value: '0' }) + .selfEvents({ + create: (P, E) => { + if (!P.options.danmakuTimeOffset) P.options.danmakuTimeOffset = 0; + E.valueAsNumber = P.options.danmakuTimeOffset; + }, + input: (P, E) => { + P.options.danmakuTimeOffset = E.valueAsNumber; + P.commentManager.clear(); + }, + }); + +const danmakuSizeOffset = new EDC('input') + .class('danmaku-size-offset') + .title('Danmaku size offset') + .attrs({ type: 'number', step: '1', value: '0' }) + .selfEvents({ + create: (P, E) => { + if (!P.options.danmakuSizeOffset) P.options.danmakuSizeOffset = 0; + E.valueAsNumber = P.options.danmakuSizeOffset; + P.temp.danmakuSizeFlag = randomStr(); + }, + input: (P, E) => { + P.options.danmakuSizeOffset = E.valueAsNumber; + P.temp.danmakuSizeFlag = randomStr(); + P.commentManager.clear(); + }, + }); + +const fullscreenToggle = new EDC('button', 'fullscreenToggle') + .class('fullscreen-toggle') + .title('Fullscreen') + .css(() => toggleDisplayByData('fullscreen', 'fullscreen-toggle')) + .selfEvents({ + click: (P) => P.toggleFullscreen(), + }) + .children(...spans(icon('exitFullscreen'), icon('fullscreen'))); + +const danmakuList = new EDC('div', 'danmakuList') + .condition(hasDanmaku) + .class('danmaku-list box hide') + .children( + new EDC('ul') // + .playerEvents({ + danmakuload: async (P, E) => { + const timeline = P.commentManager.timeline; + const overHour = timeline ? timeline[timeline.length - 1].stime >= 36e5 : false; + let html = ''; + for (const data of timeline) { + html += // for performance, do not use document.createElement + `
  • ${fTime(data.stime / 1e3, overHour)}` + + `${data.text}
  • `; + } + E.innerHTML = html; + }, + }) + ); + +const subtitleStage = new EDC('div', 'subtitleStage') + .class('subtitle-stage container') + .condition(hasSubtitle) + .selfEvents({ + create: (P, E) => { + P.subtitleManager = initSubtitle(E, P.video, P.subtitleUrl); + P.firePlayerEvent('subtitleload'); + P.temp.subtitleOn = true; + P.setContainerData('subtitleOn', true); + }, + }) + .videoEvents({ + resize: (P) => P.subtitleManager.resize(), + }); + +const danmakuStage = new EDC('div', 'danmakuStage') + .class('danmaku-stage container') + .condition(hasDanmaku) + .selfEvents({ + create: (P, E) => { + P.commentManager = initDanmaku(E, P.danmakuUrl, () => P.firePlayerEvent('danmakuload')); + if (P.options.danmakuSizeOffset) { + P.commentManager.filter.addModifier(function (commentData) { + const override = commentData; + const size = commentData['size']; + let sizeBak = commentData['sizeBackup']; + if (size && override['sizeFlag'] != P.temp.danmakuSizeFlag) { + if (!sizeBak) { + override['sizeBackup'] = size; + sizeBak = size; + } + override['size'] = sizeBak + P.options.danmakuSizeOffset; + override['sizeFlag'] = P.temp.danmakuSizeFlag; + } + return override; + }); + } + P.temp.danmakuOn = true; + P.setContainerData('danmakuOn', true); + if (!P.options.danmakuTimeOffset) P.options.danmakuTimeOffset = 0; + }, + }) + .videoEvents({ + timeupdate: (P, _, V) => { + if (!P.temp.danmakuOn) return; + const cm = P.commentManager; + const time = Math.floor(1e3 * (V.currentTime - P.options.danmakuTimeOffset)); + const deltaTime = time - cm._lastPosition; + if (deltaTime < 0 || deltaTime > cm.options.seekTrigger) { + cm.clear(); + } + cm.time(time); + }, + play: (P) => P.commentManager.start(), + pause: (P) => P.commentManager.stop(), + resize: (P, _, V) => { + const cm = P.commentManager; + cm.clear(); + cm.setBounds(); + cm.options.scroll.scale = V.offsetWidth / 680 / 1; + }, + }); + +// ====================================================================== // + +const mouseIdle = (P: Player) => { + clearTimeout(P.temp.mouseTimer); + P.setContainerData('mouseIdle', false); + P.temp.mouseTimer = setTimeout(() => { + P.setContainerData('mouseIdle', true); + }, 1e3); +}; + +const hotkeys = (P: Player, T: KeyboardEvent) => { + if (T.target === P.container) { + switch (T.keyCode) { + case 32: // Space + P.togglePlay(); + break; + case 77: // M + P.toggleMute(); + break; + case 70: // F + P.toggleFullscreen(); + break; + case 38: // Up + P.adjustVolume(0.05); + break; + case 40: // Down + P.adjustVolume(-0.05); + break; + case 37: // Left + P.skip(T.ctrlKey ? -10 : T.shiftKey ? -1 : -5); + break; + case 39: // Right + P.skip(T.ctrlKey ? 10 : T.shiftKey ? 1 : 5); + break; + case 73: // I + P.toast( + [ + ['LocalTime', new Date().toLocaleString()], + ['File', `${P.title} @ ${P.video.videoWidth}x${P.video.videoHeight}`], + ['Time', `${P.fCurrentTime()} / ${fTime(P.video.duration)}`], + ] + .map(([l, t]) => `${l}: ${t}`) + .join('
    ') + ); + break; + case 68: // D + if (P.elements.danmakuToggle) P.elements.danmakuToggle.click(); + break; + case 81: // Q + console.log(P.video.currentTime); + P.toast(`Time: ${P.fCurrentTime()} (${P.video.currentTime})`); + break; + default: + break; + } + } +}; + +// ====================================================================== // + +export const defaultPlayerMetadata = { + elements: [ + toastBox, + new EDC('div') // + .class('controls-wrapper') + .selfEvents({ + mousemove: (P) => opacityVisible(P.elements.controls), + mouseleave: (P) => { + opacityInvisible(P.elements.controls); + P.focus(); + }, + }) + .children( + new EDC('div', 'controls') // + .class('controls box visibility-transition invisible') + .playerEvents({ + fullscreen: (_, E) => opacityInvisible(E), + }) + .children( + playToggle, + new EDC('div') // + .class('volume-wrapper') + .children(muteToggle, volumeInput), + new EDC('div') // + .class('progress-wrapper') + .children(progressBar, progressPopup), + new EDC('div') // + .class('time-label') + .selfEvents({ + mouseleave: (P) => toggleDisplayBi(P.elements.timeCurrent, P.elements.timeInput), + }) + .children(timeInput, timeCurrent, new EDC('span').html(' / '), timeTotal), + playbackRate, + subtitleToggle, + new EDC('div') + .condition(hasDanmaku) + .class('danmaku-controls') + .children(danmakuToggle, danmakuListToggle, danmakuTimeOffset, danmakuSizeOffset), + fullscreenToggle + ) + ), + danmakuList, + new EDC('div') + .class('overlays abp') + .selfEvents({ + click: (P) => P.togglePlay(), + }) + .children(subtitleStage, danmakuStage), + ], + playerEvent: { + mousemove: mouseIdle, + keydown: hotkeys, + }, +} as PlayerMetadata; diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 0000000..23ea439 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,14 @@ +import { defaultPlayerMetadata } from './data'; +import Player from './player'; + +/** + * This field will be automatically replaced to + * current version text during build. + * Do not modify it! + **/ +const version = '_version_'; + +Object.defineProperty(window, 'Player', { value: Player }); +Object.defineProperty(window, '__player_metadata__', { value: defaultPlayerMetadata }); + +export { defaultPlayerMetadata, Player, version }; diff --git a/src/player.metadata.ts b/src/core/metadata.ts similarity index 92% rename from src/player.metadata.ts rename to src/core/metadata.ts index 707ab82..d1d06c2 100644 --- a/src/player.metadata.ts +++ b/src/core/metadata.ts @@ -18,6 +18,9 @@ * along with this program. If not, see . */ +import Player from './player'; +import { AnyFunction, EventListenerMap, HTMLTagNames, StrKV, appendChild, bindEvents } from './utils'; + type MetaEventTypes = 'selfEvent' | 'playerEvent' | 'videoEvent'; type MetaEvents = EventListenerMap | (() => EventListenerMap); @@ -29,14 +32,14 @@ type ElementVideoMetaEvents = MetaEvents< (player: Player, element: HTMLElementTagNameMap[T], video: HTMLVideoElement) => void >; -function bindMetaEvent(target: HTMLElement, listeners: MetaEvents, ...params: any[]) { +export function bindMetaEvent(target: HTMLElement, listeners: MetaEvents, ...params: any[]) { const l = typeof listeners === 'function' ? listeners() : listeners; if (l) { bindEvents(target, l, params); } } -class EDC { +export class EDC { #tag: T; #name: string; #condition: (player: Player) => boolean; @@ -134,7 +137,7 @@ class EDC { } } -interface PlayerMetadata { +export interface PlayerMetadata { elements?: EDC[]; playerEvent?: MetaEvents<(player: Player) => void>; videoEvent?: MetaEvents<(player: Player, video: HTMLVideoElement) => void>; diff --git a/src/player.main.ts b/src/core/player.ts similarity index 94% rename from src/player.main.ts rename to src/core/player.ts index 4435365..555846f 100644 --- a/src/player.main.ts +++ b/src/core/player.ts @@ -18,19 +18,18 @@ * along with this program. If not, see . */ +import ASS from 'assjs'; +import { CommentManager } from '../lib/CommentCoreLibrary'; +import { bindMetaEvent, PlayerMetadata } from './metadata'; +import { appendChild, clamp, fTime, StrAnyKV, StrGenKV } from './utils'; + interface PlayerOptions extends StrAnyKV { autoPlay?: boolean; muted?: boolean; fullscreen?: boolean; } -class Player { - /** - * This field will be automatically replaced to - * current version text during the CI build. - * Do not modify it! - **/ - readonly version = '{version}'; +export default class Player { options: PlayerOptions; readonly #metadata: PlayerMetadata; readonly title: string; @@ -40,9 +39,9 @@ class Player { readonly style: HTMLStyleElement; readonly container: HTMLDivElement; readonly danmakuUrl: string; - commentManager; + commentManager: CommentManager; readonly subtitleUrl: string; - subtitleManager: SubtitleManager; + subtitleManager: ASS; #constructed: boolean; elements: StrGenKV = {}; temp: StrAnyKV = {}; @@ -56,7 +55,6 @@ class Player { subtitleUrl: string, options: PlayerOptions ) { - console.log('Version:', this.version); this.#metadata = metadata; this.options = options; container.classList.add('player'); @@ -77,7 +75,7 @@ class Player { } this.container = container; this.danmakuUrl = danmakuUrl; - if (0) this.commentManager = new CommentManager(); // for type intellisense + // if (0) this.commentManager = new CommentManager(); // for type intellisense this.subtitleUrl = subtitleUrl; this.#bindElements(); this.#bindEvents(); diff --git a/src/utils.ts b/src/core/utils.ts similarity index 62% rename from src/utils.ts rename to src/core/utils.ts index a278a7d..fa9f8b5 100644 --- a/src/utils.ts +++ b/src/core/utils.ts @@ -18,58 +18,43 @@ * along with this program. If not, see . */ -interface StrAnyKV { +export interface StrAnyKV { [x: string]: any; } -interface StrGenKV { +export interface StrGenKV { [x: string]: T; } -interface StrKV { +export interface StrKV { [x: string]: string; } -type AnyFunction = (...args: any[]) => any; +export type AnyFunction = (...args: any[]) => any; -type AppendArguments = F extends (...arg: [...infer P]) => infer R +export type AppendArguments = F extends (...arg: [...infer P]) => infer R ? (...args: [...P, ...A]) => R : never; -type PrependArguments = F extends (...arg: [...infer P]) => infer R +export type PrependArguments = F extends (...arg: [...infer P]) => infer R ? (...args: [...A, ...P]) => R : never; -type ExtendedHTMLEventMap = StrAnyKV & HTMLElementEventMap; +export type ExtendedHTMLEventMap = StrAnyKV & HTMLElementEventMap; -type EventListenerMap = { +export type EventListenerMap = { [key in keyof ExtendedHTMLEventMap]?: AppendArguments; }; -type HTMLTagNames = keyof HTMLElementTagNameMap; - -interface SubtitleManager { - new ( - subtitle: string, - media: HTMLMediaElement, - options: { - container?: HTMLElement; - resampling?: 'video_width' | 'video_height' | 'script_width' | 'script_height'; - } - ): SubtitleManager; - resize(): void; - hide(): void; - show(): void; - destory(): void; -} +export type HTMLTagNames = keyof HTMLElementTagNameMap; // ================================================================================================================== // -function clamp(number: number, min: number, max: number) { +export function clamp(number: number, min: number, max: number) { return Math.max(min, Math.min(number, max)); } -function ternaryWithCallback( +export function ternaryWithCallback( value: T, predicate: (value: T) => boolean, aOperator: (value: T) => A, @@ -81,36 +66,36 @@ function ternaryWithCallback( return bOperator(value); } -function span(html: string) { +export function span(html: string) { const span = document.createElement('span'); span.innerHTML = html; return span; } -function spans(...html: string[]) { +export function spans(...html: string[]) { return html.map(span); } -function appendChild(parent: HTMLElement, child?: HTMLElement) { +export function appendChild(parent: HTMLElement, child?: HTMLElement) { if (child instanceof HTMLElement) { parent.appendChild(child); } } -function addClass(element: HTMLElement, clazz: string) { +export function addClass(element: HTMLElement, clazz: string) { element.classList.add(clazz); } -function removeClass(element: HTMLElement, clazz: string) { +export function removeClass(element: HTMLElement, clazz: string) { element.classList.remove(clazz); } -function toggleDisplayBi(display: HTMLElement, hide: HTMLElement) { +export function toggleDisplayBi(display: HTMLElement, hide: HTMLElement) { addClass(hide, 'hide'); removeClass(display, 'hide'); } -function toggleDisplay(...element: HTMLElement[]) { +export function toggleDisplay(...element: HTMLElement[]) { for (const el of element) { if (el.classList.contains('hide')) { removeClass(el, 'hide'); @@ -120,17 +105,17 @@ function toggleDisplay(...element: HTMLElement[]) { } } -function opacityVisible(element: HTMLElement) { +export function opacityVisible(element: HTMLElement) { addClass(element, 'visible'); removeClass(element, 'invisible'); } -function opacityInvisible(element: HTMLElement) { +export function opacityInvisible(element: HTMLElement) { removeClass(element, 'visible'); addClass(element, 'invisible'); } -function fTime(seconds: number, alwaysHour?: boolean) { +export function fTime(seconds: number, alwaysHour?: boolean) { const h = Math.floor(seconds / 3600); const m = Math.floor(seconds / 60) % 60; const s = Math.floor(seconds % 60); @@ -140,20 +125,20 @@ function fTime(seconds: number, alwaysHour?: boolean) { .join(':'); } -function timeToSeconds(time: string): number { +export function timeToSeconds(time: string): number { // @ts-expect-error return time.split(':').reduce((acc, time) => 60 * acc + parseInt(time)); } -function randomInt(min: number, max: number) { +export function randomInt(min: number, max: number) { return Math.floor(Math.random() * (max - min)) + min; } -function randomStr() { +export function randomStr() { return Math.random().toString(36).slice(-8); } -function bindEvents(target: HTMLElement, listeners: EventListenerMap, params: any[]) { +export function bindEvents(target: HTMLElement, listeners: EventListenerMap, params: any[]) { for (const [type, listener] of Object.entries(listeners)) { target.addEventListener(type, (event) => listener(...params, event)); } diff --git a/src/html/index.html b/src/html/index.html new file mode 100644 index 0000000..b58b1b7 --- /dev/null +++ b/src/html/index.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + BilibiliLocalPlayerH5 +
    + GitHub ·  + {version} +
    +
    + +
    Last: + None +
    + +
    Time Offset: + +
    Size Offset:
    + +
    Auto Play:
    Muted:
    Fullscreen:
    + +
    + + + + + + + diff --git a/src/lib/CommentCoreLibrary.d.ts b/src/lib/CommentCoreLibrary.d.ts new file mode 100644 index 0000000..aa722dc --- /dev/null +++ b/src/lib/CommentCoreLibrary.d.ts @@ -0,0 +1,78 @@ +type Method = 'GET' | 'POST' | 'PUT'; + +type SourceType = + | typeof CommentProvider.SOURCE_JSON + | typeof CommentProvider.SOURCE_XML + | typeof CommentProvider.SOURCE_TEXT; + +interface CommentParser {} + +export interface ICommentData { + [x: string]: any; + + text: string; + mode: number; + stime: number; + size: number; + color: number; +} + +export class CommentProvider { + constructor(); + + static readonly SOURCE_JSON = 'JSON'; + static readonly SOURCE_XML = 'XML'; + static readonly SOURCE_TEXT = 'TEXT'; + + static BaseHttpProvider(method: Method, url: string, type: string, args: Object, body: any): Promise; + static JSONProvider(method: Method, url: string, args: Object, body: any): Promise; + static XMLProvider(method: Method, url: string, args: Object, body: any): Promise; + static TextProvider(method: Method, url: string, args: Object, body: any): Promise; + + addStaticSource(source: Promise, type: SourceType): CommentProvider; + addParser(parser: CommentParser, type: SourceType): CommentProvider; + addTarget(commentManager: CommentManager): CommentProvider; + load(): Promise; +} + +export namespace BilibiliFormat { + export class XMLParser implements CommentParser { + constructor(); + } +} + +interface CommentManagerOptions { + global: { + opacity: number; + scale: number; + className: string; + }; + scroll: { + opacity: number; + scale: number; + }; + limit: number; + seekTrigger: number; +} + +export class CommentManager { + options: CommentManagerOptions; + timeline: any[]; + filter: CommentFilter; + _lastPosition: number; + + constructor(stage: HTMLElement); + + init(renderer: 'legacy' | 'css'): void; + start(): void; + stop(): void; + clear(): void; + time(time: number): void; + setBounds(): void; +} + +export class CommentFilter { + constructor(); + + addModifier(modififer: (data: ICommentData) => ICommentData): void; +} diff --git a/src/lib/assjs.d.ts b/src/lib/assjs.d.ts new file mode 100644 index 0000000..682d148 --- /dev/null +++ b/src/lib/assjs.d.ts @@ -0,0 +1,14 @@ +declare module 'assjs' { + export default class ASS { + constructor(content: string, video: HTMLVideoElement, { container, resampling }?: ASSOption); + resize(): void; + show(): void; + hide(): void; + destroy(): void; + } + + export type ASSOption = { + container?: HTMLElement; + resampling?: `${'video' | 'script'}_${'width' | 'height'}`; + }; +} diff --git a/src/player.player.ts b/src/player.player.ts deleted file mode 100644 index d634a67..0000000 --- a/src/player.player.ts +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright (C) 2022-2024 AUIOC.ORG - * Copyright (C) 2018-2022 PCC-Studio - * - * This file is part of BilibiliLocalPlayerH5. - * - * BilibiliLocalPlayerH5 is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, - * or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more - * details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -const __player_metadata__ = (function () { - function toggleDisplayByData(dataName: string, clazz: string) { - let r = ''; - for (let i = 0; i < 4; i++) { - r += `.player[data-${dataName}='${i < 2 ? 'true' : 'false'}'] `; - r += `.${clazz}>span:${i % 2 === 0 ? 'first' : 'last'}-child`; - r += `{display: ${i > 0 && i - 3 < 0 ? 'none' : 'unset'};}\n`; - } - return r; - } - - function toggleComponent(P: Player, key: string, on: Function, onMsg: string, off: Function, offMsg: string) { - if (!P.temp[key]) { - P.temp[key] = true; - on(); - P.setContainerData(key, true); - P.toast(onMsg); - } else { - P.temp[key] = false; - off(); - P.setContainerData(key, false); - P.toast(offMsg); - } - } - - function option(value: string, text = value, selected = false) { - const option = document.createElement('option'); - option.value = value; - option.innerHTML = text; - if (selected) { - option.selected = true; - } - return option; - } - - const icon_danmaku_off = - ''; - const icon_subtitle_on = - ''; - - const icons = { - play: '', - pause: '', - volume: '', - mute: '', - danmakuOff: icon_danmaku_off, - danmakuOn: - icon_danmaku_off + - '', - subtitleOn: icon_subtitle_on, - subtitleOff: icon_subtitle_on.replace('d=', 'fill="#7e7e7e" d='), - exitFullscreen: - '', - fullscreen: - '', - } as const; - - function icon(p: K) { - return ( - '' + - icons[p] + - '' - ); - } - - // ====================================================================== // - - function initDanmaku(stage: HTMLElement, url: string, onload: () => void) { - const provider = new CommentProvider(); - provider.addStaticSource(CommentProvider.XMLProvider('GET', url, null, null), CommentProvider.SOURCE_XML); - provider.addParser(new BilibiliFormat.XMLParser(), CommentProvider.SOURCE_XML); - const commentManager = new CommentManager(stage); - // @ts-expect-error - provider.addTarget(commentManager); - commentManager.init('css'); - provider - .load() - .then(() => { - commentManager.start(); - onload(); - }) - .catch((e) => alert('DanmakuError: ' + e)); - return commentManager; - } - - function hasDanmaku(p: Player) { - return p.danmakuUrl ? true : false; - } - - function initSubtitle(stage: HTMLElement, video: HTMLVideoElement, url: string) { - const req = new XMLHttpRequest(); - req.open('GET', url, false); - req.send(); - if (req.status === 200) { - // @ts-expect-error - const ass = new ASS(req.responseText, video, { container: stage, resampling: 'video_height' }); - return ass as SubtitleManager; - } - return null; - } - - function hasSubtitle(p: Player) { - return p.subtitleUrl ? true : false; - } - - // ====================================================================== // - - const toastBox = new EDC('div') // - .class('toast box visibility-transition invisible') - .playerEvents({ - toast: (P, E, T: CustomEvent) => { - E.innerHTML = T.detail.content; - opacityVisible(E); - clearTimeout(P.temp.toastTimer); - P.temp.toastTimer = setTimeout(() => opacityInvisible(E), 800); - }, - }); - - const playToggle = new EDC('button', 'playToggle') // - .class('play-toggle') - .title('Play/Pause') - .css(() => toggleDisplayByData('paused', 'play-toggle')) - .selfEvents({ - click: (P) => P.togglePlay(), - }) - .children(...spans(icon('play'), icon('pause'))); - - const muteToggle = new EDC('button', 'muteToggle') - .class('mute-toggle') - .title('Mute/Unmute') - .css(() => toggleDisplayByData('muted', 'mute-toggle')) - .selfEvents({ - click: (P) => P.toggleMute(), - }) - .children(...spans(icon('mute'), icon('volume'))); - - const volumeInput = new EDC('input', 'volume') - .class('volume') - .title('Volume') - .attrs({ type: 'number', min: '0', max: '100', step: '5', value: '100' }) - .selfEvents({ - input: (P, E) => P.setVolume(E.valueAsNumber / 100), - }) - .playerEvents({ - mute: (_, E) => (E.disabled = true), - unmute: (_, E) => (E.disabled = false), - }) - .videoEvents({ - volumechange: (_, E, V) => (E.valueAsNumber = Math.round(V.volume * 100)), - }); - - const progressBar = new EDC('input', 'progress') - .class('progress') - .attrs({ - type: 'range', - min: '0', - max: '1', - step: '0.0001', - 'default-value': '0', - 'data-seeking': 'false', - }) - .selfEvents({ - create: (_, E) => (E.valueAsNumber = 0), - change: (P, E) => { - P.seekPercent(E.valueAsNumber); - opacityInvisible(P.elements.progressPopup); - P.temp.progressInputting = false; - }, - input: (P, E) => { - P.temp.progressInputting = true; - const value = E.valueAsNumber; - const popup = P.elements.progressPopup; - popup.textContent = fTime(P.video.duration * value); - popup.style.left = `calc(${value * 100}% + (${8 - value * 100 * 0.15}px))`; - popup.style.transform = 'translateX(' + -popup.offsetWidth / 2 + 'px)'; - opacityVisible(popup); - }, - }) - .videoEvents({ - timeupdate: (P, E, V) => { - if (!P.temp.progressInputting) { - const v = V.currentTime / V.duration; - E.valueAsNumber = v ? v : 0; - } - }, - }); - - const progressPopup = new EDC('div', 'progressPopup') // - .class('progress-popup box visibility-transition invisible'); - - const timeInput = new EDC('input', 'timeInput') - .class('time-input hide') - .attrs({ type: 'time', step: '1' }) - .selfEvents({ - change: (P, E) => { - if (E.validity.valid) { - P.seek(timeToSeconds(E.value)); - } else { - E.value = fTime(P.video.currentTime, true); - } - }, - }) - .videoEvents({ - canplay: (P, E, V) => { - E.value = P.fCurrentTime(true); - E.max = fTime(V.duration, true); - }, - timeupdate: (P, E, V) => { - E.value = P.fCurrentTime(true); - }, - }); - - const timeCurrent = new EDC('span', 'timeCurrent') // - .html('--:--') - .videoEvents({ - canplay: (P, E, V) => (E.textContent = P.fCurrentTime()), - timeupdate: (P, E, V) => (E.textContent = P.fCurrentTime()), - }); - - const timeTotal = new EDC('span') - .html('--:--') - .selfEvents({ - click: (P) => toggleDisplay(P.elements.timeInput, P.elements.timeCurrent), - }) - .videoEvents({ - canplay: (_, E, V) => (E.textContent = fTime(V.duration)), - }); - - const playbackRate = new EDC('select', 'playbackRate') // - .title('Playback Rate') - .class('playback-rate') - .videoEvents({ - canplay: (_, E, V) => (V.playbackRate = parseFloat(E.value)), - }) - .selfEvents({ - change: (P, E) => { - P.video.playbackRate = parseFloat(E.value); - P.toast('Playback Rate: ' + E.value); - }, - }) - .children( - option('2.0'), // - option('1.5'), - option('1.0', '1.0', true), - option('0.5') - ); - - const subtitleToggle = new EDC('button', 'subtitleToggle') - .condition(hasSubtitle) - .class('subtitle-toggle') - .title('Subtitle') - .css(() => toggleDisplayByData('subtitle-on', 'subtitle-toggle')) - .selfEvents({ - click: (P) => - toggleComponent( - P, - 'subtitleOn', - () => P.subtitleManager.show(), - 'Subtitle On', - () => P.subtitleManager.hide(), - 'Subtitle Off' - ), - }) - .children(...spans(icon('subtitleOn'), icon('subtitleOff'))); - - const danmakuToggle = new EDC('button', 'danmakuToggle') - .class('danmaku-toggle') - .title('Danmaku') - .css(() => toggleDisplayByData('danmaku-on', 'danmaku-toggle')) - .selfEvents({ - click: (P) => - toggleComponent( - P, - 'danmakuOn', - () => P.commentManager.start(), - 'Danmaku On', - () => { - P.commentManager.clear(); - P.commentManager.stop(); - }, - 'Danmaku Off' - ), - }) - .children(...spans(icon('danmakuOn'), icon('danmakuOff'))); - - const danmakuListToggle = new EDC('button', 'danmakuListToggle') // - .html('?') - .title('Danmaku list') - .selfEvents({ - click: (P) => toggleDisplay(P.elements.danmakuList), - }) - .playerEvents({ - danmakuload: (P, E) => (E.innerHTML = `(${P.commentManager.timeline.length})`), - }); - - const danmakuTimeOffset = new EDC('input') - .class('danmaku-time-offset') - .title('Danmaku time offset') - .attrs({ type: 'number', step: '1', value: '0' }) - .selfEvents({ - create: (P, E) => { - if (!P.options.danmakuTimeOffset) P.options.danmakuTimeOffset = 0; - E.valueAsNumber = P.options.danmakuTimeOffset; - }, - input: (P, E) => { - P.options.danmakuTimeOffset = E.valueAsNumber; - P.commentManager.clear(); - }, - }); - - const danmakuSizeOffset = new EDC('input') - .class('danmaku-size-offset') - .title('Danmaku size offset') - .attrs({ type: 'number', step: '1', value: '0' }) - .selfEvents({ - create: (P, E) => { - if (!P.options.danmakuSizeOffset) P.options.danmakuSizeOffset = 0; - E.valueAsNumber = P.options.danmakuSizeOffset; - P.temp.danmakuSizeFlag = randomStr(); - }, - input: (P, E) => { - P.options.danmakuSizeOffset = E.valueAsNumber; - P.temp.danmakuSizeFlag = randomStr(); - P.commentManager.clear(); - }, - }); - - const fullscreenToggle = new EDC('button', 'fullscreenToggle') - .class('fullscreen-toggle') - .title('Fullscreen') - .css(() => toggleDisplayByData('fullscreen', 'fullscreen-toggle')) - .selfEvents({ - click: (P) => P.toggleFullscreen(), - }) - .children(...spans(icon('exitFullscreen'), icon('fullscreen'))); - - const danmakuList = new EDC('div', 'danmakuList') - .condition(hasDanmaku) - .class('danmaku-list box hide') - .children( - new EDC('ul') // - .playerEvents({ - danmakuload: async (P, E) => { - const timeline = P.commentManager.timeline; - const overHour = timeline ? timeline[timeline.length - 1].stime >= 36e5 : false; - let html = ''; - for (const data of timeline) { - html += // for performance, do not use document.createElement - `
  • ${fTime(data.stime / 1e3, overHour)}` + - `${data.text}
  • `; - } - E.innerHTML = html; - }, - }) - ); - - const subtitleStage = new EDC('div', 'subtitleStage') - .class('subtitle-stage container') - .condition(hasSubtitle) - .selfEvents({ - create: (P, E) => { - P.subtitleManager = initSubtitle(E, P.video, P.subtitleUrl); - P.firePlayerEvent('subtitleload'); - P.temp.subtitleOn = true; - P.setContainerData('subtitleOn', true); - }, - }) - .videoEvents({ - resize: (P) => P.subtitleManager.resize(), - }); - - const danmakuStage = new EDC('div', 'danmakuStage') - .class('danmaku-stage container') - .condition(hasDanmaku) - .selfEvents({ - create: (P, E) => { - P.commentManager = initDanmaku(E, P.danmakuUrl, () => P.firePlayerEvent('danmakuload')); - if (P.options.danmakuSizeOffset) { - P.commentManager.filter.addModifier(function (commentData: StrAnyKV) { - const override = commentData; - const size = commentData['size']; - let sizeBak = commentData['sizeBackup']; - if (size && override['sizeFlag'] != P.temp.danmakuSizeFlag) { - if (!sizeBak) { - override['sizeBackup'] = size; - sizeBak = size; - } - override['size'] = sizeBak + P.options.danmakuSizeOffset; - override['sizeFlag'] = P.temp.danmakuSizeFlag; - } - return override; - }); - } - P.temp.danmakuOn = true; - P.setContainerData('danmakuOn', true); - if (!P.options.danmakuTimeOffset) P.options.danmakuTimeOffset = 0; - }, - }) - .videoEvents({ - timeupdate: (P, _, V) => { - if (!P.temp.danmakuOn) return; - const cm = P.commentManager; - const time = Math.floor(1e3 * (V.currentTime - P.options.danmakuTimeOffset)); - const deltaTime = time - cm._lastPosition; - if (deltaTime < 0 || deltaTime > cm.options.seekTrigger) { - cm.clear(); - } - cm.time(time); - }, - play: (P) => P.commentManager.start(), - pause: (P) => P.commentManager.stop(), - resize: (P, _, V) => { - const cm = P.commentManager; - cm.clear(); - cm.setBounds(); - cm.options.scroll.scale = V.offsetWidth / 680 / 1; - }, - }); - - // ====================================================================== // - - const mouseIdle = (P: Player) => { - clearTimeout(P.temp.mouseTimer); - P.setContainerData('mouseIdle', false); - P.temp.mouseTimer = setTimeout(() => { - P.setContainerData('mouseIdle', true); - }, 1e3); - }; - - const hotkeys = (P: Player, T: KeyboardEvent) => { - if (T.target === P.container) { - switch (T.keyCode) { - case 32: // Space - P.togglePlay(); - break; - case 77: // M - P.toggleMute(); - break; - case 70: // F - P.toggleFullscreen(); - break; - case 38: // Up - P.adjustVolume(0.05); - break; - case 40: // Down - P.adjustVolume(-0.05); - break; - case 37: // Left - P.skip(T.ctrlKey ? -10 : T.shiftKey ? -1 : -5); - break; - case 39: // Right - P.skip(T.ctrlKey ? 10 : T.shiftKey ? 1 : 5); - break; - case 73: // I - P.toast( - [ - ['LocalTime', new Date().toLocaleString()], - ['File', `${P.title} @ ${P.video.videoWidth}x${P.video.videoHeight}`], - ['Time', `${P.fCurrentTime()} / ${fTime(P.video.duration)}`], - ] - .map(([l, t]) => `${l}: ${t}`) - .join('
    ') - ); - break; - case 68: // D - if (P.elements.danmakuToggle) P.elements.danmakuToggle.click(); - break; - case 81: // Q - console.log(P.video.currentTime); - P.toast(`Time: ${P.fCurrentTime()} (${P.video.currentTime})`); - break; - default: - break; - } - } - }; - - // ====================================================================== // - - return { - elements: [ - toastBox, - new EDC('div') // - .class('controls-wrapper') - .selfEvents({ - mousemove: (P) => opacityVisible(P.elements.controls), - mouseleave: (P) => { - opacityInvisible(P.elements.controls); - P.focus(); - }, - }) - .children( - new EDC('div', 'controls') // - .class('controls box visibility-transition invisible') - .playerEvents({ - fullscreen: (_, E) => opacityInvisible(E), - }) - .children( - playToggle, - new EDC('div') // - .class('volume-wrapper') - .children(muteToggle, volumeInput), - new EDC('div') // - .class('progress-wrapper') - .children(progressBar, progressPopup), - new EDC('div') // - .class('time-label') - .selfEvents({ - mouseleave: (P) => toggleDisplayBi(P.elements.timeCurrent, P.elements.timeInput), - }) - .children(timeInput, timeCurrent, new EDC('span').html(' / '), timeTotal), - playbackRate, - subtitleToggle, - new EDC('div') - .condition(hasDanmaku) - .class('danmaku-controls') - .children(danmakuToggle, danmakuListToggle, danmakuTimeOffset, danmakuSizeOffset), - fullscreenToggle - ) - ), - danmakuList, - new EDC('div') - .class('overlays abp') - .selfEvents({ - click: (P) => P.togglePlay(), - }) - .children(subtitleStage, danmakuStage), - ], - playerEvent: { - mousemove: mouseIdle, - keydown: hotkeys, - }, - } as PlayerMetadata; -})(); - -Object.defineProperty(window, '__player_metadata__', __player_metadata__); diff --git a/src/style/player.css b/src/style/player.css new file mode 100644 index 0000000..40c2ab2 --- /dev/null +++ b/src/style/player.css @@ -0,0 +1,226 @@ +.hide { + display: none !important; +} +.invisible { + visibility: hidden !important; +} + +.visible { + opacity: 1; + visibility: visible; +} +.invisible { + opacity: 0; + visibility: hidden; +} +.visibility-transition { + /* transition: opacity 400ms ease, visibility 400ms ease; */ +} + +.center { + text-align: center; +} + +#start-panel { + margin: 2em auto; + max-width: 75%; +} + +#start-panel, +#start-panel tr, +#start-panel td { + border: 1px solid; + border-collapse: collapse; + padding: 4px 6px; +} + +#start-panel th { + padding: 0.5em; +} + +#start-panel th > div { + font-weight: lighter; + font-size: smaller; + color: #7e7e7e; +} + +#start-panel tr > td:nth-child(1) { + text-wrap: nowrap; +} + +body { + margin: 0; + background-color: #eee; +} + +input[type='number']::-webkit-inner-spin-button, +input[type='number']::-webkit-outer-spin-button { + opacity: 1; +} + +input::-webkit-datetime-edit { + display: contents; +} + +input::-webkit-calendar-picker-indicator { + display: none; +} + +input::-webkit-datetime-edit-fields-wrapper, +input::-webkit-datetime-edit-hour-field, +input::-webkit-datetime-edit-minute-field, +input::-webkit-datetime-edit-second-field { + padding: 0; +} + +li::marker { + display: none; +} + +.player { + position: absolute; + height: 100%; + width: 100%; + background-color: black; + user-select: none; +} + +video { + position: absolute; + width: 100%; + height: 100%; +} + +.overlays { + position: fixed !important; + bottom: 0; + top: 0; + left: 0; + right: 0; +} + +.player[data-mouse-idle='true'] .overlays { + cursor: none; +} + +.player .box { + background-color: white; + border-width: 1px; + border-style: solid; + border-color: #7e7e7e; +} + +.controls-wrapper { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 999999; +} + +.controls { + position: relative; + display: flex; + align-items: center; + gap: 4px; + padding: 2px 4px; + border-width: 1px 0 0 0 !important; +} + +.controls > button.play-toggle { + padding: 2px 5px; +} + +.controls > .volume-wrapper { + display: flex; + gap: 2px; + height: 22px; +} + +.volume-wrapper > .volume { + max-width: 3em; +} + +.controls > .progress-wrapper { + flex: 1; + position: relative; + display: flex; + height: 22px; +} + +.progress-wrapper > .progress { + width: 100%; +} + +.progress-wrapper > .progress-popup { + position: absolute; + padding: 2px 4px; + top: -40px; +} + +.controls > .time-label { + line-height: 0; +} + +.time-label > .time-input { + font-family: auto; + font-size: medium; + height: 1em; + padding: 0; + cursor: text; + border: none; +} + +.controls > .playback-rate { + height: 22px; +} + +.controls > .danmaku-controls { + display: flex; + gap: 2px; +} + +.danmaku-controls > input { + max-width: 3em; +} + +.toast { + position: absolute; + padding: 2px 4px; + width: fit-content; + top: 4px; + left: 6px; + z-index: 999999; +} + +.danmaku-list { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + margin: auto; + padding: 12px 10px 12px 18px; + max-width: 460px; + max-height: 540px; + z-index: 999990; +} + +.danmaku-list > ul { + width: 100%; + height: 100%; + overflow-y: auto; + padding: 0; + margin: 0; +} + +.danmaku-list li { + padding: 4px 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.danmaku-list li > span:first-child { + margin-right: 12px; +} diff --git a/tsconfig.json b/tsconfig.json index dec2d5a..2a9fbe3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,13 @@ { "compilerOptions": { - "allowJs": true, "alwaysStrict": true, "baseUrl": "./src", - "lib": ["ES2017", "DOM"], - "module": "None", - "esModuleInterop": true, - "moduleResolution": "classic", + "lib": ["ESNext", "DOM"], + "module": "ESNext", + "moduleResolution": "Node10", "noImplicitAny": true, "noImplicitReturns": true, - "outFile": "./public/player.js", + "outDir": "./public", "removeComments": true, "rootDir": "./src", "sourceMap": true,