feat: initialize VidBee download extension with core functionality

* Add .gitignore files for main and monkey directories.
* Create initial project structure for VidBee download extension.
* Implement main functionality in src/main/index.ts to handle deep links.
* Develop UI components including DownloadDialog and VidBee download button.
* Set up TypeScript configuration and Vite for build process.
* Add styles for download button and tooltips.
* Integrate localization for user interface elements.
* Establish deep link handling in the renderer for improved user experience.
This commit is contained in:
Nexmoe
2025-12-20 19:03:13 +08:00
parent a1c44baf65
commit 79a78d4322
16 changed files with 1564 additions and 39 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,5 @@
node_modules
dist
/dist
out
.conductor/
.DS_Store

24
monkey/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

16
monkey/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "vidbee-quick-download",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.9.2",
"vite": "^7.1.3",
"vite-plugin-monkey": "^7.1.1"
}
}

943
monkey/pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,943 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
typescript:
specifier: ^5.9.2
version: 5.9.3
vite:
specifier: ^7.1.3
version: 7.3.0
vite-plugin-monkey:
specifier: ^7.1.1
version: 7.1.8(postcss@8.5.6)(vite@7.3.0)
packages:
'@esbuild/aix-ppc64@0.27.2':
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.27.2':
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.27.2':
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.27.2':
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.27.2':
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.27.2':
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.27.2':
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.27.2':
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.27.2':
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.27.2':
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.27.2':
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.27.2':
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.27.2':
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.27.2':
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.27.2':
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.27.2':
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.27.2':
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-arm64@0.27.2':
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-x64@0.27.2':
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.27.2':
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.27.2':
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openharmony-arm64@0.27.2':
resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openharmony]
'@esbuild/sunos-x64@0.27.2':
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.27.2':
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.27.2':
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.27.2':
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@rollup/rollup-android-arm-eabi@4.54.0':
resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==}
cpu: [arm]
os: [android]
'@rollup/rollup-android-arm64@4.54.0':
resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==}
cpu: [arm64]
os: [android]
'@rollup/rollup-darwin-arm64@4.54.0':
resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==}
cpu: [arm64]
os: [darwin]
'@rollup/rollup-darwin-x64@4.54.0':
resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==}
cpu: [x64]
os: [darwin]
'@rollup/rollup-freebsd-arm64@4.54.0':
resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==}
cpu: [arm64]
os: [freebsd]
'@rollup/rollup-freebsd-x64@4.54.0':
resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==}
cpu: [x64]
os: [freebsd]
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
cpu: [arm]
os: [linux]
'@rollup/rollup-linux-arm64-gnu@4.54.0':
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-arm64-musl@4.54.0':
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
cpu: [arm64]
os: [linux]
'@rollup/rollup-linux-loong64-gnu@4.54.0':
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
cpu: [loong64]
os: [linux]
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
cpu: [ppc64]
os: [linux]
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-riscv64-musl@4.54.0':
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
cpu: [riscv64]
os: [linux]
'@rollup/rollup-linux-s390x-gnu@4.54.0':
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
cpu: [s390x]
os: [linux]
'@rollup/rollup-linux-x64-gnu@4.54.0':
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
cpu: [x64]
os: [linux]
'@rollup/rollup-linux-x64-musl@4.54.0':
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
cpu: [x64]
os: [linux]
'@rollup/rollup-openharmony-arm64@4.54.0':
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
cpu: [arm64]
os: [openharmony]
'@rollup/rollup-win32-arm64-msvc@4.54.0':
resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==}
cpu: [arm64]
os: [win32]
'@rollup/rollup-win32-ia32-msvc@4.54.0':
resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==}
cpu: [ia32]
os: [win32]
'@rollup/rollup-win32-x64-gnu@4.54.0':
resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==}
cpu: [x64]
os: [win32]
'@rollup/rollup-win32-x64-msvc@4.54.0':
resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==}
cpu: [x64]
os: [win32]
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
acorn-walk@8.3.4:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
acorn@8.15.0:
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
engines: {node: '>=0.4.0'}
hasBin: true
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
brace-expansion@1.1.12:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
cuint@0.2.2:
resolution: {integrity: sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw==}
default-browser-id@5.0.1:
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
engines: {node: '>=18'}
default-browser@5.4.0:
resolution: {integrity: sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==}
engines: {node: '>=18'}
define-lazy-prop@3.0.0:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
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.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
entities@6.0.1:
resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==}
engines: {node: '>=0.12'}
esbuild@0.27.2:
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
engines: {node: '>=18'}
hasBin: true
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
peerDependencies:
picomatch: ^3 || ^4
peerDependenciesMeta:
picomatch:
optional: true
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
htmlparser2@10.0.0:
resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==}
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
hasBin: true
is-wsl@3.1.0:
resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==}
engines: {node: '>=16'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
make-dir@3.1.0:
resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==}
engines: {node: '>=8'}
mime@2.5.2:
resolution: {integrity: sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==}
engines: {node: '>=4.0.0'}
hasBin: true
minimatch@3.0.8:
resolution: {integrity: sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==}
mrmime@2.0.1:
resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
engines: {node: '>=10'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
open@10.2.0:
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
engines: {node: '>=18'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@4.0.3:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
postcss-url@10.1.3:
resolution: {integrity: sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw==}
engines: {node: '>=10'}
peerDependencies:
postcss: ^8.0.0
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
rollup@4.54.0:
resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
run-applescript@7.1.0:
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
engines: {node: '>=18'}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
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'}
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
systemjs@6.15.1:
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
vite-plugin-monkey@7.1.8:
resolution: {integrity: sha512-FGz1jlHcodt+L120UcHLiN0jXqQZdZHxLuHcGQg8Zjas6BulxLM1oiGUg3M95HbjYVFH6ygrOJoZ5YCYu1purw==}
peerDependencies:
vite: ^6.0.0 || ^7.0.0
peerDependenciesMeta:
vite:
optional: true
vite@7.3.0:
resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
'@types/node': ^20.19.0 || >=22.12.0
jiti: '>=1.21.0'
less: ^4.0.0
lightningcss: ^1.21.0
sass: ^1.70.0
sass-embedded: ^1.70.0
stylus: '>=0.54.8'
sugarss: ^5.0.0
terser: ^5.16.0
tsx: ^4.8.1
yaml: ^2.4.2
peerDependenciesMeta:
'@types/node':
optional: true
jiti:
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
sass-embedded:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
tsx:
optional: true
yaml:
optional: true
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wsl-utils@0.1.0:
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
engines: {node: '>=18'}
xxhashjs@0.2.2:
resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==}
snapshots:
'@esbuild/aix-ppc64@0.27.2':
optional: true
'@esbuild/android-arm64@0.27.2':
optional: true
'@esbuild/android-arm@0.27.2':
optional: true
'@esbuild/android-x64@0.27.2':
optional: true
'@esbuild/darwin-arm64@0.27.2':
optional: true
'@esbuild/darwin-x64@0.27.2':
optional: true
'@esbuild/freebsd-arm64@0.27.2':
optional: true
'@esbuild/freebsd-x64@0.27.2':
optional: true
'@esbuild/linux-arm64@0.27.2':
optional: true
'@esbuild/linux-arm@0.27.2':
optional: true
'@esbuild/linux-ia32@0.27.2':
optional: true
'@esbuild/linux-loong64@0.27.2':
optional: true
'@esbuild/linux-mips64el@0.27.2':
optional: true
'@esbuild/linux-ppc64@0.27.2':
optional: true
'@esbuild/linux-riscv64@0.27.2':
optional: true
'@esbuild/linux-s390x@0.27.2':
optional: true
'@esbuild/linux-x64@0.27.2':
optional: true
'@esbuild/netbsd-arm64@0.27.2':
optional: true
'@esbuild/netbsd-x64@0.27.2':
optional: true
'@esbuild/openbsd-arm64@0.27.2':
optional: true
'@esbuild/openbsd-x64@0.27.2':
optional: true
'@esbuild/openharmony-arm64@0.27.2':
optional: true
'@esbuild/sunos-x64@0.27.2':
optional: true
'@esbuild/win32-arm64@0.27.2':
optional: true
'@esbuild/win32-ia32@0.27.2':
optional: true
'@esbuild/win32-x64@0.27.2':
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@rollup/rollup-android-arm-eabi@4.54.0':
optional: true
'@rollup/rollup-android-arm64@4.54.0':
optional: true
'@rollup/rollup-darwin-arm64@4.54.0':
optional: true
'@rollup/rollup-darwin-x64@4.54.0':
optional: true
'@rollup/rollup-freebsd-arm64@4.54.0':
optional: true
'@rollup/rollup-freebsd-x64@4.54.0':
optional: true
'@rollup/rollup-linux-arm-gnueabihf@4.54.0':
optional: true
'@rollup/rollup-linux-arm-musleabihf@4.54.0':
optional: true
'@rollup/rollup-linux-arm64-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-arm64-musl@4.54.0':
optional: true
'@rollup/rollup-linux-loong64-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-ppc64-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-riscv64-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-riscv64-musl@4.54.0':
optional: true
'@rollup/rollup-linux-s390x-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-x64-gnu@4.54.0':
optional: true
'@rollup/rollup-linux-x64-musl@4.54.0':
optional: true
'@rollup/rollup-openharmony-arm64@4.54.0':
optional: true
'@rollup/rollup-win32-arm64-msvc@4.54.0':
optional: true
'@rollup/rollup-win32-ia32-msvc@4.54.0':
optional: true
'@rollup/rollup-win32-x64-gnu@4.54.0':
optional: true
'@rollup/rollup-win32-x64-msvc@4.54.0':
optional: true
'@types/estree@1.0.8': {}
acorn-walk@8.3.4:
dependencies:
acorn: 8.15.0
acorn@8.15.0: {}
balanced-match@1.0.2: {}
brace-expansion@1.1.12:
dependencies:
balanced-match: 1.0.2
concat-map: 0.0.1
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
concat-map@0.0.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
cuint@0.2.2: {}
default-browser-id@5.0.1: {}
default-browser@5.4.0:
dependencies:
bundle-name: 4.1.0
default-browser-id: 5.0.1
define-lazy-prop@3.0.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.2.2:
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
entities@4.5.0: {}
entities@6.0.1: {}
esbuild@0.27.2:
optionalDependencies:
'@esbuild/aix-ppc64': 0.27.2
'@esbuild/android-arm': 0.27.2
'@esbuild/android-arm64': 0.27.2
'@esbuild/android-x64': 0.27.2
'@esbuild/darwin-arm64': 0.27.2
'@esbuild/darwin-x64': 0.27.2
'@esbuild/freebsd-arm64': 0.27.2
'@esbuild/freebsd-x64': 0.27.2
'@esbuild/linux-arm': 0.27.2
'@esbuild/linux-arm64': 0.27.2
'@esbuild/linux-ia32': 0.27.2
'@esbuild/linux-loong64': 0.27.2
'@esbuild/linux-mips64el': 0.27.2
'@esbuild/linux-ppc64': 0.27.2
'@esbuild/linux-riscv64': 0.27.2
'@esbuild/linux-s390x': 0.27.2
'@esbuild/linux-x64': 0.27.2
'@esbuild/netbsd-arm64': 0.27.2
'@esbuild/netbsd-x64': 0.27.2
'@esbuild/openbsd-arm64': 0.27.2
'@esbuild/openbsd-x64': 0.27.2
'@esbuild/openharmony-arm64': 0.27.2
'@esbuild/sunos-x64': 0.27.2
'@esbuild/win32-arm64': 0.27.2
'@esbuild/win32-ia32': 0.27.2
'@esbuild/win32-x64': 0.27.2
fdir@6.5.0(picomatch@4.0.3):
optionalDependencies:
picomatch: 4.0.3
fsevents@2.3.3:
optional: true
htmlparser2@10.0.0:
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
domutils: 3.2.2
entities: 6.0.1
import-meta-resolve@4.2.0: {}
is-docker@3.0.0: {}
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
is-wsl@3.1.0:
dependencies:
is-inside-container: 1.0.0
isexe@2.0.0: {}
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
make-dir@3.1.0:
dependencies:
semver: 6.3.1
mime@2.5.2: {}
minimatch@3.0.8:
dependencies:
brace-expansion: 1.1.12
mrmime@2.0.1: {}
nanoid@3.3.11: {}
open@10.2.0:
dependencies:
default-browser: 5.4.0
define-lazy-prop: 3.0.0
is-inside-container: 1.0.0
wsl-utils: 0.1.0
path-key@3.1.1: {}
picocolors@1.1.1: {}
picomatch@4.0.3: {}
postcss-url@10.1.3(postcss@8.5.6):
dependencies:
make-dir: 3.1.0
mime: 2.5.2
minimatch: 3.0.8
postcss: 8.5.6
xxhashjs: 0.2.2
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
rollup@4.54.0:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.54.0
'@rollup/rollup-android-arm64': 4.54.0
'@rollup/rollup-darwin-arm64': 4.54.0
'@rollup/rollup-darwin-x64': 4.54.0
'@rollup/rollup-freebsd-arm64': 4.54.0
'@rollup/rollup-freebsd-x64': 4.54.0
'@rollup/rollup-linux-arm-gnueabihf': 4.54.0
'@rollup/rollup-linux-arm-musleabihf': 4.54.0
'@rollup/rollup-linux-arm64-gnu': 4.54.0
'@rollup/rollup-linux-arm64-musl': 4.54.0
'@rollup/rollup-linux-loong64-gnu': 4.54.0
'@rollup/rollup-linux-ppc64-gnu': 4.54.0
'@rollup/rollup-linux-riscv64-gnu': 4.54.0
'@rollup/rollup-linux-riscv64-musl': 4.54.0
'@rollup/rollup-linux-s390x-gnu': 4.54.0
'@rollup/rollup-linux-x64-gnu': 4.54.0
'@rollup/rollup-linux-x64-musl': 4.54.0
'@rollup/rollup-openharmony-arm64': 4.54.0
'@rollup/rollup-win32-arm64-msvc': 4.54.0
'@rollup/rollup-win32-ia32-msvc': 4.54.0
'@rollup/rollup-win32-x64-gnu': 4.54.0
'@rollup/rollup-win32-x64-msvc': 4.54.0
fsevents: 2.3.3
run-applescript@7.1.0: {}
semver@6.3.1: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
source-map-js@1.2.1: {}
systemjs@6.15.1: {}
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
typescript@5.9.3: {}
vite-plugin-monkey@7.1.8(postcss@8.5.6)(vite@7.3.0):
dependencies:
acorn: 8.15.0
acorn-walk: 8.3.4
cross-spawn: 7.0.6
htmlparser2: 10.0.0
import-meta-resolve: 4.2.0
magic-string: 0.30.21
mrmime: 2.0.1
open: 10.2.0
picocolors: 1.1.1
postcss-url: 10.1.3(postcss@8.5.6)
systemjs: 6.15.1
optionalDependencies:
vite: 7.3.0
transitivePeerDependencies:
- postcss
vite@7.3.0:
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
rollup: 4.54.0
tinyglobby: 0.2.15
optionalDependencies:
fsevents: 2.3.3
which@2.0.2:
dependencies:
isexe: 2.0.0
wsl-utils@0.1.0:
dependencies:
is-wsl: 3.1.0
xxhashjs@0.2.2:
dependencies:
cuint: 0.2.2

146
monkey/src/main.ts Normal file
View File

@@ -0,0 +1,146 @@
import './style.css'
// Get current video URL
// yt-dlp can handle all URLs directly, so we just return the current URL
function getVideoUrl(): string | null {
return window.location.href
}
// Temporarily hide button
function hideButtonTemporarily(): void {
const container = document.getElementById('vidbee-download-btn')
if (container) {
container.classList.add('vidbee-hidden')
// Auto restore after 5 seconds
setTimeout(() => {
if (container) {
container.classList.remove('vidbee-hidden')
}
}, 5000)
}
}
// Create VidBee download button
function createVidBeeButton(): void {
// Check if button already exists
if (document.getElementById('vidbee-download-btn')) {
return
}
const videoUrl = getVideoUrl()
if (!videoUrl) {
return
}
// Create button container
const container = document.createElement('div')
container.id = 'vidbee-download-btn'
container.className = 'vidbee-download-container'
// Create main download button
const button = document.createElement('button')
button.className = 'vidbee-download-button'
button.setAttribute('aria-label', 'Download with VidBee')
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span class="vidbee-tooltip">Download with VidBee</span>
`
// Create close button
const closeButton = document.createElement('button')
closeButton.className = 'vidbee-close-button'
closeButton.setAttribute('aria-label', 'Hide button')
closeButton.innerHTML = `
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
<span class="vidbee-tooltip">Hide</span>
`
// Handle close button click - temporarily hide
closeButton.addEventListener('click', (e) => {
e.stopPropagation()
hideButtonTemporarily()
})
let clickTimer: number | null = null
let clickCount = 0
// Handle main button click event - single click for download
button.addEventListener('click', () => {
clickCount++
if (clickCount === 1) {
clickTimer = window.setTimeout(() => {
// Single click - trigger download
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
window.location.href = vidbeeUrl
clickCount = 0
}, 300)
} else if (clickCount === 2) {
// Double click - temporarily hide
if (clickTimer !== null) {
clearTimeout(clickTimer)
}
clickCount = 0
hideButtonTemporarily()
}
})
// Assemble container
container.appendChild(button)
container.appendChild(closeButton)
// Insert container directly to body (fixed position)
document.body.appendChild(container)
}
// Initialize when page loads
function init(): void {
// Wait for page to fully load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createVidBeeButton)
} else {
createVidBeeButton()
}
// Handle SPA navigation (when navigating between videos on sites like YouTube, Bilibili, etc.)
let lastUrl = location.href
let urlCheckTimer: number | null = null
const checkUrlChange = () => {
const currentUrl = location.href
if (currentUrl !== lastUrl) {
lastUrl = currentUrl
// Remove old button and create new one
const oldButton = document.getElementById('vidbee-download-btn')
if (oldButton) {
oldButton.remove()
}
// Wait a bit for the page to update (different sites have different update speeds)
const hostname = window.location.hostname
const delay = hostname.includes('bilibili.com') ? 800 : 500
setTimeout(createVidBeeButton, delay)
}
}
// Use MutationObserver for DOM changes (works for most SPA sites)
new MutationObserver(() => {
if (urlCheckTimer !== null) {
clearTimeout(urlCheckTimer)
}
urlCheckTimer = window.setTimeout(checkUrlChange, 100)
}).observe(document.body, { childList: true, subtree: true })
// Also listen to popstate for browser navigation
window.addEventListener('popstate', () => {
setTimeout(checkUrlChange, 300)
})
}
init()

169
monkey/src/style.css Normal file
View File

@@ -0,0 +1,169 @@
/* VidBee Download Button Container */
.vidbee-download-container {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 9999;
transition: all 0.2s ease;
overflow: visible;
}
/* Hidden state */
.vidbee-download-container.vidbee-hidden {
opacity: 0;
pointer-events: none;
transform: scale(0);
}
/* Main Download Button */
.vidbee-download-button {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
padding: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
cursor: pointer;
font-size: 0;
transition: all 0.2s ease;
opacity: 0.6;
overflow: visible;
}
.vidbee-download-button:hover {
opacity: 1;
background: rgba(0, 0, 0, 0.7);
border-color: rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transform: scale(1.1);
}
.vidbee-download-button:active {
transform: scale(0.95);
}
.vidbee-download-button svg {
flex-shrink: 0;
stroke: currentColor;
width: 16px;
height: 16px;
}
/* Tooltip */
.vidbee-tooltip {
position: absolute;
right: calc(100% + 8px);
top: 50%;
padding: 6px 10px;
background: rgba(0, 0, 0, 0.9);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
color: white;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
transform: translateY(-50%) translateX(4px);
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1;
}
.vidbee-tooltip::after {
content: '';
position: absolute;
left: 100%;
top: 50%;
transform: translateY(-50%);
border: 4px solid transparent;
border-left-color: rgba(0, 0, 0, 0.9);
}
.vidbee-download-button:hover .vidbee-tooltip {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
/* Close Button - Hidden by default */
.vidbee-close-button {
position: absolute;
top: -6px;
right: -6px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
background: rgba(255, 77, 77, 0.9);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
cursor: pointer;
font-size: 0;
transition: all 0.2s ease;
z-index: 1;
opacity: 0;
pointer-events: none;
overflow: visible;
}
/* Show close button on container hover */
.vidbee-download-container:hover .vidbee-close-button {
opacity: 1;
pointer-events: auto;
}
.vidbee-close-button:hover {
background: rgba(255, 77, 77, 1);
transform: scale(1.15);
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.4);
}
.vidbee-close-button:active {
transform: scale(0.9);
}
.vidbee-close-button svg {
flex-shrink: 0;
stroke: currentColor;
width: 10px;
height: 10px;
}
/* Close button tooltip positioning */
.vidbee-close-button .vidbee-tooltip {
right: calc(100% + 6px);
top: 50%;
left: auto;
transform: translateY(-50%) translateX(4px);
}
.vidbee-close-button .vidbee-tooltip::after {
left: 100%;
top: 50%;
right: auto;
transform: translateY(-50%);
border-left-color: rgba(0, 0, 0, 0.9);
border-top-color: transparent;
}
.vidbee-download-container:hover .vidbee-close-button:hover .vidbee-tooltip {
opacity: 1;
transform: translateY(-50%) translateX(0);
}

4
monkey/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-monkey/client" />
//// <reference types="vite-plugin-monkey/global" />
/// <reference types="vite-plugin-monkey/style" />

24
monkey/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

13
monkey/tsconfig.node.json Normal file
View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

67
monkey/vite.config.ts Normal file
View File

@@ -0,0 +1,67 @@
import { defineConfig } from 'vite'
import monkey from 'vite-plugin-monkey'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.ts',
userscript: {
icon: 'https://vidbee.org/favicon.svg',
namespace: 'vidbee',
match: [
// YouTube
'https://www.youtube.com/*',
'https://youtube.com/*',
'https://music.youtube.com/*',
// Bilibili (哔哩哔哩)
'https://www.bilibili.com/*',
'https://bilibili.com/*',
// TikTok
'https://www.tiktok.com/*',
'https://tiktok.com/*',
// Vimeo
'https://vimeo.com/*',
'https://www.vimeo.com/*',
// Dailymotion
'https://www.dailymotion.com/*',
'https://dailymotion.com/*',
// Twitch
'https://www.twitch.tv/*',
'https://twitch.tv/*',
// Twitter/X
'https://twitter.com/*',
'https://www.twitter.com/*',
'https://x.com/*',
'https://www.x.com/*',
// Instagram
'https://www.instagram.com/*',
'https://instagram.com/*',
// Facebook
'https://www.facebook.com/*',
'https://facebook.com/*',
'https://fb.com/*',
'https://www.fb.com/*',
// Reddit
'https://www.reddit.com/*',
'https://reddit.com/*',
// SoundCloud
'https://soundcloud.com/*',
'https://www.soundcloud.com/*',
// NicoNico
'https://www.nicovideo.jp/*',
'https://nicovideo.jp/*',
// Kick
'https://kick.com/*',
'https://www.kick.com/*',
// Bandcamp
'https://bandcamp.com/*',
'https://*.bandcamp.com/*',
// Mixcloud
'https://www.mixcloud.com/*',
'https://mixcloud.com/*'
]
}
})
]
})

View File

@@ -39,10 +39,14 @@ protocol.registerSchemesAsPrivileged([
let mainWindow: BrowserWindow | null = null
let isQuitting = false
const pendingDeepLinkUrls: string[] = []
interface DeepLinkData {
url: string
type: 'single' | 'playlist'
}
const pendingDeepLinkUrls: DeepLinkData[] = []
let isRendererReady = false
const parseDownloadDeepLink = (rawUrl: string): string | null => {
const parseDownloadDeepLink = (rawUrl: string): DeepLinkData | null => {
try {
const parsed = new URL(rawUrl)
if (parsed.protocol !== `${APP_PROTOCOL}:`) {
@@ -61,16 +65,22 @@ const parseDownloadDeepLink = (rawUrl: string): string | null => {
return null
}
return targetUrl.trim()
const typeParam = parsed.searchParams.get('type')
const type = typeParam === 'playlist' ? 'playlist' : 'single'
return {
url: targetUrl.trim(),
type
}
} catch (error) {
log.warn('Failed to parse deep link:', error)
return null
}
}
const deliverDeepLink = (videoUrl: string): void => {
const deliverDeepLink = (data: DeepLinkData): void => {
if (!mainWindow || !isRendererReady) {
pendingDeepLinkUrls.push(videoUrl)
pendingDeepLinkUrls.push(data)
return
}
@@ -81,7 +91,7 @@ const deliverDeepLink = (videoUrl: string): void => {
mainWindow.show()
}
mainWindow.focus()
mainWindow.webContents.send('download:deeplink', videoUrl)
mainWindow.webContents.send('download:deeplink', data)
}
const flushPendingDeepLinks = (): void => {
@@ -90,18 +100,18 @@ const flushPendingDeepLinks = (): void => {
}
const pending = pendingDeepLinkUrls.splice(0, pendingDeepLinkUrls.length)
for (const url of pending) {
mainWindow.webContents.send('download:deeplink', url)
for (const data of pending) {
mainWindow.webContents.send('download:deeplink', data)
}
}
const handleDeepLinkUrl = (rawUrl: string): void => {
const videoUrl = parseDownloadDeepLink(rawUrl)
if (!videoUrl) {
const data = parseDownloadDeepLink(rawUrl)
if (!data) {
log.warn('Ignored unsupported deep link:', rawUrl)
return
}
deliverDeepLink(videoUrl)
deliverDeepLink(data)
}
const handleDeepLinkArgv = (argv: string[]): void => {

View File

@@ -5,7 +5,7 @@ import { TitleBar } from '@renderer/components/ui/title-bar'
import type { SubscriptionRule } from '@shared/types'
import { useAtom, useSetAtom } from 'jotai'
import { ThemeProvider } from 'next-themes'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { HashRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router'
import { toast } from 'sonner'
@@ -59,12 +59,15 @@ function AppContent() {
const currentPage = pathToPage(location.pathname)
const supportedSitesUrl = 'https://vidbee.org/supported-sites/'
const handlePageChange = (page: Page) => {
const targetPath = pageToPath[page] ?? '/'
if (normalizePathname(location.pathname) !== targetPath) {
navigate(targetPath)
}
}
const handlePageChange = useCallback(
(page: Page) => {
const targetPath = pageToPath[page] ?? '/'
if (normalizePathname(location.pathname) !== targetPath) {
navigate(targetPath)
}
},
[location.pathname, navigate]
)
const handleOpenSupportedSites = () => {
window.open(supportedSitesUrl, '_blank')
@@ -80,8 +83,9 @@ function AppContent() {
if (!url) {
return
}
// Switch to home page to show download dialog
handlePageChange('home')
// TODO: Handle deep link URL in download dialog
// The DownloadDialog component will handle opening the dialog and parsing the video
}
ipcEvents.on('download:deeplink', handleDeepLink)

View File

@@ -229,6 +229,81 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
[addHistoryRecord, removeDownload]
)
// Listen for deep link events
useEffect(() => {
const handleDeepLink = async (data: unknown) => {
// Support both old format (string) and new format (object with url and type)
let url: string
let type: 'single' | 'playlist' = 'single'
if (typeof data === 'string') {
// Legacy format: just URL string
url = data.trim()
} else if (data && typeof data === 'object' && 'url' in data) {
// New format: object with url and type
url = typeof data.url === 'string' ? data.url.trim() : ''
if ('type' in data && data.type === 'playlist') {
type = 'playlist'
}
} else {
return
}
if (!url) {
return
}
// Open dialog and set URL
setOpen(true)
setActiveTab(type)
if (type === 'playlist') {
// Handle playlist
setPlaylistUrl(url)
setPlaylistInfo(null)
setPlaylistPreviewError(null)
setPlaylistCustomDownloadPath('')
// Wait for dialog to open, then fetch playlist info
setTimeout(async () => {
setPlaylistPreviewError(null)
setPlaylistPreviewLoading(true)
try {
const info = await ipcServices.download.getPlaylistInfo(url)
setPlaylistInfo(info)
if (info.entryCount === 0) {
toast.error(t('playlist.noEntries'))
return
}
toast.success(t('playlist.foundVideos', { count: info.entryCount }))
} catch (error) {
console.error('Failed to fetch playlist info:', error)
const message =
error instanceof Error && error.message ? error.message : t('playlist.previewFailed')
setPlaylistPreviewError(message)
setPlaylistInfo(null)
toast.error(t('playlist.previewFailed'))
} finally {
setPlaylistPreviewLoading(false)
}
}, 100)
} else {
// Handle single video
setUrl(url)
// Wait for dialog to open and settings to load, then fetch video info
setTimeout(async () => {
await fetchVideoInfo(url)
}, 100)
}
}
ipcEvents.on('download:deeplink', handleDeepLink)
return () => {
ipcEvents.removeListener('download:deeplink', handleDeepLink)
}
}, [fetchVideoInfo, t])
useEffect(() => {
if (!open) return

View File

@@ -78,24 +78,24 @@ export function PlaylistDownloadGroup({
const aggregatePercent = totalCount > 0 ? Math.min((totalProgress / totalCount) * 100, 100) : 0
return (
<div className="space-y-2 rounded-md bg-muted/20 px-2.5 py-2">
<div className="space-y-2 rounded-md bg-muted/30 px-2.5 py-2">
<div className="flex items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
<button
type="button"
className="flex shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground"
onClick={() => setIsExpanded((prev) => !prev)}
aria-expanded={isExpanded}
aria-label={toggleLabel}
title={toggleLabel}
>
<button
type="button"
className="flex min-w-0 flex-1 items-center gap-2 rounded-md p-1.5 transition-colors hover:bg-muted/40 active:bg-muted/60 -ml-1.5 -mr-1.5"
onClick={() => setIsExpanded((prev) => !prev)}
aria-expanded={isExpanded}
aria-label={toggleLabel}
title={toggleLabel}
>
<div className="flex shrink-0 items-center justify-center text-muted-foreground">
{isExpanded ? (
<ChevronDown className="h-3.5 w-3.5" />
<ChevronDown className="h-5 w-5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
<ChevronRight className="h-5 w-5" />
)}
</button>
<div className="min-w-0 flex-1">
</div>
<div className="min-w-0 flex-1 text-left">
<p className="truncate text-sm font-medium text-foreground">{displayTitle}</p>
<div className="flex items-center gap-1.5 text-[11px] text-muted-foreground">
<span>
@@ -117,7 +117,7 @@ export function PlaylistDownloadGroup({
)}
</div>
</div>
</div>
</button>
<div className="flex shrink-0 items-center gap-1">
{canDeletePlaylist && (
<Button
@@ -125,13 +125,14 @@ export function PlaylistDownloadGroup({
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full"
onClick={() =>
onClick={(e) => {
e.stopPropagation()
onDeletePlaylist?.(
groupId,
displayTitle,
historyRecords.map((record) => record.id)
)
}
}}
aria-label={t('history.deletePlaylist')}
title={t('history.deletePlaylist')}
>

View File

@@ -1,5 +1,6 @@
import { Button } from '@renderer/components/ui/button'
import { CardContent, CardHeader } from '@renderer/components/ui/card'
import { Checkbox } from '@renderer/components/ui/checkbox'
import {
Dialog,
DialogContent,
@@ -11,7 +12,7 @@ import {
import { cn } from '@renderer/lib/utils'
import { useAtomValue, useSetAtom } from 'jotai'
import { History as HistoryIcon } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useId, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useHistorySync } from '../../hooks/use-history-sync'
@@ -130,6 +131,8 @@ export function UnifiedDownloadHistory({
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
const [confirmAction, setConfirmAction] = useState<ConfirmAction | null>(null)
const [confirmBusy, setConfirmBusy] = useState(false)
const [alsoDeleteFiles, setAlsoDeleteFiles] = useState(false)
const alsoDeleteFilesId = useId()
useHistorySync()
@@ -309,6 +312,11 @@ export function UnifiedDownloadHistory({
if (confirmAction.type === 'delete-selected') {
await ipcServices.history.removeHistoryItems(confirmAction.ids)
removeHistoryRecords(confirmAction.ids)
if (alsoDeleteFiles) {
const idSet = new Set(confirmAction.ids)
const recordsToDelete = historyRecords.filter((record) => idSet.has(record.id))
await deleteHistoryFiles(recordsToDelete)
}
pruneSelectedIds(confirmAction.ids)
toast.success(t('notifications.itemsRemoved', { count: confirmAction.ids.length }))
}
@@ -324,6 +332,7 @@ export function UnifiedDownloadHistory({
)
}
setConfirmAction(null)
setAlsoDeleteFiles(false)
} catch (error) {
if (confirmAction.type === 'delete-selected') {
console.error('Failed to remove selected history items:', error)
@@ -501,6 +510,7 @@ export function UnifiedDownloadHistory({
onOpenChange={(open) => {
if (!open && !confirmBusy) {
setConfirmAction(null)
setAlsoDeleteFiles(false)
}
}}
>
@@ -510,10 +520,28 @@ export function UnifiedDownloadHistory({
<DialogTitle>{confirmContent.title}</DialogTitle>
<DialogDescription>{confirmContent.description}</DialogDescription>
</DialogHeader>
{confirmAction?.type === 'delete-selected' && (
<div className="flex items-center space-x-2">
<Checkbox
id={alsoDeleteFilesId}
checked={alsoDeleteFiles}
onCheckedChange={(checked) => setAlsoDeleteFiles(checked === true)}
/>
<label
htmlFor={alsoDeleteFilesId}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{t('history.alsoDeleteFiles')}
</label>
</div>
)}
<DialogFooter>
<Button
variant="outline"
onClick={() => setConfirmAction(null)}
onClick={() => {
setConfirmAction(null)
setAlsoDeleteFiles(false)
}}
disabled={confirmBusy}
>
{t('download.cancel')}

View File

@@ -222,6 +222,7 @@
"confirmClearAllDescription": "Remove {{count}} items from your history. Files stay on disk.",
"confirmDeleteSelectedTitle": "Remove selected items?",
"confirmDeleteSelectedDescription": "Remove {{count}} items from your history. Files stay on disk.",
"alsoDeleteFiles": "Also delete files",
"confirmDeletePlaylistTitle": "Remove playlist history?",
"confirmDeletePlaylistDescription": "Remove {{count}} items from {{title}} and delete their files.",
"copyToClipboard": "Copy to clipboard",