feat: improve media playback experience
This commit is contained in:
90
package-lock.json
generated
90
package-lock.json
generated
@@ -56,6 +56,7 @@
|
|||||||
"franc-min": "^6.2.0",
|
"franc-min": "^6.2.0",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
|
"jotai": "^2.15.0",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
@@ -113,7 +114,7 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/gen-mapping": "^0.3.5",
|
"@jridgewell/gen-mapping": "^0.3.5",
|
||||||
"@jridgewell/trace-mapping": "^0.3.24"
|
"@jridgewell/trace-mapping": "^0.3.24"
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
"version": "7.26.2",
|
"version": "7.26.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
"js-tokens": "^4.0.0",
|
"js-tokens": "^4.0.0",
|
||||||
@@ -140,7 +141,7 @@
|
|||||||
"version": "7.26.3",
|
"version": "7.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz",
|
||||||
"integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
|
"integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@
|
|||||||
"version": "7.26.0",
|
"version": "7.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
|
||||||
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
"integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.2.0",
|
"@ampproject/remapping": "^2.2.0",
|
||||||
"@babel/code-frame": "^7.26.0",
|
"@babel/code-frame": "^7.26.0",
|
||||||
@@ -179,7 +180,7 @@
|
|||||||
"version": "7.26.3",
|
"version": "7.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz",
|
||||||
"integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
|
"integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.26.3",
|
"@babel/parser": "^7.26.3",
|
||||||
"@babel/types": "^7.26.3",
|
"@babel/types": "^7.26.3",
|
||||||
@@ -207,7 +208,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
|
||||||
"integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
|
"integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/compat-data": "^7.25.9",
|
"@babel/compat-data": "^7.25.9",
|
||||||
"@babel/helper-validator-option": "^7.25.9",
|
"@babel/helper-validator-option": "^7.25.9",
|
||||||
@@ -223,7 +224,7 @@
|
|||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -299,7 +300,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
|
||||||
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
"integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/traverse": "^7.25.9",
|
"@babel/traverse": "^7.25.9",
|
||||||
"@babel/types": "^7.25.9"
|
"@babel/types": "^7.25.9"
|
||||||
@@ -312,7 +313,7 @@
|
|||||||
"version": "7.26.0",
|
"version": "7.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
|
||||||
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
|
"integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-imports": "^7.25.9",
|
"@babel/helper-module-imports": "^7.25.9",
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
@@ -397,7 +398,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -406,7 +407,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -415,7 +416,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
|
||||||
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
|
"integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -438,7 +439,7 @@
|
|||||||
"version": "7.26.0",
|
"version": "7.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
|
||||||
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
"integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/template": "^7.25.9",
|
"@babel/template": "^7.25.9",
|
||||||
"@babel/types": "^7.26.0"
|
"@babel/types": "^7.26.0"
|
||||||
@@ -451,7 +452,7 @@
|
|||||||
"version": "7.26.3",
|
"version": "7.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz",
|
||||||
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
"integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^7.26.3"
|
"@babel/types": "^7.26.3"
|
||||||
},
|
},
|
||||||
@@ -1537,7 +1538,7 @@
|
|||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
|
||||||
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
"integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.25.9",
|
"@babel/code-frame": "^7.25.9",
|
||||||
"@babel/parser": "^7.25.9",
|
"@babel/parser": "^7.25.9",
|
||||||
@@ -1551,7 +1552,7 @@
|
|||||||
"version": "7.26.4",
|
"version": "7.26.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz",
|
||||||
"integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
|
"integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.26.2",
|
"@babel/code-frame": "^7.26.2",
|
||||||
"@babel/generator": "^7.26.3",
|
"@babel/generator": "^7.26.3",
|
||||||
@@ -1569,7 +1570,7 @@
|
|||||||
"version": "11.12.0",
|
"version": "11.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
|
||||||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
@@ -1578,7 +1579,7 @@
|
|||||||
"version": "7.26.3",
|
"version": "7.26.3",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz",
|
||||||
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
"integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^7.25.9",
|
"@babel/helper-string-parser": "^7.25.9",
|
||||||
"@babel/helper-validator-identifier": "^7.25.9"
|
"@babel/helper-validator-identifier": "^7.25.9"
|
||||||
@@ -5998,7 +5999,7 @@
|
|||||||
"version": "4.24.3",
|
"version": "4.24.3",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
|
||||||
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
|
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6124,7 +6125,7 @@
|
|||||||
"version": "1.0.30001690",
|
"version": "1.0.30001690",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
|
||||||
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -6727,7 +6728,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/core-js-compat": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.39.0",
|
"version": "3.39.0",
|
||||||
@@ -7002,7 +7003,7 @@
|
|||||||
"version": "1.5.75",
|
"version": "1.5.75",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
|
||||||
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel": {
|
"node_modules/embla-carousel": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
@@ -7267,7 +7268,7 @@
|
|||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -7750,7 +7751,7 @@
|
|||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -8694,6 +8695,35 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jotai": {
|
||||||
|
"version": "2.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.15.0.tgz",
|
||||||
|
"integrity": "sha512-nbp/6jN2Ftxgw0VwoVnOg0m5qYM1rVcfvij+MZx99Z5IK13eGve9FJoCwGv+17JvVthTjhSmNtT5e1coJnr6aw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@babel/core": ">=7.0.0",
|
||||||
|
"@babel/template": ">=7.0.0",
|
||||||
|
"@types/react": ">=17.0.0",
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@babel/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@babel/template": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -8715,7 +8745,7 @@
|
|||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jsesc": "bin/jsesc"
|
"jsesc": "bin/jsesc"
|
||||||
},
|
},
|
||||||
@@ -8751,7 +8781,7 @@
|
|||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
},
|
},
|
||||||
@@ -9870,7 +9900,7 @@
|
|||||||
"version": "2.0.19",
|
"version": "2.0.19",
|
||||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -11153,7 +11183,7 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
@@ -12213,7 +12243,7 @@
|
|||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
|
||||||
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
|
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -13021,7 +13051,7 @@
|
|||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
||||||
"dev": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
"franc-min": "^6.2.0",
|
"franc-min": "^6.2.0",
|
||||||
"i18next": "^24.2.0",
|
"i18next": "^24.2.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.4",
|
"i18next-browser-languagedetector": "^8.0.4",
|
||||||
|
"jotai": "^2.15.0",
|
||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
|
import BackgroundAudio from './components/BackgroundAudio'
|
||||||
import BottomNavigationBar from './components/BottomNavigationBar'
|
import BottomNavigationBar from './components/BottomNavigationBar'
|
||||||
|
import CreateWalletGuideToast from './components/CreateWalletGuideToast'
|
||||||
import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog'
|
import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog'
|
||||||
import { normalizeUrl } from './lib/url'
|
import { normalizeUrl } from './lib/url'
|
||||||
import ExplorePage from './pages/primary/ExplorePage'
|
import ExplorePage from './pages/primary/ExplorePage'
|
||||||
@@ -28,7 +30,6 @@ import { NotificationProvider } from './providers/NotificationProvider'
|
|||||||
import { useScreenSize } from './providers/ScreenSizeProvider'
|
import { useScreenSize } from './providers/ScreenSizeProvider'
|
||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
import modalManager from './services/modal-manager.service'
|
import modalManager from './services/modal-manager.service'
|
||||||
import CreateWalletGuideToast from './components/CreateWalletGuideToast'
|
|
||||||
|
|
||||||
export type TPrimaryPageName = keyof typeof PRIMARY_PAGE_MAP
|
export type TPrimaryPageName = keyof typeof PRIMARY_PAGE_MAP
|
||||||
|
|
||||||
@@ -385,6 +386,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
|
|||||||
</div>
|
</div>
|
||||||
<TooManyRelaysAlertDialog />
|
<TooManyRelaysAlertDialog />
|
||||||
<CreateWalletGuideToast />
|
<CreateWalletGuideToast />
|
||||||
|
<BackgroundAudio className="fixed bottom-20 right-0 z-50 w-80 rounded-l-full rounded-r-none overflow-hidden shadow-lg border" />
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
</CurrentRelaysProvider>
|
</CurrentRelaysProvider>
|
||||||
</SecondaryPageContext.Provider>
|
</SecondaryPageContext.Provider>
|
||||||
|
|||||||
@@ -2,16 +2,25 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Slider } from '@/components/ui/slider'
|
import { Slider } from '@/components/ui/slider'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import mediaManager from '@/services/media-manager.service'
|
import mediaManager from '@/services/media-manager.service'
|
||||||
import { Pause, Play } from 'lucide-react'
|
import { Minimize2, Pause, Play, X } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import ExternalLink from '../ExternalLink'
|
import ExternalLink from '../ExternalLink'
|
||||||
|
|
||||||
interface AudioPlayerProps {
|
interface AudioPlayerProps {
|
||||||
src: string
|
src: string
|
||||||
|
autoPlay?: boolean
|
||||||
|
startTime?: number
|
||||||
|
isMinimized?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
export default function AudioPlayer({
|
||||||
|
src,
|
||||||
|
autoPlay = false,
|
||||||
|
startTime,
|
||||||
|
isMinimized = false,
|
||||||
|
className
|
||||||
|
}: AudioPlayerProps) {
|
||||||
const audioRef = useRef<HTMLAudioElement>(null)
|
const audioRef = useRef<HTMLAudioElement>(null)
|
||||||
const [isPlaying, setIsPlaying] = useState(false)
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
const [currentTime, setCurrentTime] = useState(0)
|
const [currentTime, setCurrentTime] = useState(0)
|
||||||
@@ -19,11 +28,21 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
|||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
const seekTimeoutRef = useRef<NodeJS.Timeout>()
|
||||||
const isSeeking = useRef(false)
|
const isSeeking = useRef(false)
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const audio = audioRef.current
|
const audio = audioRef.current
|
||||||
if (!audio) return
|
if (!audio) return
|
||||||
|
|
||||||
|
if (startTime) {
|
||||||
|
setCurrentTime(startTime)
|
||||||
|
audio.currentTime = startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if (autoPlay) {
|
||||||
|
togglePlay()
|
||||||
|
}
|
||||||
|
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
if (!isSeeking.current) {
|
if (!isSeeking.current) {
|
||||||
setCurrentTime(audio.currentTime)
|
setCurrentTime(audio.currentTime)
|
||||||
@@ -49,6 +68,28 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current
|
||||||
|
const container = containerRef.current
|
||||||
|
|
||||||
|
if (!audio || !container) return
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
|
audio.pause()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(container)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(container)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const togglePlay = () => {
|
const togglePlay = () => {
|
||||||
const audio = audioRef.current
|
const audio = audioRef.current
|
||||||
if (!audio) return
|
if (!audio) return
|
||||||
@@ -86,8 +127,9 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center gap-3 py-2 pl-2 pr-4 border rounded-full max-w-md',
|
'flex items-center gap-3 py-2 px-2 border rounded-full max-w-md bg-background',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -114,6 +156,25 @@ export default function AudioPlayer({ src, className }: AudioPlayerProps) {
|
|||||||
<div className="text-sm font-mono text-muted-foreground">
|
<div className="text-sm font-mono text-muted-foreground">
|
||||||
{formatTime(Math.max(duration - currentTime, 0))}
|
{formatTime(Math.max(duration - currentTime, 0))}
|
||||||
</div>
|
</div>
|
||||||
|
{isMinimized ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full shrink-0 text-muted-foreground"
|
||||||
|
onClick={() => mediaManager.stopAudioBackground()}
|
||||||
|
>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="rounded-full shrink-0 text-muted-foreground"
|
||||||
|
onClick={() => mediaManager.playAudioBackground(src, audioRef.current?.currentTime || 0)}
|
||||||
|
>
|
||||||
|
<Minimize2 />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/components/BackgroundAudio/index.tsx
Normal file
46
src/components/BackgroundAudio/index.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import mediaManager from '@/services/media-manager.service'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import AudioPlayer from '../AudioPlayer'
|
||||||
|
|
||||||
|
export default function BackgroundAudio({ className }: { className?: string }) {
|
||||||
|
const [backgroundAudioSrc, setBackgroundAudioSrc] = useState<string | null>(null)
|
||||||
|
const [backgroundAudio, setBackgroundAudio] = useState<React.ReactNode>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handlePlayAudioBackground = (event: Event) => {
|
||||||
|
const { src, time } = (event as CustomEvent).detail
|
||||||
|
if (backgroundAudioSrc === src) return
|
||||||
|
|
||||||
|
setBackgroundAudio(
|
||||||
|
<FloatingAudioPlayer key={src + time} src={src} time={time} className={className} />
|
||||||
|
)
|
||||||
|
setBackgroundAudioSrc(src)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStopAudioBackground = () => {
|
||||||
|
setBackgroundAudio(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaManager.addEventListener('playAudioBackground', handlePlayAudioBackground)
|
||||||
|
mediaManager.addEventListener('stopAudioBackground', handleStopAudioBackground)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mediaManager.removeEventListener('playAudioBackground', handlePlayAudioBackground)
|
||||||
|
mediaManager.removeEventListener('stopAudioBackground', handleStopAudioBackground)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return backgroundAudio
|
||||||
|
}
|
||||||
|
|
||||||
|
function FloatingAudioPlayer({
|
||||||
|
src,
|
||||||
|
time,
|
||||||
|
className
|
||||||
|
}: {
|
||||||
|
src: string
|
||||||
|
time?: number
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
return <AudioPlayer src={src} className={className} startTime={time} autoPlay isMinimized />
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import BackgroundAudio from '../BackgroundAudio'
|
||||||
import AccountButton from './AccountButton'
|
import AccountButton from './AccountButton'
|
||||||
import ExploreButton from './ExploreButton'
|
import ExploreButton from './ExploreButton'
|
||||||
import HomeButton from './HomeButton'
|
import HomeButton from './HomeButton'
|
||||||
@@ -7,18 +8,18 @@ import NotificationsButton from './NotificationsButton'
|
|||||||
export default function BottomNavigationBar() {
|
export default function BottomNavigationBar() {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn('fixed bottom-0 w-full z-40 bg-background border-t')}
|
||||||
'fixed bottom-0 w-full z-40 bg-background border-t flex items-center justify-around [&_svg]:size-4 [&_svg]:shrink-0'
|
|
||||||
)}
|
|
||||||
style={{
|
style={{
|
||||||
height: 'calc(3rem + env(safe-area-inset-bottom))',
|
|
||||||
paddingBottom: 'env(safe-area-inset-bottom)'
|
paddingBottom: 'env(safe-area-inset-bottom)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<BackgroundAudio className="rounded-none border-x-0 border-t-0 border-b bg-background" />
|
||||||
|
<div className="w-full flex justify-around items-center [&_svg]:size-4 [&_svg]:shrink-0">
|
||||||
<HomeButton />
|
<HomeButton />
|
||||||
<ExploreButton />
|
<ExploreButton />
|
||||||
<NotificationsButton />
|
<NotificationsButton />
|
||||||
<AccountButton />
|
<AccountButton />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function MediaPlayer({
|
|||||||
const { autoLoadMedia } = useContentPolicy()
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
const [display, setDisplay] = useState(autoLoadMedia)
|
const [display, setDisplay] = useState(autoLoadMedia)
|
||||||
const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null)
|
const [mediaType, setMediaType] = useState<'video' | 'audio' | null>(null)
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoLoadMedia) {
|
if (autoLoadMedia) {
|
||||||
@@ -51,11 +52,12 @@ export default function MediaPlayer({
|
|||||||
video.crossOrigin = 'anonymous'
|
video.crossOrigin = 'anonymous'
|
||||||
|
|
||||||
video.onloadedmetadata = () => {
|
video.onloadedmetadata = () => {
|
||||||
|
setError(false)
|
||||||
setMediaType(video.videoWidth > 0 || video.videoHeight > 0 ? 'video' : 'audio')
|
setMediaType(video.videoWidth > 0 || video.videoHeight > 0 ? 'video' : 'audio')
|
||||||
}
|
}
|
||||||
|
|
||||||
video.onerror = () => {
|
video.onerror = () => {
|
||||||
setMediaType(null)
|
setError(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -63,6 +65,10 @@ export default function MediaPlayer({
|
|||||||
}
|
}
|
||||||
}, [src, display, mustLoad])
|
}, [src, display, mustLoad])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ExternalLink url={src} />
|
||||||
|
}
|
||||||
|
|
||||||
if (!mustLoad && !display) {
|
if (!mustLoad && !display) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -78,7 +84,7 @@ export default function MediaPlayer({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaType) {
|
if (!mediaType) {
|
||||||
return <ExternalLink url={src} />
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mediaType === 'video') {
|
if (mediaType === 'video') {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
import { SimpleUserAvatar } from '@/components/UserAvatar'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { hasBackgroundAudioAtom } from '@/services/media-manager.service'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { ArrowUp } from 'lucide-react'
|
import { ArrowUp } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
@@ -16,6 +18,7 @@ export default function NewNotesButton({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
|
const hasBackgroundAudio = useAtomValue(hasBackgroundAudioAtom)
|
||||||
const pubkeys = useMemo(() => {
|
const pubkeys = useMemo(() => {
|
||||||
const arr: string[] = []
|
const arr: string[] = []
|
||||||
for (const event of newEvents) {
|
for (const event of newEvents) {
|
||||||
@@ -33,9 +36,13 @@ export default function NewNotesButton({
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex justify-center z-40 pointer-events-none',
|
'w-full flex justify-center z-40 pointer-events-none',
|
||||||
isSmallScreen ? 'fixed' : 'absolute bottom-6'
|
isSmallScreen ? 'fixed' : 'absolute'
|
||||||
)}
|
)}
|
||||||
style={isSmallScreen ? { bottom: 'calc(4rem + env(safe-area-inset-bottom))' } : undefined}
|
style={{
|
||||||
|
bottom: isSmallScreen
|
||||||
|
? `calc(${hasBackgroundAudio ? 7.35 : 4}rem + env(safe-area-inset-bottom))`
|
||||||
|
: '1rem'
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
|
import { hasBackgroundAudioAtom } from '@/services/media-manager.service'
|
||||||
|
import { useAtomValue } from 'jotai'
|
||||||
import { ChevronUp } from 'lucide-react'
|
import { ChevronUp } from 'lucide-react'
|
||||||
|
|
||||||
export default function ScrollToTopButton({
|
export default function ScrollToTopButton({
|
||||||
@@ -13,6 +15,7 @@ export default function ScrollToTopButton({
|
|||||||
}) {
|
}) {
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
|
const { deepBrowsing, lastScrollTop } = useDeepBrowsing()
|
||||||
|
const hasBackgroundAudio = useAtomValue(hasBackgroundAudioAtom)
|
||||||
const visible = !deepBrowsing && lastScrollTop > 800
|
const visible = !deepBrowsing && lastScrollTop > 800
|
||||||
|
|
||||||
const handleScrollToTop = () => {
|
const handleScrollToTop = () => {
|
||||||
@@ -31,8 +34,8 @@ export default function ScrollToTopButton({
|
|||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
bottom: isSmallScreen
|
bottom: isSmallScreen
|
||||||
? 'calc(env(safe-area-inset-bottom) + 3.75rem)'
|
? `calc(env(safe-area-inset-bottom) + ${hasBackgroundAudio ? 7.25 : 3.85}rem)`
|
||||||
: 'calc(env(safe-area-inset-bottom) + 0.75rem)'
|
: `calc(env(safe-area-inset-bottom) + 0.85rem)`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,32 +1,34 @@
|
|||||||
import { cn, isInViewport } from '@/lib/utils'
|
import { cn, isInViewport } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
import mediaManager from '@/services/media-manager.service'
|
import mediaManager from '@/services/media-manager.service'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import ExternalLink from '../ExternalLink'
|
import ExternalLink from '../ExternalLink'
|
||||||
|
|
||||||
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
||||||
const { autoplay } = useContentPolicy()
|
const { autoplay } = useContentPolicy()
|
||||||
|
const { muteMedia, updateMuteMedia } = useUserPreferences()
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!autoplay) return
|
|
||||||
|
|
||||||
const video = videoRef.current
|
const video = videoRef.current
|
||||||
const container = containerRef.current
|
const container = containerRef.current
|
||||||
|
|
||||||
if (!video || !container) return
|
if (!video || !container || error) return
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
([entry]) => {
|
([entry]) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting && autoplay) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isInViewport(container)) {
|
if (isInViewport(container)) {
|
||||||
mediaManager.autoPlay(video)
|
mediaManager.autoPlay(video)
|
||||||
}
|
}
|
||||||
}, 200)
|
}, 200)
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (!entry.isIntersecting) {
|
||||||
mediaManager.pause(video)
|
mediaManager.pause(video)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -38,7 +40,34 @@ export default function VideoPlayer({ src, className }: { src: string; className
|
|||||||
return () => {
|
return () => {
|
||||||
observer.unobserve(container)
|
observer.unobserve(container)
|
||||||
}
|
}
|
||||||
}, [autoplay])
|
}, [autoplay, error])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
|
||||||
|
const video = videoRef.current
|
||||||
|
|
||||||
|
const handleVolumeChange = () => {
|
||||||
|
updateMuteMedia(video.muted)
|
||||||
|
}
|
||||||
|
|
||||||
|
video.addEventListener('volumechange', handleVolumeChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
video.removeEventListener('volumechange', handleVolumeChange)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video || video.muted === muteMedia) return
|
||||||
|
|
||||||
|
if (muteMedia) {
|
||||||
|
video.muted = true
|
||||||
|
} else {
|
||||||
|
video.muted = false
|
||||||
|
}
|
||||||
|
}, [muteMedia])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ExternalLink url={src} />
|
return <ExternalLink url={src} />
|
||||||
@@ -56,7 +85,7 @@ export default function VideoPlayer({ src, className }: { src: string; className
|
|||||||
onPlay={(event) => {
|
onPlay={(event) => {
|
||||||
mediaManager.play(event.currentTarget)
|
mediaManager.play(event.currentTarget)
|
||||||
}}
|
}}
|
||||||
muted
|
muted={muteMedia}
|
||||||
onError={() => setError(true)}
|
onError={() => setError(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
|
||||||
|
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
|
||||||
import mediaManager from '@/services/media-manager.service'
|
import mediaManager from '@/services/media-manager.service'
|
||||||
import { YouTubePlayer } from '@/types/youtube'
|
import { YouTubePlayer } from '@/types/youtube'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@@ -17,12 +18,15 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { autoLoadMedia } = useContentPolicy()
|
const { autoLoadMedia } = useContentPolicy()
|
||||||
|
const { muteMedia, updateMuteMedia } = useUserPreferences()
|
||||||
const [display, setDisplay] = useState(autoLoadMedia)
|
const [display, setDisplay] = useState(autoLoadMedia)
|
||||||
const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url])
|
const { videoId, isShort } = useMemo(() => parseYoutubeUrl(url), [url])
|
||||||
const [initSuccess, setInitSuccess] = useState(false)
|
const [initSuccess, setInitSuccess] = useState(false)
|
||||||
const [error, setError] = useState(false)
|
const [error, setError] = useState(false)
|
||||||
const playerRef = useRef<YouTubePlayer | null>(null)
|
const playerRef = useRef<YouTubePlayer | null>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const muteStateRef = useRef(muteMedia)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoLoadMedia) {
|
if (autoLoadMedia) {
|
||||||
@@ -47,24 +51,48 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
initPlayer()
|
initPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let checkMutedInterval: NodeJS.Timeout | null = null
|
||||||
function initPlayer() {
|
function initPlayer() {
|
||||||
try {
|
try {
|
||||||
if (!videoId || !containerRef.current || !window.YT.Player) return
|
if (!videoId || !containerRef.current || !window.YT.Player) return
|
||||||
|
|
||||||
|
let currentMuteState = muteStateRef.current
|
||||||
playerRef.current = new window.YT.Player(containerRef.current, {
|
playerRef.current = new window.YT.Player(containerRef.current, {
|
||||||
videoId: videoId,
|
videoId: videoId,
|
||||||
playerVars: {
|
playerVars: {
|
||||||
mute: 1
|
mute: currentMuteState ? 1 : 0
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
onStateChange: (event: any) => {
|
onStateChange: (event: any) => {
|
||||||
if (event.data === window.YT.PlayerState.PLAYING) {
|
if (event.data === window.YT.PlayerState.PLAYING) {
|
||||||
mediaManager.play(playerRef.current)
|
mediaManager.play(playerRef.current)
|
||||||
} else if (event.data === window.YT.PlayerState.PAUSED) {
|
} else if (
|
||||||
|
event.data === window.YT.PlayerState.PAUSED ||
|
||||||
|
event.data === window.YT.PlayerState.ENDED
|
||||||
|
) {
|
||||||
mediaManager.pause(playerRef.current)
|
mediaManager.pause(playerRef.current)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onReady: () => {
|
onReady: () => {
|
||||||
setInitSuccess(true)
|
setInitSuccess(true)
|
||||||
|
checkMutedInterval = setInterval(() => {
|
||||||
|
if (playerRef.current) {
|
||||||
|
const mute = playerRef.current.isMuted()
|
||||||
|
if (mute !== currentMuteState) {
|
||||||
|
currentMuteState = mute
|
||||||
|
|
||||||
|
if (mute !== muteStateRef.current) {
|
||||||
|
updateMuteMedia(currentMuteState)
|
||||||
|
}
|
||||||
|
} else if (muteStateRef.current !== mute) {
|
||||||
|
if (muteStateRef.current) {
|
||||||
|
playerRef.current.mute()
|
||||||
|
} else {
|
||||||
|
playerRef.current.unMute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200)
|
||||||
},
|
},
|
||||||
onError: () => setError(true)
|
onError: () => setError(true)
|
||||||
}
|
}
|
||||||
@@ -80,9 +108,46 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
if (playerRef.current) {
|
if (playerRef.current) {
|
||||||
playerRef.current.destroy()
|
playerRef.current.destroy()
|
||||||
}
|
}
|
||||||
|
if (checkMutedInterval) {
|
||||||
|
clearInterval(checkMutedInterval)
|
||||||
|
checkMutedInterval = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [videoId, display, mustLoad])
|
}, [videoId, display, mustLoad])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
muteStateRef.current = muteMedia
|
||||||
|
}, [muteMedia])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wrapper = wrapperRef.current
|
||||||
|
|
||||||
|
if (!wrapper || !initSuccess) return
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
const player = playerRef.current
|
||||||
|
if (!player) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
!entry.isIntersecting &&
|
||||||
|
[window.YT.PlayerState.PLAYING, window.YT.PlayerState.BUFFERING].includes(
|
||||||
|
player.getPlayerState()
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
mediaManager.pause(player)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 1 }
|
||||||
|
)
|
||||||
|
|
||||||
|
observer.observe(wrapper)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.unobserve(wrapper)
|
||||||
|
}
|
||||||
|
}, [videoId, display, mustLoad, initSuccess])
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ExternalLink url={url} />
|
return <ExternalLink url={url} />
|
||||||
}
|
}
|
||||||
@@ -104,8 +169,10 @@ export default function YoutubeEmbeddedPlayer({
|
|||||||
if (!videoId && !initSuccess) {
|
if (!videoId && !initSuccess) {
|
||||||
return <ExternalLink url={url} />
|
return <ExternalLink url={url} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg border overflow-hidden',
|
'rounded-lg border overflow-hidden',
|
||||||
isShort ? 'aspect-[9/16] max-h-[80vh] sm:max-h-[60vh]' : 'aspect-video max-h-[60vh]',
|
isShort ? 'aspect-[9/16] max-h-[80vh] sm:max-h-[60vh]' : 'aspect-video max-h-[60vh]',
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import { createContext, useContext, useState } from 'react'
|
|||||||
type TUserPreferencesContext = {
|
type TUserPreferencesContext = {
|
||||||
notificationListStyle: TNotificationStyle
|
notificationListStyle: TNotificationStyle
|
||||||
updateNotificationListStyle: (style: TNotificationStyle) => void
|
updateNotificationListStyle: (style: TNotificationStyle) => void
|
||||||
|
|
||||||
|
muteMedia: boolean
|
||||||
|
updateMuteMedia: (mute: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
|
||||||
@@ -21,6 +24,7 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
|||||||
const [notificationListStyle, setNotificationListStyle] = useState(
|
const [notificationListStyle, setNotificationListStyle] = useState(
|
||||||
storage.getNotificationListStyle()
|
storage.getNotificationListStyle()
|
||||||
)
|
)
|
||||||
|
const [muteMedia, setMuteMedia] = useState(true)
|
||||||
|
|
||||||
const updateNotificationListStyle = (style: TNotificationStyle) => {
|
const updateNotificationListStyle = (style: TNotificationStyle) => {
|
||||||
setNotificationListStyle(style)
|
setNotificationListStyle(style)
|
||||||
@@ -31,7 +35,9 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
|
|||||||
<UserPreferencesContext.Provider
|
<UserPreferencesContext.Provider
|
||||||
value={{
|
value={{
|
||||||
notificationListStyle,
|
notificationListStyle,
|
||||||
updateNotificationListStyle
|
updateNotificationListStyle,
|
||||||
|
muteMedia,
|
||||||
|
updateMuteMedia: setMuteMedia
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
import { YouTubePlayer } from '@/types/youtube'
|
import { YouTubePlayer } from '@/types/youtube'
|
||||||
|
import { atom, getDefaultStore } from 'jotai'
|
||||||
|
|
||||||
|
export const hasBackgroundAudioAtom = atom(false)
|
||||||
|
const store = getDefaultStore()
|
||||||
|
|
||||||
type Media = HTMLMediaElement | YouTubePlayer
|
type Media = HTMLMediaElement | YouTubePlayer
|
||||||
|
|
||||||
class MediaManagerService {
|
class MediaManagerService extends EventTarget {
|
||||||
static instance: MediaManagerService
|
static instance: MediaManagerService
|
||||||
|
|
||||||
private currentMedia: Media | null = null
|
private currentMedia: Media | null = null
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): MediaManagerService {
|
||||||
if (!MediaManagerService.instance) {
|
if (!MediaManagerService.instance) {
|
||||||
MediaManagerService.instance = this
|
MediaManagerService.instance = new MediaManagerService()
|
||||||
}
|
}
|
||||||
return MediaManagerService.instance
|
return MediaManagerService.instance
|
||||||
}
|
}
|
||||||
@@ -24,7 +32,7 @@ class MediaManagerService {
|
|||||||
if (this.currentMedia === media) {
|
if (this.currentMedia === media) {
|
||||||
this.currentMedia = null
|
this.currentMedia = null
|
||||||
}
|
}
|
||||||
pause(media)
|
_pause(media)
|
||||||
}
|
}
|
||||||
|
|
||||||
autoPlay(media: Media) {
|
autoPlay(media: Media) {
|
||||||
@@ -34,6 +42,13 @@ class MediaManagerService {
|
|||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
store.get(hasBackgroundAudioAtom) &&
|
||||||
|
this.currentMedia &&
|
||||||
|
isMediaPlaying(this.currentMedia)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.play(media)
|
this.play(media)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,21 +60,31 @@ class MediaManagerService {
|
|||||||
;(document.pictureInPictureElement as HTMLMediaElement).pause()
|
;(document.pictureInPictureElement as HTMLMediaElement).pause()
|
||||||
}
|
}
|
||||||
if (this.currentMedia && this.currentMedia !== media) {
|
if (this.currentMedia && this.currentMedia !== media) {
|
||||||
pause(this.currentMedia)
|
_pause(this.currentMedia)
|
||||||
}
|
}
|
||||||
this.currentMedia = media
|
this.currentMedia = media
|
||||||
if (isMediaPlaying(media)) {
|
if (isMediaPlaying(media)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
play(this.currentMedia).catch((error) => {
|
_play(this.currentMedia).catch((error) => {
|
||||||
console.error('Error playing media:', error)
|
console.error('Error playing media:', error)
|
||||||
this.currentMedia = null
|
this.currentMedia = null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playAudioBackground(src: string, time: number = 0) {
|
||||||
|
this.dispatchEvent(new CustomEvent('playAudioBackground', { detail: { src, time } }))
|
||||||
|
store.set(hasBackgroundAudioAtom, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const instance = new MediaManagerService()
|
stopAudioBackground() {
|
||||||
|
this.dispatchEvent(new Event('stopAudioBackground'))
|
||||||
|
store.set(hasBackgroundAudioAtom, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = MediaManagerService.getInstance()
|
||||||
export default instance
|
export default instance
|
||||||
|
|
||||||
function isYouTubePlayer(media: Media): media is YouTubePlayer {
|
function isYouTubePlayer(media: Media): media is YouTubePlayer {
|
||||||
@@ -83,14 +108,14 @@ function isPipElement(media: Media) {
|
|||||||
return (media as any).webkitPresentationMode === 'picture-in-picture'
|
return (media as any).webkitPresentationMode === 'picture-in-picture'
|
||||||
}
|
}
|
||||||
|
|
||||||
function pause(media: Media) {
|
function _pause(media: Media) {
|
||||||
if (isYouTubePlayer(media)) {
|
if (isYouTubePlayer(media)) {
|
||||||
return media.pauseVideo()
|
return media.pauseVideo()
|
||||||
}
|
}
|
||||||
return media.pause()
|
return media.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function play(media: Media) {
|
async function _play(media: Media) {
|
||||||
if (isYouTubePlayer(media)) {
|
if (isYouTubePlayer(media)) {
|
||||||
return media.playVideo()
|
return media.playVideo()
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/types/youtube.d.ts
vendored
3
src/types/youtube.d.ts
vendored
@@ -41,6 +41,9 @@ export interface YouTubePlayer {
|
|||||||
getCurrentTime(): number
|
getCurrentTime(): number
|
||||||
getDuration(): number
|
getDuration(): number
|
||||||
getPlayerState(): number
|
getPlayerState(): number
|
||||||
|
isMuted(): boolean
|
||||||
|
mute(): void
|
||||||
|
unMute(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
Reference in New Issue
Block a user