初步完成框架

This commit is contained in:
2025-08-16 22:41:14 +08:00
parent b0eed27371
commit ec0010aa93
36 changed files with 2996 additions and 743 deletions

View File

@@ -1,73 +1,14 @@
export default defineAppConfig({
app: {
name: "Nuxtor",
author: "Nicola Spadari",
repo: "https://github.com/NicolaSpadari/nuxtor",
tauriSite: "https://tauri.app",
nuxtSite: "https://nuxt.com",
nuxtUiSite: "https://ui.nuxt.dev"
},
pageCategories: {
system: {
label: "System",
icon: "lucide:square-terminal"
},
storage: {
label: "Storage",
icon: "lucide:archive"
},
interface: {
label: "Interface",
icon: "lucide:app-window-mac"
},
other: {
label: "Other",
icon: "lucide:folder"
}
name: "视频控制器",
version: "1.0.0",
author: "estel",
description: "基于 Nuxt 4 + Tauri 2 的视频播放控制应用"
},
ui: {
colors: {
primary: "green",
primary: "blue",
neutral: "zinc"
},
button: {
slots: {
base: "cursor-pointer"
}
},
formField: {
slots: {
root: "w-full"
}
},
input: {
slots: {
root: "w-full"
}
},
textarea: {
slots: {
root: "w-full",
base: "resize-none"
}
},
accordion: {
slots: {
trigger: "cursor-pointer",
item: "md:py-2"
}
},
navigationMenu: {
slots: {
link: "cursor-pointer"
},
variants: {
disabled: {
true: {
link: "cursor-text"
}
}
}
}
}
});

View File

@@ -1,11 +0,0 @@
<template>
<div class="top=1/5 pointer-events-none absolute inset-x-0 transform-gpu blur-3xl -z-10" aria-hidden="true">
<div class="blob relative left-[calc(50%+36rem)] aspect-[1155/678] w-[72.1875rem] from-(--color-warning) to-(--color-success) bg-gradient-to-br opacity-30 -translate-x-1/2" />
</div>
</template>
<style scoped>
.blob{
clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%);
}
</style>

View File

@@ -1,11 +0,0 @@
<template>
<div class="pointer-events-none absolute inset-x-0 transform-gpu blur-3xl -top-1/4 -z-10" aria-hidden="true">
<div class="blob relative left-[calc(50%-30rem)] aspect-[1155/678] w-[72.1875rem] rotate-30 from-(--color-warning) to-(--color-success) bg-gradient-to-tr opacity-30 -translate-x-1/2" />
</div>
</template>
<style scoped>
.blob{
clip-path: polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, 80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, 47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, 17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%);
}
</style>

View File

@@ -1,72 +0,0 @@
<template>
<header class="top-0 z-10">
<UContainer class="md:py-2">
<UNavigationMenu
:items="mobileItems"
variant="link"
:ui="{
root: 'md:hidden'
}"
/>
<UNavigationMenu
:items="desktopItems"
variant="link"
:ui="{
root: 'hidden md:flex',
viewportWrapper: 'max-w-2xl absolute-center-h',
list: 'md:gap-x-2'
}"
/>
</UContainer>
</header>
</template>
<script lang="ts" setup>
const { pages } = usePages();
const { showSidebar } = useSidebar();
const tauriVersion = await useTauriAppGetTauriVersion();
const mobileItems = ref<any[]>([
[
{
avatar: {
icon: "local:logo",
size: "xl",
ui: {
root: "bg-transparent"
}
},
to: "/"
}
],
[
{
icon: "lucide:menu",
onSelect: () => showSidebar.value = true
}
]
]);
const desktopItems = ref<any[]>([
[
{
avatar: {
icon: "local:logo",
size: "3xl",
ui: {
root: "group bg-transparent",
icon: "opacity-70 group-hover:opacity-100"
}
},
to: "/"
}
],
pages,
[
{
label: `v${tauriVersion}`,
disabled: true
}
]
]);
</script>

View File

@@ -1,37 +0,0 @@
<template>
<USlideover :open="showSidebar" @update:open="showSidebar = false">
<template #title>
<div class="flex gap-x-3">
<Icon name="local:logo" class="size-6" />
<span class="uppercase">{{ name }}</span>
</div>
</template>
<template #description>
<VisuallyHidden>Description</VisuallyHidden>
</template>
<template #body>
<UNavigationMenu
orientation="vertical"
:items="items"
/>
</template>
</USlideover>
</template>
<script lang="ts" setup>
const { app: { name } } = useAppConfig();
const { pages } = usePages();
const { showSidebar } = useSidebar();
const tauriVersion = await useTauriAppGetTauriVersion();
const items = ref<any[]>([
pages,
[
{
label: `v${tauriVersion}`,
disabled: true
}
]
]);
</script>

View File

@@ -1,8 +0,0 @@
<template>
<div>
<SiteNavbar class="sticky bg-(--ui-bg)/75 backdrop-blur" />
<SiteSidebar />
<slot />
</div>
</template>

View File

@@ -1,10 +1,60 @@
<template>
<div>
<SiteNavbar class="sticky bg-(--ui-bg)/75 backdrop-blur" />
<SiteSidebar />
<div class="min-h-screen bg-gray-50 dark:bg-gray-900 flex">
<!-- 左侧边栏 -->
<aside class="w-64 bg-white dark:bg-gray-800 shadow-sm border-r border-gray-200 dark:border-gray-700 flex flex-col">
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
<h1 class="text-xl font-semibold text-gray-900 dark:text-white">视频控制器</h1>
</div>
<nav class="flex-1 p-4">
<ul class="space-y-2">
<li>
<NuxtLink
to="/"
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors"
:class="$route.path === '/'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
>
<UIcon name="i-heroicons-home" class="mr-3 h-5 w-5" />
首页
</NuxtLink>
</li>
<li>
<NuxtLink
to="/settings"
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors"
:class="$route.path === '/settings'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
>
<UIcon name="i-heroicons-cog-6-tooth" class="mr-3 h-5 w-5" />
设置
</NuxtLink>
</li>
</ul>
</nav>
<!-- 关于 -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<NuxtLink
to="/about"
class="flex items-center px-3 py-2 text-sm font-medium rounded-md transition-colors"
:class="$route.path === '/about'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'"
>
<UIcon name="i-heroicons-information-circle" class="mr-3 h-5 w-5" />
关于
</NuxtLink>
</div>
</aside>
<UContainer>
<slot />
</UContainer>
<!-- 主体区域 -->
<main class="flex-1 overflow-auto">
<div class="p-6">
<slot />
</div>
</main>
</div>
</template>

View File

@@ -1,13 +0,0 @@
<template>
<div>
<SiteNavbar class="fixed w-full" />
<SiteSidebar />
<div class="relative overflow-hidden px-6 lg:px-8">
<DesignTopBlob />
<DesignBottomBlob />
<slot />
</div>
</div>
</template>

View File

@@ -1,26 +0,0 @@
<template>
<div class="grid place-items-center py-12 md:py-24">
<div class="flex flex-col items-center gap-y-4 md:gap-y-8">
<p class="text-(--ui-success) font-semibold">
404
</p>
<div class="text-center space-y-3">
<h1 class="text-3xl font-bold tracking-tight" sm="text-5xl">
Page not found
</h1>
<p class="text-base text-(--ui-muted) leading-7">
Sorry, we couldn't find the page you're looking for.
</p>
</div>
<UButton to="/" variant="outline" size="lg" :ui="{ base: 'px-5' }">
Go home
</UButton>
</div>
</div>
</template>
<script lang="ts" setup>
definePageMeta({
layout: "blank"
});
</script>

252
app/pages/about.vue Normal file
View File

@@ -0,0 +1,252 @@
<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="space-y-4">
<div class="flex items-center space-x-4">
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900 rounded-lg flex items-center justify-center">
<UIcon name="i-heroicons-tv" class="w-8 h-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">{{ appInfo.name }}</h3>
<p class="text-gray-600 dark:text-gray-400">版本 {{ appInfo.version }}</p>
</div>
</div>
<div class="space-y-2">
<p class="text-gray-700 dark:text-gray-300">{{ appInfo.description }}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
构建时间: {{ appInfo.buildDate }}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
作者: {{ appInfo.author }}
</p>
</div>
</div>
</UCard>
<!-- 技术栈 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">技术栈</h2>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-3">
<h4 class="font-medium text-gray-900 dark:text-white">前端技术</h4>
<div class="space-y-2">
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-green-100 dark:bg-green-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-green-500 rounded"></div>
</div>
<span class="text-sm">Nuxt 4 - 全栈 Vue.js 框架</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-blue-100 dark:bg-blue-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-blue-500 rounded"></div>
</div>
<span class="text-sm">Vue 3 - 渐进式 JavaScript 框架</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-cyan-100 dark:bg-cyan-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-cyan-500 rounded"></div>
</div>
<span class="text-sm">Tailwind CSS - 原子化 CSS 框架</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-emerald-100 dark:bg-emerald-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-emerald-500 rounded"></div>
</div>
<span class="text-sm">Nuxt UI - 现代化 UI 组件库</span>
</div>
</div>
</div>
<div class="space-y-3">
<h4 class="font-medium text-gray-900 dark:text-white">后端技术</h4>
<div class="space-y-2">
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-orange-100 dark:bg-orange-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-orange-500 rounded"></div>
</div>
<span class="text-sm">Tauri 2 - 跨平台桌面应用框架</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-red-100 dark:bg-red-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-red-500 rounded"></div>
</div>
<span class="text-sm">Rust - 系统级编程语言</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-6 h-6 bg-purple-100 dark:bg-purple-900 rounded flex items-center justify-center">
<div class="w-3 h-3 bg-purple-500 rounded"></div>
</div>
<span class="text-sm">WebSocket - 实时通信协议</span>
</div>
</div>
</div>
</div>
</UCard>
<!-- 功能特性 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">功能特性</h2>
</template>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="space-y-2">
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">远程视频播放控制</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">实时播放进度同步</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">音量调节控制</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">播放列表管理</span>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">循环播放设置</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">全屏控制</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">自动重连功能</span>
</div>
<div class="flex items-center space-x-2">
<UIcon name="i-heroicons-check-circle" class="w-5 h-5 text-green-500" />
<span class="text-sm">响应式界面设计</span>
</div>
</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>
<h4 class="font-medium text-gray-900 dark:text-white mb-2">支持的操作系统</h4>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="flex items-center space-x-2 text-sm">
<UIcon name="i-heroicons-computer-desktop" class="w-5 h-5 text-gray-500" />
<span>Windows 10/11</span>
</div>
<div class="flex items-center space-x-2 text-sm">
<UIcon name="i-heroicons-computer-desktop" class="w-5 h-5 text-gray-500" />
<span>macOS 10.15+</span>
</div>
<div class="flex items-center space-x-2 text-sm">
<UIcon name="i-heroicons-computer-desktop" class="w-5 h-5 text-gray-500" />
<span>Linux (x64)</span>
</div>
</div>
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white mb-2">网络要求</h4>
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
<li> 与视频播放器处于同一局域网</li>
<li> TCP/IP 网络连接</li>
<li> 端口访问权限 (默认 8080)</li>
</ul>
</div>
</div>
</UCard>
<!-- 开源许可 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">开源许可</h2>
</template>
<div class="space-y-3">
<p class="text-sm text-gray-600 dark:text-gray-400">
本软件基于 MIT 许可证开源您可以自由使用修改和分发
</p>
<div class="flex space-x-4">
<UButton variant="outline" size="sm">
<UIcon name="i-heroicons-code-bracket" class="w-4 h-4 mr-2" />
查看源码
</UButton>
<UButton variant="outline" size="sm">
<UIcon name="i-heroicons-document-text" class="w-4 h-4 mr-2" />
许可协议
</UButton>
</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">
<p class="text-sm text-gray-600 dark:text-gray-400">
遇到问题或有建议欢迎通过以下方式联系我们
</p>
<div class="flex flex-wrap gap-2">
<UButton variant="outline" size="sm">
<UIcon name="i-heroicons-bug-ant" class="w-4 h-4 mr-2" />
报告问题
</UButton>
<UButton variant="outline" size="sm">
<UIcon name="i-heroicons-light-bulb" class="w-4 h-4 mr-2" />
功能建议
</UButton>
<UButton variant="outline" size="sm">
<UIcon name="i-heroicons-question-mark-circle" class="w-4 h-4 mr-2" />
使用帮助
</UButton>
</div>
</div>
</UCard>
</div>
</template>
<script lang="ts" setup>
interface AppInfo {
name: string
version: string
description: string
author: string
buildDate: string
}
const { app } = useAppConfig()
// 应用信息
const appInfo = ref<AppInfo>({
name: app.name,
version: app.version,
description: app.description,
author: app.author,
buildDate: new Date().toLocaleDateString('zh-CN')
})
// 页面加载时获取构建信息
onMounted(async () => {
try {
// TODO: 调用 Tauri API 获取应用构建信息
console.log('获取应用信息')
} catch (error) {
console.error('获取应用信息失败:', error)
}
})
</script>

View File

@@ -1,63 +0,0 @@
<template>
<LayoutTile
title="Commands"
description="Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
>
<div class="space-y-6 md:space-y-8">
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="sendCommand">
<UFormField label="Command input" name="input">
<UInput v-model="inputState.input" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Send command
</UButton>
</UForm>
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
<UFormField label="Command output" name="command-output">
<UTextarea v-model="outputState.output" variant="subtle" size="lg" :rows="8" readonly />
</UFormField>
</UForm>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Shell commands",
icon: "lucide:terminal",
description: "Execute shell commands",
category: "system"
});
const schema = z.object({
input: z.string({
error: "Input is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
input: undefined
});
const outputState = ref({
output: ""
});
const sendCommand = async () => {
try {
const response = await useTauriShellCommand.create("exec-sh", [
"-c",
inputState.value.input!
]).execute();
outputState.value.output = JSON.stringify(response, null, 4);
} catch (error) {
outputState.value.output = JSON.stringify(error, null, 4);
} finally {
inputState.value.input = undefined;
}
};
</script>

View File

@@ -1,83 +0,0 @@
<template>
<LayoutTile
title="File System"
description="Access the file system. For this demo the only allowed permission is read/write to the Documents folder (no sub directories)."
>
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createFile">
<UFormField label="Text file name (with extension)" name="fileName">
<UInput v-model="inputState.fileName" variant="subtle" size="lg" />
</UFormField>
<UFormField label="File content" name="fileContent">
<UTextarea v-model="inputState.fileContent" variant="subtle" size="lg" :rows="10" />
</UFormField>
<UButton type="submit" size="lg">
Create file
</UButton>
</UForm>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Files",
icon: "lucide:file-text",
category: "storage",
description: "Create and manage files"
});
const schema = z.object({
fileName: z.string({
error: "File name is required"
}).nonempty().regex(/^[\w,\s-]+\.[A-Z0-9]+$/i, {
message: "Invalid filename format"
}),
fileContent: z.string({
error: "File content is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
fileName: undefined,
fileContent: undefined
});
const toast = useToast();
const createFile = async () => {
try {
const fileExists = await useTauriFsExists(inputState.value.fileName!, {
baseDir: useTauriFsBaseDirectory.Document
});
if (fileExists) {
toast.add({
title: "Error",
description: "The file already exists",
color: "error"
});
return;
}
await useTauriFsWriteTextFile(inputState.value.fileName!, inputState.value.fileContent!, {
baseDir: useTauriFsBaseDirectory.Document
});
toast.add({
title: "Success",
description: "The file has been created",
color: "success"
});
inputState.value.fileName = inputState.value.fileContent = undefined;
} catch (err) {
toast.add({
title: "Error",
description: String(err),
color: "error"
});
}
};
</script>

View File

@@ -1,73 +1,252 @@
<template>
<UContainer class="relative overflow-hidden h-screen">
<div class="grid size-full place-content-center gap-y-8">
<SvgoLogo :filled="true" :font-controlled="false" class="mx-auto size-40" />
<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"></div>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ connectionStatusText }}
</span>
</div>
<UButton
@click="toggleConnection"
:variant="connectionStatus === 'connected' ? 'soft' : 'solid'"
:color="connectionStatus === 'connected' ? 'red' : 'blue'"
size="sm"
>
{{ connectionStatus === 'connected' ? '断开连接' : '连接' }}
</UButton>
</div>
<div class="mt-4 text-sm text-gray-600 dark:text-gray-400">
<p>视频播放器地址: {{ playerAddress }}</p>
</div>
</UCard>
<div class="flex flex-col items-center gap-y-3">
<h1 class="animate-pulse text-3xl sm:text-4xl text-pretty font-bold font-heading md:mb-5">
{{ app.name.toUpperCase() }}
</h1>
<p class="leading-7 text-pretty">
Powered by
</p>
<!-- 视频预览区域 -->
<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>
<div class="flex flex-wrap justify-center gap-1 md:gap-3">
<UButton
variant="ghost"
size="xl"
:to="app.nuxtSite"
target="_blank"
external
>
Nuxt 4
<!-- 播放控制 -->
<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 @click="playVideo" :disabled="!isConnected" color="green" size="lg">
<UIcon name="i-heroicons-play" class="w-5 h-5 mr-2" />
播放
</UButton>
<UButton
variant="ghost"
size="xl"
:to="app.tauriSite"
target="_blank"
external
>
Tauri 2
<UButton @click="pauseVideo" :disabled="!isConnected" color="orange" size="lg">
<UIcon name="i-heroicons-pause" class="w-5 h-5 mr-2" />
暂停
</UButton>
<UButton
variant="ghost"
size="xl"
:to="app.nuxtUiSite"
target="_blank"
external
>
NuxtUI 3
<UButton @click="stopVideo" :disabled="!isConnected" color="red" size="lg">
<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 @click="toggleLoop" :disabled="!isConnected" :variant="isLooping ? 'solid' : 'outline'" size="sm">
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 mr-1" />
循环播放
</UButton>
<UButton @click="toggleFullscreen" :disabled="!isConnected" variant="outline" size="sm">
<UIcon name="i-heroicons-arrows-pointing-out" class="w-4 h-4 mr-1" />
全屏
</UButton>
<UButton @click="openVideoFile" variant="outline" size="sm">
<UIcon name="i-heroicons-folder-open" class="w-4 h-4 mr-1" />
打开文件
</UButton>
</div>
</div>
</UCard>
<div class="flex justify-center">
<UButton
:to="app.repo"
>
Star on GitHub
</UButton>
<!-- 播放列表 -->
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">播放列表</h2>
<UButton @click="clearPlaylist" variant="ghost" size="sm" color="red">
<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 @click="playVideoFromPlaylist(index)" size="xs" variant="ghost">
<UIcon name="i-heroicons-play" class="w-4 h-4" />
</UButton>
<UButton @click="removeFromPlaylist(index)" size="xs" variant="ghost" color="red">
<UIcon name="i-heroicons-x-mark" class="w-4 h-4" />
</UButton>
</div>
</div>
</div>
</div>
</div>
<div class="fixed bottom-6 text-sm absolute-center-h">
<div class="flex items-center gap-1 text-(--ui-text-muted)">
<p class="text-sm">
Made by
</p>
<ULink :to="app.repo" external target="_blank">
{{ app.author }}
</ULink>
</div>
</div>
</UContainer>
</UCard>
</div>
</template>
<script lang="ts" setup>
const { app } = useAppConfig();
interface VideoControllerState {
connectionStatus: 'connected' | 'connecting' | 'disconnected'
currentVideo: string | null
progress: number
volume: number
isLooping: boolean
playlist: string[]
playerAddress: string
}
definePageMeta({
layout: "home"
});
// 响应式状态
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('192.168.1.100:8080')
// 计算属性
const isConnected = computed(() => connectionStatus.value === 'connected')
const connectionStatusText = computed(() => {
switch (connectionStatus.value) {
case 'connected': return '已连接'
case 'connecting': return '连接中...'
default: return '未连接'
}
})
// 方法
const toggleConnection = async () => {
if (connectionStatus.value === 'connected') {
// 断开连接
connectionStatus.value = 'disconnected'
} else {
// 尝试连接
connectionStatus.value = 'connecting'
// TODO: 在这里调用 Tauri API 进行实际连接
setTimeout(() => {
connectionStatus.value = 'connected' // 模拟连接成功
}, 1000)
}
}
const playVideo = async () => {
// TODO: 调用 Tauri API
console.log('播放视频')
}
const pauseVideo = async () => {
// TODO: 调用 Tauri API
console.log('暂停视频')
}
const stopVideo = async () => {
// TODO: 调用 Tauri API
console.log('停止视频')
}
const toggleLoop = async () => {
isLooping.value = !isLooping.value
// TODO: 调用 Tauri API
console.log('循环播放:', isLooping.value)
}
const toggleFullscreen = async () => {
// TODO: 调用 Tauri API
console.log('切换全屏')
}
const openVideoFile = async () => {
// TODO: 调用 Tauri API 打开文件选择器
console.log('打开文件')
// 模拟添加到播放列表
playlist.value.push(`示例视频${playlist.value.length + 1}.mp4`)
}
const playVideoFromPlaylist = async (index: number) => {
currentVideo.value = playlist.value[index]
// TODO: 调用 Tauri API 播放指定视频
console.log('播放:', currentVideo.value)
}
const removeFromPlaylist = (index: number) => {
playlist.value.splice(index, 1)
}
const clearPlaylist = () => {
playlist.value = []
currentVideo.value = null
}
// 监听进度变化
watch(progress, (newProgress) => {
// TODO: 调用 Tauri API 设置播放进度
console.log('设置进度:', newProgress)
})
// 监听音量变化
watch(volume, (newVolume) => {
// TODO: 调用 Tauri API 设置音量
console.log('设置音量:', newVolume)
})
</script>

View File

@@ -1,70 +0,0 @@
<template>
<LayoutTile
title="Notifications"
description="Send native notifications to the client using the notification plugin."
>
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="createNotification">
<UFormField label="Notification title" name="notificationTitle">
<UInput v-model="inputState.notificationTitle" variant="subtle" size="lg" />
</UFormField>
<UFormField label="Notification body (optional)" name="notificationBody">
<UInput v-model="inputState.notificationBody" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Send notification
</UButton>
</UForm>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Notifications",
icon: "lucide:message-square-more",
category: "interface",
description: "Send native notifications"
});
const schema = z.object({
notificationTitle: z.string({
error: "Title is required"
}).nonempty(),
notificationBody: z.string().optional()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
notificationTitle: undefined,
notificationBody: undefined
});
const toast = useToast();
const permissionGranted = ref(false);
const createNotification = async () => {
permissionGranted.value = await useTauriNotificationIsPermissionGranted();
if (!permissionGranted.value) {
const permission = await useTauriNotificationRequestPermission();
permissionGranted.value = permission === "granted";
}
if (permissionGranted.value) {
useTauriNotificationSendNotification({
title: inputState.value.notificationTitle!,
body: inputState.value.notificationBody
});
inputState.value.notificationTitle = inputState.value.notificationBody = undefined;
} else {
toast.add({
title: "Error",
description: "Missing notifications permission",
color: "error"
});
}
};
</script>

View File

@@ -1,35 +0,0 @@
<template>
<LayoutTile
title="OS Information"
description="Read information about the operating system using the OS Information plugin."
>
<UAccordion :items="items" type="multiple" />
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "OS Informations",
icon: "lucide:info",
category: "system",
description: "Read operating system informations."
});
const items = ref([
{
label: "System",
icon: "lucide:monitor",
content: `${useTauriOsPlatform()} ${useTauriOsVersion()}`
},
{
label: "Arch",
icon: "lucide:microchip",
content: useTauriOsArch()
},
{
label: "Locale",
icon: "lucide:globe",
content: await useTauriOsLocale() || "Not detectable"
}
]);
</script>

268
app/pages/settings.vue Normal file
View File

@@ -0,0 +1,268 @@
<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="space-y-4">
<UFormGroup label="视频播放器地址" description="设置要连接的视频播放器的IP地址和端口">
<div class="flex space-x-2">
<UInput v-model="settings.playerHost" placeholder="192.168.1.100" class="flex-1" />
<span class="self-center text-gray-500">:</span>
<UInput v-model="settings.playerPort" type="number" placeholder="8080" class="w-24" />
</div>
</UFormGroup>
<UFormGroup label="连接超时时间 (秒)" description="连接超时的时间设置">
<UInput v-model.number="settings.connectionTimeout" type="number" min="1" max="60" />
</UFormGroup>
<UFormGroup label="自动重连" description="连接断开后是否自动尝试重连">
<UToggle v-model="settings.autoReconnect" />
</UFormGroup>
<UFormGroup label="重连间隔 (秒)" description="自动重连的间隔时间">
<UInput
v-model.number="settings.reconnectInterval"
type="number"
min="1"
max="300"
:disabled="!settings.autoReconnect"
/>
</UFormGroup>
</div>
</UCard>
<!-- 播放设置 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">播放设置</h2>
</template>
<div class="space-y-4">
<UFormGroup label="默认音量" description="新视频播放时的默认音量">
<div class="flex items-center space-x-4">
<URange v-model="settings.defaultVolume" :min="0" :max="100" class="flex-1" />
<span class="text-sm text-gray-600 dark:text-gray-400 min-w-[3rem]">{{ settings.defaultVolume }}%</span>
</div>
</UFormGroup>
<UFormGroup label="默认循环播放" description="是否默认启用循环播放">
<UToggle v-model="settings.defaultLoop" />
</UFormGroup>
<UFormGroup label="自动全屏" description="播放视频时是否自动全屏">
<UToggle v-model="settings.autoFullscreen" />
</UFormGroup>
<UFormGroup label="播放完成后行为">
<USelectMenu
v-model="settings.playbackEndBehavior"
:options="playbackEndOptions"
option-attribute="label"
value-attribute="value"
/>
</UFormGroup>
</div>
</UCard>
<!-- 界面设置 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">界面设置</h2>
</template>
<div class="space-y-4">
<UFormGroup label="主题模式">
<USelectMenu
v-model="settings.theme"
:options="themeOptions"
option-attribute="label"
value-attribute="value"
/>
</UFormGroup>
<UFormGroup label="语言">
<USelectMenu
v-model="settings.language"
:options="languageOptions"
option-attribute="label"
value-attribute="value"
/>
</UFormGroup>
<UFormGroup label="显示通知" description="是否显示操作通知">
<UToggle v-model="settings.showNotifications" />
</UFormGroup>
</div>
</UCard>
<!-- 高级设置 -->
<UCard>
<template #header>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">高级设置</h2>
</template>
<div class="space-y-4">
<UFormGroup label="调试模式" description="启用详细的日志记录">
<UToggle v-model="settings.debugMode" />
</UFormGroup>
<UFormGroup label="缓存大小 (MB)" description="播放列表和缩略图缓存大小">
<UInput v-model.number="settings.cacheSize" type="number" min="10" max="1000" />
</UFormGroup>
<UFormGroup label="网络代理" description="设置网络代理(可选)">
<UInput v-model="settings.proxy" placeholder="http://proxy.example.com:8080" />
</UFormGroup>
</div>
</UCard>
<!-- 操作按钮 -->
<div class="flex justify-between">
<UButton @click="resetSettings" variant="outline" color="gray">
<UIcon name="i-heroicons-arrow-path" class="w-4 h-4 mr-2" />
重置设置
</UButton>
<div class="space-x-2">
<UButton @click="exportSettings" variant="outline">
<UIcon name="i-heroicons-arrow-up-tray" class="w-4 h-4 mr-2" />
导出配置
</UButton>
<UButton @click="importSettings" variant="outline">
<UIcon name="i-heroicons-arrow-down-tray" class="w-4 h-4 mr-2" />
导入配置
</UButton>
<UButton @click="saveSettings" color="blue">
<UIcon name="i-heroicons-check" class="w-4 h-4 mr-2" />
保存设置
</UButton>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
interface AppSettings {
playerHost: string
playerPort: number
connectionTimeout: number
autoReconnect: boolean
reconnectInterval: number
defaultVolume: number
defaultLoop: boolean
autoFullscreen: boolean
playbackEndBehavior: string
theme: string
language: string
showNotifications: boolean
debugMode: boolean
cacheSize: number
proxy: string
}
// 响应式设置数据
const settings = ref<AppSettings>({
playerHost: '192.168.1.100',
playerPort: 8080,
connectionTimeout: 10,
autoReconnect: true,
reconnectInterval: 5,
defaultVolume: 50,
defaultLoop: false,
autoFullscreen: false,
playbackEndBehavior: 'stop',
theme: 'system',
language: 'zh-CN',
showNotifications: true,
debugMode: false,
cacheSize: 100,
proxy: ''
})
// 选项数据
const playbackEndOptions = [
{ label: '停止播放', value: 'stop' },
{ label: '播放下一个', value: 'next' },
{ label: '重复播放', value: 'repeat' }
]
const themeOptions = [
{ label: '跟随系统', value: 'system' },
{ label: '浅色模式', value: 'light' },
{ label: '深色模式', value: 'dark' }
]
const languageOptions = [
{ label: '简体中文', value: 'zh-CN' },
{ label: 'English', value: 'en' },
{ label: '日本語', value: 'ja' }
]
// 方法
const saveSettings = async () => {
try {
// TODO: 调用 Tauri API 保存设置
console.log('保存设置:', settings.value)
// 显示成功通知
// useToast().add({ title: '设置已保存', color: 'green' })
} catch (error) {
console.error('保存设置失败:', error)
// 显示错误通知
// useToast().add({ title: '保存失败', color: 'red' })
}
}
const resetSettings = async () => {
// 重置为默认设置
settings.value = {
playerHost: '192.168.1.100',
playerPort: 8080,
connectionTimeout: 10,
autoReconnect: true,
reconnectInterval: 5,
defaultVolume: 50,
defaultLoop: false,
autoFullscreen: false,
playbackEndBehavior: 'stop',
theme: 'system',
language: 'zh-CN',
showNotifications: true,
debugMode: false,
cacheSize: 100,
proxy: ''
}
}
const exportSettings = async () => {
try {
// TODO: 调用 Tauri API 导出设置到文件
console.log('导出设置')
} catch (error) {
console.error('导出设置失败:', error)
}
}
const importSettings = async () => {
try {
// TODO: 调用 Tauri API 从文件导入设置
console.log('导入设置')
} catch (error) {
console.error('导入设置失败:', error)
}
}
// 页面加载时读取设置
onMounted(async () => {
try {
// TODO: 调用 Tauri API 读取保存的设置
console.log('加载设置')
} catch (error) {
console.error('加载设置失败:', error)
}
})
// 监听设置变化并自动保存关键设置
watch(() => [settings.value.playerHost, settings.value.playerPort], async () => {
// 自动保存连接相关设置
await saveSettings()
}, { debounce: 1000 })
</script>

View File

@@ -1,91 +0,0 @@
<template>
<LayoutTile
title="Store"
description="Persistent key-value store. Allows you to handle state to a file which can be saved and loaded on demand including between app restarts."
>
<div class="space-y-6 md:space-y-8">
<UForm :state="inputState" :schema="schema" class="flex flex-col gap-y-4 items-end" @submit="setStoreValue">
<UFormField label="Store value" name="value">
<UInput v-model="inputState.value" variant="subtle" size="lg" />
</UFormField>
<UButton type="submit" size="lg">
Set value
</UButton>
</UForm>
<UForm :state="outputState" class="flex flex-col gap-y-4 items-end">
<UFormField label="Store content" name="content">
<UTextarea v-model="outputState.content" variant="subtle" size="lg" :rows="8" readonly />
</UFormField>
</UForm>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Store",
icon: "lucide:database",
category: "storage",
description: "Handle file creation in the file system"
});
const schema = z.object({
value: z.string({
error: "Store key is required"
}).nonempty()
});
type Schema = zInfer<typeof schema>;
const inputState = ref<Partial<Schema>>({
value: undefined
});
const outputState = ref({
content: ""
});
const toast = useToast();
const autosave = ref(false);
const store = await useTauriStoreLoad("store.bin", {
autoSave: autosave.value
});
const getStoreValue = async () => {
try {
outputState.value.content = await store.get<string>("myData") || "";
} catch (error) {
toast.add({
title: "Error",
description: String(error),
color: "error"
});
outputState.value.content = JSON.stringify(error, null, 4);
}
};
await getStoreValue();
const setStoreValue = async () => {
try {
await store.set("myData", inputState.value!.value);
await getStoreValue();
toast.add({
title: "Success",
description: "Store value retieved",
color: "success"
});
} catch (error) {
toast.add({
title: "Error",
description: String(error),
color: "error"
});
outputState.value.content = JSON.stringify(error, null, 4);
} finally {
inputState.value.value = undefined;
}
};
</script>

View File

@@ -1,51 +0,0 @@
<template>
<LayoutTile
title="Webview window"
description="Create new webview in a detached window. This will create a new window flagged 'secondary' that has the same permissions as the main one. If you need more windows, update the permissions under capabilities > main or create a new capabilities file for the new window only."
>
<div class="flex flex-col items-center gap-6">
<UButton variant="subtle" @click="openWindow((new Date).valueOf().toString(), app.repo)">
Create external Webview
</UButton>
<UButton variant="subtle" @click="openWindow('secondary', '/os')">
Create internal Webview
</UButton>
</div>
</LayoutTile>
</template>
<script lang="ts" setup>
definePageMeta({
name: "Webview",
icon: "lucide:app-window",
category: "interface",
description: "Create new webview in a detached window"
});
const { app } = useAppConfig();
const toast = useToast();
const openWindow = async (id: string, page: string) => {
const webview = new useTauriWebviewWindowWebviewWindow(id, {
title: "Nuxtor webview",
url: page,
width: 1280,
height: 720
});
webview.once("tauri://created", () => {
toast.add({
title: "Success",
description: "Webview created",
color: "success"
});
});
webview.once("tauri://error", (error) => {
toast.add({
title: "Error",
description: (error as any).payload,
color: "error"
});
});
};
</script>