mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +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
20 changed files with 663 additions and 36 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -4,5 +4,8 @@
|
|||
"eslint.format.enable": true,
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "ms-vsliveshare.vsliveshare"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.7.0",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@movie-web/providers": "^1.0.1",
|
||||
"@react-spring/web": "^9.7.1",
|
||||
"@sentry/integrations": "^7.49.0",
|
||||
"@sentry/react": "^7.49.0",
|
||||
|
|
144
pnpm-lock.yaml
144
pnpm-lock.yaml
|
@ -11,6 +11,9 @@ dependencies:
|
|||
'@headlessui/react':
|
||||
specifier: ^1.5.0
|
||||
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':
|
||||
specifier: ^9.7.1
|
||||
version: 9.7.3(react-dom@17.0.2)(react@17.0.2)
|
||||
|
@ -1826,6 +1829,19 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
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:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -2634,7 +2650,6 @@ packages:
|
|||
|
||||
/asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
dev: true
|
||||
|
||||
/at-least-node@1.0.0:
|
||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||
|
@ -2718,6 +2733,10 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
dev: false
|
||||
|
||||
/brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
dependencies:
|
||||
|
@ -2818,6 +2837,30 @@ packages:
|
|||
resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
@ -2890,7 +2933,6 @@ packages:
|
|||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
dev: true
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
|
@ -2964,6 +3006,16 @@ packages:
|
|||
hyphenate-style-name: 1.0.4
|
||||
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:
|
||||
resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
@ -2972,6 +3024,11 @@ packages:
|
|||
source-map: 0.6.1
|
||||
dev: false
|
||||
|
||||
/css-what@6.1.0:
|
||||
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
|
||||
engines: {node: '>= 6'}
|
||||
dev: false
|
||||
|
||||
/cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
|
@ -3055,7 +3112,6 @@ packages:
|
|||
/delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
|
@ -3107,6 +3163,18 @@ packages:
|
|||
csstype: 3.1.2
|
||||
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:
|
||||
resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -3114,10 +3182,25 @@ packages:
|
|||
webidl-conversions: 7.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-F9e6wPGtY+8KNMRAVfxeCOHU0/NPWMSENNq4pQctuXRqqdEPW7q3CrLbR5Nse044WwacyjHGOMlvNsBe1y6z9A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
dev: true
|
||||
|
@ -3145,7 +3228,6 @@ packages:
|
|||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
dev: true
|
||||
|
||||
/error-stack-parser@2.1.4:
|
||||
resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==}
|
||||
|
@ -3719,7 +3801,6 @@ packages:
|
|||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
dev: true
|
||||
|
||||
/fraction.js@4.3.5:
|
||||
resolution: {integrity: sha512-58DncB2bO/8ZvTHapG7U2KEbeFFyUbbrFFkHakecpdUSqJrQnEuBeTUPEggIVkx5cnugZJ4IVzk2Nbb32MOxBg==}
|
||||
|
@ -3997,6 +4078,15 @@ packages:
|
|||
void-elements: 3.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -4588,14 +4678,12 @@ packages:
|
|||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
dev: true
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
@ -4673,7 +4761,6 @@ packages:
|
|||
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/nanoid@4.0.2:
|
||||
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
|
||||
|
@ -4697,6 +4784,18 @@ packages:
|
|||
resolution: {integrity: sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
|
||||
dev: true
|
||||
|
@ -4718,6 +4817,12 @@ packages:
|
|||
path-key: 3.1.1
|
||||
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:
|
||||
resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==}
|
||||
dev: true
|
||||
|
@ -4851,11 +4956,17 @@ packages:
|
|||
callsites: 3.1.0
|
||||
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:
|
||||
resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==}
|
||||
dependencies:
|
||||
entities: 4.5.0
|
||||
dev: true
|
||||
|
||||
/path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
|
@ -5910,6 +6021,10 @@ packages:
|
|||
url-parse: 1.5.10
|
||||
dev: true
|
||||
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: false
|
||||
|
||||
/tr46@1.0.1:
|
||||
resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
|
||||
dependencies:
|
||||
|
@ -6399,6 +6514,10 @@ packages:
|
|||
xml-name-validator: 4.0.0
|
||||
dev: true
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@4.0.2:
|
||||
resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
|
||||
dev: true
|
||||
|
@ -6428,6 +6547,13 @@ packages:
|
|||
webidl-conversions: 7.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==}
|
||||
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 "./Fullscreen";
|
||||
export * from "./ProgressBar";
|
||||
export * from "./Skips";
|
||||
export * from "./Time";
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
import { Transition } from "@/components/Transition";
|
||||
import { PlayerHoverState } from "@/stores/player/slices/interface";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function BottomControls(props: {
|
||||
show: boolean;
|
||||
show?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { hovering } = usePlayerStore((s) => s.interface);
|
||||
const visible =
|
||||
(hovering !== PlayerHoverState.NOT_HOVERING || props.show) ?? false;
|
||||
|
||||
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
|
||||
animation="slide-up"
|
||||
show={props.show}
|
||||
className="pointer-events-auto px-4 pb-2 flex justify-end"
|
||||
show={visible}
|
||||
className="pointer-events-auto px-4 pb-3 absolute bottom-0 w-full"
|
||||
>
|
||||
{props.children}
|
||||
</Transition>
|
||||
|
|
|
@ -5,7 +5,9 @@ import {
|
|||
DisplayInterfaceEvents,
|
||||
} from "@/components/player/display/displayInterface";
|
||||
import { Source } from "@/components/player/hooks/usePlayer";
|
||||
import { handleBuffered } from "@/components/player/utils/handleBuffered";
|
||||
import {
|
||||
canChangeVolume,
|
||||
canFullscreen,
|
||||
canFullscreenAnyElement,
|
||||
canWebkitFullscreen,
|
||||
|
@ -18,12 +20,29 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||
let videoElement: HTMLVideoElement | null = null;
|
||||
let containerElement: HTMLElement | null = null;
|
||||
let isFullscreen = false;
|
||||
let isPausedBeforeSeeking = false;
|
||||
|
||||
function setSource() {
|
||||
if (!videoElement || !source) return;
|
||||
videoElement.src = source.url;
|
||||
videoElement.addEventListener("play", () => emit("play", 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() {
|
||||
|
@ -58,6 +77,36 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
|
|||
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() {
|
||||
if (isFullscreen) {
|
||||
isFullscreen = false;
|
||||
|
|
|
@ -5,6 +5,10 @@ export type DisplayInterfaceEvents = {
|
|||
play: void;
|
||||
pause: void;
|
||||
fullscreen: boolean;
|
||||
volumechange: number;
|
||||
time: number;
|
||||
duration: number;
|
||||
buffered: number;
|
||||
};
|
||||
|
||||
export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
||||
|
@ -14,5 +18,8 @@ export interface DisplayInterface extends Listener<DisplayInterfaceEvents> {
|
|||
processVideoElement(video: HTMLVideoElement): void;
|
||||
processContainerElement(container: HTMLElement): void;
|
||||
toggleFullscreen(): void;
|
||||
setSeeking(active: boolean): void;
|
||||
setVolume(vol: number): void;
|
||||
setTime(t: number): 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 { playerStatus } from "@/stores/player/slices/source";
|
||||
|
@ -26,6 +26,20 @@ function useShouldShowVideoElement() {
|
|||
function VideoElement() {
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
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
|
||||
useEffect(() => {
|
||||
|
@ -34,7 +48,15 @@ function VideoElement() {
|
|||
}
|
||||
}, [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() {
|
||||
|
|
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 { Player } from "@/components/player";
|
||||
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 { usePlayerStore } from "@/stores/player/store";
|
||||
|
||||
export function PlayerView() {
|
||||
const { status, playMedia, setScrapeStatus } = usePlayer();
|
||||
const hovering = usePlayerStore((s) => s.interface.hovering);
|
||||
|
||||
function scrape() {
|
||||
const startStream = useCallback(() => {
|
||||
playMedia({
|
||||
type: MWStreamType.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",
|
||||
});
|
||||
}
|
||||
|
||||
const showControlElements = hovering !== PlayerHoverState.NOT_HOVERING;
|
||||
}, [playMedia]);
|
||||
|
||||
return (
|
||||
<Player.Container onLoad={setScrapeStatus}>
|
||||
<Player.BottomControls show={showControlElements}>
|
||||
<Player.Pause />
|
||||
<Player.Fullscreen />
|
||||
<Player.BottomControls>
|
||||
<Player.ProgressBar />
|
||||
<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>
|
||||
|
||||
{status === playerStatus.SCRAPING ? (
|
||||
<div className="w-full h-screen">
|
||||
<p>Its now scraping</p>
|
||||
<button type="button" onClick={scrape}>
|
||||
Finish scraping
|
||||
</button>
|
||||
</div>
|
||||
<ScrapingPart
|
||||
onGetStream={startStream}
|
||||
media={{
|
||||
type: "movie",
|
||||
title: "Hamilton",
|
||||
tmdbId: "556574",
|
||||
releaseYear: 2020,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</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;
|
||||
})
|
||||
);
|
||||
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) => {
|
||||
s.display = newDisplay;
|
||||
|
|
|
@ -14,6 +14,7 @@ export enum PlayerHoverState {
|
|||
export interface InterfaceSlice {
|
||||
interface: {
|
||||
isFullscreen: boolean;
|
||||
isSeeking: boolean;
|
||||
hovering: PlayerHoverState;
|
||||
|
||||
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
|
||||
};
|
||||
updateInterfaceHovering(newState: PlayerHoverState): void;
|
||||
setSeeking(seeking: boolean): void;
|
||||
}
|
||||
|
||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set) => ({
|
||||
export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set, get) => ({
|
||||
interface: {
|
||||
isFullscreen: false,
|
||||
isSeeking: false,
|
||||
leftControlHovering: false,
|
||||
hovering: PlayerHoverState.NOT_HOVERING,
|
||||
volumeChangedWithKeybind: false,
|
||||
|
@ -37,8 +40,14 @@ export const createInterfaceSlice: MakeSlice<InterfaceSlice> = (set) => ({
|
|||
|
||||
updateInterfaceHovering(newState: PlayerHoverState) {
|
||||
set((s) => {
|
||||
console.log("setting", 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
|
||||
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: {
|
||||
time: 0,
|
||||
duration: 0,
|
||||
buffered: 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: {
|
||||
buttonBackground: "#444B5C"
|
||||
buttonBackground: "#444B5C",
|
||||
|
||||
progress: {
|
||||
background: "#8787A8",
|
||||
preloaded: "#8787A8",
|
||||
watched: "#A75FC9"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,9 @@ export default defineConfig(({ mode }) => {
|
|||
}),
|
||||
loadVersion(),
|
||||
checker({
|
||||
overlay: {
|
||||
position: "tr",
|
||||
},
|
||||
typescript: true, // check typescript build errors in dev server
|
||||
eslint: {
|
||||
// check lint errors in dev server
|
||||
|
@ -94,6 +97,7 @@ export default defineConfig(({ mode }) => {
|
|||
},
|
||||
}),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
|
|
Loading…
Reference in a new issue