Files
ti2026h/src/components/RecorderControls.vue
T
SkyLiCoder d2ecb1a4ed refactor: stop recording triggers file download instead of browser storage
Recording flow: click Stop → frames packed into .mjpeg blob → browser
download triggered immediately. No more IndexedDB or OPFS.

Playback: use 'Load Recording File' button to open a .mjpeg file.
Removed all storage dependencies — pure file-based, 100% reliable.
2026-08-01 18:59:32 +08:00

156 lines
2.6 KiB
Vue

<script setup lang="ts">
import type { RecorderState } from '../composables/useRecorder'
defineProps<{
state: RecorderState
elapsed: string
frameCount: number
isLive: boolean
error: string | null
}>()
const emit = defineEmits<{
start: []
stop: []
}>()
</script>
<template>
<div class="recorder-controls">
<div class="controls-row">
<!-- Idle: show Record button -->
<button
v-if="state === 'idle'"
class="btn btn-record"
:disabled="!isLive"
@click="emit('start')"
title="Start recording"
>
<span class="btn-icon">⏺</span>
Record
</button>
<!-- Recording: show Stop -->
<button
v-if="state === 'recording'"
class="btn btn-stop"
@click="emit('stop')"
title="Stop and download recording"
>
<span class="btn-icon">⏹</span>
Stop &amp; Download
</button>
</div>
<!-- Recording info -->
<div v-if="state === 'recording'" class="recording-info">
<span class="rec-dot"></span>
<span class="rec-time">{{ elapsed }}</span>
<span class="rec-frames">{{ frameCount }} frames</span>
</div>
<!-- Recorder error -->
<div v-if="error" class="recorder-error">
⚠ {{ error }}
</div>
</div>
</template>
<style scoped>
.recorder-controls {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.controls-row {
display: flex;
gap: 10px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s, transform 0.15s;
}
.btn:active:not(:disabled) {
transform: scale(0.97);
}
.btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-icon {
font-size: 1.1rem;
}
.btn-record {
background: #e74c3c;
color: #fff;
}
.btn-record:hover:not(:disabled) {
background: #c0392b;
}
.btn-stop {
background: #e74c3c;
color: #fff;
}
.btn-stop:hover {
background: #c0392b;
}
.recording-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.9rem;
color: #e74c3c;
font-variant-numeric: tabular-nums;
}
.rec-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e74c3c;
animation: pulse 0.8s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.rec-time {
font-weight: 700;
}
.rec-frames {
color: #999;
}
.recorder-error {
font-size: 0.85rem;
color: #f39c12;
background: rgba(243, 156, 18, 0.1);
padding: 6px 12px;
border-radius: 4px;
max-width: 100%;
word-break: break-all;
}
</style>