Browse Source

Subscriptions to channels added

Filter added that removes characters that Operating Systems don't allow in filenames when saving file

VideoList-styles.tsx uses Radio button instead of Checkbox for main page video/playlist filter

Video player has aspect ratio of 16 / 9, doesn't put controls over video, and removes controls if mouse exits video when in fullscreen (but only when playing for some reason)

Created new redux slice called settingsSlice.ts. It is used to store settings that are saved to disk automatically
pull/9/head
Qortal Dev 7 months ago
parent
commit
6fd206d6fb
  1. 496
      package-lock.json
  2. 2
      package.json
  3. 36
      src/App.tsx
  4. 30
      src/components/common/SubscribeButton.tsx
  5. 32
      src/components/common/VideoPlayer/VideoPlayer-styles.ts
  6. 77
      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. 589
      src/pages/Home/Home.tsx
  14. 10
      src/pages/Home/VideoList-styles.tsx
  15. 925
      src/pages/Home/VideoList.tsx
  16. 3
      src/pages/PlaylistContent/PlaylistContent.tsx
  17. 19
      src/pages/VideoContent/VideoContent.tsx
  18. 30
      src/state/features/settingsSlice.ts
  19. 208
      src/state/features/videoSlice.ts
  20. 61
      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>
);
}

30
src/components/common/SubscribeButton.tsx

@ -1,17 +1,39 @@
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/videoSlice.ts";
interface SubscribeButtonProps extends ButtonProps {
name: string;
}
const isSubscribed = false;
export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
const dispatch = useDispatch();
const select = useSelector((state: RootState) => {
return state.video;
});
const [isSubscribed, setIsSubscribed] = useState<boolean>(false);
useEffect(() => {
setIsSubscribed(select.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 = {
@ -23,7 +45,7 @@ export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
paddingLeft: horizontalPadding,
paddingRight: horizontalPadding,
borderRadius: 28,
display: "none",
height: "40px",
};
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);
`;

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

@ -12,44 +12,20 @@ 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";
interface VideoPlayerProps {
src?: string;
@ -65,6 +41,7 @@ interface VideoPlayerProps {
nextVideo?: any;
onEnd?: () => void;
autoPlay?: boolean;
style?: CSS.Properties;
}
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
@ -80,6 +57,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
nextVideo,
onEnd,
autoPlay,
style = {},
}) => {
const dispatch = useDispatch();
const videoRef = useRef<HTMLVideoElement | null>(null);
@ -94,16 +72,21 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1);
const [anchorEl, setAnchorEl] = useState(null);
const [showControlsFullScreen, setShowControlsFullScreen] =
useState<boolean>(true);
const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying
);
const settingsState = useSelector((state: RootState) => state.settings);
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];
@ -678,6 +661,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onKeyDown={keyboardShortcutsDown}
style={{
padding: from === "create" ? "8px" : 0,
...customStyle,
}}
>
{isLoading && (
@ -781,28 +765,31 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay}
onMouseEnter={e => {
setShowControlsFullScreen(true);
console.log("entering video, fullscreen is: ", isFullscreen);
}}
onMouseLeave={e => {
setShowControlsFullScreen(false);
console.log("leaving video, fullscreen is: ", isFullscreen);
}}
preload="metadata"
style={
startPlay
? {
...customStyle,
objectFit: "fill",
objectFit: settingsState.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.video.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>
)
}
);
};

589
src/pages/Home/Home.tsx

@ -1,15 +1,582 @@
import React from 'react'
import { VideoList } from './VideoList'
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,
Tab,
Tabs,
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,
VideoCardCol,
ProductManagerRow,
VideoCardContainer,
VideoCard,
VideoCardName,
VideoCardTitle,
VideoContainer,
VideoUploadDate,
FiltersRadioButton,
} 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";
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";
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 { TabContext, TabList, TabPanel } from "@mui/lab";
import VideoList from "./VideoList.tsx";
import { allTabValue, subscriptionTabValue } from "../../constants/Misc.ts";
import { setHomePageSelectedTab } from "../../state/features/settingsSlice.ts";
import { useSelector } from 'react-redux'
import { RootState } from '../../state/store'
interface HomeProps {
mode?: string;
}
export const Home = ({ mode }: HomeProps) => {
const theme = useTheme();
const prevVal = useRef("");
const isFiltering = useSelector(
(state: RootState) => state.video.isFiltering
);
const filterValue = useSelector(
(state: RootState) => state.video.filterValue
);
const settingsState = useSelector((state: RootState) => state.settings);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [tabValue, setTabValue] = useState<string>(settingsState.selectedTab);
const tabFontSize = "20px";
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 isFilterMode = useRef(false);
const firstFetch = useRef(false);
const afterFetch = useRef(false);
const isFetchingFiltered = useRef(false);
const isFetching = useRef(false);
const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos
);
const videoSelector = useSelector((state: RootState) => state.video);
const { videos: globalVideos } = useSelector(
(state: RootState) => state.video
);
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,
resetFilers,
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]);
export const Home = () => {
const getVideosHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return;
firstFetch.current = true;
setIsLoading(true);
await getVideos({}, 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",
}}
/>
<FiltersTitle>
Categories
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<FiltersSubContainer>
<FormControl sx={{ width: "100%" }}>
<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>
<FiltersTitle>
Type
<ExpandMoreSVG
color={theme.palette.text.primary}
height={"22"}
width={"22"}
/>
</FiltersTitle>
<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="Subsciptions"
value={subscriptionTabValue}
sx={{ fontSize: tabFontSize }}
/>
</TabList>
<TabPanel value="all" sx={{ width: "100%" }}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
<TabPanel value="subscriptions" sx={{ width: "100%" }}>
<VideoList videos={videos} />
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</TabPanel>
</TabContext>
</Box>
</ProductManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
);
};

10
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",
@ -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",

925
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,224 @@ 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, 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={{
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 && (
<VideoUploadDate>
{formatDate(videoObj.created)}
</VideoUploadDate>
)}
</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>

30
src/state/features/settingsSlice.ts

@ -0,0 +1,30 @@
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;
}
const initialState: settingsState = {
selectedTab: subscriptionTabValue,
stretchVideoSetting: "contain",
};
export const settingsSlice = createSlice({
name: "settings",
initialState,
reducers: {
setHomePageSelectedTab: (state, action) => {
state.selectedTab = action.payload;
},
setStretchVideoSetting: (state, action) => {
state.stretchVideoSetting = action.payload;
},
},
});
export const { setHomePageSelectedTab } = settingsSlice.actions;
export default settingsSlice.reducer;

208
src/state/features/videoSlice.ts

@ -1,23 +1,23 @@
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;
filterType: string;
filterSearch: string;
filterName: string;
selectedCategoryVideos: any;
selectedSubCategoryVideos: any;
editVideoProperties: any;
editPlaylistProperties: any;
subscriptionList: string[];
}
const initialState: GlobalState = {
videos: [],
filteredVideos: [],
@ -25,159 +25,168 @@ const initialState: GlobalState = {
hashMapSuperlikes: {},
countNewVideos: 0,
isFiltering: false,
filterValue: '',
filterType: 'videos',
filterSearch: '',
filterName: '',
filterValue: "",
filterType: "videos",
filterSearch: "",
filterName: "",
selectedCategoryVideos: null,
selectedSubCategoryVideos: null,
editVideoProperties: null,
editPlaylistProperties: null
}
editPlaylistProperties: null,
subscriptionList: [],
};
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
state.editPlaylistProperties = action.payload;
},
changeFilterType: (state, action) => {
state.filterType = action.payload
state.filterType = 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);
},
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
);
},
},
});
export const {
setCountNewVideos,
@ -204,8 +213,9 @@ export const {
blockUser,
setEditVideo,
setEditPlaylist,
addtoHashMapSuperlikes
} = videoSlice.actions
export default videoSlice.reducer
addtoHashMapSuperlikes,
subscribe,
unSubscribe,
} = videoSlice.actions;
export default videoSlice.reducer;

61
src/state/store.ts

@ -1,27 +1,54 @@
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/settingsSlice";
import {
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import storage from "redux-persist/lib/storage";
const persistVideoConfig = {
key: "video",
version: 1,
storage,
whitelist: ["subscriptionList"],
};
const persistSettingsConfig = {
key: "settings",
version: 1,
storage,
};
const reducer = combineReducers({
notifications: notificationsReducer,
auth: authReducer,
global: globalReducer,
video: persistReducer(persistVideoConfig, videoReducer),
settings: 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