Merge branch 'master' into refactor/default-config

merge from master
This commit is contained in:
KaiyiWing
2023-05-13 11:51:57 +08:00
23 changed files with 153 additions and 34 deletions

View File

@@ -752,145 +752,145 @@
{
"name": "math",
"trans": [
"Income is the money a person or organization earns through work or investments."
"A subject that deals with numbers, quantities, and shapes through various operations and formulas."
]
},
{
"name": "moment",
"trans": [
"Marriage is a legally recognized union between two people, typically involving commitment, companionship, and mutual support."
"A brief period of time that has significance or importance."
]
},
{
"name": "painting",
"trans": [
"A user is a person or entity that utilizes a particular product or service."
"An artistic creation made using paint on a surface, typically to convey an image or idea."
]
},
{
"name": "politics",
"trans": [
"A combination is a mixture or blend of two or more things or people."
"The activities associated with governing a country or area, including decision-making and the formulation of laws and policies."
]
},
{
"name": "attention",
"trans": [
"Failure is the lack of success in achieving a desired goal or objective."
"The act of focusing one's mental faculties on a particular object, task, or person."
]
},
{
"name": "decision",
"trans": [
"Meaning is the significance or purpose behind something or someone."
"The act of making a choice or coming to a conclusion after considering options and information."
]
},
{
"name": "event",
"trans": [
"Medicine is the science and practice of diagnosing, treating, and preventing illness or injury."
"Something that happens, especially something notable or significant."
]
},
{
"name": "property",
"trans": [
"Philosophy is the study of fundamental questions about existence, reality, knowledge, values, reason, and ethics."
"A thing or things that someone owns, typically a physical object or piece of land."
]
},
{
"name": "shopping",
"trans": [
"A teacher is a person who instructs or educates others, typically in a school or academic setting."
"The activity of browsing and purchasing goods, typically in a retail store or online."
]
},
{
"name": "student",
"trans": [
"Communication is the exchange of information or ideas between people or groups."
"A person who is studying or attending an educational institution."
]
},
{
"name": "wood",
"trans": [
"Night is the period of darkness between sunset and sunrise."
"A hard fibrous material that makes up the stems and branches of trees, used for building, fuel, and various other purposes."
]
},
{
"name": "competition",
"trans": [
"Chemistry is the scientific study of the properties, composition, and behavior of matter."
"A contest between individuals or groups to determine a winner, typically in a sport or game."
]
},
{
"name": "distribution",
"trans": [
"A disease is a disorder of the body or mind that causes specific symptoms or affects certain organs or systems."
"The act of sharing or delivering something to various people or places."
]
},
{
"name": "entertainment",
"trans": [
"A disk is a flat, circular object or storage device that can store or read digital information."
"Activities or performances that provide amusement or enjoyment for an audience."
]
},
{
"name": "office",
"trans": [
"Energy is the ability to do work, produce light or heat, or cause movement."
"A place where administrative or professional work is done, typically in a building with rooms for offices and meeting spaces."
]
},
{
"name": "population",
"trans": [
"A nation is a large group of people who share a common language, culture, history, or government."
"The group of individuals living in a particular area or region."
]
},
{
"name": "president",
"trans": [
"A road is a paved or unpaved pathway used for transportation, typically by vehicles or pedestrians."
"The elected or appointed head of a country or organization."
]
},
{
"name": "unit",
"trans": [
"A role is the function or position someone or something plays in a particular situation or setting."
"A single entity or component of a larger group or system."
]
},
{
"name": "category",
"trans": [
"Soup is a liquid dish typically made by boiling vegetables, meat, or fish in water or stock."
"A class or division of things that share common characteristics or features."
]
},
{
"name": "cigarette",
"trans": [
"Advertising is the practice of promoting or selling products or services through various media channels."
"A small roll of finely cut tobacco wrapped in paper, typically smoked."
]
},
{
"name": "context",
"trans": [
"A location is a specific place or position in space, typically with a physical address or geographical coordinates."
"The circumstances or conditions in which something occurs or exists, influencing the meaning or interpretation of it."
]
},
{
"name": "introduction",
"trans": [
"Success is the achievement of a desired goal or objective."
"The act of presenting or making something known to others for the first time."
]
},
{
"name": "opportunity",
"trans": [
"Addition is the act or process of adding something to something else."
"A chance or possibility for advancement or progress."
]
},
{
"name": "performance",
"trans": [
"An apartment is a self-contained living space within a larger building, typically rented out to tenants."
"The act of carrying out a task, duty, or function, often in a public setting such as a theater or sports arena."
]
},
{

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,13 +1,15 @@
import { SOUND_URL_PREFIX } from '@/resources/soundResource'
import { SOUND_URL_PREFIX, KEY_SOUND_URL_PREFIX, keySoundResources } from '@/resources/soundResource'
import { keySoundsConfigAtom, hintSoundsConfigAtom } from '@/store'
import noop from '@/utils/noop'
import { useAtomValue } from 'jotai'
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect, useState } from 'react'
import useSound from 'use-sound'
export type PlayFunction = ReturnType<typeof useSound>[0]
export default function useKeySound(): [PlayFunction, PlayFunction, PlayFunction] {
const { isOpen: isKeyOpen, isOpenClickSound, volume: keyVolume, resource: keyResource } = useAtomValue(keySoundsConfigAtom)
const setKeySoundsConfig = useSetAtom(keySoundsConfigAtom)
const {
isOpen: isHintOpen,
isOpenWrongSound,
@@ -16,8 +18,18 @@ export default function useKeySound(): [PlayFunction, PlayFunction, PlayFunction
wrongResource,
correctResource,
} = useAtomValue(hintSoundsConfigAtom)
const [keySoundUrl, setKeySoundUrl] = useState(`${KEY_SOUND_URL_PREFIX}${keyResource.filename}`)
const [playClickSound] = useSound(`${SOUND_URL_PREFIX}${keyResource.filename}`, {
useEffect(() => {
if (!keySoundResources.some((item) => item.filename === keyResource.filename && item.key === keyResource.key)) {
const defaultKeySoundResource = keySoundResources.find((item) => item.key === 'Default') || keySoundResources[0]
setKeySoundUrl(`${KEY_SOUND_URL_PREFIX}${defaultKeySoundResource.filename}`)
setKeySoundsConfig((prev) => ({ ...prev, resource: defaultKeySoundResource }))
}
}, [keyResource, setKeySoundsConfig])
const [playClickSound] = useSound(keySoundUrl, {
volume: keyVolume,
interrupt: true,
})
@@ -30,8 +42,6 @@ export default function useKeySound(): [PlayFunction, PlayFunction, PlayFunction
interrupt: true,
})
// todo: add volume control
return [
isKeyOpen && isOpenClickSound ? playClickSound : noop,
isHintOpen && isOpenWrongSound ? playWrongSound : noop,

View File

@@ -182,7 +182,7 @@ const PronunciationSwitcher = () => {
<>
<span>{item.name}</span>
{selected ? (
<span className="listbox-options-icon ">
<span className="listbox-options-icon">
<IconCheck className="focus:outline-none" />
</span>
) : null}

View File

@@ -1,10 +1,18 @@
import styles from './index.module.css'
import { keySoundResources } from '@/resources/soundResource'
import { hintSoundsConfigAtom, keySoundsConfigAtom, pronunciationConfigAtom } from '@/store'
import { SoundResource } from '@/typings'
import { toFixedNumber } from '@/utils'
import { Switch } from '@headlessui/react'
import { playKeySoundResource } from '@/utils/sounds/keySounds'
import { Switch, Transition } from '@headlessui/react'
import { Listbox } from '@headlessui/react'
import * as Slider from '@radix-ui/react-slider'
import { useAtom } from 'jotai'
import { useCallback } from 'react'
import { Fragment } from 'react'
import IconCheck from '~icons/tabler/check'
import IconChevronDown from '~icons/tabler/chevron-down'
import IconEar from '~icons/tabler/ear'
export default function SoundSetting() {
const [pronunciationConfig, setPronunciationConfig] = useAtom(pronunciationConfigAtom)
@@ -29,7 +37,6 @@ export default function SoundSetting() {
},
[setPronunciationConfig],
)
const onChangePronunciationRate = useCallback(
(value: [number]) => {
setPronunciationConfig((prev) => ({
@@ -59,6 +66,23 @@ export default function SoundSetting() {
[setKeySoundsConfig],
)
const onChangeKeySoundsResource = useCallback(
(key: string) => {
const soundResource = keySoundResources.find((item: SoundResource) => item.key === key) as SoundResource
if (!soundResource) return
setKeySoundsConfig((prev) => ({
...prev,
resource: soundResource,
}))
},
[setKeySoundsConfig],
)
const onPlayKeySound = useCallback((soundResource: SoundResource) => {
playKeySoundResource(soundResource)
}, [])
const onToggleHintSounds = useCallback(
(checked: boolean) => {
setHintSoundsConfig((prev) => ({
@@ -162,6 +186,46 @@ export default function SoundSetting() {
<span className="ml-4 w-10 text-xs font-normal text-gray-600">{`${Math.floor(keySoundsConfig.volume * 100)}%`}</span>
</div>
</div>
<div className={`${styles.block}`}>
<span className={styles.blockLabel}></span>
<Listbox value={keySoundsConfig.resource.key} onChange={onChangeKeySoundsResource}>
<div className="relative">
<Listbox.Button className="listbox-button w-60">
<span>{keySoundsConfig.resource.name}</span>
<span>
<IconChevronDown className="focus:outline-none" />
</span>
</Listbox.Button>
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
<Listbox.Options className="listbox-options z-10">
{keySoundResources.map((keySoundResource) => (
<Listbox.Option key={keySoundResource.key} value={keySoundResource.key}>
{({ selected }) => (
<>
<div className="group flex cursor-pointer items-center justify-between">
<span>{keySoundResource.name}</span>
{selected ? (
<span className="listbox-options-icon">
<IconCheck className="focus:outline-none" />
</span>
) : null}
<IconEar
onClick={(e) => {
e.stopPropagation()
onPlayKeySound(keySoundResource)
}}
className="mr-2 hidden cursor-pointer text-neutral-500 hover:text-indigo-400 group-hover:block dark:text-neutral-300"
/>
</div>
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
</div>
<div className={styles.section}>

View File

@@ -105,7 +105,11 @@ export default function Word({ word, onFinish }: { word: string; onFinish: () =>
const inputChar = wordState.inputWord[inputLength - 1]
const correctChar = wordState.displayWord[inputLength - 1]
const isEqual = isIgnoreCase ? inputChar.toLowerCase() === correctChar.toLowerCase() : inputChar === correctChar
let isEqual = false
if (inputChar != undefined && correctChar != undefined) {
isEqual = isIgnoreCase ? inputChar.toLowerCase() === correctChar.toLowerCase() : inputChar === correctChar
}
if (isEqual) {
// 输入正确时

View File

@@ -1,9 +1,37 @@
import { SoundResource, LanguagePronunciationMap } from '@/typings'
export const SOUND_URL_PREFIX = REACT_APP_DEPLOY_ENV === 'pages' ? '/qwerty-learner/sounds/' : './sounds/'
export const KEY_SOUND_URL_PREFIX = SOUND_URL_PREFIX + 'key-sound/'
// will add more sound resource and add config ui in the future
export const keySoundResources: SoundResource[] = [{ key: '1', name: '声音1', filename: 'click.wav' }]
const videoList = import.meta.glob(['../../public/sounds/key-sound/*.(wav|mp3)'], {
eager: false,
})
/**
* the Mechanical keyboard sound from https://github.com/tplai/kbsim
*/
export const keySoundResources: SoundResource[] = Object.keys(videoList)
.map((k) => {
const name = k.replace(/(.*\/)*([^.]+).*/gi, '$2')
const suffix = k.substring(k.lastIndexOf('.'))
return {
key: name,
name: `${name}`,
filename: `${name}${suffix}`,
}
})
.sort((a, b) => {
// default key should be the first one
if (a.key === 'Default') {
return -1
}
if (b.key === 'Default') {
return 1
}
return a.key.localeCompare(b.key)
})
export const wrongSoundResources: SoundResource[] = [{ key: '1', name: '声音1', filename: 'beep.wav' }]

View File

@@ -0,0 +1,13 @@
import { KEY_SOUND_URL_PREFIX } from '@/resources/soundResource'
import { SoundResource } from '@/typings'
import { Howl, Howler } from 'howler'
export function playKeySoundResource(soundResource: SoundResource) {
const path = KEY_SOUND_URL_PREFIX + soundResource.filename
const sound = new Howl({
src: path,
format: ['wav'],
})
Howler.volume(1)
sound.play()
}