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