Skip to content

Commit

Permalink
fix: update page read
Browse files Browse the repository at this point in the history
  • Loading branch information
ymzuiku committed Oct 14, 2023
1 parent 6fda389 commit 41965bc
Show file tree
Hide file tree
Showing 25 changed files with 728 additions and 13 deletions.
Binary file modified bun.lockb
Binary file not shown.
Binary file modified i18n.sqlite
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@
"dependencies": {
"@faker-js/faker": "^8.0.2",
"@google-cloud/text-to-speech": "^5.0.1",
"@minht11/solid-virtual-container": "^0.2.1",
"@prisma/client": "5.2.0",
"@sveltejs/adapter-node": "^1.3.1",
"@sveltejs/svelte-virtual-list": "^3.0.1",
"@trpc/client": "^10.38.1",
"@trpc/server": "^10.38.1",
"@types/node": "^20.6.3",
Expand Down
44 changes: 44 additions & 0 deletions src/lib/components/lazy-show.svelte
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>
3 changes: 3 additions & 0 deletions src/lib/components/speech-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const speechCache = {
lastAudio: null as HTMLAudioElement | null,
};
83 changes: 83 additions & 0 deletions src/lib/components/speech-controller.svelte
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>
75 changes: 75 additions & 0 deletions src/lib/components/speech.svelte
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>
1 change: 1 addition & 0 deletions src/lib/components/tab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export let selected = 0;
const tabs = [
{ title: i18n`添加单词`, icon: 'ci:table-add', href: '/home/' + $user.learn },
{ title: i18n`文章`, icon: 'grommet-icons:article', href: '/article' },
{ title: i18n`我的大脑`, icon: 'icon-park-outline:brain', href: '/brain' },
{ title: i18n`设置`, icon: 'mingcute:settings-3-line', href: '/setting' },
];
Expand Down
32 changes: 32 additions & 0 deletions src/lib/components/translate-word.svelte
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>
65 changes: 65 additions & 0 deletions src/lib/helpers/loop-play-hooks.ts
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);
}
});
}
11 changes: 11 additions & 0 deletions src/lib/helpers/scroll-to-element.ts
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',
});
}
}
5 changes: 5 additions & 0 deletions src/lib/helpers/split-sentences.ts
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;
}
5 changes: 5 additions & 0 deletions src/lib/helpers/split-words.ts
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;
}
26 changes: 26 additions & 0 deletions src/lib/helpers/translate-google.ts
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';
}
Loading

0 comments on commit 41965bc

Please sign in to comment.