tauri-app: fix video control bar
This commit is contained in:
@@ -524,193 +524,195 @@ onUnmounted(() => {
|
|||||||
<div class="review-content">
|
<div class="review-content">
|
||||||
<!-- Video Section -->
|
<!-- Video Section -->
|
||||||
<div class="video-section with-sidebar">
|
<div class="video-section with-sidebar">
|
||||||
<!-- Video Player -->
|
<div class="video-wrapper">
|
||||||
<div class="video-container">
|
<!-- Video Player -->
|
||||||
<div v-if="videoError" class="video-error">
|
<div class="video-container">
|
||||||
<div class="error-icon">⚠️</div>
|
<div v-if="videoError" class="video-error">
|
||||||
<p>{{ videoError }}</p>
|
<div class="error-icon">⚠️</div>
|
||||||
</div>
|
<p>{{ videoError }}</p>
|
||||||
|
|
||||||
<video
|
|
||||||
v-else-if="videoUrl"
|
|
||||||
ref="videoRef"
|
|
||||||
class="video-player"
|
|
||||||
@loadedmetadata="onVideoLoaded"
|
|
||||||
@timeupdate="onVideoTimeUpdate"
|
|
||||||
@play="onVideoPlay"
|
|
||||||
@pause="onVideoPause"
|
|
||||||
@ended="onVideoEnded"
|
|
||||||
@error="onVideoError"
|
|
||||||
>
|
|
||||||
<source :src="videoUrl" type="video/mp4" />
|
|
||||||
Your browser does not support the video tag.
|
|
||||||
</video>
|
|
||||||
|
|
||||||
<div v-else class="video-loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Loading video...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Overlay Controls -->
|
|
||||||
<div class="video-overlay" v-if="videoUrl && !videoError">
|
|
||||||
<!-- Play/Pause Overlay -->
|
|
||||||
<div class="play-overlay" @click="togglePlay">
|
|
||||||
<div class="play-btn-large" v-if="!isPlaying">
|
|
||||||
<span>▶</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Video Controls -->
|
|
||||||
<div class="video-controls" v-if="videoUrl && !videoError">
|
|
||||||
<!-- Timeline with events -->
|
|
||||||
<div class="timeline-container">
|
|
||||||
<div class="timeline-bar" @click="onTimelineClick">
|
|
||||||
<!-- Progress bar -->
|
|
||||||
<div class="timeline-progress" :style="{ width: `${(currentTime / duration) * 100}%` }"></div>
|
|
||||||
|
|
||||||
<!-- Clip selection region -->
|
|
||||||
<div
|
|
||||||
v-if="clipStart !== null && clipEnd !== null"
|
|
||||||
class="clip-region"
|
|
||||||
:style="{
|
|
||||||
left: `${(clipStart / duration) * 100}%`,
|
|
||||||
width: `${((clipEnd - clipStart) / duration) * 100}%`
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Event markers -->
|
|
||||||
<div
|
|
||||||
v-for="(event, idx) in sortedEvents"
|
|
||||||
:key="idx"
|
|
||||||
class="event-marker"
|
|
||||||
:style="{
|
|
||||||
left: `${getEventPosition(event)}%`,
|
|
||||||
backgroundColor: getEventColor(event)
|
|
||||||
}"
|
|
||||||
:title="`${formatEventTime(event)} - ${event.event_type}: ${event.description}`"
|
|
||||||
@click.stop="seekToEvent(event)"
|
|
||||||
@mouseenter="hoveredEvent = event"
|
|
||||||
@mouseleave="hoveredEvent = null"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<!-- Current time indicator -->
|
|
||||||
<div class="time-indicator" :style="{ left: `${(currentTime / duration) * 100}%` }"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Event tooltip -->
|
<video
|
||||||
<div
|
v-else-if="videoUrl"
|
||||||
v-if="hoveredEvent"
|
ref="videoRef"
|
||||||
class="event-tooltip"
|
class="video-player"
|
||||||
:style="{ left: `${getEventPosition(hoveredEvent)}%` }"
|
@loadedmetadata="onVideoLoaded"
|
||||||
|
@timeupdate="onVideoTimeUpdate"
|
||||||
|
@play="onVideoPlay"
|
||||||
|
@pause="onVideoPause"
|
||||||
|
@ended="onVideoEnded"
|
||||||
|
@error="onVideoError"
|
||||||
>
|
>
|
||||||
<div class="tooltip-time">{{ formatEventTime(hoveredEvent) }}</div>
|
<source :src="videoUrl" type="video/mp4" />
|
||||||
<div class="tooltip-type">{{ hoveredEvent.event_type }}</div>
|
Your browser does not support the video tag.
|
||||||
<div class="tooltip-desc">{{ hoveredEvent.description }}</div>
|
</video>
|
||||||
|
|
||||||
|
<div v-else class="video-loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Loading video...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Overlay Controls -->
|
||||||
|
<div class="video-overlay" v-if="videoUrl && !videoError">
|
||||||
|
<!-- Play/Pause Overlay -->
|
||||||
|
<div class="play-overlay" @click="togglePlay">
|
||||||
|
<div class="play-btn-large" v-if="!isPlaying">
|
||||||
|
<span>▶</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Control buttons -->
|
<!-- Video Controls -->
|
||||||
<div class="controls-row">
|
<div class="video-controls" v-if="videoUrl && !videoError">
|
||||||
<div class="controls-left">
|
<!-- Timeline with events -->
|
||||||
<button class="control-btn" @click="togglePlay" :title="isPlaying ? 'Pause (Space)' : 'Play (Space)'">
|
<div class="timeline-container">
|
||||||
<span v-if="isPlaying">⏸</span>
|
<div class="timeline-bar" @click="onTimelineClick">
|
||||||
<span v-else>▶</span>
|
<!-- Progress bar -->
|
||||||
</button>
|
<div class="timeline-progress" :style="{ width: `${(currentTime / duration) * 100}%` }"></div>
|
||||||
|
|
||||||
<button class="control-btn" @click="seekRelative(-10)" title="Back 10s (←)">
|
<!-- Clip selection region -->
|
||||||
<span>⏪</span>
|
<div
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="control-btn" @click="seekRelative(10)" title="Forward 10s (→)">
|
|
||||||
<span>⏩</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="time-display">
|
|
||||||
<span>{{ formatDuration(currentTime) }}</span>
|
|
||||||
<span class="time-sep">/</span>
|
|
||||||
<span>{{ formatDuration(duration) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="controls-center">
|
|
||||||
<!-- Clip editing -->
|
|
||||||
<div class="clip-controls">
|
|
||||||
<button
|
|
||||||
class="clip-btn"
|
|
||||||
:class="{ active: clipStart !== null }"
|
|
||||||
@click="setClipStartPoint"
|
|
||||||
title="Set clip start (I)"
|
|
||||||
>
|
|
||||||
[I] Start
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="clip-btn"
|
|
||||||
:class="{ active: clipEnd !== null }"
|
|
||||||
@click="setClipEndPoint"
|
|
||||||
title="Set clip end (O)"
|
|
||||||
>
|
|
||||||
[O] End
|
|
||||||
</button>
|
|
||||||
<span v-if="clipDuration" class="clip-duration">
|
|
||||||
{{ formatDuration(clipDuration) }}
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
v-if="clipStart !== null || clipEnd !== null"
|
|
||||||
class="clip-btn clear"
|
|
||||||
@click="clearClipPoints"
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
v-if="clipStart !== null && clipEnd !== null"
|
v-if="clipStart !== null && clipEnd !== null"
|
||||||
class="clip-btn export"
|
class="clip-region"
|
||||||
@click="exportClip"
|
:style="{
|
||||||
:disabled="isExporting"
|
left: `${(clipStart / duration) * 100}%`,
|
||||||
>
|
width: `${((clipEnd - clipStart) / duration) * 100}%`
|
||||||
{{ isExporting ? 'Exporting...' : 'Export Clip' }}
|
}"
|
||||||
</button>
|
></div>
|
||||||
|
|
||||||
|
<!-- Event markers -->
|
||||||
|
<div
|
||||||
|
v-for="(event, idx) in sortedEvents"
|
||||||
|
:key="idx"
|
||||||
|
class="event-marker"
|
||||||
|
:style="{
|
||||||
|
left: `${getEventPosition(event)}%`,
|
||||||
|
backgroundColor: getEventColor(event)
|
||||||
|
}"
|
||||||
|
:title="`${formatEventTime(event)} - ${event.event_type}: ${event.description}`"
|
||||||
|
@click.stop="seekToEvent(event)"
|
||||||
|
@mouseenter="hoveredEvent = event"
|
||||||
|
@mouseleave="hoveredEvent = null"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- Current time indicator -->
|
||||||
|
<div class="time-indicator" :style="{ left: `${(currentTime / duration) * 100}%` }"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Event tooltip -->
|
||||||
|
<div
|
||||||
|
v-if="hoveredEvent"
|
||||||
|
class="event-tooltip"
|
||||||
|
:style="{ left: `${getEventPosition(hoveredEvent)}%` }"
|
||||||
|
>
|
||||||
|
<div class="tooltip-time">{{ formatEventTime(hoveredEvent) }}</div>
|
||||||
|
<div class="tooltip-type">{{ hoveredEvent.event_type }}</div>
|
||||||
|
<div class="tooltip-desc">{{ hoveredEvent.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls-right">
|
<!-- Control buttons -->
|
||||||
<!-- Playback speed -->
|
<div class="controls-row">
|
||||||
<select
|
<div class="controls-left">
|
||||||
class="speed-select"
|
<button class="control-btn" @click="togglePlay" :title="isPlaying ? 'Pause (Space)' : 'Play (Space)'">
|
||||||
:value="playbackRate"
|
<span v-if="isPlaying">⏸</span>
|
||||||
@change="setPlaybackRate(parseFloat(($event.target as HTMLSelectElement).value))"
|
<span v-else>▶</span>
|
||||||
>
|
|
||||||
<option value="0.25">0.25x</option>
|
|
||||||
<option value="0.5">0.5x</option>
|
|
||||||
<option value="0.75">0.75x</option>
|
|
||||||
<option value="1">1x</option>
|
|
||||||
<option value="1.25">1.25x</option>
|
|
||||||
<option value="1.5">1.5x</option>
|
|
||||||
<option value="2">2x</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- Volume -->
|
|
||||||
<div class="volume-control">
|
|
||||||
<button class="control-btn" @click="toggleMute" :title="isMuted ? 'Unmute (M)' : 'Mute (M)'">
|
|
||||||
<span v-if="isMuted || volume === 0">🔇</span>
|
|
||||||
<span v-else-if="volume < 0.5">🔉</span>
|
|
||||||
<span v-else>🔊</span>
|
|
||||||
</button>
|
</button>
|
||||||
<input
|
|
||||||
type="range"
|
<button class="control-btn" @click="seekRelative(-10)" title="Back 10s (←)">
|
||||||
class="volume-slider"
|
<span>⏪</span>
|
||||||
min="0"
|
</button>
|
||||||
max="1"
|
|
||||||
step="0.1"
|
<button class="control-btn" @click="seekRelative(10)" title="Forward 10s (→)">
|
||||||
:value="isMuted ? 0 : volume"
|
<span>⏩</span>
|
||||||
@input="setVolume(parseFloat(($event.target as HTMLInputElement).value))"
|
</button>
|
||||||
/>
|
|
||||||
|
<div class="time-display">
|
||||||
|
<span>{{ formatDuration(currentTime) }}</span>
|
||||||
|
<span class="time-sep">/</span>
|
||||||
|
<span>{{ formatDuration(duration) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="control-btn" @click="toggleFullscreen" title="Fullscreen (F)">
|
<div class="controls-center">
|
||||||
<span>⛶</span>
|
<!-- Clip editing -->
|
||||||
</button>
|
<div class="clip-controls">
|
||||||
|
<button
|
||||||
|
class="clip-btn"
|
||||||
|
:class="{ active: clipStart !== null }"
|
||||||
|
@click="setClipStartPoint"
|
||||||
|
title="Set clip start (I)"
|
||||||
|
>
|
||||||
|
[I] Start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="clip-btn"
|
||||||
|
:class="{ active: clipEnd !== null }"
|
||||||
|
@click="setClipEndPoint"
|
||||||
|
title="Set clip end (O)"
|
||||||
|
>
|
||||||
|
[O] End
|
||||||
|
</button>
|
||||||
|
<span v-if="clipDuration" class="clip-duration">
|
||||||
|
{{ formatDuration(clipDuration) }}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
v-if="clipStart !== null || clipEnd !== null"
|
||||||
|
class="clip-btn clear"
|
||||||
|
@click="clearClipPoints"
|
||||||
|
>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="clipStart !== null && clipEnd !== null"
|
||||||
|
class="clip-btn export"
|
||||||
|
@click="exportClip"
|
||||||
|
:disabled="isExporting"
|
||||||
|
>
|
||||||
|
{{ isExporting ? 'Exporting...' : 'Export Clip' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="controls-right">
|
||||||
|
<!-- Playback speed -->
|
||||||
|
<select
|
||||||
|
class="speed-select"
|
||||||
|
:value="playbackRate"
|
||||||
|
@change="setPlaybackRate(parseFloat(($event.target as HTMLSelectElement).value))"
|
||||||
|
>
|
||||||
|
<option value="0.25">0.25x</option>
|
||||||
|
<option value="0.5">0.5x</option>
|
||||||
|
<option value="0.75">0.75x</option>
|
||||||
|
<option value="1">1x</option>
|
||||||
|
<option value="1.25">1.25x</option>
|
||||||
|
<option value="1.5">1.5x</option>
|
||||||
|
<option value="2">2x</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- Volume -->
|
||||||
|
<div class="volume-control">
|
||||||
|
<button class="control-btn" @click="toggleMute" :title="isMuted ? 'Unmute (M)' : 'Mute (M)'">
|
||||||
|
<span v-if="isMuted || volume === 0">🔇</span>
|
||||||
|
<span v-else-if="volume < 0.5">🔉</span>
|
||||||
|
<span v-else>🔊</span>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="volume-slider"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.1"
|
||||||
|
:value="isMuted ? 0 : volume"
|
||||||
|
@input="setVolume(parseFloat(($event.target as HTMLInputElement).value))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="control-btn" @click="toggleFullscreen" title="Fullscreen (F)">
|
||||||
|
<span>⛶</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -974,16 +976,21 @@ onUnmounted(() => {
|
|||||||
max-width: calc(100% - 350px);
|
max-width: calc(100% - 350px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-wrapper {
|
||||||
|
width: min(100%, calc(75vh * 16 / 9));
|
||||||
|
max-height: calc(75vh + 120px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #000;
|
background: #000;
|
||||||
aspect-ratio: 16/9;
|
aspect-ratio: 16/9;
|
||||||
max-height: 75vh;
|
width: 100%;
|
||||||
width: min(100%, calc(75vh * 16 / 9));
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player {
|
.video-player {
|
||||||
|
|||||||
Reference in New Issue
Block a user