mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-09 01:27:27 +08:00
feat: 前端支持管理插件
This commit is contained in:
@@ -8,6 +8,7 @@ import DownloadSettings from './components/DownloadSettings.vue'
|
|||||||
import FmtSettings from './components/FmtSettings.vue'
|
import FmtSettings from './components/FmtSettings.vue'
|
||||||
import NetworkSettings from './components/NetworkSettings.vue'
|
import NetworkSettings from './components/NetworkSettings.vue'
|
||||||
import AssDanmakuSettings from './components/AssDanmakuSettings.vue'
|
import AssDanmakuSettings from './components/AssDanmakuSettings.vue'
|
||||||
|
import PluginSettings from './components/PluginSettings.vue'
|
||||||
import { NButton, NDialog, NModal, NTabPane, NTabs, useMessage } from 'naive-ui'
|
import { NButton, NDialog, NModal, NTabPane, NTabs, useMessage } from 'naive-ui'
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
@@ -53,7 +54,7 @@ async function showConfigInFileManager() {
|
|||||||
<n-modal v-if="store.config !== undefined" v-model:show="showing">
|
<n-modal v-if="store.config !== undefined" v-model:show="showing">
|
||||||
<n-dialog :showIcon="false" content-style="" @close="showing = false">
|
<n-dialog :showIcon="false" content-style="" @close="showing = false">
|
||||||
<div class="flex flex-col gap-row-2">
|
<div class="flex flex-col gap-row-2">
|
||||||
<n-tabs class="h-full" v-model:value="currentTabName" type="line" size="small" animated>
|
<n-tabs class="h-full settings-tabs" v-model:value="currentTabName" type="line" size="small" animated>
|
||||||
<n-tab-pane name="download_settings" tab="下载内容">
|
<n-tab-pane name="download_settings" tab="下载内容">
|
||||||
<DownloadSettings />
|
<DownloadSettings />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -66,9 +67,18 @@ async function showConfigInFileManager() {
|
|||||||
<n-tab-pane name="network_settings" tab="网络">
|
<n-tab-pane name="network_settings" tab="网络">
|
||||||
<NetworkSettings />
|
<NetworkSettings />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="plugin_settings" tab="插件">
|
||||||
|
<PluginSettings />
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|
||||||
<n-button class="ml-auto mt-2" size="small" @click="showConfigInFileManager">打开配置目录</n-button>
|
<n-button
|
||||||
|
v-if="currentTabName !== 'plugin_settings'"
|
||||||
|
class="ml-auto mt-2"
|
||||||
|
size="small"
|
||||||
|
@click="showConfigInFileManager">
|
||||||
|
打开配置目录
|
||||||
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-dialog>
|
</n-dialog>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
<script setup lang="tsx">
|
||||||
|
import { computed, defineComponent, onMounted, onUnmounted, ref, type PropType } from 'vue'
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { NButton, NCollapseTransition, NInputNumber, NSwitch, NTag, useMessage, NEmpty, NIcon } from 'naive-ui'
|
||||||
|
import { commands, events, PluginInfo } from '../../../bindings.ts'
|
||||||
|
import { PhPlusCircle } from '@phosphor-icons/vue'
|
||||||
|
import { path } from '@tauri-apps/api'
|
||||||
|
import { appDataDir } from '@tauri-apps/api/path'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const pluginInfos = ref<Map<string, PluginInfo>>(new Map())
|
||||||
|
|
||||||
|
const sortedPluginInfos = computed<PluginInfo[]>(() =>
|
||||||
|
Array.from(pluginInfos.value.values()).sort((a, b) => b.priority - a.priority),
|
||||||
|
)
|
||||||
|
const expandedPluginIds = ref<Set<string>>(new Set())
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const infos = await commands.getPluginInfos()
|
||||||
|
|
||||||
|
const pluginMap = new Map<string, PluginInfo>()
|
||||||
|
|
||||||
|
for (const info of infos) {
|
||||||
|
pluginMap.set(info.path, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginInfos.value = pluginMap
|
||||||
|
})
|
||||||
|
|
||||||
|
let unListenPluginEvent: (() => void) | undefined
|
||||||
|
onMounted(() => {
|
||||||
|
events.pluginEvent
|
||||||
|
.listen(({ payload: { event, data } }) => {
|
||||||
|
if (event === 'Loaded') {
|
||||||
|
const info = data.plugin_info
|
||||||
|
|
||||||
|
pluginInfos.value.set(info.path, info)
|
||||||
|
} else if (event === 'Update') {
|
||||||
|
const info = data.plugin_info
|
||||||
|
|
||||||
|
const pluginInfo = pluginInfos.value.get(info.path)
|
||||||
|
if (pluginInfo !== undefined) {
|
||||||
|
Object.assign(pluginInfo, info)
|
||||||
|
}
|
||||||
|
} else if (event === 'Uninstall') {
|
||||||
|
const pluginPath = data.plugin_path
|
||||||
|
|
||||||
|
pluginInfos.value.delete(pluginPath)
|
||||||
|
expandedPluginIds.value.delete(pluginPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((unListenFn) => {
|
||||||
|
unListenPluginEvent = unListenFn
|
||||||
|
})
|
||||||
|
})
|
||||||
|
onUnmounted(() => {
|
||||||
|
unListenPluginEvent?.()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function addPlugin() {
|
||||||
|
const selectedPath = await open({
|
||||||
|
directory: false,
|
||||||
|
multiple: false,
|
||||||
|
filters: [{ name: '', extensions: ['dll', 'so', 'dylib'] }],
|
||||||
|
})
|
||||||
|
if (selectedPath === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await commands.addPlugin(selectedPath)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('添加插件成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showPluginConfigInFileManager() {
|
||||||
|
const configPath = await path.join(await appDataDir(), 'plugin.json')
|
||||||
|
const result = await commands.showPathInFileManager(configPath)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PluginCard = defineComponent({
|
||||||
|
name: 'PluginCard',
|
||||||
|
props: {
|
||||||
|
pluginInfo: {
|
||||||
|
type: Object as PropType<PluginInfo>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
|
function toggleAdvanced(pluginPath: string) {
|
||||||
|
if (expandedPluginIds.value.has(pluginPath)) {
|
||||||
|
expandedPluginIds.value.delete(pluginPath)
|
||||||
|
} else {
|
||||||
|
expandedPluginIds.value.add(pluginPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAdvancedShown(pluginPath: string): boolean {
|
||||||
|
return expandedPluginIds.value.has(pluginPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
async function uninstallPlugin(pluginPath: string) {
|
||||||
|
const result = await commands.uninstallPlugin(pluginPath)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expandedPluginIds.value.delete(pluginPath)
|
||||||
|
message.success('卸载插件成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateEnabled(pluginPath: string, enabled: boolean) {
|
||||||
|
const pluginInfo = pluginInfos.value.get(pluginPath)
|
||||||
|
if (pluginInfo === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevEnabled = pluginInfo.enabled
|
||||||
|
|
||||||
|
pluginInfo.enabled = enabled
|
||||||
|
|
||||||
|
const result = await commands.setPluginEnabled(pluginPath, enabled)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
pluginInfo.enabled = prevEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updatePriority(pluginPath: string, priority: number | null) {
|
||||||
|
if (priority === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginInfo = pluginInfos.value.get(pluginPath)
|
||||||
|
if (pluginInfo === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevPriority = pluginInfo.priority
|
||||||
|
|
||||||
|
const result = await commands.setPluginPriority(pluginPath, priority)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
pluginInfo.priority = prevPriority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TagType = 'error' | 'default' | 'primary' | 'info' | 'success' | 'warning'
|
||||||
|
|
||||||
|
function getRuntimeStatusMeta(status: PluginInfo['runtime_status']): { text: string; type: TagType } {
|
||||||
|
switch (status) {
|
||||||
|
case 'Loaded':
|
||||||
|
return { text: '已加载', type: 'success' }
|
||||||
|
case 'Disabled':
|
||||||
|
return { text: '未启用', type: 'warning' }
|
||||||
|
case 'LoadFailed':
|
||||||
|
return { text: '加载失败', type: 'error' }
|
||||||
|
case 'Unknown':
|
||||||
|
return { text: '待加载', type: 'info' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFailurePolicyMeta(policy: PluginInfo['descriptor']['failure_policy']): {
|
||||||
|
text: string
|
||||||
|
type: TagType
|
||||||
|
} {
|
||||||
|
if (policy === 'FailClosed') {
|
||||||
|
return { text: 'FailClosed', type: 'warning' }
|
||||||
|
}
|
||||||
|
return { text: 'FailOpen', type: 'info' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeStatus = getRuntimeStatusMeta(props.pluginInfo.runtime_status)
|
||||||
|
const failurePolicy = getFailurePolicyMeta(props.pluginInfo.descriptor.failure_policy)
|
||||||
|
const advancedShowing = isAdvancedShown(props.pluginInfo.path)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="rounded border-1 border-solid border-gray-3 px-3 py-2 flex flex-col gap-1">
|
||||||
|
<div class="flex items-center flex-wrap gap-2">
|
||||||
|
<span class="font-bold text-sm">{props.pluginInfo.descriptor.name}</span>
|
||||||
|
<NTag size="small">v{props.pluginInfo.descriptor.version}</NTag>
|
||||||
|
<NTag size="small" type={runtimeStatus.type}>
|
||||||
|
{runtimeStatus.text}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-6 break-words">{props.pluginInfo.descriptor.description}</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<NCollapseTransition show={advancedShowing}>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="text-xs break-all">
|
||||||
|
<span class="font-bold">SDK API:</span>
|
||||||
|
<span class="ml-1">{props.pluginInfo.descriptor.sdk_api_version}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs break-all">
|
||||||
|
<span class="font-bold">ID:</span>
|
||||||
|
<span class="ml-1">{props.pluginInfo.descriptor.id}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs break-all">
|
||||||
|
<span class="font-bold">路径:</span>
|
||||||
|
<span class="ml-1">{props.pluginInfo.path}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1 flex-wrap">
|
||||||
|
<span class="text-xs font-bold">Hooks:</span>
|
||||||
|
{props.pluginInfo.descriptor.hooks.map((hookPoint) => (
|
||||||
|
<NTag key={hookPoint} size="small">
|
||||||
|
{hookPoint}
|
||||||
|
</NTag>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<span class="text-xs font-bold">失败策略:</span>
|
||||||
|
<NTag size="small" type={failurePolicy.type}>
|
||||||
|
{failurePolicy.text}
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NCollapseTransition>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">启用</span>
|
||||||
|
<NSwitch
|
||||||
|
size="small"
|
||||||
|
value={props.pluginInfo.enabled}
|
||||||
|
onUpdate:value={(value: boolean) => updateEnabled(props.pluginInfo.path, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm">优先级</span>
|
||||||
|
<NInputNumber
|
||||||
|
class="w-26"
|
||||||
|
size="small"
|
||||||
|
value={props.pluginInfo.priority}
|
||||||
|
onUpdate:value={(value: number | null) => updatePriority(props.pluginInfo.path, value)}
|
||||||
|
parse={(x: string) => Number(x)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full items-center gap-2">
|
||||||
|
<NButton size="small" onClick={() => toggleAdvanced(props.pluginInfo.path)}>
|
||||||
|
{advancedShowing ? '隐藏高级信息' : '显示高级信息'}
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<NButton
|
||||||
|
class="ml-auto"
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
ghost
|
||||||
|
onClick={() => uninstallPlugin(props.pluginInfo.path)}>
|
||||||
|
卸载
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-row-2">
|
||||||
|
<n-empty v-if="sortedPluginInfos.length === 0" description="暂无插件,点击 添加插件 按钮导入插件" />
|
||||||
|
<div v-else class="flex flex-col gap-2 max-h-60vh overflow-auto overflow-x-hidden pr-1">
|
||||||
|
<PluginCard v-for="pluginInfo in sortedPluginInfos" :key="pluginInfo.path" :plugin-info="pluginInfo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-2">
|
||||||
|
<n-button class="mr-auto" size="small" type="primary" @click="addPlugin">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="20">
|
||||||
|
<PhPlusCircle />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
添加插件
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button size="small" @click="showPluginConfigInFileManager">打开配置目录</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user