-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
728 additions
and
13 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
<script lang="ts"> | ||
import { afterUpdate, onMount } from 'svelte'; | ||
let isVisible = false; | ||
let className = ''; | ||
export { className as class }; | ||
// 获取元素的引用 | ||
let element: HTMLSpanElement; | ||
// 在组件挂载后和更新后检查元素是否在屏幕中可见 | ||
afterUpdate(() => { | ||
if (element) { | ||
checkVisibility(); | ||
} | ||
}); | ||
function checkVisibility() { | ||
if (!element) { | ||
return; | ||
} | ||
const rect = element.getBoundingClientRect(); | ||
const windowHeight = window.innerHeight || document.documentElement.clientHeight; | ||
if (rect.top >= 0 && rect.bottom <= windowHeight) { | ||
isVisible = true; | ||
} else { | ||
isVisible = false; | ||
} | ||
} | ||
onMount(() => { | ||
window.addEventListener('scroll', checkVisibility); | ||
return () => { | ||
window.removeEventListener('scroll', checkVisibility); | ||
}; | ||
}); | ||
</script> | ||
|
||
<div bind:this={element} class={className}> | ||
{#if isVisible} | ||
<slot /> | ||
{/if} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export const speechCache = { | ||
lastAudio: null as HTMLAudioElement | null, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
<script> | ||
import { copyWords } from '$lib/helpers/copy-words'; | ||
import { loopPlay, speechConnect, speedAudio, speeds } from '$lib/stores/brain-store'; | ||
import { twMerge } from 'tailwind-merge'; | ||
import { css } from './atom-css'; | ||
export let text = ''; | ||
export let copy = false; | ||
export let loop = false; | ||
export let connect = false; | ||
</script> | ||
|
||
<div | ||
class="flex flex-col justify-center gap-4 p-4 pb-10 fixed w-full sm:max-w-2xl bottom-0 bg-white z-20 shadow-up" | ||
> | ||
<div class="flex flex-row items-center mb-1 gap-4 w-full"> | ||
{#if copy} | ||
<button class={twMerge(css.miniCard)} on:click={() => copyWords(text)}> | ||
<iconify-icon width="1.4rem" class="text-gray-400" icon="ci:copy" /> | ||
</button> | ||
{/if} | ||
<div class="flex-1" /> | ||
{#if connect} | ||
<button on:click={() => ($speechConnect = !$speechConnect)} class={twMerge(css.miniCard)}> | ||
<iconify-icon | ||
icon="teenyicons:curved-connector-solid" | ||
width="1.3rem" | ||
class={$speechConnect ? 'text-primary-500' : 'text-gray-400'} | ||
/> | ||
</button> | ||
{/if} | ||
{#if loop} | ||
<button on:click={() => ($loopPlay = !$loopPlay)} class={twMerge(css.miniCard)}> | ||
<iconify-icon | ||
icon="eos-icons:infinity" | ||
width="1.3rem" | ||
class={$loopPlay ? 'text-primary-500' : 'text-gray-400'} | ||
/> | ||
</button> | ||
{/if} | ||
|
||
<div class="inline-flex rounded-md"> | ||
<button | ||
class={twMerge(css.miniCard, '-mr-0 rounded-r-none')} | ||
on:click={() => ($speedAudio = 0.26)} | ||
> | ||
<iconify-icon | ||
icon="lucide:snail" | ||
width="1.4rem" | ||
class={$speedAudio === 0.26 ? 'text-primary-500' : 'text-gray-400'} | ||
/> | ||
</button> | ||
{#each speeds as item} | ||
<button | ||
aria-current="page" | ||
class="{twMerge(css.miniCard, 'rounded-none px-2')} {$speedAudio === item | ||
? 'text-primary-500' | ||
: 'text-gray-400'}" | ||
on:click={() => ($speedAudio = item)} | ||
> | ||
{#if item === 1} | ||
<iconify-icon width="1.4rem" icon="tabler:walk" /> | ||
{:else if item === 0.75} | ||
<iconify-icon width="1.4rem" icon="ic:round-assist-walker" /> | ||
{:else if item === 0.5} | ||
<iconify-icon width="1.9rem" icon="fluent:animal-turtle-16-regular" /> | ||
{:else} | ||
{item} | ||
{/if} | ||
</button> | ||
{/each} | ||
<button | ||
class={twMerge(css.miniCard, '-mr-0 rounded-l-none')} | ||
on:click={() => ($speedAudio = 1.25)} | ||
> | ||
<iconify-icon | ||
icon="fluent:animal-rabbit-16-regular" | ||
width="1.6rem" | ||
class={$speedAudio === 1.25 ? 'text-primary-500' : 'text-gray-400'} | ||
/> | ||
</button> | ||
</div> | ||
</div> | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
<script lang="ts"> | ||
import { speechPeople, speedAudio } from '$lib/stores/brain-store'; | ||
import { user } from '$lib/stores/user'; | ||
import { onMount, tick } from 'svelte'; | ||
import { speechCache } from './speech-cache'; | ||
export let text = ''; | ||
let className = ''; | ||
export let connect = -1; | ||
export { className as class }; | ||
let audioLoaded: Record<string, string> = {}; | ||
const handleSpeech = async (e: { currentTarget: HTMLElement }, connectNext?: boolean) => { | ||
const button = e.currentTarget; | ||
const nowAudio = button.querySelector('audio'); | ||
if (nowAudio && !connectNext) { | ||
if (speechCache.lastAudio === nowAudio) { | ||
nowAudio.currentTime = 0.01; | ||
nowAudio.pause(); | ||
speechCache.lastAudio = null; | ||
return; | ||
} | ||
speechCache.lastAudio = nowAudio; | ||
} | ||
if (text) { | ||
audioLoaded = { | ||
...audioLoaded, | ||
[text]: `/brain/audio?${new URLSearchParams({ | ||
text: text || '', | ||
people: $speechPeople, | ||
learn: $user.learn, | ||
}).toString()}`, | ||
}; | ||
await tick(); | ||
nowAudio?.load(); | ||
} | ||
document.querySelectorAll('audio').forEach((audio) => { | ||
if (!audio.paused && audio !== nowAudio) { | ||
audio.currentTime = 0.1; | ||
audio.pause(); | ||
} | ||
}); | ||
if (nowAudio) { | ||
nowAudio.load(); | ||
nowAudio.playbackRate = $speedAudio; | ||
nowAudio.volume = 1; | ||
nowAudio.currentTime = 0.1; | ||
nowAudio.play(); | ||
nowAudio.currentTime = 0.1; | ||
} | ||
}; | ||
let audio: HTMLAudioElement; | ||
onMount(() => { | ||
if (connect >= 0 && audio) { | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
(audio as any).nowConnect = connect; | ||
// (audio as any).handleSpeech = handleSpeech; | ||
} | ||
}); | ||
</script> | ||
|
||
<!-- svelte-ignore a11y-no-static-element-interactions --> | ||
<!-- svelte-ignore a11y-click-events-have-key-events --> | ||
<span on:click={(e) => handleSpeech(e)} class={className}> | ||
<audio | ||
data-text={text} | ||
data-connect-audio={connect} | ||
bind:this={audio} | ||
class="pointer-events-none" | ||
src={audioLoaded[text || '']} | ||
/> | ||
<slot /> | ||
</span> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<script lang="ts"> | ||
import { translateGoogle } from '$lib/helpers/translate-google'; | ||
import { learnWorls } from '$lib/stores/learn-words'; | ||
import { user } from '$lib/stores/user'; | ||
import Speech from './speech.svelte'; | ||
export let text = ''; | ||
$: translate = $learnWorls[text] || ''; | ||
const handleTranslate = async () => { | ||
if (translate) { | ||
learnWorls.update((v) => { | ||
delete v[text]; | ||
return { ...v }; | ||
}); | ||
return; | ||
} | ||
const value = await translateGoogle(text, $user.local); | ||
learnWorls.update((v) => { | ||
return { ...v, [text]: value }; | ||
}); | ||
}; | ||
</script> | ||
|
||
<!-- svelte-ignore a11y-click-events-have-key-events --> | ||
<!-- svelte-ignore a11y-no-static-element-interactions --> | ||
<span on:click={handleTranslate} class="cursor-pointer hover:opacity-75"> | ||
<Speech {text}>{text}</Speech> | ||
{#if translate} | ||
( {translate}) | ||
{/if} | ||
</span> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { browser } from '$app/environment'; | ||
import { speechCache } from '$lib/components/speech-cache'; | ||
import { loopPlay, speechConnect, speechPeople, speedAudio } from '$lib/stores/brain-store'; | ||
import { user } from '$lib/stores/user'; | ||
import { onDestroy, onMount } from 'svelte'; | ||
import { get } from 'svelte/store'; | ||
import { scrollToElement } from './scroll-to-element'; | ||
|
||
export function loopPlayHooks() { | ||
let loopUpdateTimer: ReturnType<typeof setTimeout>; | ||
|
||
onMount(() => { | ||
if (browser) { | ||
loopUpdateTimer = setInterval(() => { | ||
if (speechCache.lastAudio?.paused) { | ||
if (get(speechConnect)) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
if ((speechCache.lastAudio as any).nowConnect === void 0) { | ||
return; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(speechCache.lastAudio as any).nowConnect += 1; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const connectId = (speechCache.lastAudio as any).nowConnect; | ||
const nextAudio = document.querySelector( | ||
`[data-connect-audio="${connectId}"]`, | ||
) as HTMLAudioElement; | ||
if (nextAudio) { | ||
scrollToElement(nextAudio.parentElement?.parentElement); | ||
const src = `/brain/audio?${new URLSearchParams({ | ||
text: nextAudio.getAttribute('data-text') || '', | ||
people: get(speechPeople), | ||
learn: get(user).learn, | ||
}).toString()}`; | ||
if (speechCache.lastAudio.src === src) { | ||
return; | ||
} | ||
speechCache.lastAudio.src = src; | ||
speechCache.lastAudio.load(); | ||
speechCache.lastAudio.playbackRate = get(speedAudio); | ||
speechCache.lastAudio.currentTime = 0.1; | ||
speechCache.lastAudio.play(); | ||
} | ||
} else if (get(loopPlay)) { | ||
speechCache.lastAudio.currentTime = 0.1; | ||
speechCache.lastAudio.playbackRate = 1; | ||
speechCache.lastAudio.pause(); | ||
speechCache.lastAudio.load(); | ||
speechCache.lastAudio.currentTime = 0.1; | ||
speechCache.lastAudio.playbackRate = get(speedAudio); | ||
speechCache.lastAudio.play(); | ||
} else { | ||
speechCache.lastAudio.pause(); | ||
speechCache.lastAudio = null; | ||
} | ||
} | ||
}, 100); | ||
} | ||
}); | ||
onDestroy(() => { | ||
if (browser) { | ||
clearInterval(loopUpdateTimer); | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export function scrollToElement(element?: HTMLElement | null) { | ||
if (element) { | ||
const offsetTop = element.offsetTop; | ||
const windowHeight = window.innerHeight || document.documentElement.clientHeight; | ||
const scrollPosition = offsetTop - windowHeight / 2; | ||
window.scrollTo({ | ||
top: scrollPosition, | ||
behavior: 'smooth', | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export function splitSentences(text: string): string[] { | ||
// 使用正则表达式将文本分割成句子,句子以句号、问号或感叹号结尾 | ||
const sentences = text.split(/(?<=[.!?])\s+/); | ||
return sentences; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export function splitWords(sentence: string): string[] { | ||
// 使用正则表达式将句子分割成单词,\w+ 匹配一个或多个字母、数字或下划线 | ||
const words = sentence.split(/\s+/); | ||
return words; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
function removePunctuation(inputString: string) { | ||
// eslint-disable-next-line no-useless-escape | ||
const regex = /[.,\/#!$%\^&\*;:{}=\-_`~()\[\]\p{P}]/gu; | ||
return inputString.replace(regex, ''); | ||
} | ||
|
||
export async function translateGoogle(q: string, to: string) { | ||
if (typeof q !== 'string') { | ||
return ''; | ||
} | ||
if (to === 'zh') { | ||
to = 'zh-CN'; | ||
} else if (to === 'es') { | ||
to = 'es'; | ||
} else if (to === 'jp') { | ||
to = 'ja'; | ||
} | ||
q = removePunctuation(q); | ||
const uri = `https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=${to}&q=${q}`; | ||
|
||
const res = await fetch(uri, { method: 'GET' }).then((v) => v.json()); | ||
if (res[0] && res[0][0] && res[0][0][0]) { | ||
return res[0][0][0]; | ||
} | ||
return 'Null'; | ||
} |
Oops, something went wrong.