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 { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
|
import { getCachedAttachment, putCachedAttachment } from '../lib/attachmentCache';
|
||||||
|
import { getVoiceSpeed, setVoiceSpeed, VOICE_SPEEDS, type VoiceSpeed } from '../lib/voiceSpeedSettings';
|
||||||
import { supabase } from '../lib/supabase';
|
import { supabase } from '../lib/supabase';
|
||||||
import { AlertIcon, MicIcon, SpinnerIcon } from './icons';
|
import { AlertIcon, MicIcon, SpinnerIcon } from './icons';
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export function AttachmentAudio({ handle }: Props) {
|
|||||||
const [duration, setDuration] = useState<number>(0);
|
const [duration, setDuration] = useState<number>(0);
|
||||||
const [position, setPosition] = useState<number>(0);
|
const [position, setPosition] = useState<number>(0);
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const [speed, setSpeed] = useState<VoiceSpeed>(() => getVoiceSpeed());
|
||||||
const audioRef = useRef<HTMLAudioElement | null>(null);
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,6 +107,12 @@ export function AttachmentAudio({ handle }: Props) {
|
|||||||
};
|
};
|
||||||
}, [arrayBuf]);
|
}, [arrayBuf]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = audioRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
el.playbackRate = speed;
|
||||||
|
}, [speed, blobUrl]);
|
||||||
|
|
||||||
const fallbackPeaks = useMemo(
|
const fallbackPeaks = useMemo(
|
||||||
() => (peaks ? null : Array.from({ length: BAR_COUNT }, () => 0.5)),
|
() => (peaks ? null : Array.from({ length: BAR_COUNT }, () => 0.5)),
|
||||||
[peaks],
|
[peaks],
|
||||||
@@ -154,6 +162,29 @@ export function AttachmentAudio({ handle }: Props) {
|
|||||||
<PlayGlyph />
|
<PlayGlyph />
|
||||||
)}
|
)}
|
||||||
</button>
|
</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 className="flex min-w-0 flex-1 flex-col gap-1">
|
||||||
<div
|
<div
|
||||||
role="slider"
|
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