3
0
mirror of https://github.com/Qortal/q-tube.git synced 2025-02-11 17:55:51 +00:00

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
This commit is contained in:
Qortal Dev 2024-02-08 13:56:53 -07:00
parent 5e5f19053f
commit 6fd206d6fb
21 changed files with 2288 additions and 1844 deletions

488
package-lock.json generated
View File

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

View File

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

View File

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

View File

@ -1,17 +1,39 @@
import { Button, ButtonProps } from "@mui/material"; 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 { interface SubscribeButtonProps extends ButtonProps {
name: string; name: string;
} }
const isSubscribed = false;
export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => { 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>) => { const manageSubscription = (e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
console.log("subscribed to: ", name); isSubscribed ? unSubscribeFromRedux() : subscribeToRedux();
}; };
const verticalPadding = "3px"; const verticalPadding = "3px";
const horizontalPadding = "8px"; const horizontalPadding = "8px";
const buttonStyle = { const buttonStyle = {
@ -23,7 +45,7 @@ export const SubscribeButton = ({ name, ...props }: SubscribeButtonProps) => {
paddingLeft: horizontalPadding, paddingLeft: horizontalPadding,
paddingRight: horizontalPadding, paddingRight: horizontalPadding,
borderRadius: 28, borderRadius: 28,
display: "none", height: "40px",
}; };
return ( return (
<Button <Button

View File

@ -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);
`;

View File

@ -12,44 +12,20 @@ import {
VolumeOff, VolumeOff,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { MyContext } from "../../wrappers/DownloadWrapper"; import { MyContext } from "../../../wrappers/DownloadWrapper.tsx";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../../state/store.ts";
import { Refresh } from "@mui/icons-material"; import { Refresh } from "@mui/icons-material";
import { Menu, MenuItem } from "@mui/material"; import { Menu, MenuItem } from "@mui/material";
import { MoreVert as MoreIcon } from "@mui/icons-material"; import { MoreVert as MoreIcon } from "@mui/icons-material";
import { setVideoPlaying } from "../../state/features/globalSlice"; import { setVideoPlaying } from "../../../state/features/globalSlice.ts";
const VideoContainer = styled(Box)` import {
position: relative; ControlsContainer,
display: flex; VideoContainer,
flex-direction: column; VideoElement,
align-items: center; } from "./VideoPlayer-styles.ts";
justify-content: center; import CSS from "csstype";
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);
`;
interface VideoPlayerProps { interface VideoPlayerProps {
src?: string; src?: string;
@ -65,6 +41,7 @@ interface VideoPlayerProps {
nextVideo?: any; nextVideo?: any;
onEnd?: () => void; onEnd?: () => void;
autoPlay?: boolean; autoPlay?: boolean;
style?: CSS.Properties;
} }
export const VideoPlayer: React.FC<VideoPlayerProps> = ({ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
@ -80,6 +57,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
nextVideo, nextVideo,
onEnd, onEnd,
autoPlay, autoPlay,
style = {},
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
@ -94,16 +72,21 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
const [isMobileView, setIsMobileView] = useState(false); const [isMobileView, setIsMobileView] = useState(false);
const [playbackRate, setPlaybackRate] = useState(1); const [playbackRate, setPlaybackRate] = useState(1);
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
const [showControlsFullScreen, setShowControlsFullScreen] =
useState<boolean>(true);
const videoPlaying = useSelector( const videoPlaying = useSelector(
(state: RootState) => state.global.videoPlaying (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 reDownload = useRef<boolean>(false);
const reDownloadNextVid = useRef<boolean>(false); const reDownloadNextVid = useRef<boolean>(false);
const isFetchingProperties = useRef<boolean>(false); const isFetchingProperties = useRef<boolean>(false);
const status = useRef<null | string>(null); const status = useRef<null | string>(null);
const { downloads } = useSelector((state: RootState) => state.global);
const download = useMemo(() => { const download = useMemo(() => {
if (!downloads || !identifier) return {}; if (!downloads || !identifier) return {};
const findDownload = downloads[identifier]; const findDownload = downloads[identifier];
@ -678,6 +661,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onKeyDown={keyboardShortcutsDown} onKeyDown={keyboardShortcutsDown}
style={{ style={{
padding: from === "create" ? "8px" : 0, padding: from === "create" ? "8px" : 0,
...customStyle,
}} }}
> >
{isLoading && ( {isLoading && (
@ -781,28 +765,31 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({
onEnded={handleEnded} onEnded={handleEnded}
// onLoadedMetadata={handleLoadedMetadata} // onLoadedMetadata={handleLoadedMetadata}
onCanPlay={handleCanPlay} 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" preload="metadata"
style={ style={
startPlay startPlay
? { ? {
...customStyle, ...customStyle,
objectFit: "fill", objectFit: settingsState.stretchVideoSetting,
height: "calc(100% - 80px)",
} }
: { ...customStyle } : { ...customStyle, height: "100%" }
} }
/> />
<ControlsContainer <ControlsContainer
style={ style={{ bottom: from === "create" ? "15px" : 0, padding: "0px" }}
startPlay display={showControlsFullScreen ? "flex" : "none"}
? {
bottom: from === "create" ? "15px" : 0,
padding: "8px",
}
: { bottom: from === "create" ? "15px" : 0, padding: "0px" }
}
> >
{isMobileView && canPlay ? ( {isMobileView && canPlay && showControlsFullScreen ? (
<> <>
<IconButton <IconButton
sx={{ sx={{

View File

@ -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>
);
};

View File

@ -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>
)
}

View File

@ -1,4 +1,6 @@
export const minPriceSuperlike = 10; export const minPriceSuperlike = 10;
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g; 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
View File

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

View File

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

View File

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

View File

@ -1,15 +1,582 @@
import React from 'react' import React, { useCallback, useEffect, useRef, useState } from "react";
import { VideoList } from './VideoList' 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' interface HomeProps {
import { RootState } from '../../state/store' 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
);
export const Home = () => { 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]);
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 ( return (
<> <Grid container sx={{ width: "100%" }}>
<VideoList /> <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>
);
};

View File

@ -7,6 +7,7 @@ import {
TextField, TextField,
InputLabel, InputLabel,
Autocomplete, Autocomplete,
Radio,
} from "@mui/material"; } from "@mui/material";
export const VideoContainer = styled(Grid)(({ theme }) => ({ export const VideoContainer = styled(Grid)(({ theme }) => ({
@ -131,7 +132,7 @@ export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
})); }));
export const VideoCardName = styled(Typography)(({ theme }) => ({ export const VideoCardName = styled(Typography)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
fontSize: "18px", fontSize: "16px",
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none", 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 }) => ({ export const FilterSelect = styled(Autocomplete)(({ theme }) => ({
"& #categories-select": { "& #categories-select": {
padding: "7px", padding: "7px",

View File

@ -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 { import {
BlockIconContainer, BlockIconContainer,
BottomParent, BottomParent,
FilterSelect,
FiltersCheckbox,
FiltersCol,
FiltersContainer,
FiltersRow,
FiltersSubContainer,
FiltersTitle,
IconsBox, IconsBox,
NameContainer, NameContainer,
VideoCardCol,
ProductManagerRow,
VideoCardContainer,
VideoCard, VideoCard,
VideoCardCol,
VideoCardContainer,
VideoCardName, VideoCardName,
VideoCardTitle, VideoCardTitle,
VideoContainer,
VideoUploadDate, VideoUploadDate,
} from "./VideoList-styles"; } from "./VideoList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage"; import { Avatar, Box, Tooltip, useTheme } from "@mui/material";
import { formatDate, formatTimestampSeconds } from "../../utils/time"; import EditIcon from "@mui/icons-material/Edit";
import { Subtitle, SubtitleContainer } from "./Home-styles";
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG";
import { import {
addVideos,
blockUser, blockUser,
changeFilterType,
changeSelectedCategoryVideos,
changeSelectedSubCategoryVideos,
changefilterName,
changefilterSearch,
clearVideoList,
setEditPlaylist, setEditPlaylist,
setEditVideo, setEditVideo,
} from "../../state/features/videoSlice"; } from "../../state/features/videoSlice.ts";
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 BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit"; import ResponsiveImage from "../../components/ResponsiveImage.tsx";
import { ListSuperLikeContainer } from "../../components/common/ListSuperLikes/ListSuperLikeContainer.tsx"; import { formatDate } from "../../utils/time.ts";
import { VideoCardImageContainer } from "./VideoCardImageContainer"; import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG.tsx";
import { SubscribeButton } from "../../components/common/SubscribeButton.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 { interface VideoListProps {
mode?: string; videos: Video[];
} }
export const VideoList = ({ mode }: VideoListProps) => { export const VideoList = ({ videos }: 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);
const [showIcons, setShowIcons] = useState(null); 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( const hashMapVideos = useSelector(
(state: RootState) => state.video.hashMapVideos (state: RootState) => state.video.hashMapVideos
); );
const countNewVideos = useSelector(
(state: RootState) => state.video.countNewVideos
);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (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 navigate = useNavigate();
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } = const dispatch = useDispatch();
useFetchVideos(); const theme = useTheme();
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 blockUserFunc = async (user: string) => { const blockUserFunc = async (user: string) => {
if (user === "Q-Tube") return; if (user === "Q-Tube") return;
@ -288,483 +62,224 @@ export const VideoList = ({ mode }: VideoListProps) => {
} catch (error) {} } catch (error) {}
}; };
const handleInputKeyDown = (event: any) => {
if (event.key === "Enter") {
getVideosHandler(true);
}
};
return ( return (
<Grid container sx={{ width: "100%" }}> <VideoCardContainer>
<FiltersCol item xs={12} md={2} lg={2} xl={2} sm={3}> {videos.map((video: any, index: number) => {
<FiltersContainer> const fullId = video ? `${video.id}-${video.user}` : undefined;
<Input const existingVideo = hashMapVideos[fullId];
id="standard-adornment-name" let hasHash = false;
onChange={e => { let videoObj = video;
setFilterSearch(e.target.value); if (existingVideo) {
}} videoObj = existingVideo;
value={filterSearch} hasHash = true;
placeholder="Search" }
onKeyDown={handleInputKeyDown}
sx={{ let avatarUrl = "";
borderBottom: "1px solid white", if (userAvatarHash[videoObj?.user]) {
"&&:before": { avatarUrl = userAvatarHash[videoObj?.user];
borderBottom: "none", }
},
"&&:after": { // nb. this prevents showing metadata for a video which
borderBottom: "none", // belongs to a different user
}, if (
"&&:hover:before": { videoObj?.user &&
borderBottom: "none", videoObj?.videoReference?.name &&
}, videoObj.user != videoObj.videoReference.name
"&&.Mui-focused:before": { ) {
borderBottom: "none", return null;
}, }
"&&.Mui-focused": {
outline: "none", if (hasHash && !videoObj?.videoImage && !videoObj?.image) {
}, return null;
fontSize: "18px", }
}} const isPlaylist = videoObj?.service === "PLAYLIST";
/>
<Input if (isPlaylist) {
id="standard-adornment-name" return (
onChange={e => { <VideoCardCol
setFilterName(e.target.value); onMouseEnter={() => setShowIcons(videoObj.id)}
}} onMouseLeave={() => setShowIcons(null)}
value={filterName} key={videoObj.id}
placeholder="User's Name (Exact)" >
onKeyDown={handleInputKeyDown} <IconsBox
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={{ sx={{
display: "flex", opacity: showIcons === videoObj.id ? 1 : 0,
gap: "20px", zIndex: 2,
alignItems: "center",
flexDirection: "column",
}} }}
> >
<FormControl fullWidth sx={{ marginBottom: 1 }}> {videoObj?.user === username && (
<InputLabel <Tooltip title="Edit playlist" placement="top">
sx={{ <BlockIconContainer>
fontSize: "16px", <EditIcon
}}
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
<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"
>
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>
<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={() => { onClick={() => {
if (!hasHash) return; dispatch(setEditPlaylist(videoObj));
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
onClick={() => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}}
>
<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
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
onClick={() => {
navigate(`/video/${videoObj?.user}/${videoObj?.id}`);
}}
>
<VideoCardImageContainer
width={266}
height={150}
videoImage={videoObj.videoImage}
frameImages={videoObj?.extracts || []}
/>
{/* <ResponsiveImage
src={videoObj.videoImage} src={videoObj.videoImage}
width={266} width={266}
height={150} height={150}
/> */} /> */}
<VideoCardTitle>{videoObj.title}</VideoCardTitle> <VideoCardTitle>{videoObj.title}</VideoCardTitle>
<BottomParent> <BottomParent>
<NameContainer <NameContainer
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${videoObj?.user}`); navigate(`/channel/${videoObj?.user}`);
}} }}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`}
alt={`${videoObj?.user}'s avatar`} alt={`${videoObj?.user}'s avatar`}
/> />
<VideoCardName <VideoCardName
sx={{ sx={{
":hover": { ":hover": {
textDecoration: "underline", textDecoration: "underline",
}, },
}} }}
> >
{videoObj?.user} {videoObj?.user}
</VideoCardName> </VideoCardName>
<SubscribeButton name={videoObj?.user} /> </NameContainer>
</NameContainer> {videoObj?.created && (
{videoObj?.created && ( <VideoUploadDate>
<VideoUploadDate> {formatDate(videoObj.created)}
{formatDate(videoObj.created)} </VideoUploadDate>
</VideoUploadDate> )}
)} </BottomParent>
</BottomParent> </VideoCard>
</VideoCard> </VideoCardCol>
</VideoCardCol> );
); })}
})} </VideoCardContainer>
</VideoCardContainer>
<LazyLoad
onLoadMore={getVideosHandler}
isLoading={isLoading}
></LazyLoad>
</Box>
</ProductManagerRow>
</Grid>
<FiltersCol item xs={0} lg={3} xl={2}>
<ListSuperLikeContainer />
</FiltersCol>
</Grid>
); );
}; };
export default VideoList;

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -1,27 +1,54 @@
import { configureStore } from '@reduxjs/toolkit' import { combineReducers, configureStore } from "@reduxjs/toolkit";
import notificationsReducer from './features/notificationsSlice' import notificationsReducer from "./features/notificationsSlice";
import authReducer from './features/authSlice' import authReducer from "./features/authSlice";
import globalReducer from './features/globalSlice' import globalReducer from "./features/globalSlice";
import videoReducer from './features/videoSlice' 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({ export const store = configureStore({
reducer: { reducer,
notifications: notificationsReducer, middleware: getDefaultMiddleware =>
auth: authReducer,
global: globalReducer,
video: videoReducer,
},
middleware: (getDefaultMiddleware) =>
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. // 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. // 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. // 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. // 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;

View File

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