feat(voice): playback-speed toggle (1x/1.5x/2x) with per-user default
This commit is contained in:
@@ -2,6 +2,7 @@ import { type AttachmentHandle, downloadAndDecryptAttachment } from '@chat-app/s
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
|
||||
import { getVoiceSpeed, setVoiceSpeed, VOICE_SPEEDS, type VoiceSpeed } from '../lib/voiceSpeedSettings';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { AlertIcon, MicIcon, SpinnerIcon } from './icons';
|
||||
|
||||
@@ -23,6 +24,7 @@ export function AttachmentAudio({ handle }: Props) {
|
||||
const [duration, setDuration] = useState<number>(0);
|
||||
const [position, setPosition] = useState<number>(0);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [speed, setSpeed] = useState<VoiceSpeed>(() => getVoiceSpeed());
|
||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,6 +107,12 @@ export function AttachmentAudio({ handle }: Props) {
|
||||
};
|
||||
}, [arrayBuf]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = audioRef.current;
|
||||
if (!el) return;
|
||||
el.playbackRate = speed;
|
||||
}, [speed, blobUrl]);
|
||||
|
||||
const fallbackPeaks = useMemo(
|
||||
() => (peaks ? null : Array.from({ length: BAR_COUNT }, () => 0.5)),
|
||||
[peaks],
|
||||
@@ -154,6 +162,29 @@ export function AttachmentAudio({ handle }: Props) {
|
||||
<PlayGlyph />
|
||||
)}
|
||||
</button>
|
||||
<div className="flex shrink-0 items-center gap-0.5 rounded-md bg-surface-3 p-0.5 text-[10px] font-semibold text-fg-muted">
|
||||
{VOICE_SPEEDS.map((s) => {
|
||||
const active = s === speed;
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSpeed(s);
|
||||
setVoiceSpeed(s);
|
||||
}}
|
||||
className={
|
||||
'flex h-6 w-7 cursor-pointer items-center justify-center rounded transition ' +
|
||||
(active ? 'bg-accent text-accent-fg' : 'hover:bg-surface hover:text-fg')
|
||||
}
|
||||
aria-pressed={active}
|
||||
title={'Wiedergabegeschwindigkeit ' + s + '×'}
|
||||
>
|
||||
{s}×
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div
|
||||
role="slider"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// Persists the preferred voice-message playback rate across sessions.
|
||||
// localStorage is fine here — non-sensitive, single source of truth per
|
||||
// device, no cross-device sync needed.
|
||||
|
||||
const KEY = 'chatapp:voice-speed';
|
||||
const ALLOWED = [1, 1.5, 2] as const;
|
||||
export type VoiceSpeed = (typeof ALLOWED)[number];
|
||||
|
||||
export function getVoiceSpeed(): VoiceSpeed {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(KEY);
|
||||
if (!raw) return 1;
|
||||
const parsed = Number(raw);
|
||||
if (ALLOWED.includes(parsed as VoiceSpeed)) return parsed as VoiceSpeed;
|
||||
} catch {
|
||||
/* localStorage unavailable */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
export function setVoiceSpeed(speed: VoiceSpeed): void {
|
||||
try {
|
||||
window.localStorage.setItem(KEY, String(speed));
|
||||
} catch {
|
||||
/* localStorage unavailable — best effort */
|
||||
}
|
||||
}
|
||||
|
||||
export const VOICE_SPEEDS = ALLOWED;
|
||||
Reference in New Issue
Block a user