396 lines
12 KiB
Vue
396 lines
12 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- 连接状态 -->
|
|
<UCard>
|
|
<template #header>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
连接状态
|
|
</h2>
|
|
</template>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<div :class="connectionStatus === 'connected' ? 'bg-green-500' : connectionStatus === 'connecting' ? 'bg-yellow-500' : 'bg-red-500'" class="w-3 h-3 rounded-full" />
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{{ connectionStatusText }}
|
|
</span>
|
|
</div>
|
|
<UButton
|
|
:variant="connectionStatus === 'connected' ? 'soft' : 'solid'"
|
|
:color="connectionStatus === 'connected' ? 'primary' : 'secondary'"
|
|
size="sm"
|
|
@click="toggleConnection"
|
|
>
|
|
{{ connectionStatus === 'connected' ? '断开连接' : '连接' }}
|
|
</UButton>
|
|
</div>
|
|
<div class="mt-4 space-y-3">
|
|
<div class="flex items-center space-x-3">
|
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300 min-w-[100px]">
|
|
播放器地址:
|
|
</label>
|
|
<UInput
|
|
v-model="playerAddress"
|
|
placeholder="192.168.1.197:6666"
|
|
class="flex-1"
|
|
:disabled="connectionStatus === 'connected'"
|
|
/>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
格式: IP地址:端口 (例如: 192.168.1.197:6666)
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- 视频预览区域 -->
|
|
<UCard>
|
|
<template #header>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
视频预览
|
|
</h2>
|
|
</template>
|
|
<div class="bg-black rounded-lg aspect-video flex items-center justify-center">
|
|
<div v-if="currentVideo" class="text-center">
|
|
<UIcon name="i-heroicons-film" class="w-16 h-16 text-gray-400 mx-auto mb-2" />
|
|
<p class="text-white text-sm">
|
|
{{ currentVideo }}
|
|
</p>
|
|
</div>
|
|
<div v-else class="text-center">
|
|
<UIcon name="i-heroicons-video-camera-slash" class="w-16 h-16 text-gray-600 mx-auto mb-2" />
|
|
<p class="text-gray-400 text-sm">
|
|
暂无视频
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- 播放控制 -->
|
|
<UCard>
|
|
<template #header>
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
播放控制
|
|
</h2>
|
|
</template>
|
|
<div class="space-y-4">
|
|
<!-- 主要控制按钮 -->
|
|
<div class="flex justify-center space-x-4">
|
|
<UButton :disabled="!isConnected" color="success" size="lg" @click="playVideo">
|
|
<UIcon name="i-heroicons-play" class="w-5 h-5 mr-2" />
|
|
播放
|
|
</UButton>
|
|
<UButton :disabled="!isConnected" color="warning" size="lg" @click="pauseVideo">
|
|
<UIcon name="i-heroicons-pause" class="w-5 h-5 mr-2" />
|
|
暂停
|
|
</UButton>
|
|
<UButton :disabled="!isConnected" color="error" size="lg" @click="stopVideo">
|
|
<UIcon name="i-heroicons-stop" class="w-5 h-5 mr-2" />
|
|
停止
|
|
</UButton>
|
|
</div>
|
|
|
|
<!-- 进度控制 -->
|
|
<div class="space-y-2">
|
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">播放进度</label>
|
|
<div class="flex items-center space-x-4">
|
|
<URange v-model="progress" :min="0" :max="100" :disabled="!isConnected" class="flex-1" />
|
|
<span class="text-sm text-gray-600 dark:text-gray-400 min-w-[3rem]">{{ progress }}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 音量控制 -->
|
|
<div class="space-y-2">
|
|
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">音量</label>
|
|
<div class="flex items-center space-x-4">
|
|
<UIcon name="i-heroicons-speaker-x-mark" class="w-5 h-5 text-gray-400" />
|
|
<URange v-model="volume" :min="0" :max="100" :disabled="!isConnected" class="flex-1" />
|
|
<UIcon name="i-heroicons-speaker-wave" class="w-5 h-5 text-gray-400" />
|
|
<span class="text-sm text-gray-600 dark:text-gray-400 min-w-[3rem]">{{ volume }}%</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 额外功能 -->
|
|
<div class="flex flex-wrap gap-2 pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<UButton :disabled="!isConnected" :variant="isLooping ? 'solid' : 'outline'" size="sm" @click="toggleLoop">
|
|
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 mr-1" />
|
|
循环播放
|
|
</UButton>
|
|
<UButton :disabled="!isConnected" variant="outline" size="sm" @click="toggleFullscreen">
|
|
<UIcon name="i-heroicons-arrows-pointing-out" class="w-4 h-4 mr-1" />
|
|
全屏
|
|
</UButton>
|
|
<UButton variant="outline" size="sm" @click="openVideoFile">
|
|
<UIcon name="i-heroicons-folder-open" class="w-4 h-4 mr-1" />
|
|
打开文件
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
|
|
<!-- 播放列表 -->
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
播放列表
|
|
</h2>
|
|
<UButton variant="ghost" size="sm" color="error" @click="clearPlaylist">
|
|
<UIcon name="i-heroicons-trash" class="w-4 h-4 mr-1" />
|
|
清空
|
|
</UButton>
|
|
</div>
|
|
</template>
|
|
<div class="space-y-2">
|
|
<div v-if="playlist.length === 0" class="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
<UIcon name="i-heroicons-queue-list" class="w-12 h-12 mx-auto mb-2 text-gray-300" />
|
|
<p>播放列表为空</p>
|
|
</div>
|
|
<div v-else>
|
|
<div
|
|
v-for="(item, index) in playlist"
|
|
:key="index"
|
|
class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg"
|
|
>
|
|
<div class="flex items-center space-x-3">
|
|
<UIcon name="i-heroicons-film" class="w-5 h-5 text-gray-400" />
|
|
<span class="text-sm font-medium">{{ item }}</span>
|
|
</div>
|
|
<div class="flex space-x-1">
|
|
<UButton size="xs" variant="ghost" @click="playVideoFromPlaylist(index)">
|
|
<UIcon name="i-heroicons-play" class="w-4 h-4" />
|
|
</UButton>
|
|
<UButton size="xs" variant="ghost" color="error" @click="removeFromPlaylist(index)">
|
|
<UIcon name="i-heroicons-x-mark" class="w-4 h-4" />
|
|
</UButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import type { UnlistenFn } from "@tauri-apps/api/event";
|
|
import type { ConnectionConfig, PlayerState } from "~/composables/useTauri";
|
|
|
|
interface VideoControllerState {
|
|
connectionStatus: "connected" | "connecting" | "disconnected"
|
|
currentVideo: string | null
|
|
progress: number
|
|
volume: number
|
|
isLooping: boolean
|
|
playlist: string[]
|
|
playerAddress: string
|
|
}
|
|
|
|
// Tauri API
|
|
const {
|
|
connectToPlayer,
|
|
disconnectFromPlayer,
|
|
sendPlaybackCommand,
|
|
selectFile,
|
|
listenPlayerStateUpdate,
|
|
listenPlaybackCommandSent
|
|
} = useTauri();
|
|
|
|
// 响应式状态
|
|
const connectionStatus = ref<VideoControllerState["connectionStatus"]>("disconnected");
|
|
const currentVideo = ref<string | null>(null);
|
|
const progress = ref(0);
|
|
const volume = ref(50);
|
|
const isLooping = ref(false);
|
|
const playlist = ref<string[]>([]);
|
|
const playerAddress = ref("127.0.0.1:6666");
|
|
const playerState = ref<PlayerState | null>(null);
|
|
|
|
// 禁用不必要的自动更新,避免循环触发
|
|
const skipProgressUpdate = ref(false);
|
|
const skipVolumeUpdate = ref(false);
|
|
|
|
// 事件监听器去除函数
|
|
let unlistenPlayerState: UnlistenFn | null = null;
|
|
let unlistenCommandSent: UnlistenFn | null = null;
|
|
|
|
// 计算属性
|
|
const isConnected = computed(() => connectionStatus.value === "connected");
|
|
|
|
const connectionStatusText = computed(() => {
|
|
switch (connectionStatus.value) {
|
|
case "connected": return "已连接";
|
|
case "connecting": return "连接中...";
|
|
default: return "未连接";
|
|
}
|
|
});
|
|
|
|
// 创建连接配置
|
|
const createConnectionConfig = (): ConnectionConfig => {
|
|
const address = playerAddress.value || "";
|
|
const parts = address.split(":");
|
|
const host = parts[0] || "192.168.1.197";
|
|
const port = Number.parseInt(parts[1] || "") || 6666;
|
|
return {
|
|
host,
|
|
port,
|
|
timeout: 10,
|
|
autoReconnect: true,
|
|
reconnectInterval: 5
|
|
};
|
|
};
|
|
|
|
// 更新UI状态
|
|
const updateUIFromPlayerState = (state: PlayerState) => {
|
|
playerState.value = state;
|
|
|
|
// 更新连接状态
|
|
if (typeof state.connectionStatus === "string") {
|
|
connectionStatus.value = state.connectionStatus as any;
|
|
} else if (state.connectionStatus && typeof state.connectionStatus === "object" && "error" in state.connectionStatus) {
|
|
connectionStatus.value = "disconnected";
|
|
}
|
|
|
|
// 更新播放状态
|
|
if (state.currentVideo) {
|
|
currentVideo.value = state.currentVideo.title || state.currentVideo.path;
|
|
}
|
|
|
|
// 避免循环更新
|
|
skipProgressUpdate.value = true;
|
|
skipVolumeUpdate.value = true;
|
|
|
|
// 更新进度和音量
|
|
if (state.duration > 0) {
|
|
progress.value = Math.round((state.position / state.duration) * 100);
|
|
}
|
|
volume.value = Math.round(state.volume);
|
|
isLooping.value = state.isLooping;
|
|
|
|
// 更新播放列表
|
|
if (state.playlist && state.playlist.length > 0) {
|
|
playlist.value = state.playlist.map((video) => video.title || video.path);
|
|
}
|
|
|
|
nextTick(() => {
|
|
skipProgressUpdate.value = false;
|
|
skipVolumeUpdate.value = false;
|
|
});
|
|
};
|
|
|
|
// 方法
|
|
const toggleConnection = async () => {
|
|
if (connectionStatus.value === "connected") {
|
|
// 断开连接
|
|
const success = await disconnectFromPlayer();
|
|
if (success) {
|
|
connectionStatus.value = "disconnected";
|
|
playerState.value = null;
|
|
currentVideo.value = null;
|
|
|
|
// 清理事件监听器
|
|
if (unlistenPlayerState) {
|
|
unlistenPlayerState();
|
|
unlistenPlayerState = null;
|
|
}
|
|
if (unlistenCommandSent) {
|
|
unlistenCommandSent();
|
|
unlistenCommandSent = null;
|
|
}
|
|
}
|
|
} else {
|
|
// 尝试连接
|
|
connectionStatus.value = "connecting";
|
|
const config = createConnectionConfig();
|
|
const state = await connectToPlayer(config);
|
|
|
|
if (state) {
|
|
connectionStatus.value = "connected";
|
|
updateUIFromPlayerState(state);
|
|
|
|
// 设置事件监听器
|
|
unlistenPlayerState = await listenPlayerStateUpdate(updateUIFromPlayerState);
|
|
unlistenCommandSent = await listenPlaybackCommandSent((command) => {
|
|
console.log("播放命令已发送:", command);
|
|
});
|
|
} else {
|
|
connectionStatus.value = "disconnected";
|
|
}
|
|
}
|
|
};
|
|
|
|
const playVideo = async () => {
|
|
await sendPlaybackCommand("play");
|
|
};
|
|
|
|
const pauseVideo = async () => {
|
|
await sendPlaybackCommand("pause");
|
|
};
|
|
|
|
const stopVideo = async () => {
|
|
await sendPlaybackCommand("stop");
|
|
};
|
|
|
|
const toggleLoop = async () => {
|
|
const newLooping = !isLooping.value;
|
|
isLooping.value = newLooping;
|
|
await sendPlaybackCommand({ setLoop: { enabled: newLooping } });
|
|
};
|
|
|
|
const toggleFullscreen = async () => {
|
|
await sendPlaybackCommand("toggleFullscreen");
|
|
// 同步将控制端窗口也切换全屏,提升操控体验
|
|
try {
|
|
const { getCurrentWindow } = await import("@tauri-apps/api/window");
|
|
const win = getCurrentWindow();
|
|
const isFs = await win.isFullscreen();
|
|
await win.setFullscreen(!isFs);
|
|
} catch (e) {
|
|
console.warn("切换控制端全屏失败:", e);
|
|
}
|
|
};
|
|
|
|
const openVideoFile = async () => {
|
|
const filePath = await selectFile();
|
|
if (filePath) {
|
|
// 加载视频文件
|
|
await sendPlaybackCommand({ loadVideo: { path: filePath } });
|
|
|
|
// 添加到播放列表
|
|
const fileName = filePath.split("/").pop() || filePath;
|
|
if (!playlist.value.includes(fileName)) {
|
|
playlist.value.push(fileName);
|
|
}
|
|
|
|
// 更新当前视频
|
|
currentVideo.value = fileName;
|
|
}
|
|
};
|
|
|
|
const playVideoFromPlaylist = async (index: number) => {
|
|
await sendPlaybackCommand({ playFromPlaylist: { index } });
|
|
currentVideo.value = playlist.value[index] || null;
|
|
};
|
|
|
|
const removeFromPlaylist = (index: number) => {
|
|
playlist.value.splice(index, 1);
|
|
// TODO: 同步更新播放器端播放列表
|
|
};
|
|
|
|
const clearPlaylist = async () => {
|
|
playlist.value = [];
|
|
currentVideo.value = null;
|
|
// 发送空播放列表到播放器
|
|
await sendPlaybackCommand({ setPlaylist: { videos: [] } });
|
|
};
|
|
|
|
// 监听进度变化
|
|
watch(progress, (newProgress) => {
|
|
// TODO: 调用 Tauri API 设置播放进度
|
|
console.log("设置进度:", newProgress);
|
|
});
|
|
|
|
// 监听音量变化
|
|
watch(volume, (newVolume) => {
|
|
// TODO: 调用 Tauri API 设置音量
|
|
console.log("设置音量:", newVolume);
|
|
});
|
|
</script>
|