Files
dating-app-frontend/src/components/chat/VoiceRecorder.vue
2026-06-08 13:23:20 +03:00

145 lines
3.9 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue';
const emit = defineEmits<{ recorded: [blob: Blob] }>();
const recording = ref(false);
const mediaRecorder = ref<MediaRecorder | null>(null);
const chunks = ref<Blob[]>([]);
const duration = ref(0);
let timer: ReturnType<typeof setInterval> | null = null;
async function start() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder.value = new MediaRecorder(stream);
chunks.value = [];
mediaRecorder.value.ondataavailable = (e) => chunks.value.push(e.data);
mediaRecorder.value.onstop = () => {
const blob = new Blob(chunks.value, { type: 'audio/webm' });
emit('recorded', blob);
stream.getTracks().forEach((t) => t.stop());
};
mediaRecorder.value.start();
recording.value = true;
duration.value = 0;
timer = setInterval(() => duration.value++, 1000);
} catch {
// Microphone access denied
}
}
function stop() {
mediaRecorder.value?.stop();
recording.value = false;
if (timer) { clearInterval(timer); timer = null; }
}
function cancel() {
if (mediaRecorder.value?.state === 'recording') {
mediaRecorder.value.ondataavailable = null;
mediaRecorder.value.onstop = null;
mediaRecorder.value.stop();
}
recording.value = false;
duration.value = 0;
if (timer) { clearInterval(timer); timer = null; }
}
function formatTime(s: number) {
const m = Math.floor(s / 60);
const sec = s % 60;
return `${m}:${sec.toString().padStart(2, '0')}`;
}
</script>
<template>
<div class="voice-recorder">
<div v-if="recording" class="voice-recorder__active">
<span class="voice-recorder__dot" aria-hidden="true" />
<span class="voice-recorder__time meta">{{ formatTime(duration) }}</span>
<button class="voice-recorder__cancel" @click="cancel" aria-label="Отменить запись">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="16" height="16">
<path d="M18 6L6 18M6 6l12 12"/>
</svg>
</button>
<button class="voice-recorder__stop" @click="stop" aria-label="Остановить запись">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16">
<rect x="3" y="3" width="18" height="18" rx="2"/>
</svg>
</button>
</div>
<button v-else class="voice-recorder__btn" @click="start" aria-label="Записать голосовое сообщение">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="20" height="20">
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
<path d="M19 10v2a7 7 0 0 1-14 0v-2M12 19v4M8 23h8"/>
</svg>
</button>
</div>
</template>
<style scoped lang="scss">
.voice-recorder {
&__btn {
width: 40px;
height: 40px;
background: none;
border: none;
color: var(--color-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: color var(--transition-fast);
&:hover { color: var(--color-cream); }
}
&__active {
display: flex;
align-items: center;
gap: 8px;
}
&__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--color-signal);
animation: pulse-signal 1s ease-in-out infinite;
}
&__time {
font-variant-numeric: tabular-nums;
}
&__cancel,
&__stop {
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
&__cancel {
background: var(--color-surface-2);
color: var(--color-muted);
&:hover { color: var(--color-cream); }
}
&__stop {
background: var(--color-signal);
color: white;
&:hover { background: #a84e30; }
}
}
</style>