Files
play/app/pages/index.vue
2025-08-17 22:07:37 +08:00

478 lines
13 KiB
Vue

<template>
<div class="h-screen w-screen flex items-center justify-center bg-black text-white relative overflow-hidden">
<!-- Connection Status -->
<div class="absolute top-4 right-4 z-10">
<div class="flex items-center gap-2 px-3 py-1 rounded-full text-sm" :class="connectionStatusClass">
<div class="w-2 h-2 rounded-full" :class="connectionDotClass"></div>
{{ connectionStatus }}
</div>
</div>
<!-- Video Player Container -->
<div class="w-full h-full flex items-center justify-center relative">
<!-- Video Element -->
<video
v-if="currentVideo"
ref="videoElement"
class="w-full h-full object-contain"
:src="videoSrc"
:volume="volume / 100"
:loop="isLooping"
playsinline
:muted="isMuted"
preload="auto"
autoplay
@loadedmetadata="onVideoLoaded"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
@error="onVideoError"
/>
<!-- Waiting Screen -->
<div v-else class="flex flex-col items-center justify-center space-y-8">
<!-- Logo or App Icon -->
<div class="w-32 h-32 rounded-full bg-gray-800 flex items-center justify-center mb-8">
<UIcon name="lucide:play-circle" class="w-16 h-16 text-gray-400" />
</div>
<!-- App Name -->
<h1 class="text-4xl font-bold text-gray-300 mb-4">
{{ appConfig.app.name }}
</h1>
<!-- Status Message -->
<p class="text-xl text-gray-400 text-center max-w-md">
{{ statusMessage }}
</p>
<!-- Loading Animation -->
<div v-if="isConnecting" class="flex items-center space-x-1">
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse"></div>
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse" style="animation-delay: 0.2s"></div>
<div class="w-2 h-2 bg-gray-500 rounded-full animate-pulse" style="animation-delay: 0.4s"></div>
</div>
</div>
<!-- Overlay Controls (hidden by default, shown on hover if enabled) -->
<div v-if="showControls && currentVideo"
class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-300">
<div class="flex items-center space-x-4">
<button @click="togglePlay" class="p-3 rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition-all">
<UIcon :name="isPlaying ? 'lucide:pause' : 'lucide:play'" class="w-8 h-8" />
</button>
<button @click="stop" class="p-3 rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition-all">
<UIcon name="lucide:square" class="w-8 h-8" />
</button>
</div>
</div>
</div>
<!-- Debug Info (only in dev mode) -->
<div v-if="isDev" class="absolute bottom-4 left-4 text-xs text-gray-500 space-y-1">
<div>Volume: {{ volume }}%</div>
<div v-if="currentVideo">Position: {{ Math.floor(position) }}s / {{ Math.floor(duration) }}s</div>
<div>Loop: {{ isLooping ? 'On' : 'Off' }}</div>
<div>Fullscreen: {{ isFullscreen ? 'On' : 'Off' }}</div>
</div>
</div>
</template>
<script setup>
// remove direct import that breaks vite in dev; we will fallback to file:// URLs
const appConfig = useAppConfig()
const isDev = process.env.NODE_ENV === 'development'
// Tauri functions will be loaded dynamically
let invoke = null
let listen = null
// Player state
const connectionStatus = ref('Disconnected')
const isConnecting = ref(false)
const currentVideo = ref(null)
const isPlaying = ref(false)
const position = ref(0)
const duration = ref(0)
const volume = ref(appConfig.player.defaultVolume)
const isLooping = ref(false)
const isFullscreen = ref(false)
const showControls = ref(false)
const isMuted = ref(true)
// Video element ref
const videoElement = ref(null)
// Video source URL - reactive ref that gets updated when currentVideo changes
const videoSrc = ref('')
// Convert file path to Tauri-compatible URL
async function updateVideoSrc() {
const raw = currentVideo.value?.path || ''
if (!raw) {
console.log('🎥 No video path available')
videoSrc.value = ''
return
}
const filePath = raw.startsWith('file://') ? raw.slice(7) : raw
console.log('🎥 Converting video path:', { raw, filePath })
// Use file:// protocol for local files in Tauri
const absolutePath = filePath.startsWith('/') ? filePath : `/${filePath}`
const fileUrl = `file://${absolutePath}`
videoSrc.value = fileUrl
console.log('🎥 Using file:// URL:')
console.log(' - Original path:', raw)
console.log(' - File path:', filePath)
console.log(' - File URL:', fileUrl)
}
// Watch for changes in currentVideo and update videoSrc
watch(currentVideo, updateVideoSrc, { immediate: true })
// Watch for changes in videoSrc to debug
watch(videoSrc, (newSrc, oldSrc) => {
console.log('🎥 videoSrc changed:')
console.log(' - Old:', oldSrc)
console.log(' - New:', newSrc)
})
// Computed properties
const connectionStatusClass = computed(() => {
switch (connectionStatus.value) {
case 'Connected':
return 'bg-green-900 bg-opacity-50 text-green-300'
case 'Connecting':
return 'bg-yellow-900 bg-opacity-50 text-yellow-300'
default:
return 'bg-red-900 bg-opacity-50 text-red-300'
}
})
const connectionDotClass = computed(() => {
switch (connectionStatus.value) {
case 'Connected':
return 'bg-green-400'
case 'Connecting':
return 'bg-yellow-400 animate-pulse'
default:
return 'bg-red-400'
}
})
const statusMessage = computed(() => {
if (isConnecting.value) {
return 'Connecting to controller...'
}
if (connectionStatus.value === 'Connected') {
return 'Ready to play. Waiting for controller commands...'
}
return 'Waiting for controller connection...'
})
// Video control functions
function togglePlay() {
if (!videoElement.value) return
if (isPlaying.value) {
videoElement.value.pause()
} else {
videoElement.value.play()
}
isPlaying.value = !isPlaying.value
}
function stop() {
if (!videoElement.value) return
videoElement.value.pause()
videoElement.value.currentTime = 0
isPlaying.value = false
position.value = 0
}
function seek(time) {
if (!videoElement.value) return
videoElement.value.currentTime = time
position.value = time
}
function setVolume(newVolume) {
volume.value = Math.max(0, Math.min(100, newVolume))
if (videoElement.value) {
videoElement.value.volume = volume.value / 100
}
}
// Video event handlers
function onVideoLoaded() {
console.log('🎬 Video loaded event triggered')
if (videoElement.value) {
duration.value = videoElement.value.duration
videoElement.value.volume = volume.value / 100
console.log('🎬 Video metadata:', {
duration: duration.value,
volume: volume.value,
currentSrc: videoElement.value.currentSrc,
readyState: videoElement.value.readyState
})
// 尝试主动播放;若被策略阻止则静音后重试
console.log('🎬 Attempting to play video...')
// Always start muted to comply with autoplay policies, then unmute
isMuted.value = true
// Small delay to ensure video is ready
setTimeout(() => {
if (videoElement.value) {
const playPromise = videoElement.value.play()
if (playPromise && typeof playPromise.then === 'function') {
playPromise.then(() => {
console.log('✅ Video started playing successfully (muted)')
isPlaying.value = true
// Try to unmute after successful playback
setTimeout(() => {
if (videoElement.value) {
videoElement.value.muted = false
isMuted.value = false
console.log('🔊 Video unmuted')
}
}, 1000)
}).catch((err) => {
console.error('❌ Autoplay failed:', err)
// Show user a play button if autoplay fails
showControls.value = true
})
} else {
// Fallback for browsers without promise-based play
try {
videoElement.value.play()
console.log('✅ Video started playing (fallback)')
isPlaying.value = true
} catch (err) {
console.error('❌ Autoplay failed (fallback):', err)
showControls.value = true
}
}
}
}, 500)
}
}
function onTimeUpdate() {
if (videoElement.value) {
position.value = videoElement.value.currentTime
}
}
function onVideoEnded() {
isPlaying.value = false
// Send playback finished event to controller
// This will be implemented when WebSocket is ready
}
function onVideoError(event) {
console.error('❌ Video playback error:', event)
console.error('❌ Video error details:', {
error: event.target?.error,
networkState: event.target?.networkState,
readyState: event.target?.readyState,
currentSrc: event.target?.currentSrc
})
currentVideo.value = null
isPlaying.value = false
}
// Initialize fullscreen if configured
onMounted(async () => {
console.log('🔧 Component mounted, initializing...')
if (appConfig.player.autoFullscreen) {
console.log('🔧 Auto fullscreen enabled')
// Request fullscreen
nextTick(() => {
document.documentElement.requestFullscreen?.() ||
document.documentElement.webkitRequestFullscreen?.()
})
}
// Setup Tauri event listeners
console.log('🔧 Setting up Tauri listeners...')
await setupTauriListeners()
// Get initial player state
console.log('🔧 Getting initial player state...')
await updatePlayerState()
console.log('✅ Component initialization complete')
})
// Setup Tauri event listeners
async function setupTauriListeners() {
try {
// Load Tauri APIs dynamically
const { invoke: tauriInvoke } = await import('@tauri-apps/api/core')
const { listen: tauriListen } = await import('@tauri-apps/api/event')
// Set global references
invoke = tauriInvoke
listen = tauriListen
// Listen for connection status changes
await listen('connection-status', (event) => {
connectionStatus.value = event.payload === 'connected' ? 'Connected' : 'Disconnected'
isConnecting.value = false
})
// Listen for player commands from backend
await listen('player-command', async (event) => {
console.log('📡 Backend player command received:', event.payload)
const command = event.payload
await handlePlayerCommand(command)
})
console.log('✅ Tauri event listeners setup complete')
} catch (error) {
console.error('❌ Failed to setup Tauri listeners:', error)
// Fallback for development mode
setTimeout(() => {
connectionStatus.value = 'Waiting for WebSocket server...'
}, 2000)
}
}
// Update player state from backend
async function updatePlayerState() {
if (!invoke) {
console.warn('Tauri invoke function not available yet')
return
}
try {
const state = await invoke('get_player_state')
currentVideo.value = state.current_video
isPlaying.value = state.playback_status === 'playing'
position.value = state.position
duration.value = state.duration
volume.value = state.volume
isLooping.value = state.is_looping
isFullscreen.value = state.is_fullscreen
connectionStatus.value = state.connection_status === 'connected' ? 'Connected' : 'Disconnected'
console.log('✅ Player state updated:', state)
} catch (error) {
console.error('❌ Failed to get player state:', error)
}
}
// Handle player commands from WebSocket
async function handlePlayerCommand(command) {
console.log('🎮 Received player command:', command)
switch (command.type) {
case 'play':
if (videoElement.value) {
try {
await videoElement.value.play()
isPlaying.value = true
} catch (error) {
console.error('Play error:', error)
}
}
break
case 'pause':
if (videoElement.value) {
videoElement.value.pause()
isPlaying.value = false
}
break
case 'stop':
stop()
break
case 'seek':
seek(command.position)
break
case 'setVolume':
setVolume(command.volume)
break
case 'setLoop':
isLooping.value = command.enabled
if (videoElement.value) {
videoElement.value.loop = command.enabled
}
break
case 'toggleFullscreen':
toggleFullscreenMode()
break
case 'loadVideo':
await loadVideoFromPath(command.path)
break
}
// Update state after command
await updatePlayerState()
}
// Load video from file path
async function loadVideoFromPath(path) {
console.log('📥 loadVideoFromPath called with:', path)
try {
const rawPath = path.startsWith('file://') ? path.slice(7) : path
console.log('📥 Processing video path:', { originalPath: path, rawPath })
const title = rawPath.split('/').pop() || rawPath
currentVideo.value = {
path: rawPath,
title,
duration: null,
size: null,
format: null
}
console.log('📹 currentVideo.value set to:', currentVideo.value)
// The videoSrc will be updated automatically via the watcher
if (invoke) {
await invoke('load_video', { path: rawPath })
}
console.log('📹 Video loaded successfully:', title)
} catch (error) {
console.error('❌ Failed to load video:', error)
}
}
// Toggle fullscreen mode
function toggleFullscreenMode() {
if (document.fullscreenElement) {
document.exitFullscreen?.()
isFullscreen.value = false
} else {
document.documentElement.requestFullscreen?.()
isFullscreen.value = true
}
}
// Global keyboard shortcuts
function handleKeyPress(event) {
switch (event.key) {
case ' ':
event.preventDefault()
togglePlay()
break
case 'f':
case 'F':
event.preventDefault()
// Toggle fullscreen
break
}
}
onMounted(() => {
document.addEventListener('keydown', handleKeyPress)
})
onUnmounted(() => {
document.removeEventListener('keydown', handleKeyPress)
})
</script>