Browse Source

Merge pull request #9 from QortalSeth/main

Subscriptions to channels added
pull/14/head
Qortal Dev 7 months ago committed by GitHub
parent
commit
29032a75d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 496
      package-lock.json
  2. 2
      package.json
  3. 36
      src/App.tsx
  4. 32
      src/components/common/SubscribeButton.tsx
  5. 32
      src/components/common/VideoPlayer/VideoPlayer-styles.ts
  6. 86
      src/components/common/VideoPlayer/VideoPlayer.tsx
  7. 682
      src/components/common/VideoPlayer/VideoPlayerGlobal.tsx
  8. 648
      src/components/common/VideoPlayerGlobal.tsx
  9. 4
      src/constants/Misc.ts
  10. 86
      src/global.d.ts
  11. 58
      src/hooks/useFetchVideos.tsx
  12. 124
      src/pages/Home/Channels.tsx
  13. 547
      src/pages/Home/Home.tsx
  14. 12
      src/pages/Home/VideoList-styles.tsx
  15. 927
      src/pages/Home/VideoList.tsx
  16. 3
      src/pages/PlaylistContent/PlaylistContent.tsx
  17. 19
      src/pages/VideoContent/VideoContent.tsx
  18. 59
      src/state/features/persistSlice.ts
  19. 194
      src/state/features/videoSlice.ts
  20. 54
      src/state/store.ts
  21. 2
      src/wrappers/GlobalWrapper.tsx

496
package-lock.json generated

@ -11,6 +11,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
"@mui/lab": "^5.0.0-alpha.163",
"@mui/material": "^5.11.13",
"@reduxjs/toolkit": "^1.9.3",
"compressorjs": "^1.2.1",
@ -27,6 +28,7 @@
"react-rnd": "^10.4.1",
"react-router-dom": "^6.9.0",
"react-toastify": "^9.1.2",
"redux-persist": "^6.0.0",
"short-unique-id": "^4.4.4",
"ts-key-enum": "^2.0.12"
},
@ -355,11 +357,11 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
"dependencies": {
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
@ -985,6 +987,40 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
"dependencies": {
"@floating-ui/utils": "^0.2.1"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.1"
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
"integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
"dependencies": {
"@floating-ui/dom": "^1.6.1"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@ -1067,25 +1103,24 @@
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz",
"integrity": "sha512-ejhtqYJpjDgHGEljjMBQWZ22yEK0OzIXNa7toJmmXsP4TT3W7xVy8bTJ0TniPDf+JNjrsgfgiFTDGdlEhV1E+g==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@emotion/is-prop-valid": "^1.2.1",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"version": "5.0.0-beta.34",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz",
"integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"@popperjs/core": "^2.11.8",
"clsx": "^1.2.1",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@ -1098,13 +1133,21 @@
}
}
},
"node_modules/@mui/base/node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.13.4",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz",
"integrity": "sha512-yFrMWcrlI0TqRN5jpb6Ma9iI7sGTHpytdzzL33oskFHNQ8UgrtPas33Y1K7sWAMwCrr1qbWDrOHLAQG4tAzuSw==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.7.tgz",
"integrity": "sha512-AuF+Wo2Mp/edaO6vJnWjg+gj4tzEz5ChMZnAQpc22DXpSvM8ddgGcZvM7D7F99pIBoSv8ub+Iz0viL+yuGVmhg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
@ -1132,19 +1175,67 @@
}
}
},
"node_modules/@mui/lab": {
"version": "5.0.0-alpha.163",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.163.tgz",
"integrity": "sha512-ieOX3LFBln78jgNsBca0JUX+zAC2p6/u2P9b7rU9eZIr0AK44b5Qr8gDOWI1JfJtib4kxLGd1Msasrbxy5cMSQ==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.34",
"@mui/system": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material": ">=5.15.0",
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@emotion/react": {
"optional": true
},
"@emotion/styled": {
"optional": true
},
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/lab/node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@mui/material": {
"version": "5.13.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.5.tgz",
"integrity": "sha512-eMay+Ue1OYXOFMQA5Aau7qbAa/kWHLAyi0McsbPTWssCbGehqkF6CIdPsfVGw6tlO+xPee1hUitphHJNL3xpOQ==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@mui/base": "5.0.0-beta.4",
"@mui/core-downloads-tracker": "^5.13.4",
"@mui/system": "^5.13.5",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"@types/react-transition-group": "^4.4.6",
"clsx": "^1.2.1",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz",
"integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.34",
"@mui/core-downloads-tracker": "^5.15.7",
"@mui/system": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
@ -1155,7 +1246,7 @@
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
@ -1176,13 +1267,21 @@
}
}
},
"node_modules/@mui/material/node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@mui/private-theming": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz",
"integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.7.tgz",
"integrity": "sha512-bcEeeXm7GyQCQvN9dwo8htGv8/6tP05p0i02Z7GXm5EoDPlBcqTNGugsjNLoGq6B0SsdyanjJGw0Jw00o1yAOA==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@mui/utils": "^5.13.1",
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.15.7",
"prop-types": "^15.8.1"
},
"engines": {
@ -1190,7 +1289,7 @@
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@ -1203,11 +1302,11 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "5.13.2",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz",
"integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.7.tgz",
"integrity": "sha512-ixSdslOjK1kzdGcxqj7O3d14By/LPQ7EWknsViQ8RaeT863EAQemS+zvUJDTcOpkfJh6q6gPnYMIb2TJCs9eWA==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@ -1217,7 +1316,7 @@
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
@ -1234,16 +1333,16 @@
}
},
"node_modules/@mui/system": {
"version": "5.13.5",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.5.tgz",
"integrity": "sha512-n0gzUxoZ2ZHZgnExkh2Htvo9uW2oakofgPRQrDoa/GQOWyRD0NH9MDszBwOb6AAoXZb+OV5TE7I4LeZ/dzgHYA==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@mui/private-theming": "^5.13.1",
"@mui/styled-engine": "^5.13.2",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"clsx": "^1.2.1",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.7.tgz",
"integrity": "sha512-9alZ4/dLxsTwUOdqakgzxiL5YW6ntqj0CfzWImgWnBMTZhgGcPsbYpBLniNkkk7/jptma4/bykWXHwju/ls/pg==",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.15.7",
"@mui/styled-engine": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
},
@ -1252,7 +1351,7 @@
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
@ -1272,12 +1371,20 @@
}
}
},
"node_modules/@mui/system/node_modules/clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@mui/types": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
"integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
"integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
"peerDependencies": {
"@types/react": "*"
"@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@ -1286,13 +1393,12 @@
}
},
"node_modules/@mui/utils": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz",
"integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.7.tgz",
"integrity": "sha512-8qhsxQRNV6aEOjjSk6YQIYJxkF5klhj8oG1FEEU4z6HV78TjNqRxMP08QGcdsibEbez+nihAaz6vu83b4XqbAg==",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/prop-types": "^15.7.5",
"@types/react-is": "^18.2.0",
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
@ -1301,10 +1407,16 @@
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui"
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
@ -1619,9 +1731,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"node_modules/@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/quill": {
"version": "1.3.10",
@ -1650,18 +1762,10 @@
"@types/react": "*"
}
},
"node_modules/@types/react-is": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
"integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
"integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"dependencies": {
"@types/react": "*"
}
@ -3988,6 +4092,14 @@
"@babel/runtime": "^7.9.2"
}
},
"node_modules/redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"peerDependencies": {
"redux": ">4.0.0"
}
},
"node_modules/redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
@ -3997,9 +4109,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
@ -4782,11 +4894,11 @@
}
},
"@babel/runtime": {
"version": "7.22.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz",
"integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==",
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
"integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
"requires": {
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
}
},
"@babel/template": {
@ -5152,6 +5264,36 @@
"integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==",
"dev": true
},
"@floating-ui/core": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz",
"integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==",
"requires": {
"@floating-ui/utils": "^0.2.1"
}
},
"@floating-ui/dom": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.1.tgz",
"integrity": "sha512-iA8qE43/H5iGozC3W0YSnVSW42Vh522yyM1gj+BqRwVsTNOyr231PsXDaV04yT39PsO0QL2QpbI/M0ZaLUQgRQ==",
"requires": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.1"
}
},
"@floating-ui/react-dom": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz",
"integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==",
"requires": {
"@floating-ui/dom": "^1.6.1"
}
},
"@floating-ui/utils": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz",
"integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q=="
},
"@humanwhocodes/config-array": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
@ -5215,24 +5357,30 @@
}
},
"@mui/base": {
"version": "5.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.4.tgz",
"integrity": "sha512-ejhtqYJpjDgHGEljjMBQWZ22yEK0OzIXNa7toJmmXsP4TT3W7xVy8bTJ0TniPDf+JNjrsgfgiFTDGdlEhV1E+g==",
"requires": {
"@babel/runtime": "^7.21.0",
"@emotion/is-prop-valid": "^1.2.1",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"version": "5.0.0-beta.34",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.34.tgz",
"integrity": "sha512-e2mbTGTtReD/y5RFwnhkl1Tgl3XwgJhY040IlfkTVaU9f5LWrVhEnpRsYXu3B1CtLrwiWs4cu7aMHV9yRd4jpw==",
"requires": {
"@babel/runtime": "^7.23.9",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"@popperjs/core": "^2.11.8",
"clsx": "^1.2.1",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"dependencies": {
"clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
}
}
},
"@mui/core-downloads-tracker": {
"version": "5.13.4",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz",
"integrity": "sha512-yFrMWcrlI0TqRN5jpb6Ma9iI7sGTHpytdzzL33oskFHNQ8UgrtPas33Y1K7sWAMwCrr1qbWDrOHLAQG4tAzuSw=="
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.7.tgz",
"integrity": "sha512-AuF+Wo2Mp/edaO6vJnWjg+gj4tzEz5ChMZnAQpc22DXpSvM8ddgGcZvM7D7F99pIBoSv8ub+Iz0viL+yuGVmhg=="
},
"@mui/icons-material": {
"version": "5.11.16",
@ -5242,75 +5390,109 @@
"@babel/runtime": "^7.21.0"
}
},
"@mui/lab": {
"version": "5.0.0-alpha.163",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.163.tgz",
"integrity": "sha512-ieOX3LFBln78jgNsBca0JUX+zAC2p6/u2P9b7rU9eZIr0AK44b5Qr8gDOWI1JfJtib4kxLGd1Msasrbxy5cMSQ==",
"requires": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.34",
"@mui/system": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"clsx": "^2.1.0",
"prop-types": "^15.8.1"
},
"dependencies": {
"clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
}
}
},
"@mui/material": {
"version": "5.13.5",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.5.tgz",
"integrity": "sha512-eMay+Ue1OYXOFMQA5Aau7qbAa/kWHLAyi0McsbPTWssCbGehqkF6CIdPsfVGw6tlO+xPee1hUitphHJNL3xpOQ==",
"requires": {
"@babel/runtime": "^7.21.0",
"@mui/base": "5.0.0-beta.4",
"@mui/core-downloads-tracker": "^5.13.4",
"@mui/system": "^5.13.5",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"@types/react-transition-group": "^4.4.6",
"clsx": "^1.2.1",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.7.tgz",
"integrity": "sha512-l6+AiKZH3iOJmZCnlpel8ghYQe9Lq0BEuKP8fGj3g5xz4arO9GydqYAtLPMvuHKtArj8lJGNuT2yHYxmejincA==",
"requires": {
"@babel/runtime": "^7.23.9",
"@mui/base": "5.0.0-beta.34",
"@mui/core-downloads-tracker": "^5.15.7",
"@mui/system": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"react-transition-group": "^4.4.5"
},
"dependencies": {
"clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
}
}
},
"@mui/private-theming": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz",
"integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.7.tgz",
"integrity": "sha512-bcEeeXm7GyQCQvN9dwo8htGv8/6tP05p0i02Z7GXm5EoDPlBcqTNGugsjNLoGq6B0SsdyanjJGw0Jw00o1yAOA==",
"requires": {
"@babel/runtime": "^7.21.0",
"@mui/utils": "^5.13.1",
"@babel/runtime": "^7.23.9",
"@mui/utils": "^5.15.7",
"prop-types": "^15.8.1"
}
},
"@mui/styled-engine": {
"version": "5.13.2",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.13.2.tgz",
"integrity": "sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.7.tgz",
"integrity": "sha512-ixSdslOjK1kzdGcxqj7O3d14By/LPQ7EWknsViQ8RaeT863EAQemS+zvUJDTcOpkfJh6q6gPnYMIb2TJCs9eWA==",
"requires": {
"@babel/runtime": "^7.21.0",
"@babel/runtime": "^7.23.9",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
}
},
"@mui/system": {
"version": "5.13.5",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.5.tgz",
"integrity": "sha512-n0gzUxoZ2ZHZgnExkh2Htvo9uW2oakofgPRQrDoa/GQOWyRD0NH9MDszBwOb6AAoXZb+OV5TE7I4LeZ/dzgHYA==",
"requires": {
"@babel/runtime": "^7.21.0",
"@mui/private-theming": "^5.13.1",
"@mui/styled-engine": "^5.13.2",
"@mui/types": "^7.2.4",
"@mui/utils": "^5.13.1",
"clsx": "^1.2.1",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.7.tgz",
"integrity": "sha512-9alZ4/dLxsTwUOdqakgzxiL5YW6ntqj0CfzWImgWnBMTZhgGcPsbYpBLniNkkk7/jptma4/bykWXHwju/ls/pg==",
"requires": {
"@babel/runtime": "^7.23.9",
"@mui/private-theming": "^5.15.7",
"@mui/styled-engine": "^5.15.7",
"@mui/types": "^7.2.13",
"@mui/utils": "^5.15.7",
"clsx": "^2.1.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
},
"dependencies": {
"clsx": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg=="
}
}
},
"@mui/types": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
"integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
"integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
"requires": {}
},
"@mui/utils": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz",
"integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==",
"version": "5.15.7",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.7.tgz",
"integrity": "sha512-8qhsxQRNV6aEOjjSk6YQIYJxkF5klhj8oG1FEEU4z6HV78TjNqRxMP08QGcdsibEbez+nihAaz6vu83b4XqbAg==",
"requires": {
"@babel/runtime": "^7.21.0",
"@types/prop-types": "^15.7.5",
"@types/react-is": "^18.2.0",
"@babel/runtime": "^7.23.9",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
}
@ -5521,9 +5703,9 @@
"integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA=="
},
"@types/prop-types": {
"version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
"integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w=="
"version": "15.7.11",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"@types/quill": {
"version": "1.3.10",
@ -5552,18 +5734,10 @@
"@types/react": "*"
}
},
"@types/react-is": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.1.tgz",
"integrity": "sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==",
"requires": {
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",
"integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==",
"version": "4.4.10",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz",
"integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==",
"requires": {
"@types/react": "*"
}
@ -7206,6 +7380,12 @@
"@babel/runtime": "^7.9.2"
}
},
"redux-persist": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==",
"requires": {}
},
"redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
@ -7213,9 +7393,9 @@
"requires": {}
},
"regenerator-runtime": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"regexp.prototype.flags": {
"version": "1.5.1",

2
package.json

@ -13,6 +13,7 @@
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11",
"@mui/lab": "^5.0.0-alpha.163",
"@mui/material": "^5.11.13",
"@reduxjs/toolkit": "^1.9.3",
"compressorjs": "^1.2.1",
@ -29,6 +30,7 @@
"react-rnd": "^10.4.1",
"react-router-dom": "^6.9.0",
"react-toastify": "^9.1.2",
"redux-persist": "^6.0.0",
"short-unique-id": "^4.4.4",
"ts-key-enum": "^2.0.12"
},

36
src/App.tsx

@ -12,28 +12,36 @@ import { VideoContent } from "./pages/VideoContent/VideoContent";
import DownloadWrapper from "./wrappers/DownloadWrapper";
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
import { PlaylistContent } from "./pages/PlaylistContent/PlaylistContent";
import { PersistGate } from "redux-persist/integration/react";
import { persistStore } from "redux-persist";
function App() {
// const themeColor = window._qdnTheme
const [theme, setTheme] = useState("dark");
let persistor = persistStore(store);
return (
<Provider store={store}>
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
<Notification />
<DownloadWrapper>
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/video/:name/:id" element={<VideoContent />} />
<Route path="/playlist/:name/:id" element={<PlaylistContent />} />
<Route path="/channel/:name" element={<IndividualProfile />} />
</Routes>
</GlobalWrapper>
</DownloadWrapper>
</ThemeProvider>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
<Notification />
<DownloadWrapper>
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
<CssBaseline />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/video/:name/:id" element={<VideoContent />} />
<Route
path="/playlist/:name/:id"
element={<PlaylistContent />}
/>
<Route path="/channel/:name" element={<IndividualProfile />} />
</Routes>
</GlobalWrapper>
</DownloadWrapper>
</ThemeProvider>
</PersistGate>
</Provider>
);
}

32
src/components/common/SubscribeButton.tsx

@ -1,21 +1,42 @@
import { Button, ButtonProps } from "@mui/material";
import { MouseEvent } from "react";
import { MouseEvent, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { subscribe, unSubscribe } from "../../state/features/persistSlice.ts";
interface SubscribeButtonProps extends ButtonProps {
name: string;
}
const isSubscribed = false;
export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
const dispatch = useDispatch();
const persistSelector = useSelector((state: RootState) => {
return state.persist;
});
const [isSubscribed, setIsSubscribed] = useState<boolean>(false);
useEffect(() => {
setIsSubscribed(persistSelector.subscriptionList.includes(name));
}, []);
const subscribeToRedux = () => {
dispatch(subscribe(name));
setIsSubscribed(true);
};
const unSubscribeFromRedux = () => {
dispatch(unSubscribe(name));
setIsSubscribed(false);
};
const manageSubscription = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
console.log("subscribed to: ", name);
isSubscribed ? unSubscribeFromRedux() : subscribeToRedux();
};
const verticalPadding = "3px";
const horizontalPadding = "8px";
const buttonStyle = {
...props.sx,
fontSize: "15px",
fontWeight: "700",
paddingTop: verticalPadding,
@ -23,7 +44,8 @@ export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
paddingLeft: horizontalPadding,
paddingRight: horizontalPadding,
borderRadius: 28,
display: "none",
height: "45px",
...props.sx,
};
return (
<Button

32
src/components/common/VideoPlayer/VideoPlayer-styles.ts

@ -0,0 +1,32 @@
import { styled } from "@mui/system";
import { Box } from "@mui/material";
export const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
max-height: 70vh;
`;
export const VideoElement = styled("video")`
width: 100%;
background: rgb(33, 33, 33);
max-height: 70vh;
`;
export const ControlsContainer = styled(Box)`
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
`;

86
src/components/common/VideoPlayer.tsx → src/components/common/VideoPlayer/VideoPlayer.tsx

@ -12,44 +12,21 @@ import {
VolumeOff,
} from "@mui/icons-material";
import { styled } from "@mui/system";
import { MyContext } from "../../wrappers/DownloadWrapper";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { RootState } from "../../../state/store.ts";
import { Refresh } from "@mui/icons-material";
import { Menu, MenuItem } from "@mui/material";
import { MoreVert as MoreIcon } from "@mui/icons-material";
import { setVideoPlaying } from "../../state/features/globalSlice";
const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
max-height: 70vh;
`;
const VideoElement = styled("video")`
width: 100%;
height: auto;
background: rgb(33, 33, 33);
max-height: 70vh;
`;
const ControlsContainer = styled(Box)`
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
`;
import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
import {
ControlsContainer,
VideoContainer,
VideoElement,
} from "./VideoPlayer-styles.ts";
import CSS from "csstype";
import { setReduxPlaybackRate } from "../../../state/features/persistSlice.ts";
interface VideoPlayerProps {
src?: string;
@ -65,6 +42,7 @@ interface VideoPlayerProps {
nextVideo?: any;
onEnd?: () => void;
autoPlay?: boolean;
style?: CSS.Properties;
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
@ -80,7 +58,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
nextVideo,
onEnd,
autoPlay,
style = {},
}) => {
const videoSelector = useSelector((state: RootState) => state.video);
const persistSelector = useSelector((state: RootState) => state.persist);
const dispatch = useDispatch();
const videoRef = useRef<HTMLVideoElement | null>(null);
const [playing, setPlaying] = useState(false);
@ -92,18 +73,24 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [canPlay, setCanPlay] = useState(false);
const [startPlay, setStartPlay] = useState(false);
const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [playbackRate, setPlaybackRate] = useState(
persistSelector.playbackRate
);
const [anchorEl, setAnchorEl] = useState(null);
const [showControlsFullScreen, setShowControlsFullScreen] =
useState<boolean>(true);
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const { downloads } = useSelector((state: RootState) => state.global);
const reDownload = useRef<boolean>(false);
const reDownloadNextVid = useRef<boolean>(false);
const isFetchingProperties = useRef<boolean>(false);
const status = useRef<null | string>(null);
const { downloads } = useSelector((state: RootState) => state.global);
const download = useMemo(() => {
if (!downloads || !identifier) return {};
const findDownload = downloads[identifier];
@ -126,8 +113,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
videoRef.current.playbackRate = playbackRate;
setPlaybackRate(newSpeed);
dispatch(setReduxPlaybackRate(newSpeed));
}
};
@ -299,6 +288,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const handleCanPlay = () => {
setIsLoading(false);
setCanPlay(true);
updatePlaybackRate(playbackRate);
};
const getSrc = React.useCallback(async () => {
@ -678,6 +668,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
...customStyle,
}}
>
{isLoading && (
@ -781,28 +772,29 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
onMouseEnter={e => {
setShowControlsFullScreen(true);
}}
onMouseLeave={e => {
setShowControlsFullScreen(false);
}}
preload="metadata"
style={
startPlay
? {
...customStyle,
objectFit: "fill",
objectFit: persistSelector.stretchVideoSetting,
height: "calc(100% - 80px)",
}
: { ...customStyle }
: { ...customStyle, height: "100%" }
}
/>
<ControlsContainer
style={
startPlay
? {
bottom: from === "create" ? "15px" : 0,
padding: "8px",
}
: { bottom: from === "create" ? "15px" : 0, padding: "0px" }
}
style={{ bottom: from === "create" ? "15px" : 0, padding: "0px" }}
display={showControlsFullScreen ? "flex" : "none"}
>
{isMobileView && canPlay ? (
{isMobileView && canPlay && showControlsFullScreen ? (
<>
<IconButton
sx={{

682
src/components/common/VideoPlayer/VideoPlayerGlobal.tsx

@ -0,0 +1,682 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { Box, IconButton, Slider, useTheme } from "@mui/material";
import { CircularProgress, Typography } from "@mui/material";
import { Key } from "ts-key-enum";
import {
PlayArrow,
Pause,
VolumeUp,
Fullscreen,
PictureInPicture,
VolumeOff,
} from "@mui/icons-material";
import { styled } from "@mui/system";
import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../state/store.ts";
import { Refresh } from "@mui/icons-material";
import CloseIcon from "@mui/icons-material/Close";
import { Menu, MenuItem } from "@mui/material";
import { MoreVert as MoreIcon } from "@mui/icons-material";
import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
`;
const VideoElement = styled("video")`
width: 100%;
height: auto;
max-height: calc(100vh - 150px);
background: rgb(33, 33, 33);
`;
const ControlsContainer = styled(Box)`
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background-color: rgba(0, 0, 0, 0.6);
`;
interface VideoPlayerProps {
src?: string;
poster?: string;
name?: string;
identifier?: string;
service?: string;
autoplay?: boolean;
from?: string | null;
customStyle?: any;
user?: string;
jsonId?: string;
element?: null | any;
checkIfDrag?: () => boolean;
}
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({
poster,
name,
identifier,
service,
autoplay = true,
from = null,
customStyle = {},
user = "",
jsonId = "",
element,
checkIfDrag,
}) => {
const theme = useTheme();
const videoRef = useRef<HTMLVideoElement | null>(null);
const [playing, setPlaying] = useState(false);
const [volume, setVolume] = useState(1);
const [mutedVolume, setMutedVolume] = useState(1);
const [isMuted, setIsMuted] = useState(false);
const [progress, setProgress] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [canPlay, setCanPlay] = useState(false);
const [startPlay, setStartPlay] = useState(false);
const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [anchorEl, setAnchorEl] = useState(null);
const dispatch = useDispatch();
const reDownload = useRef<boolean>(false);
const { downloads } = useSelector((state: RootState) => state.global);
const download = useMemo(() => {
if (!downloads || !identifier) return {};
const findDownload = downloads[identifier];
if (!findDownload) return {};
return findDownload;
}, [downloads, identifier]);
const resourceStatus = useMemo(() => {
return download?.status || {};
}, [download]);
const minSpeed = 0.25;
const maxSpeed = 4.0;
const speedChange = 0.25;
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed) newSpeed = minSpeed;
videoRef.current.playbackRate = newSpeed;
setPlaybackRate(newSpeed);
}
};
const increaseSpeed = (wrapOverflow = true) => {
const changedSpeed = playbackRate + speedChange;
let newSpeed = wrapOverflow
? changedSpeed
: Math.min(changedSpeed, maxSpeed);
if (videoRef.current) {
updatePlaybackRate(newSpeed);
}
};
const decreaseSpeed = () => {
if (videoRef.current) {
updatePlaybackRate(playbackRate - speedChange);
}
};
const toggleRef = useRef<any>(null);
const { downloadVideo } = useContext(MyContext);
const togglePlay = async () => {
if (checkIfDrag && checkIfDrag()) return;
if (!videoRef.current) return;
if (playing) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setPlaying(prev => !prev);
};
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return;
videoRef.current.volume = value as number;
setVolume(value as number);
setIsMuted(false);
};
const onProgressChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return;
videoRef.current.currentTime = value as number;
setProgress(value as number);
if (!playing) {
videoRef.current.play();
setPlaying(true);
}
};
const handleEnded = () => {
setPlaying(false);
};
const updateProgress = () => {
if (!videoRef.current) return;
setProgress(videoRef.current.currentTime);
};
const [isFullscreen, setIsFullscreen] = useState(false);
const enterFullscreen = () => {
if (!videoRef.current) return;
if (videoRef.current.requestFullscreen) {
videoRef.current.requestFullscreen();
}
};
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
}
};
const toggleFullscreen = () => {
isFullscreen ? exitFullscreen() : enterFullscreen();
};
const togglePictureInPicture = async () => {
if (!videoRef.current) return;
if (document.pictureInPictureElement === videoRef.current) {
await document.exitPictureInPicture();
} else {
await videoRef.current.requestPictureInPicture();
}
};
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener("fullscreenchange", handleFullscreenChange);
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange);
};
}, []);
const handleCanPlay = () => {
setIsLoading(false);
setCanPlay(true);
};
useEffect(() => {
const videoElement = videoRef.current;
const handleLeavePictureInPicture = async (event: any) => {
const target = event?.target;
if (target) {
target.pause();
if (setPlaying) {
setPlaying(false);
}
}
};
if (videoElement) {
videoElement.addEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
return () => {
if (videoElement) {
videoElement.removeEventListener(
"leavepictureinpicture",
handleLeavePictureInPicture
);
}
};
}, []);
function formatTime(seconds: number): string {
seconds = Math.floor(seconds);
let minutes: number | string = Math.floor(seconds / 60);
let hours: number | string = Math.floor(minutes / 60);
let remainingSeconds: number | string = seconds % 60;
let remainingMinutes: number | string = minutes % 60;
if (remainingSeconds < 10) {
remainingSeconds = "0" + remainingSeconds;
}
if (remainingMinutes < 10) {
remainingMinutes = "0" + remainingMinutes;
}
if (hours === 0) {
hours = "";
} else {
hours = hours + ":";
}
return hours + remainingMinutes + ":" + remainingSeconds;
}
const reloadVideo = () => {
if (!videoRef.current) return;
const src = videoRef.current.src;
const currentTime = videoRef.current.currentTime;
videoRef.current.src = src;
videoRef.current.load();
videoRef.current.currentTime = currentTime;
if (playing) {
videoRef.current.play();
}
};
const handleMenuOpen = (event: any) => {
setAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setAnchorEl(null);
};
useEffect(() => {
const videoWidth = videoRef?.current?.offsetWidth;
if (videoWidth && videoWidth <= 600) {
setIsMobileView(true);
}
}, [canPlay]);
const getDownloadProgress = (current: number, total: number) => {
const progress = (current / total) * 100;
return Number.isNaN(progress) ? "" : progress.toFixed(0) + "%";
};
const mute = () => {
setIsMuted(true);
setMutedVolume(volume);
setVolume(0);
if (videoRef.current) videoRef.current.volume = 0;
};
const unMute = () => {
setIsMuted(false);
setVolume(mutedVolume);
if (videoRef.current) videoRef.current.volume = mutedVolume;
};
const toggleMute = () => {
isMuted ? unMute() : mute();
};
const changeVolume = (volumeChange: number) => {
if (videoRef.current) {
const minVolume = 0;
const maxVolume = 1;
let newVolume = volumeChange + volume;
newVolume = Math.max(newVolume, minVolume);
newVolume = Math.min(newVolume, maxVolume);
setIsMuted(false);
setMutedVolume(newVolume);
videoRef.current.volume = newVolume;
setVolume(newVolume);
}
};
const setProgressRelative = (secondsChange: number) => {
if (videoRef.current) {
const currentTime = videoRef.current?.currentTime;
const minTime = 0;
const maxTime = videoRef.current?.duration || 100;
let newTime = currentTime + secondsChange;
newTime = Math.max(newTime, minTime);
newTime = Math.min(newTime, maxTime);
videoRef.current.currentTime = newTime;
setProgress(newTime);
}
};
const setProgressAbsolute = (videoPercent: number) => {
if (videoRef.current) {
videoPercent = Math.min(videoPercent, 100);
videoPercent = Math.max(videoPercent, 0);
const finalTime = (videoRef.current?.duration * videoPercent) / 100;
videoRef.current.currentTime = finalTime;
setProgress(finalTime);
}
};
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault();
switch (e.key) {
case Key.Add:
increaseSpeed(false);
break;
case "+":
increaseSpeed(false);
break;
case ">":
increaseSpeed(false);
break;
case Key.Subtract:
decreaseSpeed();
break;
case "-":
decreaseSpeed();
break;
case "<":
decreaseSpeed();
break;
case Key.ArrowLeft:
{
if (e.shiftKey) setProgressRelative(-300);
else if (e.ctrlKey) setProgressRelative(-60);
else if (e.altKey) setProgressRelative(-10);
else setProgressRelative(-5);
}
break;
case Key.ArrowRight:
{
if (e.shiftKey) setProgressRelative(300);
else if (e.ctrlKey) setProgressRelative(60);
else if (e.altKey) setProgressRelative(10);
else setProgressRelative(5);
}
break;
case Key.ArrowDown:
changeVolume(-0.05);
break;
case Key.ArrowUp:
changeVolume(0.05);
break;
}
};
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault();
switch (e.key) {
case " ":
togglePlay();
break;
case "m":
toggleMute();
break;
case "f":
enterFullscreen();
break;
case Key.Escape:
exitFullscreen();
break;
case "0":
setProgressAbsolute(0);
break;
case "1":
setProgressAbsolute(10);
break;
case "2":
setProgressAbsolute(20);
break;
case "3":
setProgressAbsolute(30);
break;
case "4":
setProgressAbsolute(40);
break;
case "5":
setProgressAbsolute(50);
break;
case "6":
setProgressAbsolute(60);
break;
case "7":
setProgressAbsolute(70);
break;
case "8":
setProgressAbsolute(80);
break;
case "9":
setProgressAbsolute(90);
break;
}
};
useEffect(() => {
if (element) {
let oldElement = document.getElementById("videoPlayer");
if (oldElement && oldElement?.parentNode) {
oldElement?.parentNode.replaceChild(element, oldElement);
videoRef.current = element;
setPlaying(true);
setCanPlay(true);
setStartPlay(true);
videoRef?.current?.addEventListener("click", () => {});
videoRef?.current?.addEventListener("timeupdate", updateProgress);
videoRef?.current?.addEventListener("ended", handleEnded);
}
}
}, [element]);
return (
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
zIndex: 1000,
backgroundColor: theme.palette.background.default,
}}
>
<div className="closePlayer">
<CloseIcon
onClick={() => {
dispatch(setVideoPlaying(null));
}}
sx={{
cursor: "pointer",
backgroundColor: "rgba(0,0,0,.5)",
}}
></CloseIcon>
</div>
<div onClick={togglePlay}>
<VideoElement id="videoPlayer" />
</div>
<ControlsContainer
style={{
bottom: from === "create" ? "15px" : 0,
}}
>
{isMobileView && canPlay ? (
<>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<IconButton
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
>
<MoreIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
width: "250px",
},
}}
>
<MenuItem>
<VolumeUp />
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
/>
</MenuItem>
<MenuItem onClick={() => increaseSpeed()}>
<Typography
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
}}
>
Speed: {playbackRate}x
</Typography>
</MenuItem>
<MenuItem onClick={togglePictureInPicture}>
<PictureInPicture />
</MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu>
</>
) : canPlay ? (
<>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<Typography
sx={{
fontSize: "14px",
marginRight: "5px",
color: "rgba(255, 255, 255, 0.7)",
visibility:
!videoRef.current?.duration || !progress
? "hidden"
: "visible",
}}
>
{progress && videoRef.current?.duration && formatTime(progress)}/
{progress &&
videoRef.current?.duration &&
formatTime(videoRef.current?.duration)}
</Typography>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginRight: "10px",
}}
onClick={toggleMute}
>
{isMuted ? <VolumeOff /> : <VolumeUp />}
</IconButton>
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
sx={{
maxWidth: "100px",
}}
/>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
fontSize: "14px",
marginLeft: "5px",
}}
onClick={e => increaseSpeed()}
>
Speed: {playbackRate}x
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
marginLeft: "15px",
}}
ref={toggleRef}
onClick={togglePictureInPicture}
>
<PictureInPicture />
</IconButton>
<IconButton
sx={{
color: "rgba(255, 255, 255, 0.7)",
}}
onClick={toggleFullscreen}
>
<Fullscreen />
</IconButton>
</>
) : null}
</ControlsContainer>
</VideoContainer>
);
};

648
src/components/common/VideoPlayerGlobal.tsx

@ -1,648 +0,0 @@
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import ReactDOM from 'react-dom'
import { Box, IconButton, Slider, useTheme } from '@mui/material'
import { CircularProgress, Typography } from '@mui/material'
import { Key } from 'ts-key-enum'
import {
PlayArrow,
Pause,
VolumeUp,
Fullscreen,
PictureInPicture, VolumeOff
} from '@mui/icons-material'
import { styled } from '@mui/system'
import { MyContext } from '../../wrappers/DownloadWrapper'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import { Refresh } from '@mui/icons-material'
import CloseIcon from '@mui/icons-material/Close';
import { Menu, MenuItem } from '@mui/material'
import { MoreVert as MoreIcon } from '@mui/icons-material'
import { setVideoPlaying } from '../../state/features/globalSlice'
const VideoContainer = styled(Box)`
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
margin: 0px;
padding: 0px;
`
const VideoElement = styled('video')`
width: 100%;
height: auto;
max-height: calc(100vh - 150px);
background: rgb(33, 33, 33);
`
const ControlsContainer = styled(Box)`
position: absolute;
display: flex;
align-items: center;
justify-content: space-between;
bottom: 0;
left: 0;
right: 0;
padding: 8px;
background-color: rgba(0, 0, 0, 0.6);
`
interface VideoPlayerProps {
src?: string
poster?: string
name?: string
identifier?: string
service?: string
autoplay?: boolean
from?: string | null
customStyle?: any
user?: string
jsonId?: string
element?: null | any
checkIfDrag?: ()=> boolean;
}
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({
poster,
name,
identifier,
service,
autoplay = true,
from = null,
customStyle = {},
user = '',
jsonId = '',
element,
checkIfDrag
}) => {
const theme = useTheme()
const videoRef = useRef<HTMLVideoElement | null>(null)
const [playing, setPlaying] = useState(false)
const [volume, setVolume] = useState(1)
const [mutedVolume, setMutedVolume] = useState(1)
const [isMuted, setIsMuted] = useState(false)
const [progress, setProgress] = useState(0)
const [isLoading, setIsLoading] = useState(false)
const [canPlay, setCanPlay] = useState(false)
const [startPlay, setStartPlay] = useState(false)
const [isMobileView, setIsMobileView] = useState(false)
const [playbackRate, setPlaybackRate] = useState(1)
const [anchorEl, setAnchorEl] = useState(null)
const dispatch = useDispatch()
const reDownload = useRef<boolean>(false)
const { downloads } = useSelector((state: RootState) => state.global)
const download = useMemo(() => {
if (!downloads || !identifier) return {}
const findDownload = downloads[identifier]
if (!findDownload) return {}
return findDownload
}, [downloads, identifier])
const resourceStatus = useMemo(() => {
return download?.status || {}
}, [download])
const minSpeed = 0.25;
const maxSpeed = 4.0;
const speedChange = 0.25;
const updatePlaybackRate = (newSpeed: number) => {
if (videoRef.current) {
if (newSpeed > maxSpeed || newSpeed < minSpeed)
newSpeed = minSpeed
videoRef.current.playbackRate = newSpeed
setPlaybackRate(newSpeed)
}
}
const increaseSpeed = (wrapOverflow = true) => {
const changedSpeed = playbackRate + speedChange
let newSpeed = wrapOverflow ? changedSpeed : Math.min(changedSpeed, maxSpeed)
if (videoRef.current) {
updatePlaybackRate(newSpeed);
}
}
const decreaseSpeed = () => {
if (videoRef.current) {
updatePlaybackRate(playbackRate - speedChange);
}
}
const toggleRef = useRef<any>(null)
const { downloadVideo } = useContext(MyContext)
const togglePlay = async () => {
if(checkIfDrag && checkIfDrag()) return
if (!videoRef.current) return
if (playing) {
videoRef.current.pause()
} else {
videoRef.current.play()
}
setPlaying((prev)=> !prev)
}
const onVolumeChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.volume = value as number
setVolume(value as number)
setIsMuted(false)
}
const onProgressChange = (_: any, value: number | number[]) => {
if (!videoRef.current) return
videoRef.current.currentTime = value as number
setProgress(value as number)
if (!playing) {
videoRef.current.play()
setPlaying(true)
}
}
const handleEnded = () => {
setPlaying(false)
}
const updateProgress = () => {
if (!videoRef.current) return
setProgress(videoRef.current.currentTime)
}
const [isFullscreen, setIsFullscreen] = useState(false)
const enterFullscreen = () => {
if (!videoRef.current) return
if (videoRef.current.requestFullscreen) {
videoRef.current.requestFullscreen()
}
}
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen()
}
}
const toggleFullscreen = () => {
isFullscreen ? exitFullscreen() : enterFullscreen()
}
const togglePictureInPicture = async () => {
if (!videoRef.current) return
if (document.pictureInPictureElement === videoRef.current) {
await document.exitPictureInPicture()
} else {
await videoRef.current.requestPictureInPicture()
}
}
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement)
}
document.addEventListener('fullscreenchange', handleFullscreenChange)
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange)
}
}, [])
const handleCanPlay = () => {
setIsLoading(false)
setCanPlay(true)
}
useEffect(() => {
const videoElement = videoRef.current
const handleLeavePictureInPicture = async (event: any) => {
const target = event?.target
if (target) {
target.pause()
if (setPlaying) {
setPlaying(false)
}
}
}
if (videoElement) {
videoElement.addEventListener(
'leavepictureinpicture',
handleLeavePictureInPicture
)
}
return () => {
if (videoElement) {
videoElement.removeEventListener(
'leavepictureinpicture',
handleLeavePictureInPicture
)
}
}
}, [])
function formatTime(seconds: number): string {
seconds = Math.floor(seconds)
let minutes: number | string = Math.floor(seconds / 60)
let hours: number | string = Math.floor(minutes / 60)
let remainingSeconds: number | string = seconds % 60
let remainingMinutes: number | string = minutes % 60
if (remainingSeconds < 10) {
remainingSeconds = '0' + remainingSeconds
}
if (remainingMinutes < 10) {
remainingMinutes = '0' + remainingMinutes
}
if (hours === 0) {
hours = ''
}
else {
hours = hours + ':'
}
return hours + remainingMinutes + ':' + remainingSeconds
}
const reloadVideo = () => {
if (!videoRef.current) return
const src = videoRef.current.src
const currentTime = videoRef.current.currentTime
videoRef.current.src = src
videoRef.current.load()
videoRef.current.currentTime = currentTime
if (playing) {
videoRef.current.play()
}
}
const handleMenuOpen = (event: any) => {
setAnchorEl(event.currentTarget)
}
const handleMenuClose = () => {
setAnchorEl(null)
}
useEffect(() => {
const videoWidth = videoRef?.current?.offsetWidth
if (videoWidth && videoWidth <= 600) {
setIsMobileView(true)
}
}, [canPlay])
const getDownloadProgress = (current: number, total: number) => {
const progress = current / total * 100;
return Number.isNaN(progress) ? '' : progress.toFixed(0) + '%'
}
const mute = () => {
setIsMuted(true)
setMutedVolume(volume)
setVolume(0)
if (videoRef.current) videoRef.current.volume = 0
}
const unMute = () => {
setIsMuted(false)
setVolume(mutedVolume)
if (videoRef.current) videoRef.current.volume = mutedVolume
}
const toggleMute = () => {
isMuted ? unMute() : mute();
}
const changeVolume = (volumeChange: number) => {
if (videoRef.current) {
const minVolume = 0;
const maxVolume = 1;
let newVolume = volumeChange + volume
newVolume = Math.max(newVolume, minVolume)
newVolume = Math.min(newVolume, maxVolume)
setIsMuted(false)
setMutedVolume(newVolume)
videoRef.current.volume = newVolume
setVolume(newVolume);
}
}
const setProgressRelative = (secondsChange: number) => {
if (videoRef.current) {
const currentTime = videoRef.current?.currentTime
const minTime = 0
const maxTime = videoRef.current?.duration || 100
let newTime = currentTime + secondsChange;
newTime = Math.max(newTime, minTime)
newTime = Math.min(newTime, maxTime)
videoRef.current.currentTime = newTime;
setProgress(newTime);
}
}
const setProgressAbsolute = (videoPercent: number) => {
if (videoRef.current) {
videoPercent = Math.min(videoPercent, 100)
videoPercent = Math.max(videoPercent, 0)
const finalTime = videoRef.current?.duration * videoPercent / 100
videoRef.current.currentTime = finalTime
setProgress(finalTime);
}
}
const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault()
switch (e.key) {
case Key.Add: increaseSpeed(false); break;
case '+': increaseSpeed(false); break;
case '>': increaseSpeed(false); break;
case Key.Subtract: decreaseSpeed(); break;
case '-': decreaseSpeed(); break;
case '<': decreaseSpeed(); break;
case Key.ArrowLeft: {
if (e.shiftKey) setProgressRelative(-300);
else if (e.ctrlKey) setProgressRelative(-60);
else if (e.altKey) setProgressRelative(-10);
else setProgressRelative(-5);
} break;
case Key.ArrowRight: {
if (e.shiftKey) setProgressRelative(300);
else if (e.ctrlKey) setProgressRelative(60);
else if (e.altKey) setProgressRelative(10);
else setProgressRelative(5);
} break;
case Key.ArrowDown: changeVolume(-0.05); break;
case Key.ArrowUp: changeVolume(0.05); break;
}
}
const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) => {
e.preventDefault()
switch (e.key) {
case ' ': togglePlay(); break;
case 'm': toggleMute(); break;
case 'f': enterFullscreen(); break;
case Key.Escape: exitFullscreen(); break;
case '0': setProgressAbsolute(0); break;
case '1': setProgressAbsolute(10); break;
case '2': setProgressAbsolute(20); break;
case '3': setProgressAbsolute(30); break;
case '4': setProgressAbsolute(40); break;
case '5': setProgressAbsolute(50); break;
case '6': setProgressAbsolute(60); break;
case '7': setProgressAbsolute(70); break;
case '8': setProgressAbsolute(80); break;
case '9': setProgressAbsolute(90); break;
}
}
useEffect(()=> {
if(element){
let oldElement = document.getElementById('videoPlayer');
if(oldElement && oldElement?.parentNode){
oldElement?.parentNode.replaceChild(element, oldElement);
videoRef.current = element
setPlaying(true)
setCanPlay(true)
setStartPlay(true)
videoRef?.current?.addEventListener('click', ()=> {})
videoRef?.current?.addEventListener('timeupdate', updateProgress)
videoRef?.current?.addEventListener('ended', handleEnded)
}
}
}, [element])
return (
<VideoContainer
tabIndex={0}
onKeyUp={keyboardShortcutsUp}
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === 'create' ? '8px' : 0,
zIndex: 1000,
backgroundColor: theme.palette.background.default,
}}
>
<div className="closePlayer">
<CloseIcon onClick={()=> {
dispatch(setVideoPlaying(null))
}} sx={{
cursor: 'pointer',
backgroundColor: 'rgba(0,0,0,.5)'
}}></CloseIcon>
</div>
<div onClick={togglePlay}>
<VideoElement
id="videoPlayer"
/>
</div>
<ControlsContainer
style={{
bottom: from === 'create' ? '15px' : 0
}}
>
{isMobileView && canPlay ? (
<>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginLeft: '15px'
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<IconButton
edge="end"
color="inherit"
aria-label="menu"
onClick={handleMenuOpen}
>
<MoreIcon />
</IconButton>
<Menu
id="simple-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleMenuClose}
PaperProps={{
style: {
width: '250px'
}
}}
>
<MenuItem>
<VolumeUp />
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01} />
</MenuItem>
<MenuItem onClick={() => increaseSpeed()}>
<Typography
sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '14px'
}}
>
Speed: {playbackRate}x
</Typography>
</MenuItem>
<MenuItem onClick={togglePictureInPicture}>
<PictureInPicture />
</MenuItem>
<MenuItem onClick={toggleFullscreen}>
<Fullscreen />
</MenuItem>
</Menu>
</>
) : canPlay ? (
<>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
}}
onClick={togglePlay}
>
{playing ? <Pause /> : <PlayArrow />}
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginLeft: '15px'
}}
onClick={reloadVideo}
>
<Refresh />
</IconButton>
<Slider
value={progress}
onChange={onProgressChange}
min={0}
max={videoRef.current?.duration || 100}
sx={{ flexGrow: 1, mx: 2 }}
/>
<Typography
sx={{
fontSize: '14px',
marginRight: '5px',
color: 'rgba(255, 255, 255, 0.7)',
visibility:
!videoRef.current?.duration || !progress
? 'hidden'
: 'visible'
}}
>
{progress && videoRef.current?.duration && formatTime(progress)}/
{progress &&
videoRef.current?.duration &&
formatTime(videoRef.current?.duration)}
</Typography>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginRight: '10px'
}}
onClick={toggleMute}
>
{isMuted ? <VolumeOff /> : <VolumeUp />}
</IconButton>
<Slider
value={volume}
onChange={onVolumeChange}
min={0}
max={1}
step={0.01}
sx={{
maxWidth: '100px'
}}
/>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontSize: '14px',
marginLeft: '5px'
}}
onClick={(e) => increaseSpeed()}
>
Speed: {playbackRate}x
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)',
marginLeft: '15px'
}}
ref={toggleRef}
onClick={togglePictureInPicture}
>
<PictureInPicture />
</IconButton>
<IconButton
sx={{
color: 'rgba(255, 255, 255, 0.7)'
}}
onClick={toggleFullscreen}
>
<Fullscreen />
</IconButton>
</>
) : null}
</ControlsContainer>
</VideoContainer>
)
}

4
src/constants/Misc.ts

@ -1,4 +1,6 @@
export const minPriceSuperlike = 10;
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
export const titleSaveFormatter = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
export const allTabValue = "all";
export const subscriptionTabValue = "subscriptions";

86
src/global.d.ts vendored

@ -1,55 +1,55 @@
// src/global.d.ts
interface QortalRequestOptions {
action: string
name?: string
service?: string
data64?: string
title?: string
description?: string
category?: string
tags?: string[]
identifier?: string
address?: string
metaData?: string
encoding?: string
includeMetadata?: boolean
limit?: numebr
offset?: number
reverse?: boolean
resources?: any[]
filename?: string
list_name?: string
item?: string
items?: strings[]
tag1?: string
tag2?: string
tag3?: string
tag4?: string
tag5?: string
coin?: string
destinationAddress?: string
amount?: number
blob?: Blob
mimeType?: string
file?: File
encryptedData?: string
name?: string
mode?: string
query?: string
excludeBlocked?: boolean
exactMatchNames?: boolean
action: string;
name?: string;
service?: string;
data64?: string;
title?: string;
description?: string;
category?: string;
tags?: string[];
identifier?: string;
address?: string;
metaData?: string;
encoding?: string;
includeMetadata?: boolean;
limit?: number;
offset?: number;
reverse?: boolean;
resources?: any[];
filename?: string;
list_name?: string;
item?: string;
items?: strings[];
tag1?: string;
tag2?: string;
tag3?: string;
tag4?: string;
tag5?: string;
coin?: string;
destinationAddress?: string;
amount?: number;
blob?: Blob;
mimeType?: string;
file?: File;
encryptedData?: string;
mode?: string;
query?: string;
excludeBlocked?: boolean;
exactMatchNames?: boolean;
nameListFilter?: string[];
}
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
declare function qortalRequestWithTimeout(
options: QortalRequestOptions,
time: number
): Promise<any>
): Promise<any>;
declare global {
interface Window {
_qdnBase: any // Replace 'any' with the appropriate type if you know it
_qdnTheme: string
_qdnBase: any; // Replace 'any' with the appropriate type if you know it
_qdnTheme: string;
}
}
@ -57,6 +57,6 @@ declare global {
interface Window {
showSaveFilePicker: (
options?: SaveFilePickerOptions
) => Promise<FileSystemFileHandle>
) => Promise<FileSystemFileHandle>;
}
}

58
src/hooks/useFetchVideos.tsx

@ -21,6 +21,7 @@ import {
QTUBE_PLAYLIST_BASE,
QTUBE_VIDEO_BASE,
} from "../constants/Identifiers.ts";
import { allTabValue, subscriptionTabValue } from "../constants/Misc.ts";
export const useFetchVideos = () => {
const dispatch = useDispatch();
@ -35,6 +36,10 @@ export const useFetchVideos = () => {
(state: RootState) => state.video.filteredVideos
);
const subscriptions = useSelector(
(state: RootState) => state.persist.subscriptionList
);
const checkAndUpdateVideo = React.useCallback(
(video: Video) => {
const existingVideo = hashMapVideos[video.id + "-" + video.user];
@ -100,27 +105,29 @@ export const useFetchVideos = () => {
try {
dispatch(setIsLoadingGlobal(true));
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=20&includemetadata=false&reverse=true&exc
// ludeblocked=true&exactmatchnames=true`;
//
// const response = await fetch(url, {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// },
// });
// const responseData = await response.json();
const responseData = await qortalRequest({
action: "SEARCH_QDN_RESOURCES",
mode: "ALL",
service: "DOCUMENT",
query: "${QTUBE_VIDEO_BASE}",
limit: 20,
includeMetadata: true,
reverse: true,
excludeBlocked: true,
exactMatchNames: true,
});
const responseData = await response.json();
// const responseData = await qortalRequest({
// action: "SEARCH_QDN_RESOURCES",
// mode: "ALL",
// service: "DOCUMENT",
// query: "${QTUBE_VIDEO_BASE}",
// limit: 20,
// includeMetadata: true,
// reverse: true,
// excludeBlocked: true,
// exactMatchNames: true,
// name: names
// })
const latestVideo = videos[0];
if (!latestVideo) return;
const findVideo = responseData?.findIndex(
@ -174,8 +181,9 @@ export const useFetchVideos = () => {
async (
filters = {},
reset?: boolean,
resetFilers?: boolean,
limit?: number
resetFilters?: boolean,
limit?: number,
listType = allTabValue
) => {
try {
const {
@ -184,7 +192,7 @@ export const useFetchVideos = () => {
subcategory = "",
keywords = "",
type = "",
}: any = resetFilers ? {} : filters;
}: any = resetFilters ? {} : filters;
let offset = videos.length;
if (reset) {
offset = 0;
@ -196,6 +204,12 @@ export const useFetchVideos = () => {
if (name) {
defaultUrl = defaultUrl + `&name=${name}`;
}
if (listType === subscriptionTabValue) {
subscriptions.map(sub => {
defaultUrl += `&name=${sub}`;
});
}
if (category) {
if (!subcategory) {
defaultUrl = defaultUrl + `&description=category:${category}`;

124
src/pages/Home/Channels.tsx

@ -1,77 +1,77 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { Box, useTheme } from "@mui/material";
import {
Avatar,
Box,
Button,
Typography,
useTheme
} from '@mui/material'
import { useFetchVideos } from '../../hooks/useFetchVideos'
import LazyLoad from '../../components/common/LazyLoad'
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles'
import ResponsiveImage from '../../components/ResponsiveImage'
import { formatDate, formatTimestampSeconds } from '../../utils/time'
import { ChannelCard, ChannelTitle } from './Home-styles'
BottomParent,
NameContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoContainer,
VideoUploadDate,
} from "./VideoList-styles";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { ChannelCard, ChannelTitle } from "./Home-styles";
interface VideoListProps {
mode?: string
mode?: string;
}
export const Channels = ({ mode }: VideoListProps) => {
const theme = useTheme()
const navigate = useNavigate()
const publishNames = useSelector((state: RootState)=> state.global.publishNames)
const theme = useTheme();
const navigate = useNavigate();
const publishNames = useSelector(
(state: RootState) => state.global.publishNames
);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
)
);
return (
<Box sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
minHeight: '50vh'
}}>
<VideoContainer>
{publishNames && publishNames?.slice(0, 10).map((name)=> {
let avatarUrl = ''
if(userAvatarHash[name]){
avatarUrl = userAvatarHash[name]
}
return (
<Box
<Box
sx={{
display: 'flex',
flex: 0,
alignItems: 'center',
width: 'auto',
position: 'relative',
' @media (max-width: 450px)': {
width: '100%'
}
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
minHeight: "50vh",
}}
key={name}
>
<ChannelCard
<VideoContainer>
{publishNames &&
publishNames?.slice(0, 10).map(name => {
let avatarUrl = "";
if (userAvatarHash[name]) {
avatarUrl = userAvatarHash[name];
}
return (
<Box
sx={{
display: "flex",
flex: 0,
alignItems: "center",
width: "auto",
position: "relative",
" @media (max-width: 450px)": {
width: "100%",
},
}}
key={name}
>
<ChannelCard
onClick={() => {
navigate(`/channel/${name}`)
navigate(`/channel/${name}`);
}}
>
<ChannelTitle>{name}</ChannelTitle>
<ResponsiveImage src={avatarUrl} width={50} height={50}/>
</ChannelCard>
</Box>
)
})}
</VideoContainer>
>
<ChannelTitle>{name}</ChannelTitle>
<ResponsiveImage src={avatarUrl} width={50} height={50} />
</ChannelCard>
</Box>
);
})}
</VideoContainer>
</Box>
)
}
);
};

547
src/pages/Home/Home.tsx

@ -1,15 +1,540 @@
import React from 'react'
import { VideoList } from './VideoList'
import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../state/store";
import {
Box,
Button,
FormControl,
Grid,
Input,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
Tab,
useTheme,
} from "@mui/material";
import { useFetchVideos } from "../../hooks/useFetchVideos";
import LazyLoad from "../../components/common/LazyLoad";
import {
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
ProductManagerRow,
FiltersRadioButton,
} from "./VideoList-styles";
import { SubtitleContainer } from "./Home-styles";
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
import {
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changefilterName,
changefilterSearch,
} from "../../state/features/videoSlice";
import { changeFilterType } from "../../state/features/persistSlice.ts";
import { categories, subCategories } from "../../constants/Categories.ts";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { TabContext, TabList, TabPanel } from "@mui/lab";
import VideoList from "./VideoList.tsx";
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts";
import { setHomePageSelectedTab } from "../../state/features/persistSlice.ts";
export const Home = () => {
interface HomeProps {
mode?: string;
}
export const Home = ({ mode }: HomeProps) => {
const prevVal = useRef("");
const isFiltering = useSelector(
(state: RootState) => state.video.isFiltering
);
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const persistSelector = useSelector((state: RootState) => state.persist);
const filterType = useSelector(
(state: RootState) => state.persist.filterType
);
const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch
);
const filterName = useSelector((state: RootState) => state.video.filterName);
const selectedCategoryVideos = useSelector(
(state: RootState) => state.video.selectedCategoryVideos
);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>(persistSelector.selectedTab);
const tabFontSize = "20px";
const setFilterType = payload => {
dispatch(changeFilterType(payload));
};
useEffect(() => {
// Makes displayed videos reload when switching filter type. Removes need to click Search button after changing type
getVideosHandler(true);
}, [filterType]);
const setFilterSearch = payload => {
dispatch(changefilterSearch(payload));
};
const setFilterName = payload => {
dispatch(changefilterName(payload));
};
const setSelectedCategoryVideos = payload => {
dispatch(changeSelectedCategoryVideos(payload));
};
const selectedSubCategoryVideos = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos
);
const setSelectedSubCategoryVideos = payload => {
dispatch(changeSelectedSubCategoryVideos(payload));
};
const dispatch = useDispatch();
const filteredVideos = useSelector(
(state: RootState) => state.video.filteredVideos
);
const isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetching = useRef(false);
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } =
useFetchVideos();
const getVideosHandler = React.useCallback(
async (reset?: boolean, resetFilters?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
await getVideos(
{
name: filterName,
category: selectedCategoryVideos?.id,
subcategory: selectedSubCategoryVideos?.id,
keywords: filterSearch,
type: filterType,
},
reset,
resetFilters,
20,
tabValue
);
isFetching.current = false;
},
[
getVideos,
filterValue,
getVideosFiltered,
isFiltering,
filterName,
selectedCategoryVideos,
selectedSubCategoryVideos,
filterSearch,
filterType,
tabValue,
]
);
useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue;
getVideosHandler();
}
}, [filterValue, isFiltering, filteredVideos]);
const getVideosHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getVideos({ type: filterType }, null, null, 20, tabValue);
afterFetch.current = true;
isFetching.current = false;
setIsLoading(false);
}, [getVideos]);
let videos = globalVideos;
if (isFiltering) {
videos = filteredVideos;
isFilterMode.current = true;
} else {
isFilterMode.current = false;
}
// const interval = useRef<any>(null);
// const checkNewVideosFunc = useCallback(() => {
// let isCalling = false;
// interval.current = setInterval(async () => {
// if (isCalling || !firstFetch.current) return;
// isCalling = true;
// await checkNewVideos();
// isCalling = false;
// }, 30000); // 1 second interval
// }, [checkNewVideos]);
// useEffect(() => {
// if (isFiltering && interval.current) {
// clearInterval(interval.current);
// return;
// }
// checkNewVideosFunc();
// return () => {
// if (interval?.current) {
// clearInterval(interval.current);
// }
// };
// }, [mode, checkNewVideosFunc, isFiltering]);
useEffect(() => {
if (
!firstFetch.current &&
!isFilterMode.current &&
globalVideos.length === 0
) {
isFetching.current = true;
getVideosHandlerMount();
} else {
firstFetch.current = true;
afterFetch.current = true;
}
}, [getVideosHandlerMount, globalVideos]);
const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch("");
setFilterName("");
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
ReactDOM.flushSync(() => {
getVideosHandler(true, true);
});
};
const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string>
) => {
const optionId = event.target.value;
const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
option => option.id === +optionId
);
setSelectedSubCategoryVideos(selectedOption || null);
};
const handleInputKeyDown = (event: any) => {
if (event.key === "Enter") {
getVideosHandler(true);
}
};
useEffect(() => {
getVideosHandler(true);
}, [tabValue]);
const changeTab = (e: React.SyntheticEvent, newValue: string) => {
setTabValue(newValue);
dispatch(setHomePageSelectedTab(newValue));
};
return (
<>
<VideoList />
</>
)
}
<Grid container sx={{ width: "100%" }}>
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
<FiltersContainer>
<Input
id="standard-adornment-name"
onChange={e => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
placeholder="Search"
onKeyDown={handleInputKeyDown}
sx={{
borderBottom: "1px solid white",
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}}
/>
<Input
id="standard-adornment-name"
onChange={e => {
setFilterName(e.target.value);
}}
value={filterName}
placeholder="User's Name (Exact)"
onKeyDown={handleInputKeyDown}
sx={{
marginTop: "20px",
borderBottom: "1px solid white",
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}}
/>
<FiltersSubContainer>
<FormControl sx={{ width: "100%", marginTop: "30px" }}>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
flexDirection: "column",
}}
>
<FormControl fullWidth sx={{ marginBottom: 1 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Category"
>
Category
</InputLabel>
<Select
labelId="Category"
input={<OutlinedInput label="Category" />}
value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
},
// Target the dropdown menu
"& .MuiMenu-paper": {
".MuiMenuItem-root": {
fontSize: "14px", // Change font size for the menu items
},
},
}}
>
{categories.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-Category"
>
Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
},
// Target the dropdown menu
"& .MuiMenu-paper": {
".MuiMenuItem-root": {
fontSize: "14px", // Change font size for the menu items
},
},
}}
>
{subCategories[selectedCategoryVideos.id].map(
option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
</Box>
</FormControl>
</FiltersSubContainer>
<FiltersSubContainer>
<FiltersRow>
Videos
<FiltersRadioButton
checked={filterType === "videos"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterType("videos");
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
Playlists
<FiltersRadioButton
checked={filterType === "playlists"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterType("playlists");
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
</FiltersSubContainer>
<Button
onClick={() => {
filtersToDefault();
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
reset
</Button>
<Button
onClick={() => {
getVideosHandler(true);
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
Search
</Button>
</FiltersContainer>
</FiltersCol>
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
<ProductManagerRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}}
>
<SubtitleContainer
sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%",
maxWidth: "1400px",
}}
></SubtitleContainer>
<TabContext value={tabValue}>
<TabList
onChange={changeTab}
textColor={"secondary"}
indicatorColor={"secondary"}
>
<Tab
label="All Videos"
value={allTabValue}
sx={{ fontSize: tabFontSize }}
/>
<Tab
label="Subscriptions"
value={subscriptionTabValue}
sx={{ fontSize: tabFontSize }}
/>
</TabList>
<TabPanel value={allTabValue} sx={{ width: "100%" }}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
<TabPanel value={subscriptionTabValue} sx={{ width: "100%" }}>
{persistSelector.subscriptionList.length > 0 ? (
<>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</>
) : (
<div style={{ textAlign: "center" }}>
You have no subscriptions
</div>
)}
</TabPanel>
</TabContext>
</Box>
</ProductManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
);
};

12
src/pages/Home/VideoList-styles.tsx

@ -7,6 +7,7 @@ import {
TextField,
InputLabel,
Autocomplete,
Radio,
} from "@mui/material";
export const VideoContainer = styled(Grid)(({ theme }) => ({
@ -131,7 +132,7 @@ export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
}));
export const VideoCardName = styled(Typography)(({ theme }) => ({
fontFamily: "Cairo",
fontSize: "18px",
fontSize: "16px",
letterSpacing: "0.4px",
color: theme.palette.text.primary,
userSelect: "none",
@ -195,7 +196,7 @@ export const NameContainer = styled(Box)(({ theme }) => ({
justifyContent: "flex-start",
alignItems: "center",
gap: "10px",
marginBottom: "10px",
marginBottom: "2px",
}));
export const MyStoresCard = styled(Box)(({ theme }) => ({
@ -270,6 +271,13 @@ export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({
},
}));
export const FiltersRadioButton = styled(Radio)(({ theme }) => ({
color: "#c0d4ff",
"&.Mui-checked": {
color: "#6596ff",
},
}));
export const FilterSelect = styled(Autocomplete)(({ theme }) => ({
"& #categories-select": {
padding: "7px",

927
src/pages/Home/VideoList.tsx

@ -1,276 +1,50 @@
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import ReactDOM from "react-dom";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "../../state/store";
import {
Avatar,
Box,
Button,
FormControl,
Grid,
Input,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SelectChangeEvent,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { useFetchVideos } from "../../hooks/useFetchVideos";
import LazyLoad from "../../components/common/LazyLoad";
import {
BlockIconContainer,
BottomParent,
FilterSelect,
FiltersCheckbox,
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
FiltersTitle,
IconsBox,
NameContainer,
VideoCard,
VideoCardCol,
ProductManagerRow,
VideoCardContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoContainer,
VideoUploadDate,
} from "./VideoList-styles";
import ResponsiveImage from "../../components/ResponsiveImage";
import { formatDate, formatTimestampSeconds } from "../../utils/time";
import { Subtitle, SubtitleContainer } from "./Home-styles";
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG";
} from "./VideoList-styles.tsx";
import { Avatar, Box, Tooltip, useTheme } from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import {
addVideos,
blockUser,
changeFilterType,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changefilterName,
changefilterSearch,
clearVideoList,
setEditPlaylist,
setEditVideo,
} from "../../state/features/videoSlice";
import { categories, subCategories } from "../../constants/Categories.ts";
import { Playlists } from "../../components/Playlists/Playlists";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG";
} from "../../state/features/videoSlice.ts";
import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx";
import { VideoCardImageContainer } from "./VideoCardImageContainer";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
import ResponsiveImage from "../../components/ResponsiveImage.tsx";
import { formatDate } from "../../utils/time.ts";
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG.tsx";
import { VideoCardImageContainer } from "./VideoCardImageContainer.tsx";
import React, { useState } from "react";
import { Video } from "../../state/features/videoSlice.ts";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { useNavigate } from "react-router-dom";
interface VideoListProps {
mode?: string;
videos: Video[];
}
export const VideoList = ({ mode }: VideoListProps) => {
const theme = useTheme();
const prevVal = useRef("");
const isFiltering = useSelector(
(state: RootState) => state.video.isFiltering
);
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const [isLoading, setIsLoading] = useState<boolean>(false);
export const VideoList = ({ videos }: VideoListProps) => {
const [showIcons, setShowIcons] = useState(null);
const filterType = useSelector((state: RootState) => state.video.filterType);
const setFilterType = payload => {
dispatch(changeFilterType(payload));
};
const filterSearch = useSelector(
(state: RootState) => state.video.filterSearch
);
const setFilterSearch = payload => {
dispatch(changefilterSearch(payload));
};
const filterName = useSelector((state: RootState) => state.video.filterName);
const setFilterName = payload => {
dispatch(changefilterName(payload));
};
const selectedCategoryVideos = useSelector(
(state: RootState) => state.video.selectedCategoryVideos
);
const setSelectedCategoryVideos = payload => {
dispatch(changeSelectedCategoryVideos(payload));
};
const selectedSubCategoryVideos = useSelector(
(state: RootState) => state.video.selectedSubCategoryVideos
);
const setSelectedSubCategoryVideos = payload => {
dispatch(changeSelectedSubCategoryVideos(payload));
};
const dispatch = useDispatch();
const filteredVideos = useSelector(
(state: RootState) => state.video.filteredVideos
);
const username = useSelector((state: RootState) => state.auth?.user?.name);
const isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetchingFiltered = useRef(false);
const isFetching = useRef(false);
const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos
);
const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos
);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const username = useSelector((state: RootState) => state.auth?.user?.name);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
const navigate = useNavigate();
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } =
useFetchVideos();
const getVideosHandler = React.useCallback(
async (reset?: boolean, resetFilers?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return;
isFetching.current = true;
await getVideos(
{
name: filterName,
category: selectedCategoryVideos?.id,
subcategory: selectedSubCategoryVideos?.id,
keywords: filterSearch,
type: filterType,
},
reset ? true : false,
resetFilers
);
isFetching.current = false;
},
[
getVideos,
filterValue,
getVideosFiltered,
isFiltering,
filterName,
selectedCategoryVideos,
selectedSubCategoryVideos,
filterSearch,
filterType,
]
);
useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue;
getVideosHandler();
}
}, [filterValue, isFiltering, filteredVideos]);
const getVideosHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getVideos();
afterFetch.current = true;
isFetching.current = false;
setIsLoading(false);
}, [getVideos]);
let videos = globalVideos;
if (isFiltering) {
videos = filteredVideos;
isFilterMode.current = true;
} else {
isFilterMode.current = false;
}
// const interval = useRef<any>(null);
// const checkNewVideosFunc = useCallback(() => {
// let isCalling = false;
// interval.current = setInterval(async () => {
// if (isCalling || !firstFetch.current) return;
// isCalling = true;
// await checkNewVideos();
// isCalling = false;
// }, 30000); // 1 second interval
// }, [checkNewVideos]);
// useEffect(() => {
// if (isFiltering && interval.current) {
// clearInterval(interval.current);
// return;
// }
// checkNewVideosFunc();
// return () => {
// if (interval?.current) {
// clearInterval(interval.current);
// }
// };
// }, [mode, checkNewVideosFunc, isFiltering]);
useEffect(() => {
if (
!firstFetch.current &&
!isFilterMode.current &&
globalVideos.length === 0
) {
isFetching.current = true;
getVideosHandlerMount();
} else {
firstFetch.current = true;
afterFetch.current = true;
}
}, [getVideosHandlerMount, globalVideos]);
const filtersToDefault = async () => {
setFilterType("videos");
setFilterSearch("");
setFilterName("");
setSelectedCategoryVideos(null);
setSelectedSubCategoryVideos(null);
ReactDOM.flushSync(() => {
getVideosHandler(true, true);
});
};
const handleOptionCategoryChangeVideos = (
event: SelectChangeEvent<string>
) => {
const optionId = event.target.value;
const selectedOption = categories.find(option => option.id === +optionId);
setSelectedCategoryVideos(selectedOption || null);
};
const handleOptionSubCategoryChangeVideos = (
event: SelectChangeEvent<string>,
subcategories: any[]
) => {
const optionId = event.target.value;
const selectedOption = subcategories.find(
option => option.id === +optionId
);
setSelectedSubCategoryVideos(selectedOption || null);
};
const dispatch = useDispatch();
const theme = useTheme();
const blockUserFunc = async (user: string) => {
if (user === "Q-Tube") return;
@ -288,483 +62,226 @@ export const VideoList = ({ mode }: VideoListProps) => {
} catch (error) {}
};
const handleInputKeyDown = (event: any) => {
if (event.key === "Enter") {
getVideosHandler(true);
}
};
return (
<Grid container sx={{ width: "100%" }}>
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}>
<FiltersContainer>
<Input
id="standard-adornment-name"
onChange={e => {
setFilterSearch(e.target.value);
}}
value={filterSearch}
placeholder="Search"
onKeyDown={handleInputKeyDown}
sx={{
borderBottom: "1px solid white",
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}}
/>
<Input
id="standard-adornment-name"
onChange={e => {
setFilterName(e.target.value);
}}
value={filterName}
placeholder="User's Name (Exact)"
onKeyDown={handleInputKeyDown}
sx={{
marginTop: "20px",
borderBottom: "1px solid white",
"&&:before": {
borderBottom: "none",
},
"&&:after": {
borderBottom: "none",
},
"&&:hover:before": {
borderBottom: "none",
},
"&&.Mui-focused:before": {
borderBottom: "none",
},
"&&.Mui-focused": {
outline: "none",
},
fontSize: "18px",
}}
/>
<FiltersTitle>
Categories
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FormControl sx={{ width: "100%" }}>
<Box
<VideoCardContainer>
{videos.map((video: any) => {
const fullId = video ? `${video.id}-${video.user}` : undefined;
const existingVideo = hashMapVideos[fullId];
let hasHash = false;
let videoObj = video;
if (existingVideo) {
videoObj = existingVideo;
hasHash = true;
}
let avatarUrl = "";
if (userAvatarHash[videoObj?.user]) {
avatarUrl = userAvatarHash[videoObj?.user];
}
// nb. this prevents showing metadata for a video which
// belongs to a different user
if (
videoObj?.user &&
videoObj?.videoReference?.name &&
videoObj.user != videoObj.videoReference.name
) {
return null;
}
if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
return null;
}
const isPlaylist = videoObj?.service === "PLAYLIST";
if (isPlaylist) {
return (
<VideoCardCol
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
key={videoObj.id}
>
<IconsBox
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
flexDirection: "column",
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
<FormControl fullWidth sx={{ marginBottom: 1 }}>
<InputLabel
sx={{
fontSize: "16px",
{videoObj?.user === username && (
<Tooltip title="Edit playlist" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditPlaylist(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
sx={{
cursor: !hasHash && "default",
}}
onClick={() => {
if (!hasHash) return;
navigate(`/playlist/${videoObj?.user}/${videoObj?.id}`);
}}
>
<ResponsiveImage
src={videoObj?.image}
width={266}
height={150}
style={{
maxHeight: "50%",
}}
/>
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
id="Category"
>
Category
</InputLabel>
<Select
labelId="Category"
input={<OutlinedInput label="Category" />}
value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
},
// Target the dropdown menu
"& .MuiMenu-paper": {
".MuiMenuItem-root": {
fontSize: "14px", // Change font size for the menu items
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
},
}}
>
{videoObj?.user}
</VideoCardName>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</NameContainer>
<Box
sx={{
display: "flex",
position: "absolute",
bottom: "5px",
right: "5px",
}}
>
{categories.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
</FormControl>
{selectedCategoryVideos &&
subCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel
sx={{
fontSize: "16px",
}}
id="Sub-Category"
>
Sub-Category
</InputLabel>
<Select
labelId="Sub-Category"
input={<OutlinedInput label="Sub-Category" />}
value={selectedSubCategoryVideos?.id || ""}
onChange={e =>
handleOptionSubCategoryChangeVideos(
e,
subCategories[selectedCategoryVideos?.id]
)
}
sx={{
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
},
// Target the dropdown menu
"& .MuiMenu-paper": {
".MuiMenuItem-root": {
fontSize: "14px", // Change font size for the menu items
},
},
}}
>
{subCategories[selectedCategoryVideos.id].map(
option => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
)}
</Select>
</FormControl>
)}
</Box>
</FormControl>
</FiltersSubContainer>
<FiltersTitle>
Type
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FiltersRow>
Videos
<FiltersCheckbox
checked={filterType === "videos"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterType("videos");
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
<FiltersRow>
Playlists
<FiltersCheckbox
checked={filterType === "playlists"}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setFilterType("playlists");
}}
inputProps={{ "aria-label": "controlled" }}
/>
</FiltersRow>
</FiltersSubContainer>
<Button
onClick={() => {
filtersToDefault();
}}
sx={{
marginTop: "20px",
}}
variant="contained"
>
reset
</Button>
<Button
onClick={() => {
getVideosHandler(true);
}}
sx={{
marginTop: "20px",
}}
variant="contained"
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent>
</VideoCard>
</VideoCardCol>
);
}
return (
<VideoCardCol
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
Search
</Button>
</FiltersContainer>
</FiltersCol>
<Grid item xs={12} md={10} lg={7} xl={8} sm={9}>
<ProductManagerRow>
<Box
sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
marginTop: "20px",
}}
>
<SubtitleContainer
<IconsBox
sx={{
justifyContent: "flex-start",
paddingLeft: "15px",
width: "100%",
maxWidth: "1400px",
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
></SubtitleContainer>
<VideoCardContainer>
{videos.map((video: any, index: number) => {
const fullId = video ? `${video.id}-${video.user}` : undefined;
const existingVideo = hashMapVideos[fullId];
let hasHash = false;
let videoObj = video;
if (existingVideo) {
videoObj = existingVideo;
hasHash = true;
}
let avatarUrl = "";
if (userAvatarHash[videoObj?.user]) {
avatarUrl = userAvatarHash[videoObj?.user];
}
// nb. this prevents showing metadata for a video which
// belongs to a different user
if (videoObj?.user && videoObj?.videoReference?.name
&& videoObj.user != videoObj.videoReference.name) {
return null;
}
if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
return null;
}
const isPlaylist = videoObj?.service === "PLAYLIST";
if (isPlaylist) {
return (
<VideoCardCol
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
key={videoObj.id}
>
<IconsBox
sx={{
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
{videoObj?.user === username && (
<Tooltip title="Edit playlist" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditPlaylist(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
sx={{
cursor: !hasHash && "default",
}}
onClick={() => {
if (!hasHash) return;
navigate(
`/playlist/${videoObj?.user}/${videoObj?.id}`
);
}}
>
<ResponsiveImage
src={videoObj?.image}
width={266}
height={150}
style={{
maxHeight: "50%",
}}
/>
<VideoCardTitle>{videoObj?.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</NameContainer>
<Box
sx={{
display: "flex",
position: "absolute",
bottom: "5px",
right: "5px",
}}
>
<PlaylistSVG
color={theme.palette.text.primary}
height="36px"
width="36px"
/>
</Box>
</BottomParent>
</VideoCard>
</VideoCardCol>
);
}
return (
<VideoCardCol
key={videoObj.id}
onMouseEnter={() => setShowIcons(videoObj.id)}
onMouseLeave={() => setShowIcons(null)}
>
<IconsBox
sx={{
opacity: showIcons === videoObj.id ? 1 : 0,
zIndex: 2,
}}
>
{videoObj?.user === username && (
<Tooltip title="Edit video properties" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
dispatch(setEditVideo(videoObj));
}}
/>
</BlockIconContainer>
</Tooltip>
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
>
{videoObj?.user === username && (
<Tooltip title="Edit video properties" placement="top">
<BlockIconContainer>
<EditIcon
onClick={() => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
dispatch(setEditVideo(videoObj));
}}
>
<VideoCardImageContainer
width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
{/* <ResponsiveImage
/>
</BlockIconContainer>
</Tooltip>
)}
<Tooltip title="Block user content" placement="top">
<BlockIconContainer>
<BlockIcon
onClick={() => {
blockUserFunc(videoObj?.user);
}}
/>
</BlockIconContainer>
</Tooltip>
</IconsBox>
<VideoCard
onClick={() => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}}
>
<VideoCardImageContainer
width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
{/* <ResponsiveImage
src={videoObj.videoImage}
width={266}
height={150}
/> */}
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
<SubscribeButton name={videoObj?.user} />
</NameContainer>
{videoObj?.created && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</BottomParent>
</VideoCard>
</VideoCardCol>
);
})}
</VideoCardContainer>
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</ProductManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
<VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent>
<NameContainer
onClick={e => {
e.stopPropagation();
navigate(`/channel/${videoObj?.user}`);
}}
>
<Avatar
sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`}
/>
<VideoCardName
sx={{
":hover": {
textDecoration: "underline",
},
}}
>
{videoObj?.user}
</VideoCardName>
</NameContainer>
{videoObj?.created && (
<Box sx={{ flexDirection: "row", width: "100%" }}>
<VideoUploadDate sx={{ display: "inline" }}>
{formatDate(videoObj.created)}
</VideoUploadDate>
</Box>
)}
</BottomParent>
</VideoCard>
</VideoCardCol>
);
})}
</VideoCardContainer>
);
};
export default VideoList;

3
src/pages/PlaylistContent/PlaylistContent.tsx

@ -9,7 +9,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer";
import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/videoSlice";
import AttachFileIcon from "@mui/icons-material/AttachFile";
@ -440,6 +440,7 @@ export const PlaylistContent = () => {
nextVideo={nextVideo}
onEnd={onEndVideo}
autoPlay={doAutoPlay}
customStyle={{ aspectRatio: "16/9" }}
/>
)}
{playlistData && (

19
src/pages/VideoContent/VideoContent.tsx

@ -9,7 +9,7 @@ import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { VideoPlayer } from "../../components/common/VideoPlayer";
import { VideoPlayer } from "../../components/common/VideoPlayer/VideoPlayer.tsx";
import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/videoSlice";
import AttachFileIcon from "@mui/icons-material/AttachFile";
@ -52,7 +52,10 @@ import {
QTUBE_VIDEO_BASE,
SUPER_LIKE_BASE,
} from "../../constants/Identifiers.ts";
import { minPriceSuperlike } from "../../constants/Misc.ts";
import {
minPriceSuperlike,
titleFormatterOnSave,
} from "../../constants/Misc.ts";
import { SubscribeButton } from "../../components/common/SubscribeButton.tsx";
export function isTimestampWithinRange(resTimestamp, resCreated) {
@ -186,12 +189,12 @@ export const VideoContent = () => {
}
}
return videoData.title + ext;
return (videoData.title + ext).replace(titleFormatterOnSave, "");
}
// otherwise use QDN filename if applicable
if (videoData?.filename) {
return videoData.filename;
return videoData.filename.replace(titleFormatterOnSave, "");
}
// TODO: this was the previous value, leaving here as the
@ -360,6 +363,9 @@ export const VideoContent = () => {
if (!nameAddress || !id) return;
getComments(id, nameAddress);
}, [getComments, id, nameAddress]);
const subList = useSelector(
(state: RootState) => state.video.subscriptionList
);
return (
<Box
@ -374,7 +380,6 @@ export const VideoContent = () => {
sx={{
marginBottom: "30px",
width: "70vw",
height: "70vw",
}}
>
{videoReference && (
@ -385,15 +390,15 @@ export const VideoContent = () => {
user={name}
jsonId={id}
poster={videoCover || ""}
customStyle={{ aspectRatio: "16/9" }}
/>
)}
<Spacer height="15px" />
<Box
sx={{
width: "100%",
display: "grid",
gridTemplateColumns: "1fr 1fr",
marginTop: "15px",
}}
>
<Box>

59
src/state/features/persistSlice.ts

@ -0,0 +1,59 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { subscriptionTabValue } from "../../constants/Misc.ts";
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
interface settingsState {
selectedTab: string;
stretchVideoSetting: StretchVideoType;
filterType: string;
subscriptionList: string[];
playbackRate: number;
}
const initialState: settingsState = {
selectedTab: subscriptionTabValue,
stretchVideoSetting: "contain",
filterType: "videos",
subscriptionList: [],
playbackRate: 1,
};
export const persistSlice = createSlice({
name: "persist",
initialState,
reducers: {
setHomePageSelectedTab: (state, action) => {
state.selectedTab = action.payload;
},
setStretchVideoSetting: (state, action) => {
state.stretchVideoSetting = action.payload;
},
subscribe: (state, action: PayloadAction<string>) => {
const currentSubscriptions = state.subscriptionList;
if (!currentSubscriptions.includes(action.payload)) {
state.subscriptionList = [...currentSubscriptions, action.payload];
}
},
unSubscribe: (state, action) => {
state.subscriptionList = state.subscriptionList.filter(
item => item !== action.payload
);
},
setReduxPlaybackRate: (state, action) => {
state.playbackRate = action.payload;
},
changeFilterType: (state, action) => {
state.filterType = action.payload;
},
},
});
export const {
setHomePageSelectedTab,
subscribe,
unSubscribe,
setReduxPlaybackRate,
changeFilterType,
} = persistSlice.actions;
export default persistSlice.reducer;

194
src/state/features/videoSlice.ts

@ -1,23 +1,21 @@
import { createSlice } from '@reduxjs/toolkit';
import { RootState } from '../store'
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface GlobalState {
videos: Video[]
filteredVideos: Video[]
hashMapVideos: Record<string, Video>
hashMapSuperlikes: Record<string, any>
countNewVideos: number
isFiltering: boolean
filterValue: string
filterType: string
filterSearch: string
filterName: string
selectedCategoryVideos: any
selectedSubCategoryVideos: any
editVideoProperties: any
editPlaylistProperties: any
videos: Video[];
filteredVideos: Video[];
hashMapVideos: Record<string, Video>;
hashMapSuperlikes: Record<string, any>;
countNewVideos: number;
isFiltering: boolean;
filterValue: string;
filterSearch: string;
filterName: string;
selectedCategoryVideos: any;
selectedSubCategoryVideos: any;
editVideoProperties: any;
editPlaylistProperties: any;
}
const initialState: GlobalState = {
videos: [],
filteredVideos: [],
@ -25,159 +23,153 @@ const initialState: GlobalState = {
hashMapSuperlikes: {},
countNewVideos: 0,
isFiltering: false,
filterValue: '',
filterType: 'videos',
filterSearch: '',
filterName: '',
filterValue: "",
filterSearch: "",
filterName: "",
selectedCategoryVideos: null,
selectedSubCategoryVideos: null,
editVideoProperties: null,
editPlaylistProperties: null
}
editPlaylistProperties: null,
};
export interface Video {
title: string
description: string
created: number | string
user: string
service?: string
videoImage?: string
id: string
category?: string
categoryName?: string
tags?: string[]
updated?: number | string
isValid?: boolean
code?: string
title: string;
description: string;
created: number | string;
user: string;
service?: string;
videoImage?: string;
id: string;
category?: string;
categoryName?: string;
tags?: string[];
updated?: number | string;
isValid?: boolean;
code?: string;
}
export const videoSlice = createSlice({
name: 'video',
name: "video",
initialState,
reducers: {
setEditVideo: (state, action) => {
state.editVideoProperties = action.payload
state.editVideoProperties = action.payload;
},
setEditPlaylist: (state, action) => {
state.editPlaylistProperties = action.payload
},
changeFilterType: (state, action) => {
state.filterType = action.payload
state.editPlaylistProperties = action.payload;
},
changefilterSearch: (state, action) => {
state.filterSearch = action.payload
state.filterSearch = action.payload;
},
changefilterName: (state, action) => {
state.filterName = action.payload
state.filterName = action.payload;
},
changeSelectedCategoryVideos: (state, action) => {
state.selectedCategoryVideos = action.payload
state.selectedCategoryVideos = action.payload;
},
changeSelectedSubCategoryVideos: (state, action) => {
state.selectedSubCategoryVideos = action.payload
state.selectedSubCategoryVideos = action.payload;
},
setCountNewVideos: (state, action) => {
state.countNewVideos = action.payload
state.countNewVideos = action.payload;
},
addVideos: (state, action) => {
state.videos = action.payload
state.videos = action.payload;
},
addFilteredVideos: (state, action) => {
state.filteredVideos = action.payload
state.filteredVideos = action.payload;
},
removeVideo: (state, action) => {
const idToDelete = action.payload
state.videos = state.videos.filter((item) => item.id !== idToDelete)
const idToDelete = action.payload;
state.videos = state.videos.filter(item => item.id !== idToDelete);
state.filteredVideos = state.filteredVideos.filter(
(item) => item.id !== idToDelete
)
item => item.id !== idToDelete
);
},
addVideoToBeginning: (state, action) => {
state.videos.unshift(action.payload)
state.videos.unshift(action.payload);
},
clearVideoList: (state) => {
state.videos = []
clearVideoList: state => {
state.videos = [];
},
updateVideo: (state, action) => {
const { id } = action.payload
const index = state.videos.findIndex((video) => video.id === id)
const { id } = action.payload;
const index = state.videos.findIndex(video => video.id === id);
if (index !== -1) {
state.videos[index] = { ...action.payload }
state.videos[index] = { ...action.payload };
}
const index2 = state.filteredVideos.findIndex((video) => video.id === id)
const index2 = state.filteredVideos.findIndex(video => video.id === id);
if (index2 !== -1) {
state.filteredVideos[index2] = { ...action.payload }
state.filteredVideos[index2] = { ...action.payload };
}
},
addToHashMap: (state, action) => {
const video = action.payload
state.hashMapVideos[video.id + '-' + video.user] = video
const video = action.payload;
state.hashMapVideos[video.id + "-" + video.user] = video;
},
addtoHashMapSuperlikes: (state, action) => {
const superlike = action.payload
state.hashMapSuperlikes[superlike.identifier] = superlike
},
const superlike = action.payload;
state.hashMapSuperlikes[superlike.identifier] = superlike;
},
updateInHashMap: (state, action) => {
const { id, user } = action.payload
const video = action.payload
state.hashMapVideos[id + '-' + user] = { ...video }
const { id, user } = action.payload;
const video = action.payload;
state.hashMapVideos[id + "-" + user] = { ...video };
},
removeFromHashMap: (state, action) => {
const idToDelete = action.payload
delete state.hashMapVideos[idToDelete]
const idToDelete = action.payload;
delete state.hashMapVideos[idToDelete];
},
addArrayToHashMap: (state, action) => {
const videos = action.payload
const videos = action.payload;
videos.forEach((video: Video) => {
state.hashMapVideos[video.id + '-' + video.user] = video
})
state.hashMapVideos[video.id + "-" + video.user] = video;
});
},
upsertVideos: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.videos.findIndex((p) => p.id === video.id)
const index = state.videos.findIndex(p => p.id === video.id);
if (index !== -1) {
state.videos[index] = video
state.videos[index] = video;
} else {
state.videos.push(video)
state.videos.push(video);
}
})
});
},
upsertFilteredVideos: (state, action) => {
action.payload.forEach((video: Video) => {
const index = state.filteredVideos.findIndex((p) => p.id === video.id)
const index = state.filteredVideos.findIndex(p => p.id === video.id);
if (index !== -1) {
state.filteredVideos[index] = video
state.filteredVideos[index] = video;
} else {
state.filteredVideos.push(video)
state.filteredVideos.push(video);
}
})
});
},
upsertVideosBeginning: (state, action) => {
action.payload.reverse().forEach((video: Video) => {
const index = state.videos.findIndex((p) => p.id === video.id)
const index = state.videos.findIndex(p => p.id === video.id);
if (index !== -1) {
state.videos[index] = video
state.videos[index] = video;
} else {
state.videos.unshift(video)
state.videos.unshift(video);
}
})
});
},
setIsFiltering: (state, action) => {
state.isFiltering = action.payload
state.isFiltering = action.payload;
},
setFilterValue: (state, action) => {
state.filterValue = action.payload
state.filterValue = action.payload;
},
blockUser: (state, action) => {
const username = action.payload
state.videos = state.videos.filter((item) => item.user !== username)
}
}
})
const username = action.payload;
state.videos = state.videos.filter(item => item.user !== username);
},
},
});
export const {
setCountNewVideos,
@ -196,7 +188,6 @@ export const {
setIsFiltering,
setFilterValue,
clearVideoList,
changeFilterType,
changefilterSearch,
changefilterName,
changeSelectedCategoryVideos,
@ -204,8 +195,7 @@ export const {
blockUser,
setEditVideo,
setEditPlaylist,
addtoHashMapSuperlikes
} = videoSlice.actions
export default videoSlice.reducer
addtoHashMapSuperlikes,
} = videoSlice.actions;
export default videoSlice.reducer;

54
src/state/store.ts

@ -1,27 +1,47 @@
import { configureStore } from '@reduxjs/toolkit'
import notificationsReducer from './features/notificationsSlice'
import authReducer from './features/authSlice'
import globalReducer from './features/globalSlice'
import videoReducer from './features/videoSlice'
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import notificationsReducer from "./features/notificationsSlice";
import authReducer from "./features/authSlice";
import globalReducer from "./features/globalSlice";
import videoReducer from "./features/videoSlice";
import settingsReducer from "./features/persistSlice.ts";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import storage from "redux-persist/lib/storage";
const persistSettingsConfig = {
key: "persist",
version: 1,
storage,
};
const reducer = combineReducers({
notifications: notificationsReducer,
auth: authReducer,
global: globalReducer,
video: videoReducer,
persist: persistReducer(persistSettingsConfig, settingsReducer),
});
export const store = configureStore({
reducer: {
notifications: notificationsReducer,
auth: authReducer,
global: globalReducer,
video: videoReducer,
},
middleware: (getDefaultMiddleware) =>
reducer,
middleware: getDefaultMiddleware =>
getDefaultMiddleware({
serializableCheck: false
serializableCheck: false,
}),
preloadedState: undefined // optional, can be any valid state object
})
preloadedState: undefined, // optional, can be any valid state object
});
// Define the RootState type, which is the type of the entire Redux state tree.
// This is useful when you need to access the state in a component or elsewhere.
export type RootState = ReturnType<typeof store.getState>
export type RootState = ReturnType<typeof store.getState>;
// Define the AppDispatch type, which is the type of the Redux store's dispatch function.
// This is useful when you need to dispatch an action in a component or elsewhere.
export type AppDispatch = typeof store.dispatch
export type AppDispatch = typeof store.dispatch;

2
src/wrappers/GlobalWrapper.tsx

@ -15,7 +15,7 @@ import {
setSuperlikesAll,
setUserAvatarHash,
} from "../state/features/globalSlice";
import { VideoPlayerGlobal } from "../components/common/VideoPlayerGlobal";
import { VideoPlayerGlobal } from "../components/common/VideoPlayer/VideoPlayerGlobal.tsx";
import { Rnd } from "react-rnd";
import { RequestQueue } from "../utils/queue";
import { EditVideo } from "../components/EditVideo/EditVideo";

Loading…
Cancel
Save