diff --git a/src/hooks/Audio.js b/src/hooks/Audio.js
index ba28c12..5b93c8e 100644
--- a/src/hooks/Audio.js
+++ b/src/hooks/Audio.js
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { apiBaiduTTS } from "../apis";
-import { kissLog } from "../libs/log";
+import { logger } from "../libs/log";
+import { fetchData } from "../libs/fetch";
/**
* 声音播放hook
@@ -12,50 +12,97 @@ export function useAudio(src) {
const [error, setError] = useState(null);
const [ready, setReady] = useState(false);
const [playing, setPlaying] = useState(false);
+ const [loading, setLoading] = useState(false);
- const onPlay = useCallback(() => {
- audioRef.current?.play();
+ const onPlay = useCallback(async () => {
+ if (!audioRef.current) return;
+ try {
+ await audioRef.current.play();
+ } catch (err) {
+ logger.info("Playback failed:", err);
+ setPlaying(false);
+ }
+ }, []);
+
+ const onPause = useCallback(() => {
+ audioRef.current?.pause();
}, []);
useEffect(() => {
- if (!src) {
- return;
- }
- const audio = new Audio(src);
- audio.addEventListener("error", (err) => setError(err));
- audio.addEventListener("canplaythrough", () => setReady(true));
- audio.addEventListener("play", () => setPlaying(true));
- audio.addEventListener("ended", () => setPlaying(false));
+ if (!src) return;
+
+ let ignore = false;
+ let objectUrl = null;
+
+ setReady(false);
+ setError(null);
+ setPlaying(false);
+ setLoading(true);
+
+ const audio = new Audio();
audioRef.current = audio;
+
+ const handleCanPlay = () => setReady(true);
+ const handlePlay = () => setPlaying(true);
+ const handlePause = () => setPlaying(false);
+ const handleEnded = () => setPlaying(false);
+ const handleError = (e) => {
+ if (!ignore) {
+ setError(audio.error || e);
+ setReady(false);
+ setLoading(false);
+ }
+ };
+
+ audio.addEventListener("canplaythrough", handleCanPlay);
+ audio.addEventListener("play", handlePlay);
+ audio.addEventListener("pause", handlePause);
+ audio.addEventListener("ended", handleEnded);
+ audio.addEventListener("error", handleError);
+
+ const loadAudio = async () => {
+ try {
+ const data = await fetchData(src, {}, { expect: "audio" });
+ if (ignore) return;
+
+ audio.src = data;
+
+ setLoading(false);
+ } catch (err) {
+ if (!ignore) {
+ logger.info("Audio fetch failed:", err);
+ setError(err);
+ setLoading(false);
+ }
+ }
+ };
+
+ loadAudio();
+
+ return () => {
+ ignore = true;
+
+ audio.pause();
+ audio.removeAttribute("src");
+
+ if (objectUrl) {
+ URL.revokeObjectURL(objectUrl);
+ }
+
+ audio.removeEventListener("canplaythrough", handleCanPlay);
+ audio.removeEventListener("play", handlePlay);
+ audio.removeEventListener("pause", handlePause);
+ audio.removeEventListener("ended", handleEnded);
+ audio.removeEventListener("error", handleError);
+ };
}, [src]);
return {
+ loading,
error,
ready,
playing,
onPlay,
+ onPause,
};
}
-
-/**
- * 获取语音hook
- * @param {*} text
- * @param {*} lan
- * @param {*} spd
- * @returns
- */
-export function useTextAudio(text, lan = "uk", spd = 3) {
- const [src, setSrc] = useState("");
-
- useEffect(() => {
- (async () => {
- try {
- setSrc(await apiBaiduTTS(text, lan, spd));
- } catch (err) {
- kissLog("baidu tts", err);
- }
- })();
- }, [text, lan, spd]);
-
- return useAudio(src);
-}
diff --git a/src/libs/cache.js b/src/libs/cache.js
index 9a9239a..2e30a6d 100644
--- a/src/libs/cache.js
+++ b/src/libs/cache.js
@@ -50,13 +50,13 @@ const newCacheReq = async (input, init) => {
* @param {*} init
* @returns
*/
-export const getHttpCache = async ({ input, init }) => {
+export const getHttpCache = async ({ input, init, expect }) => {
try {
const request = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(request);
if (response) {
- const res = await parseResponse(response);
+ const res = await parseResponse(response, expect);
return res;
}
} catch (err) {
@@ -99,7 +99,7 @@ export const putHttpCache = async ({
* @param {*} res
* @returns
*/
-export const parseResponse = async (res) => {
+export const parseResponse = async (res, expect = null) => {
if (!res) {
throw new Error("Response object does not exist");
}
@@ -108,21 +108,45 @@ export const parseResponse = async (res) => {
const msg = {
url: res.url,
status: res.status,
+ statusText: res.statusText,
};
- if (res.headers.get("Content-Type")?.includes("json")) {
- msg.response = await res.json();
+
+ try {
+ const errorText = await res.clone().text();
+ try {
+ msg.response = JSON.parse(errorText);
+ } catch {
+ msg.response = errorText;
+ }
+ } catch (e) {
+ msg.response = "Unable to read error body";
}
+
throw new Error(JSON.stringify(msg));
}
- const contentType = res.headers.get("Content-Type");
- if (contentType?.includes("json")) {
- return res.json();
- } else if (contentType?.includes("audio")) {
+ const contentType = res.headers.get("Content-Type") || "";
+ if (expect === "blob") return res.blob();
+ if (expect === "text") return res.text();
+ if (expect === "json") return res.json();
+ if (
+ expect === "audio" ||
+ contentType.includes("audio") ||
+ contentType.includes("image") ||
+ contentType.includes("video")
+ ) {
const blob = await res.blob();
return blobToBase64(blob);
}
- return res.text();
+
+ const text = await res.text();
+ if (!text) return null;
+
+ try {
+ return JSON.parse(text);
+ } catch (err) {
+ return text;
+ }
};
/**
diff --git a/src/libs/fetch.js b/src/libs/fetch.js
index 5ea2a96..551d88b 100644
--- a/src/libs/fetch.js
+++ b/src/libs/fetch.js
@@ -99,7 +99,7 @@ export const fetchPatcher = async (input, init = {}, opts) => {
*/
export const fetchHandle = async ({ input, init, opts }) => {
const res = await fetchPatcher(input, init, opts);
- return parseResponse(res);
+ return parseResponse(res, opts.expect);
};
/**
diff --git a/src/views/Selection/AudioBtn.js b/src/views/Selection/AudioBtn.js
index 17810bb..3bcd21d 100644
--- a/src/views/Selection/AudioBtn.js
+++ b/src/views/Selection/AudioBtn.js
@@ -1,8 +1,9 @@
import IconButton from "@mui/material/IconButton";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import { useAudio } from "../../hooks/Audio";
+import queryString from "query-string";
-export default function AudioBtn({ src }) {
+export function AudioBtn({ src }) {
const { error, ready, playing, onPlay } = useAudio(src);
if (error || !ready) {
@@ -27,3 +28,10 @@ export default function AudioBtn({ src }) {
);
}
+
+export function BaiduAudioBtn({ text, lan = "uk", spd = 3 }) {
+ if (!text) return null;
+
+ const src = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
+ return ;
+}
diff --git a/src/views/Selection/DictHandler.js b/src/views/Selection/DictHandler.js
index 6720ea3..6adf32a 100644
--- a/src/views/Selection/DictHandler.js
+++ b/src/views/Selection/DictHandler.js
@@ -1,5 +1,5 @@
import Typography from "@mui/material/Typography";
-import AudioBtn from "./AudioBtn";
+import { AudioBtn, BaiduAudioBtn } from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
@@ -48,12 +48,14 @@ export const dictHandlers = {
style={{ display: "inline-block", paddingRight: "1em" }}
>
{`UK [${data?.ec?.word?.ukphone}]`}
+
{`US [${data?.ec?.word?.usphone}]`}
+
),