mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 02:05:09 +01:00
progress bar, skips and more
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com>
This commit is contained in:
parent
7e182a4b7a
commit
860671be00
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -4,5 +4,8 @@
|
|||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
"[json]": {
|
"[json]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "ms-vsliveshare.vsliveshare"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.7.0",
|
"@formkit/auto-animate": "^0.7.0",
|
||||||
"@headlessui/react": "^1.5.0",
|
"@headlessui/react": "^1.5.0",
|
||||||
|
"@movie-web/providers": "^1.0.1",
|
||||||
"@react-spring/web": "^9.7.1",
|
"@react-spring/web": "^9.7.1",
|
||||||
"@sentry/integrations": "^7.49.0",
|
"@sentry/integrations": "^7.49.0",
|
||||||
"@sentry/react": "^7.49.0",
|
"@sentry/react": "^7.49.0",
|
||||||
|
144
pnpm-lock.yaml
144
pnpm-lock.yaml
@ -11,6 +11,9 @@ dependencies:
|
|||||||
'@headlessui/react':
|
'@headlessui/react':
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.7.17(react-dom@17.0.2)(react@17.0.2)
|
version: 1.7.17(react-dom@17.0.2)(react@17.0.2)
|
||||||
|
'@movie-web/providers':
|
||||||
|
specifier: ^1.0.1
|
||||||
|
version: 1.0.1
|
||||||
'@react-spring/web':
|
'@react-spring/web':
|
||||||
specifier: ^9.7.1
|
specifier: ^9.7.1
|
||||||
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
||||||
@ -1826,6 +1829,19 @@ packages:
|
|||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@movie-web/providers@1.0.1:
|
||||||
|
resolution: {integrity: sha512-7f3uQKhym+4F5rC5r+6qHjL8Rx3b8P9r1UJcENlkgULUEjX7I/w4B6FzdRlHnTig+DVwuUabNWHE+hzS/tQQPw==}
|
||||||
|
dependencies:
|
||||||
|
cheerio: 1.0.0-rc.12
|
||||||
|
crypto-js: 4.1.1
|
||||||
|
form-data: 4.0.0
|
||||||
|
nanoid: 3.3.6
|
||||||
|
node-fetch: 2.7.0
|
||||||
|
unpacker: 1.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nodelib/fs.scandir@2.1.5:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -2634,7 +2650,6 @@ packages:
|
|||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/at-least-node@1.0.0:
|
/at-least-node@1.0.0:
|
||||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||||
@ -2718,6 +2733,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/boolbase@1.0.0:
|
||||||
|
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/brace-expansion@1.1.11:
|
/brace-expansion@1.1.11:
|
||||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2818,6 +2837,30 @@ packages:
|
|||||||
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
|
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/cheerio-select@2.1.0:
|
||||||
|
resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-select: 5.1.0
|
||||||
|
css-what: 6.1.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/cheerio@1.0.0-rc.12:
|
||||||
|
resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
cheerio-select: 2.1.0
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
htmlparser2: 8.0.2
|
||||||
|
parse5: 7.1.2
|
||||||
|
parse5-htmlparser2-tree-adapter: 7.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/chokidar@3.5.3:
|
/chokidar@3.5.3:
|
||||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||||
engines: {node: '>= 8.10.0'}
|
engines: {node: '>= 8.10.0'}
|
||||||
@ -2890,7 +2933,6 @@ packages:
|
|||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/commander@2.20.3:
|
/commander@2.20.3:
|
||||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||||
@ -2964,6 +3006,16 @@ packages:
|
|||||||
hyphenate-style-name: 1.0.4
|
hyphenate-style-name: 1.0.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/css-select@5.1.0:
|
||||||
|
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
css-what: 6.1.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
nth-check: 2.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/css-tree@1.1.3:
|
/css-tree@1.1.3:
|
||||||
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
|
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
@ -2972,6 +3024,11 @@ packages:
|
|||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/css-what@6.1.0:
|
||||||
|
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/cssesc@3.0.0:
|
/cssesc@3.0.0:
|
||||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -3055,7 +3112,6 @@ packages:
|
|||||||
/delayed-stream@1.0.0:
|
/delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/dequal@2.0.3:
|
/dequal@2.0.3:
|
||||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||||
@ -3107,6 +3163,18 @@ packages:
|
|||||||
csstype: 3.1.2
|
csstype: 3.1.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dom-serializer@2.0.0:
|
||||||
|
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/domelementtype@2.3.0:
|
||||||
|
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/domexception@4.0.0:
|
/domexception@4.0.0:
|
||||||
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@ -3114,10 +3182,25 @@ packages:
|
|||||||
webidl-conversions: 7.0.0
|
webidl-conversions: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/domhandler@5.0.3:
|
||||||
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
|
engines: {node: '>= 4'}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/dompurify@3.0.5:
|
/dompurify@3.0.5:
|
||||||
resolution: {integrity: sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==}
|
resolution: {integrity: sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/domutils@3.1.0:
|
||||||
|
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||||
|
dependencies:
|
||||||
|
dom-serializer: 2.0.0
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
/eastasianwidth@0.2.0:
|
/eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3145,7 +3228,6 @@ packages:
|
|||||||
/entities@4.5.0:
|
/entities@4.5.0:
|
||||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||||
engines: {node: '>=0.12'}
|
engines: {node: '>=0.12'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/error-stack-parser@2.1.4:
|
/error-stack-parser@2.1.4:
|
||||||
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
||||||
@ -3719,7 +3801,6 @@ packages:
|
|||||||
asynckit: 0.4.0
|
asynckit: 0.4.0
|
||||||
combined-stream: 1.0.8
|
combined-stream: 1.0.8
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fraction.js@4.3.5:
|
/fraction.js@4.3.5:
|
||||||
resolution: {integrity: sha512-58DncB2bO/8ZvTHapG7U2KEbeFFyUbbrFFkHakecpdUSqJrQnEuBeTUPEggIVkx5cnugZJ4IVzk2Nbb32MOxBg==}
|
resolution: {integrity: sha512-58DncB2bO/8ZvTHapG7U2KEbeFFyUbbrFFkHakecpdUSqJrQnEuBeTUPEggIVkx5cnugZJ4IVzk2Nbb32MOxBg==}
|
||||||
@ -3997,6 +4078,15 @@ packages:
|
|||||||
void-elements: 3.1.0
|
void-elements: 3.1.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/htmlparser2@8.0.2:
|
||||||
|
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||||
|
dependencies:
|
||||||
|
domelementtype: 2.3.0
|
||||||
|
domhandler: 5.0.3
|
||||||
|
domutils: 3.1.0
|
||||||
|
entities: 4.5.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/http-proxy-agent@5.0.0:
|
/http-proxy-agent@5.0.0:
|
||||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@ -4588,14 +4678,12 @@ packages:
|
|||||||
/mime-db@1.52.0:
|
/mime-db@1.52.0:
|
||||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime-types@2.1.35:
|
/mime-types@2.1.35:
|
||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/minimatch@3.1.2:
|
/minimatch@3.1.2:
|
||||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||||
@ -4673,7 +4761,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
|
||||||
|
|
||||||
/nanoid@4.0.2:
|
/nanoid@4.0.2:
|
||||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||||
@ -4697,6 +4784,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
|
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-fetch@2.7.0:
|
||||||
|
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||||
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
encoding: ^0.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
encoding:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
whatwg-url: 5.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-releases@2.0.13:
|
/node-releases@2.0.13:
|
||||||
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -4718,6 +4817,12 @@ packages:
|
|||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/nth-check@2.1.1:
|
||||||
|
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||||
|
dependencies:
|
||||||
|
boolbase: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/nwsapi@2.2.7:
|
/nwsapi@2.2.7:
|
||||||
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
|
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -4851,11 +4956,17 @@ packages:
|
|||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/parse5-htmlparser2-tree-adapter@7.0.0:
|
||||||
|
resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==}
|
||||||
|
dependencies:
|
||||||
|
domhandler: 5.0.3
|
||||||
|
parse5: 7.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/parse5@7.1.2:
|
/parse5@7.1.2:
|
||||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
entities: 4.5.0
|
entities: 4.5.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/path-exists@4.0.0:
|
/path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
@ -5910,6 +6021,10 @@ packages:
|
|||||||
url-parse: 1.5.10
|
url-parse: 1.5.10
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tr46@0.0.3:
|
||||||
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tr46@1.0.1:
|
/tr46@1.0.1:
|
||||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6399,6 +6514,10 @@ packages:
|
|||||||
xml-name-validator: 4.0.0
|
xml-name-validator: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/webidl-conversions@3.0.1:
|
||||||
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webidl-conversions@4.0.2:
|
/webidl-conversions@4.0.2:
|
||||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -6428,6 +6547,13 @@ packages:
|
|||||||
webidl-conversions: 7.0.0
|
webidl-conversions: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/whatwg-url@5.0.0:
|
||||||
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
dependencies:
|
||||||
|
tr46: 0.0.3
|
||||||
|
webidl-conversions: 3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/whatwg-url@7.1.0:
|
/whatwg-url@7.1.0:
|
||||||
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
78
src/components/player/atoms/ProgressBar.tsx
Normal file
78
src/components/player/atoms/ProgressBar.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
import { useProgressBar } from "@/hooks/useProgressBar";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
export function ProgressBar() {
|
||||||
|
const { duration, time, buffered } = usePlayerStore((s) => s.progress);
|
||||||
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const setDraggingTime = usePlayerStore((s) => s.setDraggingTime);
|
||||||
|
const setSeeking = usePlayerStore((s) => s.setSeeking);
|
||||||
|
const { isSeeking } = usePlayerStore((s) => s.interface);
|
||||||
|
|
||||||
|
const commitTime = useCallback(
|
||||||
|
(percentage) => {
|
||||||
|
display?.setTime(percentage * duration);
|
||||||
|
},
|
||||||
|
[duration, display]
|
||||||
|
);
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { dragging, dragPercentage, dragMouseDown } = useProgressBar(
|
||||||
|
ref,
|
||||||
|
commitTime
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
setSeeking(dragging);
|
||||||
|
}, [setSeeking, dragging]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraggingTime((dragPercentage / 100) * duration);
|
||||||
|
}, [setDraggingTime, duration, dragPercentage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<div
|
||||||
|
className="group w-full h-8 flex items-center"
|
||||||
|
onMouseDown={dragMouseDown}
|
||||||
|
onTouchStart={dragMouseDown}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"relative w-full h-1 bg-video-progress-background bg-opacity-25 rounded-full transition-[height] duration-100 group-hover:h-1.5",
|
||||||
|
dragging ? "!h-1.5" : "",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{/* Pre-loaded content bar */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 h-full rounded-full bg-video-progress-preloaded bg-opacity-25 flex justify-end items-center"
|
||||||
|
style={{
|
||||||
|
width: `${(buffered / duration) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Actual progress bar */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 h-full rounded-full bg-video-progress-watched flex justify-end items-center"
|
||||||
|
style={{
|
||||||
|
width: `${
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(1, dragging ? dragPercentage / 100 : time / duration)
|
||||||
|
) * 100
|
||||||
|
}%`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"w-[1rem] min-w-[1rem] h-[1rem] rounded-full transform translate-x-1/2 scale-0 group-hover:scale-100 bg-white transition-[transform] duration-100",
|
||||||
|
isSeeking ? "scale-100" : "",
|
||||||
|
].join(" ")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
27
src/components/player/atoms/Skips.tsx
Normal file
27
src/components/player/atoms/Skips.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
|
export function SkipForward() {
|
||||||
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const time = usePlayerStore((s) => s.progress.time);
|
||||||
|
|
||||||
|
const commit = useCallback(() => {
|
||||||
|
display?.setTime(time + 10);
|
||||||
|
}, [display, time]);
|
||||||
|
|
||||||
|
return <VideoPlayerButton onClick={commit} icon={Icons.SKIP_FORWARD} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SkipBackward() {
|
||||||
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const time = usePlayerStore((s) => s.progress.time);
|
||||||
|
|
||||||
|
const commit = useCallback(() => {
|
||||||
|
display?.setTime(time - 10);
|
||||||
|
}, [display, time]);
|
||||||
|
|
||||||
|
return <VideoPlayerButton onClick={commit} icon={Icons.SKIP_BACKWARD} />;
|
||||||
|
}
|
47
src/components/player/atoms/Time.tsx
Normal file
47
src/components/player/atoms/Time.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { VideoPlayerButton } from "@/components/player/internals/Button";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { formatSeconds } from "@/utils/formatSeconds";
|
||||||
|
|
||||||
|
export function Time() {
|
||||||
|
const [timeMode, setTimeMode] = useState(true);
|
||||||
|
|
||||||
|
const { duration, time, draggingTime } = usePlayerStore((s) => s.progress);
|
||||||
|
const { isSeeking } = usePlayerStore((s) => s.interface);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
function toggleMode() {
|
||||||
|
setTimeMode(!timeMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTime = Math.min(
|
||||||
|
Math.max(isSeeking ? draggingTime : time, 0),
|
||||||
|
duration
|
||||||
|
);
|
||||||
|
const secondsRemaining = Math.abs(currentTime - duration);
|
||||||
|
const timeFinished = new Date(Date.now() + secondsRemaining * 1e3);
|
||||||
|
|
||||||
|
const formattedTimeFinished = t("videoPlayer.finishAt", {
|
||||||
|
timeFinished,
|
||||||
|
formatParams: {
|
||||||
|
timeFinished: { hour: "numeric", minute: "numeric" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const child = timeMode ? (
|
||||||
|
<>
|
||||||
|
{formatSeconds(currentTime)} <span>/ {formatSeconds(duration)}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{t("videoPlayer.timeLeft", { timeLeft: formatSeconds(secondsRemaining) })}{" "}
|
||||||
|
• {formattedTimeFinished}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoPlayerButton onClick={() => toggleMode()}>{child}</VideoPlayerButton>
|
||||||
|
);
|
||||||
|
}
|
@ -1,2 +1,5 @@
|
|||||||
export * from "./Pause";
|
export * from "./Pause";
|
||||||
export * from "./Fullscreen";
|
export * from "./Fullscreen";
|
||||||
|
export * from "./ProgressBar";
|
||||||
|
export * from "./Skips";
|
||||||
|
export * from "./Time";
|
||||||
|
@ -1,15 +1,26 @@
|
|||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
|
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||||
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
|
||||||
export function BottomControls(props: {
|
export function BottomControls(props: {
|
||||||
show: boolean;
|
show?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const { hovering } = usePlayerStore((s) => s.interface);
|
||||||
|
const visible =
|
||||||
|
(hovering !== PlayerHoverState.NOT_HOVERING || props.show) ?? false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full absolute bottom-0 flex flex-col pt-32 bg-gradient-to-t from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)]">
|
<div className="w-full text-white">
|
||||||
|
<Transition
|
||||||
|
animation="fade"
|
||||||
|
show={visible}
|
||||||
|
className="pointer-events-none flex justify-end pt-32 bg-gradient-to-t from-black to-transparent [margin-bottom:env(safe-area-inset-bottom)] transition-opacity duration-200 absolute bottom-0 w-full"
|
||||||
|
/>
|
||||||
<Transition
|
<Transition
|
||||||
animation="slide-up"
|
animation="slide-up"
|
||||||
show={props.show}
|
show={visible}
|
||||||
className="pointer-events-auto px-4 pb-2 flex justify-end"
|
className="pointer-events-auto px-4 pb-3 absolute bottom-0 w-full"
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
@ -5,7 +5,9 @@ import {
|
|||||||
DisplayInterfaceEvents,
|
DisplayInterfaceEvents,
|
||||||
} from "@/components/player/display/displayInterface";
|
} from "@/components/player/display/displayInterface";
|
||||||
import { Source } from "@/components/player/hooks/usePlayer";
|
import { Source } from "@/components/player/hooks/usePlayer";
|
||||||
|
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
||||||
import {
|
import {
|
||||||
|
canChangeVolume,
|
||||||
canFullscreen,
|
canFullscreen,
|
||||||
canFullscreenAnyElement,
|
canFullscreenAnyElement,
|
||||||
canWebkitFullscreen,
|
canWebkitFullscreen,
|
||||||
@ -18,12 +20,29 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
let videoElement: HTMLVideoElement | null = null;
|
let videoElement: HTMLVideoElement | null = null;
|
||||||
let containerElement: HTMLElement | null = null;
|
let containerElement: HTMLElement | null = null;
|
||||||
let isFullscreen = false;
|
let isFullscreen = false;
|
||||||
|
let isPausedBeforeSeeking = false;
|
||||||
|
|
||||||
function setSource() {
|
function setSource() {
|
||||||
if (!videoElement || !source) return;
|
if (!videoElement || !source) return;
|
||||||
videoElement.src = source.url;
|
videoElement.src = source.url;
|
||||||
videoElement.addEventListener("play", () => emit("play", undefined));
|
videoElement.addEventListener("play", () => emit("play", undefined));
|
||||||
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
videoElement.addEventListener("pause", () => emit("pause", undefined));
|
||||||
|
videoElement.addEventListener("volumechange", () =>
|
||||||
|
emit("volumechange", videoElement?.volume ?? 0)
|
||||||
|
);
|
||||||
|
videoElement.addEventListener("timeupdate", () =>
|
||||||
|
emit("time", videoElement?.currentTime ?? 0)
|
||||||
|
);
|
||||||
|
videoElement.addEventListener("loadedmetadata", () => {
|
||||||
|
emit("duration", videoElement?.duration ?? 0);
|
||||||
|
});
|
||||||
|
videoElement.addEventListener("progress", () => {
|
||||||
|
if (videoElement)
|
||||||
|
emit(
|
||||||
|
"buffered",
|
||||||
|
handleBuffered(videoElement.currentTime, videoElement.buffered)
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fullscreenChange() {
|
function fullscreenChange() {
|
||||||
@ -58,6 +77,36 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||||||
play() {
|
play() {
|
||||||
videoElement?.play();
|
videoElement?.play();
|
||||||
},
|
},
|
||||||
|
setSeeking(active) {
|
||||||
|
// if it was playing when starting to seek, play again
|
||||||
|
if (!active) {
|
||||||
|
if (!isPausedBeforeSeeking) this.play();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPausedBeforeSeeking = videoElement?.paused ?? true;
|
||||||
|
this.pause();
|
||||||
|
},
|
||||||
|
setTime(t) {
|
||||||
|
if (!videoElement) return;
|
||||||
|
// clamp time between 0 and max duration
|
||||||
|
let time = Math.min(t, videoElement.duration);
|
||||||
|
time = Math.max(0, time);
|
||||||
|
|
||||||
|
if (Number.isNaN(time)) return;
|
||||||
|
emit("time", time);
|
||||||
|
videoElement.currentTime = time;
|
||||||
|
},
|
||||||
|
async setVolume(v) {
|
||||||
|
if (!videoElement) return;
|
||||||
|
|
||||||
|
// clamp time between 0 and 1
|
||||||
|
let volume = Math.min(v, 1);
|
||||||
|
volume = Math.max(0, volume);
|
||||||
|
|
||||||
|
// update state
|
||||||
|
if (await canChangeVolume()) videoElement.volume = volume;
|
||||||
|
},
|
||||||
toggleFullscreen() {
|
toggleFullscreen() {
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
isFullscreen = false;
|
isFullscreen = false;
|
||||||
|
@ -5,6 +5,10 @@ export type DisplayInterfaceEvents = {
|
|||||||
play: void;
|
play: void;
|
||||||
pause: void;
|
pause: void;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
|
volumechange: number;
|
||||||
|
time: number;
|
||||||
|
duration: number;
|
||||||
|
buffered: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||||
@ -14,5 +18,8 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
|||||||
processVideoElement(video: HTMLVideoElement): void;
|
processVideoElement(video: HTMLVideoElement): void;
|
||||||
processContainerElement(container: HTMLElement): void;
|
processContainerElement(container: HTMLElement): void;
|
||||||
toggleFullscreen(): void;
|
toggleFullscreen(): void;
|
||||||
|
setSeeking(active: boolean): void;
|
||||||
|
setVolume(vol: number): void;
|
||||||
|
setTime(t: number): void;
|
||||||
destroy(): void;
|
destroy(): void;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { PointerEvent, useCallback, useEffect, useRef } from "react";
|
||||||
|
|
||||||
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
import { makeVideoElementDisplayInterface } from "@/components/player/display/base";
|
||||||
import { playerStatus } from "@/stores/player/slices/source";
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
@ -26,6 +26,20 @@ function useShouldShowVideoElement() {
|
|||||||
function VideoElement() {
|
function VideoElement() {
|
||||||
const videoEl = useRef<HTMLVideoElement>(null);
|
const videoEl = useRef<HTMLVideoElement>(null);
|
||||||
const display = usePlayerStore((s) => s.display);
|
const display = usePlayerStore((s) => s.display);
|
||||||
|
const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
display?.toggleFullscreen();
|
||||||
|
}, [display]);
|
||||||
|
|
||||||
|
const togglePause = useCallback(
|
||||||
|
(e: PointerEvent<HTMLVideoElement>) => {
|
||||||
|
if (e.pointerType !== "mouse") return;
|
||||||
|
if (isPaused) display?.play();
|
||||||
|
else display?.pause();
|
||||||
|
},
|
||||||
|
[display, isPaused]
|
||||||
|
);
|
||||||
|
|
||||||
// report video element to display interface
|
// report video element to display interface
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -34,7 +48,15 @@ function VideoElement() {
|
|||||||
}
|
}
|
||||||
}, [display, videoEl]);
|
}, [display, videoEl]);
|
||||||
|
|
||||||
return <video className="w-full h-screen" autoPlay ref={videoEl} />;
|
return (
|
||||||
|
<video
|
||||||
|
className="w-full h-screen bg-black"
|
||||||
|
autoPlay
|
||||||
|
ref={videoEl}
|
||||||
|
onDoubleClick={toggleFullscreen}
|
||||||
|
onPointerUp={togglePause}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoContainer() {
|
export function VideoContainer() {
|
||||||
|
8
src/components/player/utils/handleBuffered.ts
Normal file
8
src/components/player/utils/handleBuffered.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export function handleBuffered(time: number, buffered: TimeRanges): number {
|
||||||
|
for (let i = 0; i < buffered.length; i += 1) {
|
||||||
|
if (buffered.start(buffered.length - 1 - i) < time) {
|
||||||
|
return buffered.end(buffered.length - 1 - i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
@ -1,38 +1,50 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
import { MWStreamType } from "@/backend/helpers/streams";
|
import { MWStreamType } from "@/backend/helpers/streams";
|
||||||
import { Player } from "@/components/player";
|
import { Player } from "@/components/player";
|
||||||
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
import { usePlayer } from "@/components/player/hooks/usePlayer";
|
||||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
||||||
import { playerStatus } from "@/stores/player/slices/source";
|
import { playerStatus } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
|
||||||
|
|
||||||
export function PlayerView() {
|
export function PlayerView() {
|
||||||
const { status, playMedia, setScrapeStatus } = usePlayer();
|
const { status, playMedia, setScrapeStatus } = usePlayer();
|
||||||
const hovering = usePlayerStore((s) => s.interface.hovering);
|
|
||||||
|
|
||||||
function scrape() {
|
const startStream = useCallback(() => {
|
||||||
playMedia({
|
playMedia({
|
||||||
type: MWStreamType.MP4,
|
type: MWStreamType.MP4,
|
||||||
// url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
// url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
|
||||||
url: "http://95.111.247.180/darude.mp4",
|
// url: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/WhatCarCanYouGetForAGrand.mp4",
|
||||||
|
url: "http://95.111.247.180/frog.mp4",
|
||||||
});
|
});
|
||||||
}
|
}, [playMedia]);
|
||||||
|
|
||||||
const showControlElements = hovering !== PlayerHoverState.NOT_HOVERING;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Player.Container onLoad={setScrapeStatus}>
|
<Player.Container onLoad={setScrapeStatus}>
|
||||||
<Player.BottomControls show={showControlElements}>
|
<Player.BottomControls>
|
||||||
<Player.Pause />
|
<Player.ProgressBar />
|
||||||
<Player.Fullscreen />
|
<div className="flex justify-between">
|
||||||
|
<div className="flex space-x-3 items-center">
|
||||||
|
<Player.Pause />
|
||||||
|
<Player.SkipBackward />
|
||||||
|
<Player.SkipForward />
|
||||||
|
<Player.Time />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Player.Fullscreen />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Player.BottomControls>
|
</Player.BottomControls>
|
||||||
|
|
||||||
{status === playerStatus.SCRAPING ? (
|
{status === playerStatus.SCRAPING ? (
|
||||||
<div className="w-full h-screen">
|
<ScrapingPart
|
||||||
<p>Its now scraping</p>
|
onGetStream={startStream}
|
||||||
<button type="button" onClick={scrape}>
|
media={{
|
||||||
Finish scraping
|
type: "movie",
|
||||||
</button>
|
title: "Hamilton",
|
||||||
</div>
|
tmdbId: "556574",
|
||||||
|
releaseYear: 2020,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</Player.Container>
|
</Player.Container>
|
||||||
);
|
);
|
||||||
|
160
src/pages/parts/player/ScrapingPart.tsx
Normal file
160
src/pages/parts/player/ScrapingPart.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
import { ScrapeMedia } from "@movie-web/providers";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
|
import { providers } from "@/utils/providers";
|
||||||
|
|
||||||
|
export interface ScrapingProps {
|
||||||
|
media: ScrapeMedia;
|
||||||
|
onGetStream?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrapingSegment {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
status: "failure" | "pending" | "notfound" | "success" | "waiting";
|
||||||
|
reason?: string;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScrapingItems {
|
||||||
|
id: string;
|
||||||
|
children: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function useScrape() {
|
||||||
|
const [sources, setSources] = useState<Record<string, ScrapingSegment>>({});
|
||||||
|
const [sourceOrder, setSourceOrder] = useState<ScrapingItems[]>([]);
|
||||||
|
|
||||||
|
const startScraping = useCallback(
|
||||||
|
async (media: ScrapeMedia) => {
|
||||||
|
if (!providers) return;
|
||||||
|
const output = await providers.runAll({
|
||||||
|
media,
|
||||||
|
events: {
|
||||||
|
init(evt) {
|
||||||
|
console.log("init", evt);
|
||||||
|
setSources(
|
||||||
|
evt.sourceIds
|
||||||
|
.map((v) => {
|
||||||
|
const source = providers.getMetadata(v);
|
||||||
|
if (!source) throw new Error("invalid source id");
|
||||||
|
const out: ScrapingSegment = {
|
||||||
|
name: source.name,
|
||||||
|
id: source.id,
|
||||||
|
status: "waiting",
|
||||||
|
percentage: 0,
|
||||||
|
};
|
||||||
|
return out;
|
||||||
|
})
|
||||||
|
.reduce<Record<string, ScrapingSegment>>((a, v) => {
|
||||||
|
a[v.id] = v;
|
||||||
|
return a;
|
||||||
|
}, {})
|
||||||
|
);
|
||||||
|
setSourceOrder(evt.sourceIds.map((v) => ({ id: v, children: [] })));
|
||||||
|
},
|
||||||
|
start(id) {
|
||||||
|
console.log("start", id);
|
||||||
|
setSources((s) => {
|
||||||
|
if (s[id]) s[id].status = "pending";
|
||||||
|
return { ...s };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
update(evt) {
|
||||||
|
console.log("update", evt);
|
||||||
|
setSources((s) => {
|
||||||
|
if (s[evt.id]) {
|
||||||
|
s[evt.id].status = evt.status;
|
||||||
|
s[evt.id].reason = evt.reason;
|
||||||
|
s[evt.id].percentage = evt.percentage;
|
||||||
|
}
|
||||||
|
return { ...s };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
discoverEmbeds(evt) {
|
||||||
|
console.log("discoverEmbeds", evt);
|
||||||
|
setSources((s) => {
|
||||||
|
evt.embeds.forEach((v) => {
|
||||||
|
const source = providers.getMetadata(v.embedScraperId);
|
||||||
|
if (!source) throw new Error("invalid source id");
|
||||||
|
const out: ScrapingSegment = {
|
||||||
|
name: source.name,
|
||||||
|
id: v.id,
|
||||||
|
status: "waiting",
|
||||||
|
percentage: 0,
|
||||||
|
};
|
||||||
|
s[v.id] = out;
|
||||||
|
});
|
||||||
|
return { ...s };
|
||||||
|
});
|
||||||
|
setSourceOrder((s) => {
|
||||||
|
const source = s.find((v) => v.id === evt.sourceId);
|
||||||
|
if (!source) throw new Error("invalid source id");
|
||||||
|
source.children = evt.embeds.map((v) => v.id);
|
||||||
|
return [...s];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(output);
|
||||||
|
return output;
|
||||||
|
},
|
||||||
|
[setSourceOrder, setSources]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
startScraping,
|
||||||
|
sourceOrder,
|
||||||
|
sources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScrapingPart(props: ScrapingProps) {
|
||||||
|
const { startScraping, sourceOrder, sources } = useScrape();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{sourceOrder.map((order) => {
|
||||||
|
const source = sources[order.id];
|
||||||
|
if (!source) return null;
|
||||||
|
return (
|
||||||
|
<div key={order.id}>
|
||||||
|
<p className="font-bold text-white">{source.name}</p>
|
||||||
|
<p>
|
||||||
|
status: {source.status} ({source.percentage}%)
|
||||||
|
</p>
|
||||||
|
<p>reason: {source.reason}</p>
|
||||||
|
{order.children.map((embedId) => {
|
||||||
|
const embed = sources[embedId];
|
||||||
|
if (!embed) return null;
|
||||||
|
return (
|
||||||
|
<div key={embedId} className="border border-blue-300 rounded">
|
||||||
|
<p className="font-bold text-white">{embed.name}</p>
|
||||||
|
<p>
|
||||||
|
status: {embed.status} ({embed.percentage}%)
|
||||||
|
</p>
|
||||||
|
<p>reason: {embed.reason}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => startScraping(props.media)}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
Start scraping
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.onGetStream?.()}
|
||||||
|
className="block"
|
||||||
|
>
|
||||||
|
Finish scraping
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -30,6 +30,26 @@ export const createDisplaySlice: MakeSlice<DisplaySlice> = (set, get) => ({
|
|||||||
s.interface.isFullscreen = isFullscreen;
|
s.interface.isFullscreen = isFullscreen;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
newDisplay.on("time", (time) =>
|
||||||
|
set((s) => {
|
||||||
|
s.progress.time = time;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
newDisplay.on("volumechange", (vol) =>
|
||||||
|
set((s) => {
|
||||||
|
s.mediaPlaying.volume = vol;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
newDisplay.on("duration", (duration) =>
|
||||||
|
set((s) => {
|
||||||
|
s.progress.duration = duration;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
newDisplay.on("buffered", (buffered) =>
|
||||||
|
set((s) => {
|
||||||
|
s.progress.buffered = buffered;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.display = newDisplay;
|
s.display = newDisplay;
|
||||||
|
@ -14,6 +14,7 @@ export enum PlayerHoverState {
|
|||||||
export interface InterfaceSlice {
|
export interface InterfaceSlice {
|
||||||
interface: {
|
interface: {
|
||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
|
isSeeking: boolean;
|
||||||
hovering: PlayerHoverState;
|
hovering: PlayerHoverState;
|
||||||
|
|
||||||
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
volumeChangedWithKeybind: boolean; // has the volume recently been adjusted with the up/down arrows recently?
|
||||||
@ -23,11 +24,13 @@ export interface InterfaceSlice {
|
|||||||
timeFormat: VideoPlayerTimeFormat; // Time format of the video player
|
timeFormat: VideoPlayerTimeFormat; // Time format of the video player
|
||||||
};
|
};
|
||||||
updateInterfaceHovering(newState: PlayerHoverState): void;
|
updateInterfaceHovering(newState: PlayerHoverState): void;
|
||||||
|
setSeeking(seeking: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set) => ({
|
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||||
interface: {
|
interface: {
|
||||||
isFullscreen: false,
|
isFullscreen: false,
|
||||||
|
isSeeking: false,
|
||||||
leftControlHovering: false,
|
leftControlHovering: false,
|
||||||
hovering: PlayerHoverState.NOT_HOVERING,
|
hovering: PlayerHoverState.NOT_HOVERING,
|
||||||
volumeChangedWithKeybind: false,
|
volumeChangedWithKeybind: false,
|
||||||
@ -37,8 +40,14 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set) => ({
|
|||||||
|
|
||||||
updateInterfaceHovering(newState: PlayerHoverState) {
|
updateInterfaceHovering(newState: PlayerHoverState) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
console.log("setting", newState);
|
|
||||||
s.interface.hovering = newState;
|
s.interface.hovering = newState;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setSeeking(seeking) {
|
||||||
|
const display = get().display;
|
||||||
|
display?.setSeeking(seeking);
|
||||||
|
set((s) => {
|
||||||
|
s.interface.isSeeking = seeking;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -7,13 +7,19 @@ export interface ProgressSlice {
|
|||||||
buffered: number; // how much is buffered
|
buffered: number; // how much is buffered
|
||||||
draggingTime: number; // when dragging, time thats at the cursor
|
draggingTime: number; // when dragging, time thats at the cursor
|
||||||
};
|
};
|
||||||
|
setDraggingTime(draggingTime: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createProgressSlice: MakeSlice<ProgressSlice> = () => ({
|
export const createProgressSlice: MakeSlice<ProgressSlice> = (set) => ({
|
||||||
progress: {
|
progress: {
|
||||||
time: 0,
|
time: 0,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
buffered: 0,
|
buffered: 0,
|
||||||
draggingTime: 0,
|
draggingTime: 0,
|
||||||
},
|
},
|
||||||
|
setDraggingTime(draggingTime: number) {
|
||||||
|
set((s) => {
|
||||||
|
s.progress.draggingTime = draggingTime;
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
28
src/utils/providers.ts
Normal file
28
src/utils/providers.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
ProviderBuilderOptions,
|
||||||
|
ProviderControls,
|
||||||
|
makeProviders,
|
||||||
|
makeSimpleProxyFetcher,
|
||||||
|
makeStandardFetcher,
|
||||||
|
targets,
|
||||||
|
} from "@movie-web/providers";
|
||||||
|
|
||||||
|
import { conf } from "@/setup/config";
|
||||||
|
|
||||||
|
const urls = conf().PROXY_URLS;
|
||||||
|
const fetchers = urls.map((v) => makeSimpleProxyFetcher(v, fetch));
|
||||||
|
let fetchersIndex = Math.floor(Math.random() * fetchers.length);
|
||||||
|
|
||||||
|
function makeLoadBalancedSimpleProxyFetcher() {
|
||||||
|
const fetcher: ProviderBuilderOptions["fetcher"] = (a, b) => {
|
||||||
|
fetchersIndex += 1 % fetchers.length;
|
||||||
|
return fetchers[fetchersIndex](a, b);
|
||||||
|
};
|
||||||
|
return fetcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const providers = makeProviders({
|
||||||
|
fetcher: makeStandardFetcher(fetch),
|
||||||
|
proxiedFetcher: makeLoadBalancedSimpleProxyFetcher(),
|
||||||
|
target: targets.BROWSER,
|
||||||
|
}) as any as ProviderControls;
|
@ -104,7 +104,13 @@ module.exports = {
|
|||||||
|
|
||||||
// video player
|
// video player
|
||||||
video: {
|
video: {
|
||||||
buttonBackground: "#444B5C"
|
buttonBackground: "#444B5C",
|
||||||
|
|
||||||
|
progress: {
|
||||||
|
background: "#8787A8",
|
||||||
|
preloaded: "#8787A8",
|
||||||
|
watched: "#A75FC9"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
}),
|
}),
|
||||||
loadVersion(),
|
loadVersion(),
|
||||||
checker({
|
checker({
|
||||||
|
overlay: {
|
||||||
|
position: "tr",
|
||||||
|
},
|
||||||
typescript: true, // check typescript build errors in dev server
|
typescript: true, // check typescript build errors in dev server
|
||||||
eslint: {
|
eslint: {
|
||||||
// check lint errors in dev server
|
// check lint errors in dev server
|
||||||
@ -94,6 +97,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
Loading…
Reference in New Issue
Block a user