Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
236
templates/components/detail/audio-player.html
Normal file
236
templates/components/detail/audio-player.html
Normal file
@@ -0,0 +1,236 @@
|
||||
<!-- Audio Player Component (Mobile) -->
|
||||
<div class="bg-[var(--bg-secondary)] px-4 py-3 border-b border-[var(--border-primary)] flex-shrink-0">
|
||||
<!-- Show message if audio has been deleted -->
|
||||
<div v-if="selectedRecording.audio_deleted_at"
|
||||
class="text-[var(--text-muted)] text-sm flex items-center gap-2">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span v-text="t('help.audioDeletedMessage')"></span>
|
||||
</div>
|
||||
<!-- Show message for incognito recordings (no audio stored) -->
|
||||
<div v-else-if="selectedRecording.incognito"
|
||||
class="text-[var(--text-muted)] text-sm flex items-center gap-2">
|
||||
<i class="fas fa-user-secret"></i>
|
||||
<span v-text="t('incognito.audioNotStored')"></span>
|
||||
</div>
|
||||
<!-- Custom Audio Player -->
|
||||
<div v-else>
|
||||
<!-- Video/Audio element wrapped in Teleport for fullscreen -->
|
||||
<Teleport to="body" :disabled="!videoFullscreen">
|
||||
<div :class="videoFullscreen ? 'video-fullscreen-overlay' : ''"
|
||||
@mousemove="videoFullscreen && handleFullscreenMouseMove()"
|
||||
@click.self="videoFullscreen && toggleAudioPlayback()">
|
||||
|
||||
<component :is="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') ? 'video' : 'audio'"
|
||||
ref="audioPlayerElement"
|
||||
:src="'/audio/' + selectedRecording.id"
|
||||
:volume="playerVolume"
|
||||
@play="handleAudioPlayPause"
|
||||
@pause="handleAudioPlayPause"
|
||||
@timeupdate="handleCustomAudioTimeUpdate"
|
||||
@loadedmetadata="handleAudioLoadedMetadata"
|
||||
@durationchange="handleAudioDurationChange"
|
||||
@ended="handleAudioEnded"
|
||||
@waiting="handleAudioWaiting"
|
||||
@canplay="handleAudioCanPlay"
|
||||
@click="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') && toggleAudioPlayback()"
|
||||
@dblclick="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') && !videoFullscreen && enterVideoFullscreen()"
|
||||
:class="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/')
|
||||
? (videoFullscreen ? 'video-fullscreen-video' : (videoCollapsed ? 'hidden' : 'w-full rounded-lg mb-2 cursor-pointer'))
|
||||
: 'hidden'">
|
||||
</component>
|
||||
|
||||
<!-- Fullscreen Subtitle Overlay -->
|
||||
<div v-if="videoFullscreen && currentSubtitle"
|
||||
class="video-fullscreen-subtitle"
|
||||
:class="{ 'subtitle-shifted': fullscreenControlsVisible }">
|
||||
<span v-if="currentSubtitle.speaker"
|
||||
class="video-fullscreen-subtitle-speaker"
|
||||
:style="{ color: 'var(--' + currentSubtitle.color + ')' }">${ currentSubtitle.speaker }:</span>
|
||||
<span>${ currentSubtitle.text }</span>
|
||||
</div>
|
||||
|
||||
<!-- Fullscreen Control Bar -->
|
||||
<div v-if="videoFullscreen"
|
||||
class="video-fullscreen-controls"
|
||||
:class="{ visible: fullscreenControlsVisible }"
|
||||
@mousemove.stop="handleFullscreenMouseMove()">
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div class="px-4 mb-2">
|
||||
<div class="w-full h-5 rounded-full cursor-pointer relative group flex items-center"
|
||||
@mousedown="startProgressDrag"
|
||||
@touchstart.prevent="startProgressDrag">
|
||||
<div class="progress-track w-full h-1 rounded-full relative bg-white/30">
|
||||
<div class="h-full bg-white rounded-full pointer-events-none"
|
||||
:style="{ width: audioProgressPercent + '%' }"></div>
|
||||
</div>
|
||||
<div class="absolute top-1/2 w-3 h-3 bg-white rounded-full shadow-md transition-transform group-hover:scale-125 pointer-events-none -translate-y-1/2"
|
||||
:style="{ left: 'clamp(0px, calc(' + audioProgressPercent + '% - 6px), calc(100% - 12px))' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex items-center gap-3 px-4 pb-4">
|
||||
<!-- Play/Pause -->
|
||||
<button @click.stop="toggleAudioPlayback"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full text-white hover:bg-white/20 transition-all">
|
||||
<i :class="audioIsPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-lg" :style="!audioIsPlaying ? 'margin-left: 2px' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Volume -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click.stop="toggleAudioMute"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full text-white hover:bg-white/20 transition-all">
|
||||
<i :class="audioIsMuted || playerVolume === 0 ? 'fas fa-volume-mute' : playerVolume < 0.5 ? 'fas fa-volume-down' : 'fas fa-volume-up'" class="text-sm"></i>
|
||||
</button>
|
||||
<input type="range" min="0" max="1" step="0.05"
|
||||
:value="audioIsMuted ? 0 : playerVolume"
|
||||
@input="(e) => setAudioVolume(parseFloat(e.target.value))"
|
||||
class="fullscreen-volume-slider w-20">
|
||||
</div>
|
||||
|
||||
<!-- Time -->
|
||||
<div class="flex items-baseline gap-1 text-white">
|
||||
<span class="text-sm font-mono">${ formatAudioTime(displayCurrentTime) }</span>
|
||||
<span class="text-xs opacity-60">/</span>
|
||||
<span class="text-xs opacity-60 font-mono">${ formatAudioTime(audioDuration) }</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Speed -->
|
||||
<button @click.stop="cyclePlaybackRate"
|
||||
class="px-2 h-8 flex items-center justify-center rounded-lg text-white hover:bg-white/20 transition-all"
|
||||
title="Playback speed">
|
||||
<span class="text-xs font-semibold font-mono">${ formatPlaybackRate(playbackRate) }</span>
|
||||
</button>
|
||||
|
||||
<!-- Exit Fullscreen -->
|
||||
<button @click.stop="exitVideoFullscreen"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full text-white hover:bg-white/20 transition-all"
|
||||
:title="t('tooltips.exitFullscreen')">
|
||||
<i class="fas fa-compress text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Normal Controls (hidden when fullscreen) -->
|
||||
<div v-show="!videoFullscreen">
|
||||
<!-- Progress Bar (draggable, touch support) -->
|
||||
<div class="w-full h-4 rounded-full cursor-pointer relative mb-2 group flex items-center"
|
||||
@mousedown="startProgressDrag"
|
||||
@touchstart.prevent="startProgressDrag">
|
||||
<!-- Track background -->
|
||||
<div class="progress-track w-full h-2 rounded-full relative bg-[var(--border-accent)] opacity-40">
|
||||
<!-- Progress fill -->
|
||||
<div class="h-full bg-[var(--text-accent)] rounded-full pointer-events-none opacity-100"
|
||||
:style="{ width: audioProgressPercent + '%' }"></div>
|
||||
</div>
|
||||
<!-- Progress dot - stays within track bounds -->
|
||||
<div class="absolute top-1/2 w-4 h-4 bg-[var(--text-accent)] rounded-full shadow-md transition-transform group-hover:scale-110 pointer-events-none -translate-y-1/2"
|
||||
:style="{ left: 'clamp(0px, calc(' + audioProgressPercent + '% - 8px), calc(100% - 16px))' }"
|
||||
style="box-shadow: 0 2px 6px rgba(0,0,0,0.3);"></div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Row -->
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Play/Pause Button -->
|
||||
<button @click="toggleAudioPlayback"
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full player-play-button transition-all duration-200 flex-shrink-0 shadow-md hover:shadow-lg active:scale-95"
|
||||
:title="audioIsPlaying ? t('tooltips.pause') : t('tooltips.play')">
|
||||
<i :class="audioIsPlaying ? 'fas fa-pause' : 'fas fa-play'" class="text-base" :style="!audioIsPlaying ? 'margin-left: 2px' : ''"></i>
|
||||
</button>
|
||||
|
||||
<!-- Time Display -->
|
||||
<div class="flex items-baseline gap-1 flex-shrink-0">
|
||||
<span class="text-sm font-semibold font-mono" :class="isDraggingProgress ? 'text-[var(--text-accent)]' : 'text-[var(--text-primary)]'">${ formatAudioTime(displayCurrentTime) }</span>
|
||||
<span class="text-xs text-[var(--text-muted)]">/</span>
|
||||
<span class="text-xs text-[var(--text-muted)] font-mono">${ formatAudioTime(audioDuration) }</span>
|
||||
</div>
|
||||
|
||||
<!-- Playback Speed Control -->
|
||||
<div class="relative flex-shrink-0">
|
||||
<button ref="speedButtonMobile"
|
||||
@click="showSpeedMenu = !showSpeedMenu; $nextTick(() => updateSpeedMenuPosition($refs.speedButtonMobile))"
|
||||
data-speed-toggle
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-primary)] text-[var(--text-accent)] hover:opacity-80 transition-all"
|
||||
:title="t('tooltips.playbackSpeed') || 'Playback speed'">
|
||||
<span class="text-xs font-semibold font-mono">${ formatPlaybackRate(playbackRate) }</span>
|
||||
</button>
|
||||
<!-- Dropdown menu (teleported to body, fixed positioning) -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showSpeedMenu" @click.stop
|
||||
data-speed-dropdown
|
||||
class="fixed bg-[var(--bg-tertiary)] border border-[var(--border-accent)] rounded-md shadow-xl z-[9999] speed-dropdown overflow-y-auto backdrop-blur-sm"
|
||||
:style="speedMenuPosition">
|
||||
<div class="py-0.5">
|
||||
<button v-for="speed in playbackSpeeds" :key="speed"
|
||||
@mousedown.prevent="setPlaybackRate(speed); showSpeedMenu = false"
|
||||
class="w-full px-2 py-0.5 text-[11px] font-mono text-left hover:bg-[var(--bg-accent-light)] transition-colors"
|
||||
:class="speed === playbackRate ? 'text-[var(--text-accent)] font-semibold bg-[var(--bg-accent-light)]' : 'text-[var(--text-primary)]'">
|
||||
${ speed }x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="flex-1"></div>
|
||||
|
||||
<!-- Volume Control -->
|
||||
<div class="relative flex items-center flex-shrink-0 group/vol"
|
||||
@mouseenter="showVolumeSlider = true"
|
||||
@mouseleave="showVolumeSlider = false">
|
||||
<button @click="toggleAudioMute"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-primary)] transition-all"
|
||||
:class="audioIsMuted || playerVolume === 0 ? 'text-[var(--text-muted)]' : 'text-[var(--text-accent)]'"
|
||||
:title="audioIsMuted ? t('tooltips.unmute') : t('tooltips.mute')">
|
||||
<i :class="audioIsMuted || playerVolume === 0 ? 'fas fa-volume-mute' : playerVolume < 0.5 ? 'fas fa-volume-down' : 'fas fa-volume-up'" class="text-sm"></i>
|
||||
</button>
|
||||
<!-- Volume Slider (expands on hover, outer div is invisible hover bridge) -->
|
||||
<div class="absolute top-full left-1/2 -translate-x-1/2 pt-2 z-50 transition-all duration-200"
|
||||
:class="showVolumeSlider ? 'opacity-100 pointer-events-auto scale-100' : 'opacity-0 pointer-events-none scale-95'"
|
||||
@mouseenter="showVolumeSlider = true"
|
||||
@mouseleave="showVolumeSlider = false">
|
||||
<div class="px-3 py-2 bg-[var(--bg-tertiary)] border border-[var(--border-accent)] rounded-lg shadow-xl flex flex-col items-center gap-1">
|
||||
<span class="text-[10px] font-mono text-[var(--text-muted)]">${ Math.round(playerVolume * 100) }</span>
|
||||
<input type="range" min="0" max="1" step="0.05"
|
||||
:value="audioIsMuted ? 0 : playerVolume"
|
||||
@input="(e) => setAudioVolume(parseFloat(e.target.value))"
|
||||
class="volume-slider-vertical"
|
||||
style="height: 80px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Button -->
|
||||
<a :href="'/audio/' + selectedRecording.id + '?download=true'"
|
||||
download
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-primary)] text-[var(--text-accent)] hover:opacity-80 transition-all flex-shrink-0"
|
||||
:title="t('buttons.downloadAudio') || 'Download audio'">
|
||||
<i class="fas fa-download text-sm"></i>
|
||||
</a>
|
||||
|
||||
<!-- Video Fullscreen Button -->
|
||||
<button v-if="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/') && !videoCollapsed"
|
||||
@click="enterVideoFullscreen"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-primary)] text-[var(--text-accent)] hover:opacity-80 transition-all flex-shrink-0"
|
||||
:title="t('tooltips.fullscreenVideo')">
|
||||
<i class="fas fa-expand text-sm"></i>
|
||||
</button>
|
||||
|
||||
<!-- Video Toggle Button -->
|
||||
<button v-if="selectedRecording.mime_type && selectedRecording.mime_type.startsWith('video/')"
|
||||
@click="videoCollapsed = !videoCollapsed"
|
||||
class="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-primary)] transition-all flex-shrink-0"
|
||||
:class="videoCollapsed ? 'text-[var(--text-muted)]' : 'text-[var(--text-accent)]'"
|
||||
:title="videoCollapsed ? t('tooltips.showVideo') : t('tooltips.hideVideo')">
|
||||
<i :class="videoCollapsed ? 'fas fa-eye-slash' : 'fas fa-eye'" class="text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user