better sound support and fix settings
This commit is contained in:
parent
f52cb54008
commit
0447fc5d61
|
@ -31,10 +31,10 @@ import useJupiterMints from '../../hooks/useJupiterMints'
|
||||||
import { RouteInfo } from 'types/jupiter'
|
import { RouteInfo } from 'types/jupiter'
|
||||||
import useJupiterSwapData from './useJupiterSwapData'
|
import useJupiterSwapData from './useJupiterSwapData'
|
||||||
import { Transaction } from '@solana/web3.js'
|
import { Transaction } from '@solana/web3.js'
|
||||||
import useAudio from 'hooks/useAudio'
|
|
||||||
import { SOUND_SETTINGS_KEY } from 'utils/constants'
|
import { SOUND_SETTINGS_KEY } from 'utils/constants'
|
||||||
import { INITIAL_SOUND_SETTINGS } from 'pages/settings'
|
import { INITIAL_SOUND_SETTINGS } from 'pages/settings'
|
||||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||||
|
import { Howl } from 'howler'
|
||||||
|
|
||||||
type JupiterRouteInfoProps = {
|
type JupiterRouteInfoProps = {
|
||||||
amountIn: Decimal
|
amountIn: Decimal
|
||||||
|
@ -109,7 +109,10 @@ const JupiterRouteInfo = ({
|
||||||
const { mangoTokens } = useJupiterMints()
|
const { mangoTokens } = useJupiterMints()
|
||||||
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
|
const { inputTokenInfo, outputTokenInfo } = useJupiterSwapData()
|
||||||
const inputBank = mangoStore((s) => s.swap.inputBank)
|
const inputBank = mangoStore((s) => s.swap.inputBank)
|
||||||
const { play } = useAudio('/sounds/swap-success.mp3')
|
const successSound = new Howl({
|
||||||
|
src: ['/sounds/swap-success.mp3'],
|
||||||
|
volume: 0.2,
|
||||||
|
})
|
||||||
const [soundSettings] = useLocalStorageState(
|
const [soundSettings] = useLocalStorageState(
|
||||||
SOUND_SETTINGS_KEY,
|
SOUND_SETTINGS_KEY,
|
||||||
INITIAL_SOUND_SETTINGS
|
INITIAL_SOUND_SETTINGS
|
||||||
|
@ -185,13 +188,14 @@ const JupiterRouteInfo = ({
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.swap.success = true
|
s.swap.success = true
|
||||||
})
|
})
|
||||||
if (soundSettings['swap-success'].active) {
|
if (soundSettings['swap-success']) {
|
||||||
play()
|
successSound.play()
|
||||||
}
|
}
|
||||||
notify({
|
notify({
|
||||||
title: 'Transaction confirmed',
|
title: 'Transaction confirmed',
|
||||||
type: 'success',
|
type: 'success',
|
||||||
txid: tx,
|
txid: tx,
|
||||||
|
noSound: true,
|
||||||
})
|
})
|
||||||
actions.fetchGroup()
|
actions.fetchGroup()
|
||||||
await actions.reloadMangoAccount()
|
await actions.reloadMangoAccount()
|
||||||
|
|
|
@ -34,9 +34,7 @@ const SwapSuccessParticles = () => {
|
||||||
}
|
}
|
||||||
}, [showSwapAnimation])
|
}, [showSwapAnimation])
|
||||||
|
|
||||||
return animationSettings['swap-success'].active &&
|
return animationSettings['swap-success'] && showSwapAnimation && tokenLogo ? (
|
||||||
showSwapAnimation &&
|
|
||||||
tokenLogo ? (
|
|
||||||
<Particles
|
<Particles
|
||||||
id="tsparticles"
|
id="tsparticles"
|
||||||
options={{
|
options={{
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
const useAudio = (url: string) => {
|
|
||||||
const [audio] = useState(new Audio(url))
|
|
||||||
const [playing, setPlaying] = useState(false)
|
|
||||||
|
|
||||||
const play = () => setPlaying(true)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
playing ? audio.play() : audio.pause()
|
|
||||||
}, [playing])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
audio.addEventListener('ended', () => setPlaying(false))
|
|
||||||
return () => {
|
|
||||||
audio.removeEventListener('ended', () => setPlaying(false))
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return { playing, play }
|
|
||||||
}
|
|
||||||
|
|
||||||
export default useAudio
|
|
|
@ -22,12 +22,14 @@
|
||||||
"@solflare-wallet/pfp": "^0.0.6",
|
"@solflare-wallet/pfp": "^0.0.6",
|
||||||
"@tanstack/react-query": "^4.10.1",
|
"@tanstack/react-query": "^4.10.1",
|
||||||
"@tippyjs/react": "^4.2.6",
|
"@tippyjs/react": "^4.2.6",
|
||||||
|
"@types/howler": "^2.2.7",
|
||||||
"@types/lodash": "^4.14.185",
|
"@types/lodash": "^4.14.185",
|
||||||
"assert": "^2.0.0",
|
"assert": "^2.0.0",
|
||||||
"big.js": "^6.2.1",
|
"big.js": "^6.2.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.29.3",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"decimal.js": "^10.4.0",
|
"decimal.js": "^10.4.0",
|
||||||
|
"howler": "^2.2.3",
|
||||||
"html-react-parser": "^3.0.4",
|
"html-react-parser": "^3.0.4",
|
||||||
"immer": "^9.0.12",
|
"immer": "^9.0.12",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {
|
||||||
SOUND_SETTINGS_KEY,
|
SOUND_SETTINGS_KEY,
|
||||||
} from 'utils/constants'
|
} from 'utils/constants'
|
||||||
import Switch from '@components/forms/Switch'
|
import Switch from '@components/forms/Switch'
|
||||||
import { useCallback, useEffect, useMemo, useReducer } from 'react'
|
import { useCallback, useMemo, useReducer } from 'react'
|
||||||
import { CheckCircleIcon } from '@heroicons/react/20/solid'
|
import { CheckCircleIcon } from '@heroicons/react/20/solid'
|
||||||
import Image from 'next/legacy/image'
|
import Image from 'next/legacy/image'
|
||||||
|
|
||||||
|
@ -63,28 +63,22 @@ const NOTIFICATION_POSITIONS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
interface ReducerItems {
|
interface ReducerItems {
|
||||||
[key: string]: {
|
[key: string]: boolean
|
||||||
active: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INITIAL_ANIMATION_SETTINGS = {
|
export const INITIAL_ANIMATION_SETTINGS = {
|
||||||
'orderbook-flash': {
|
'orderbook-flash': true,
|
||||||
active: true,
|
'swap-success': true,
|
||||||
},
|
|
||||||
'swap-success': {
|
|
||||||
active: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const INITIAL_SOUND_SETTINGS = {
|
export const INITIAL_SOUND_SETTINGS = {
|
||||||
'swap-success': {
|
'swap-success': true,
|
||||||
active: true,
|
'transaction-success': true,
|
||||||
},
|
'transaction-fail': true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsReducer = (state: ReducerItems, name: string) => {
|
const settingsReducer = (state: ReducerItems, name: string) => {
|
||||||
const updatedState = { ...state, [name]: { active: !state[name].active } }
|
const updatedState = { ...state, [name]: !state[name] }
|
||||||
return updatedState
|
return updatedState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,30 +103,35 @@ const Settings: NextPage = () => {
|
||||||
const themes = useMemo(() => {
|
const themes = useMemo(() => {
|
||||||
return [t('settings:light'), t('settings:mango'), t('settings:dark')]
|
return [t('settings:light'), t('settings:mango'), t('settings:dark')]
|
||||||
}, [t])
|
}, [t])
|
||||||
const [sounds, soundsDispatch] = useReducer(
|
const [, soundsDispatch] = useReducer(settingsReducer, INITIAL_SOUND_SETTINGS)
|
||||||
settingsReducer,
|
const [soundSettings, setSoundSettings] = useLocalStorageState(
|
||||||
INITIAL_SOUND_SETTINGS
|
|
||||||
)
|
|
||||||
const [, setSoundSettings] = useLocalStorageState(
|
|
||||||
SOUND_SETTINGS_KEY,
|
SOUND_SETTINGS_KEY,
|
||||||
INITIAL_SOUND_SETTINGS
|
INITIAL_SOUND_SETTINGS
|
||||||
)
|
)
|
||||||
const [animations, animationsDispatch] = useReducer(
|
const [, animationsDispatch] = useReducer(
|
||||||
settingsReducer,
|
settingsReducer,
|
||||||
INITIAL_ANIMATION_SETTINGS
|
INITIAL_ANIMATION_SETTINGS
|
||||||
)
|
)
|
||||||
const [, setAnimationSettings] = useLocalStorageState(
|
const [animationSettings, setAnimationSettings] = useLocalStorageState(
|
||||||
ANIMATION_SETTINGS_KEY,
|
ANIMATION_SETTINGS_KEY,
|
||||||
INITIAL_ANIMATION_SETTINGS
|
INITIAL_ANIMATION_SETTINGS
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
const handleToggleAnimationSetting = (settingName: string) => {
|
||||||
setAnimationSettings(animations)
|
animationsDispatch(settingName)
|
||||||
}, [animations])
|
setAnimationSettings({
|
||||||
|
...animationSettings,
|
||||||
|
[settingName]: !animationSettings[settingName],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
const handleToggleSoundSetting = (settingName: string) => {
|
||||||
setSoundSettings(sounds)
|
soundsDispatch(settingName)
|
||||||
}, [sounds])
|
setSoundSettings({
|
||||||
|
...soundSettings,
|
||||||
|
[settingName]: !soundSettings[settingName],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleLangChange = useCallback(
|
const handleLangChange = useCallback(
|
||||||
(l: string) => {
|
(l: string) => {
|
||||||
|
@ -206,25 +205,39 @@ const Settings: NextPage = () => {
|
||||||
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
||||||
<p className="mb-2 lg:mb-0">{t('settings:orderbook-flash')}</p>
|
<p className="mb-2 lg:mb-0">{t('settings:orderbook-flash')}</p>
|
||||||
<Switch
|
<Switch
|
||||||
checked={animations['orderbook-flash'].active}
|
checked={animationSettings['orderbook-flash']}
|
||||||
onChange={() => animationsDispatch('orderbook-flash')}
|
onChange={() => handleToggleAnimationSetting('orderbook-flash')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
||||||
<p className="mb-2 lg:mb-0">{t('settings:swap-success')}</p>
|
<p className="mb-2 lg:mb-0">{t('settings:swap-success')}</p>
|
||||||
<Switch
|
<Switch
|
||||||
checked={animations['swap-success'].active}
|
checked={animationSettings['swap-success']}
|
||||||
onChange={() => animationsDispatch('swap-success')}
|
onChange={() => handleToggleAnimationSetting('swap-success')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-8 lg:col-start-3">
|
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-8 lg:col-start-3">
|
||||||
<h2 className="mb-4 text-base">{t('settings:sounds')}</h2>
|
<h2 className="mb-4 text-base">{t('settings:sounds')}</h2>
|
||||||
|
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
||||||
|
<p className="mb-2 lg:mb-0">{t('settings:transaction-success')}</p>
|
||||||
|
<Switch
|
||||||
|
checked={soundSettings['transaction-success']}
|
||||||
|
onChange={() => handleToggleSoundSetting('transaction-success')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
||||||
|
<p className="mb-2 lg:mb-0">{t('settings:transaction-fail')}</p>
|
||||||
|
<Switch
|
||||||
|
checked={soundSettings['transaction-fail']}
|
||||||
|
onChange={() => handleToggleSoundSetting('transaction-fail')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
<div className="flex items-center justify-between border-t border-th-bkg-3 py-4 md:px-4">
|
||||||
<p className="mb-2 lg:mb-0">{t('settings:swap-success')}</p>
|
<p className="mb-2 lg:mb-0">{t('settings:swap-success')}</p>
|
||||||
<Switch
|
<Switch
|
||||||
checked={sounds['swap-success'].active}
|
checked={soundSettings['swap-success']}
|
||||||
onChange={() => soundsDispatch('swap-success')}
|
onChange={() => handleToggleSoundSetting('swap-success')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,8 +23,10 @@
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"swap-success": "Swap Success",
|
"swap-success": "Swap Success",
|
||||||
|
"swap-trade-size-selector": "Swap/Trade Size Selector",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"top-left": "Top-Left",
|
"top-left": "Top-Left",
|
||||||
"top-right": "Top-Right",
|
"top-right": "Top-Right",
|
||||||
"swap-trade-size-selector": "Swap/Trade Size Selector"
|
"transaction-fail": "Transaction Fail",
|
||||||
|
"transaction-success": "Transaction Success"
|
||||||
}
|
}
|
|
@ -23,8 +23,10 @@
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"swap-success": "Swap Success",
|
"swap-success": "Swap Success",
|
||||||
|
"swap-trade-size-selector": "Swap/Trade Size Selector",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"top-left": "Top-Left",
|
"top-left": "Top-Left",
|
||||||
"top-right": "Top-Right",
|
"top-right": "Top-Right",
|
||||||
"swap-trade-size-selector": "Swap/Trade Size Selector"
|
"transaction-fail": "Transaction Fail",
|
||||||
|
"transaction-success": "Transaction Success"
|
||||||
}
|
}
|
|
@ -23,8 +23,10 @@
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"swap-success": "Swap Success",
|
"swap-success": "Swap Success",
|
||||||
|
"swap-trade-size-selector": "Swap/Trade Size Selector",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"top-left": "Top-Left",
|
"top-left": "Top-Left",
|
||||||
"top-right": "Top-Right",
|
"top-right": "Top-Right",
|
||||||
"swap-trade-size-selector": "Swap/Trade Size Selector"
|
"transaction-fail": "Transaction Fail",
|
||||||
|
"transaction-success": "Transaction Success"
|
||||||
}
|
}
|
|
@ -23,8 +23,10 @@
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"swap-success": "Swap Success",
|
"swap-success": "Swap Success",
|
||||||
|
"swap-trade-size-selector": "Swap/Trade Size Selector",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"top-left": "Top-Left",
|
"top-left": "Top-Left",
|
||||||
"top-right": "Top-Right",
|
"top-right": "Top-Right",
|
||||||
"swap-trade-size-selector": "Swap/Trade Size Selector"
|
"transaction-fail": "Transaction Fail",
|
||||||
|
"transaction-success": "Transaction Success"
|
||||||
}
|
}
|
|
@ -23,8 +23,10 @@
|
||||||
"sounds": "Sounds",
|
"sounds": "Sounds",
|
||||||
"spanish": "Español",
|
"spanish": "Español",
|
||||||
"swap-success": "Swap Success",
|
"swap-success": "Swap Success",
|
||||||
|
"swap-trade-size-selector": "Swap/Trade Size Selector",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"top-left": "Top-Left",
|
"top-left": "Top-Left",
|
||||||
"top-right": "Top-Right",
|
"top-right": "Top-Right",
|
||||||
"swap-trade-size-selector": "Swap/Trade Size Selector"
|
"transaction-fail": "Transaction Fail",
|
||||||
|
"transaction-success": "Transaction Success"
|
||||||
}
|
}
|
Binary file not shown.
Binary file not shown.
|
@ -1,4 +1,7 @@
|
||||||
import mangoStore from '@store/mangoStore'
|
import mangoStore from '@store/mangoStore'
|
||||||
|
import { Howl } from 'howler'
|
||||||
|
import { INITIAL_SOUND_SETTINGS } from 'pages/settings'
|
||||||
|
import { SOUND_SETTINGS_KEY } from './constants'
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
type: 'success' | 'info' | 'error' | 'confirm'
|
type: 'success' | 'info' | 'error' | 'confirm'
|
||||||
|
@ -9,16 +12,51 @@ export type Notification = {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReducerItems {
|
||||||
|
[key: string]: {
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function notify(newNotification: {
|
export function notify(newNotification: {
|
||||||
type?: 'success' | 'info' | 'error' | 'confirm'
|
type?: 'success' | 'info' | 'error' | 'confirm'
|
||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
txid?: string
|
txid?: string
|
||||||
|
noSound?: boolean
|
||||||
}) {
|
}) {
|
||||||
const setMangoStore = mangoStore.getState().set
|
const setMangoStore = mangoStore.getState().set
|
||||||
const notifications = mangoStore.getState().notifications
|
const notifications = mangoStore.getState().notifications
|
||||||
const lastId = mangoStore.getState().notificationIdCounter
|
const lastId = mangoStore.getState().notificationIdCounter
|
||||||
const newId = lastId + 1
|
const newId = lastId + 1
|
||||||
|
const successSound = new Howl({
|
||||||
|
src: ['/sounds/transaction-success.mp3'],
|
||||||
|
volume: 0.5,
|
||||||
|
})
|
||||||
|
const failSound = new Howl({
|
||||||
|
src: ['/sounds/transaction-fail.mp3'],
|
||||||
|
volume: 0.2,
|
||||||
|
})
|
||||||
|
const savedSoundSettings = localStorage.getItem(SOUND_SETTINGS_KEY)
|
||||||
|
const soundSettings = savedSoundSettings
|
||||||
|
? JSON.parse(savedSoundSettings)
|
||||||
|
: INITIAL_SOUND_SETTINGS
|
||||||
|
|
||||||
|
if (newNotification.type && !newNotification.noSound) {
|
||||||
|
switch (newNotification.type) {
|
||||||
|
case 'success': {
|
||||||
|
if (soundSettings['transaction-success']) {
|
||||||
|
successSound.play()
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'error': {
|
||||||
|
if (soundSettings['transaction-fail']) {
|
||||||
|
failSound.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newNotif: Notification = {
|
const newNotif: Notification = {
|
||||||
id: newId,
|
id: newId,
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -1516,6 +1516,11 @@
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
|
"@types/howler@^2.2.7":
|
||||||
|
version "2.2.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/howler/-/howler-2.2.7.tgz#5acfbed57f9e1d99b8dabe1b824729e1c1ea1fae"
|
||||||
|
integrity sha512-PEZldwZqJJw1PWRTpupyC7ajVTZA8aHd8nB/Y0n6zRZi5u8ktYDntsHj13ltEiBRqWwF06pASxBEvCTxniG8eA==
|
||||||
|
|
||||||
"@types/json-schema@^7.0.9":
|
"@types/json-schema@^7.0.9":
|
||||||
version "7.0.11"
|
version "7.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||||
|
@ -3910,6 +3915,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is "^16.7.0"
|
react-is "^16.7.0"
|
||||||
|
|
||||||
|
howler@^2.2.3:
|
||||||
|
version "2.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da"
|
||||||
|
integrity sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==
|
||||||
|
|
||||||
html-dom-parser@3.1.2:
|
html-dom-parser@3.1.2:
|
||||||
version "3.1.2"
|
version "3.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-3.1.2.tgz#c137c42df80e17d185ff35a806925d96cc73f408"
|
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-3.1.2.tgz#c137c42df80e17d185ff35a806925d96cc73f408"
|
||||||
|
|
Loading…
Reference in New Issue