refactor: 使用 sonner、zustand persist、partysocket 替换手写实现
This commit is contained in:
@@ -5,8 +5,10 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"partysocket": "^1.1.16",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"zustand": "^5.0.11",
|
"zustand": "^5.0.11",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -262,6 +264,8 @@
|
|||||||
|
|
||||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||||
|
|
||||||
|
"event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
@@ -312,6 +316,8 @@
|
|||||||
|
|
||||||
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
|
||||||
|
|
||||||
|
"partysocket": ["partysocket@1.1.16", "", { "dependencies": { "event-target-polyfill": "^0.0.4" }, "peerDependencies": { "react": ">=17" }, "optionalPeers": ["react"] }, "sha512-d7xFv+ZC7x0p/DAHWJ5FhxQhimIx+ucyZY+kxL0cKddLBmK9c4p2tEA/L+dOOrWm6EYrRwrBjKQV0uSzOY9x1w=="],
|
||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
@@ -330,6 +336,8 @@
|
|||||||
|
|
||||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
"tailwindcss": ["tailwindcss@4.2.1", "", {}, "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw=="],
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
"vite": "^7.3.1"
|
"vite": "^7.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"partysocket": "^1.1.16",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
|
"sonner": "^2.0.7",
|
||||||
"zustand": "^5.0.11"
|
"zustand": "^5.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { Toaster } from "sonner";
|
||||||
import { HistoryList } from "./components/HistoryList";
|
import { HistoryList } from "./components/HistoryList";
|
||||||
import { MicButton } from "./components/MicButton";
|
import { MicButton } from "./components/MicButton";
|
||||||
import { PreviewBox } from "./components/PreviewBox";
|
import { PreviewBox } from "./components/PreviewBox";
|
||||||
import { StatusBadge } from "./components/StatusBadge";
|
import { StatusBadge } from "./components/StatusBadge";
|
||||||
import { Toast } from "./components/Toast";
|
|
||||||
import { useRecorder } from "./hooks/useRecorder";
|
import { useRecorder } from "./hooks/useRecorder";
|
||||||
import { useWebSocket } from "./hooks/useWebSocket";
|
import { useWebSocket } from "./hooks/useWebSocket";
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ export function App() {
|
|||||||
<MicButton onStart={startRecording} onStop={stopRecording} />
|
<MicButton onStart={startRecording} onStop={stopRecording} />
|
||||||
<HistoryList sendJSON={sendJSON} />
|
<HistoryList sendJSON={sendJSON} />
|
||||||
</div>
|
</div>
|
||||||
<Toast />
|
<Toaster theme="dark" position="bottom-center" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useAppStore } from "../stores/app-store";
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
function formatTime(ts: number): string {
|
function formatTime(ts: number): string {
|
||||||
@@ -13,14 +14,13 @@ interface HistoryListProps {
|
|||||||
export function HistoryList({ sendJSON }: HistoryListProps) {
|
export function HistoryList({ sendJSON }: HistoryListProps) {
|
||||||
const history = useAppStore((s) => s.history);
|
const history = useAppStore((s) => s.history);
|
||||||
const clearHistory = useAppStore((s) => s.clearHistory);
|
const clearHistory = useAppStore((s) => s.clearHistory);
|
||||||
const showToast = useAppStore((s) => s.showToast);
|
|
||||||
|
|
||||||
const handleItemClick = useCallback(
|
const handleItemClick = useCallback(
|
||||||
(text: string) => {
|
(text: string) => {
|
||||||
sendJSON({ type: "paste", text });
|
sendJSON({ type: "paste", text });
|
||||||
showToast("发送粘贴…");
|
toast.info("发送粘贴…");
|
||||||
},
|
},
|
||||||
[sendJSON, showToast],
|
[sendJSON],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,9 +39,7 @@ export function HistoryList({ sendJSON }: HistoryListProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<p className="py-10 text-center text-fg-dim text-sm">
|
<p className="py-10 text-center text-fg-dim text-sm">{"暂无记录"}</p>
|
||||||
{"暂无记录"}
|
|
||||||
</p>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="scrollbar-thin flex-1 overflow-y-auto">
|
<div className="scrollbar-thin flex-1 overflow-y-auto">
|
||||||
{history.map((item, i) => (
|
{history.map((item, i) => (
|
||||||
|
|||||||
@@ -98,9 +98,7 @@ export function MicButton({ onStart, onStop }: MicButtonProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-medium text-fg-dim text-sm">
|
<p className="font-medium text-fg-dim text-sm">{"按住说话"}</p>
|
||||||
{"按住说话"}
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useEffect, useRef } from "react";
|
|
||||||
import { useAppStore } from "../stores/app-store";
|
|
||||||
|
|
||||||
export function Toast() {
|
|
||||||
const toast = useAppStore((s) => s.toast);
|
|
||||||
const dismissToast = useAppStore((s) => s.dismissToast);
|
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!toast) return;
|
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
|
||||||
timerRef.current = setTimeout(dismissToast, 2000);
|
|
||||||
return () => {
|
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
|
||||||
};
|
|
||||||
}, [toast, dismissToast]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`pointer-events-none fixed bottom-[calc(80px+env(safe-area-inset-bottom,0px))] left-1/2 z-[999] rounded-3xl border border-edge bg-[rgba(28,28,37,0.85)] px-[22px] py-2.5 font-medium text-[13px] text-fg shadow-[0_8px_32px_rgba(0,0,0,0.4)] backdrop-blur-[16px] transition-all duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
|
|
||||||
toast
|
|
||||||
? "-translate-x-1/2 translate-y-0 opacity-100"
|
|
||||||
: "-translate-x-1/2 translate-y-2 opacity-0"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{toast}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useRef } from "react";
|
import { useCallback, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { resampleTo16kInt16 } from "../lib/resample";
|
import { resampleTo16kInt16 } from "../lib/resample";
|
||||||
import { useAppStore } from "../stores/app-store";
|
import { useAppStore } from "../stores/app-store";
|
||||||
import audioProcessorUrl from "../workers/audio-processor.ts?worker&url";
|
import audioProcessorUrl from "../workers/audio-processor.ts?worker&url";
|
||||||
@@ -89,9 +90,7 @@ export function useRecorder({ sendJSON, sendBinary }: UseRecorderOptions) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
useAppStore.getState().setPendingStart(false);
|
useAppStore.getState().setPendingStart(false);
|
||||||
abortRef.current = null;
|
abortRef.current = null;
|
||||||
useAppStore
|
toast.error(`麦克风错误: ${(err as Error).message}`);
|
||||||
.getState()
|
|
||||||
.showToast(`麦克风错误: ${(err as Error).message}`);
|
|
||||||
}
|
}
|
||||||
}, [initAudio]);
|
}, [initAudio]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { WebSocket as ReconnectingWebSocket } from "partysocket";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback, useEffect, useRef } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import { useAppStore } from "../stores/app-store";
|
import { useAppStore } from "../stores/app-store";
|
||||||
|
|
||||||
function getWsUrl(): string {
|
function getWsUrl(): string {
|
||||||
@@ -9,13 +11,8 @@ function getWsUrl(): string {
|
|||||||
return `${proto}//${location.host}/ws${q}`;
|
return `${proto}//${location.host}/ws${q}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WS_RECONNECT_BASE = 1000;
|
|
||||||
const WS_RECONNECT_MAX = 16000;
|
|
||||||
|
|
||||||
export function useWebSocket() {
|
export function useWebSocket() {
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<ReconnectingWebSocket | null>(null);
|
||||||
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
||||||
const reconnectDelayRef = useRef(WS_RECONNECT_BASE);
|
|
||||||
|
|
||||||
const sendJSON = useCallback((obj: Record<string, unknown>) => {
|
const sendJSON = useCallback((obj: Record<string, unknown>) => {
|
||||||
const ws = wsRef.current;
|
const ws = wsRef.current;
|
||||||
@@ -27,78 +24,59 @@ export function useWebSocket() {
|
|||||||
const sendBinary = useCallback((data: Int16Array) => {
|
const sendBinary = useCallback((data: Int16Array) => {
|
||||||
const ws = wsRef.current;
|
const ws = wsRef.current;
|
||||||
if (ws?.readyState === WebSocket.OPEN) {
|
if (ws?.readyState === WebSocket.OPEN) {
|
||||||
ws.send(data.buffer);
|
ws.send(data.buffer as ArrayBuffer);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function connect() {
|
useAppStore.getState().setConnectionStatus("connecting");
|
||||||
if (wsRef.current) return;
|
|
||||||
|
|
||||||
useAppStore.getState().setConnectionStatus("connecting");
|
const ws = new ReconnectingWebSocket(getWsUrl(), undefined, {
|
||||||
const ws = new WebSocket(getWsUrl());
|
minReconnectionDelay: 1000,
|
||||||
ws.binaryType = "arraybuffer";
|
maxReconnectionDelay: 16000,
|
||||||
|
});
|
||||||
|
ws.binaryType = "arraybuffer";
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
reconnectDelayRef.current = WS_RECONNECT_BASE;
|
useAppStore.getState().setConnectionStatus("connected");
|
||||||
useAppStore.getState().setConnectionStatus("connected");
|
};
|
||||||
};
|
|
||||||
|
|
||||||
ws.onmessage = (e: MessageEvent) => {
|
ws.onmessage = (e: MessageEvent) => {
|
||||||
if (typeof e.data !== "string") return;
|
if (typeof e.data !== "string") return;
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
const store = useAppStore.getState();
|
|
||||||
switch (msg.type) {
|
|
||||||
case "partial":
|
|
||||||
store.setPreview(msg.text || "", false);
|
|
||||||
break;
|
|
||||||
case "final":
|
|
||||||
store.setPreview(msg.text || "", true);
|
|
||||||
if (msg.text) store.addHistory(msg.text);
|
|
||||||
break;
|
|
||||||
case "pasted":
|
|
||||||
store.showToast("✅ 已粘贴");
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
store.showToast(`❌ ${msg.message || "错误"}`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore malformed messages
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
ws.onclose = () => {
|
|
||||||
wsRef.current = null;
|
|
||||||
const store = useAppStore.getState();
|
const store = useAppStore.getState();
|
||||||
store.setConnectionStatus("disconnected");
|
switch (msg.type) {
|
||||||
if (store.recording) store.setRecording(false);
|
case "partial":
|
||||||
if (store.pendingStart) store.setPendingStart(false);
|
store.setPreview(msg.text || "", false);
|
||||||
scheduleReconnect();
|
break;
|
||||||
};
|
case "final":
|
||||||
|
store.setPreview(msg.text || "", true);
|
||||||
|
if (msg.text) store.addHistory(msg.text);
|
||||||
|
break;
|
||||||
|
case "pasted":
|
||||||
|
toast.success("已粘贴");
|
||||||
|
break;
|
||||||
|
case "error":
|
||||||
|
toast.error(msg.message || "错误");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore malformed messages
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
ws.onerror = () => ws.close();
|
ws.onclose = () => {
|
||||||
wsRef.current = ws;
|
const store = useAppStore.getState();
|
||||||
}
|
store.setConnectionStatus("disconnected");
|
||||||
|
if (store.recording) store.setRecording(false);
|
||||||
|
if (store.pendingStart) store.setPendingStart(false);
|
||||||
|
};
|
||||||
|
|
||||||
function scheduleReconnect() {
|
wsRef.current = ws;
|
||||||
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
|
|
||||||
reconnectTimerRef.current = setTimeout(
|
|
||||||
connect,
|
|
||||||
reconnectDelayRef.current,
|
|
||||||
);
|
|
||||||
reconnectDelayRef.current = Math.min(
|
|
||||||
reconnectDelayRef.current * 2,
|
|
||||||
WS_RECONNECT_MAX,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
|
ws.close();
|
||||||
wsRef.current?.close();
|
|
||||||
wsRef.current = null;
|
wsRef.current = null;
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
export interface HistoryItem {
|
export interface HistoryItem {
|
||||||
text: string;
|
text: string;
|
||||||
ts: number;
|
ts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HISTORY_KEY = "voicepaste_history";
|
|
||||||
const HISTORY_MAX = 50;
|
const HISTORY_MAX = 50;
|
||||||
|
|
||||||
type ConnectionStatus = "connected" | "disconnected" | "connecting";
|
type ConnectionStatus = "connected" | "disconnected" | "connecting";
|
||||||
@@ -21,8 +21,6 @@ interface AppState {
|
|||||||
previewActive: boolean;
|
previewActive: boolean;
|
||||||
// History
|
// History
|
||||||
history: HistoryItem[];
|
history: HistoryItem[];
|
||||||
// Toast
|
|
||||||
toast: string | null;
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
setConnectionStatus: (status: ConnectionStatus) => void;
|
setConnectionStatus: (status: ConnectionStatus) => void;
|
||||||
@@ -32,55 +30,43 @@ interface AppState {
|
|||||||
clearPreview: () => void;
|
clearPreview: () => void;
|
||||||
addHistory: (text: string) => void;
|
addHistory: (text: string) => void;
|
||||||
clearHistory: () => void;
|
clearHistory: () => void;
|
||||||
showToast: (message: string) => void;
|
|
||||||
dismissToast: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadHistoryFromStorage(): HistoryItem[] {
|
export const useAppStore = create<AppState>()(
|
||||||
try {
|
persist(
|
||||||
return JSON.parse(
|
(set, get) => ({
|
||||||
localStorage.getItem(HISTORY_KEY) || "[]",
|
connectionStatus: "connecting",
|
||||||
) as HistoryItem[];
|
recording: false,
|
||||||
} catch {
|
pendingStart: false,
|
||||||
return [];
|
previewText: "",
|
||||||
}
|
previewActive: false,
|
||||||
}
|
history: [],
|
||||||
|
|
||||||
export const useAppStore = create<AppState>((set, get) => ({
|
setConnectionStatus: (connectionStatus) => set({ connectionStatus }),
|
||||||
connectionStatus: "connecting",
|
setRecording: (recording) => set({ recording }),
|
||||||
recording: false,
|
setPendingStart: (pendingStart) => set({ pendingStart }),
|
||||||
pendingStart: false,
|
|
||||||
previewText: "",
|
|
||||||
previewActive: false,
|
|
||||||
history: loadHistoryFromStorage(),
|
|
||||||
toast: null,
|
|
||||||
|
|
||||||
setConnectionStatus: (connectionStatus) => set({ connectionStatus }),
|
setPreview: (text, isFinal) =>
|
||||||
setRecording: (recording) => set({ recording }),
|
set({
|
||||||
setPendingStart: (pendingStart) => set({ pendingStart }),
|
previewText: text,
|
||||||
|
previewActive: !isFinal && text.length > 0,
|
||||||
|
}),
|
||||||
|
|
||||||
setPreview: (text, isFinal) =>
|
clearPreview: () => set({ previewText: "", previewActive: false }),
|
||||||
set({
|
|
||||||
previewText: text,
|
addHistory: (text) => {
|
||||||
previewActive: !isFinal && text.length > 0,
|
const items = [{ text, ts: Date.now() }, ...get().history].slice(
|
||||||
|
0,
|
||||||
|
HISTORY_MAX,
|
||||||
|
);
|
||||||
|
set({ history: items });
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHistory: () => set({ history: [] }),
|
||||||
}),
|
}),
|
||||||
|
{
|
||||||
clearPreview: () => set({ previewText: "", previewActive: false }),
|
name: "voicepaste_history",
|
||||||
|
partialize: (state) => ({ history: state.history }),
|
||||||
addHistory: (text) => {
|
},
|
||||||
const items = [{ text, ts: Date.now() }, ...get().history].slice(
|
),
|
||||||
0,
|
);
|
||||||
HISTORY_MAX,
|
|
||||||
);
|
|
||||||
localStorage.setItem(HISTORY_KEY, JSON.stringify(items));
|
|
||||||
set({ history: items });
|
|
||||||
},
|
|
||||||
|
|
||||||
clearHistory: () => {
|
|
||||||
localStorage.removeItem(HISTORY_KEY);
|
|
||||||
set({ history: [] });
|
|
||||||
},
|
|
||||||
|
|
||||||
showToast: (message) => set({ toast: message }),
|
|
||||||
dismissToast: () => set({ toast: null }),
|
|
||||||
}));
|
|
||||||
|
|||||||
Reference in New Issue
Block a user