mirror of
https://github.com/lanyeeee/bilibili-video-downloader.git
synced 2026-09-08 17:16:52 +08:00
feat: 个人收藏夹和收藏夹搜索
This commit is contained in:
Vendored
+1
@@ -20,6 +20,7 @@ declare module 'vue' {
|
|||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
NEl: typeof import('naive-ui')['NEl']
|
NEl: typeof import('naive-ui')['NEl']
|
||||||
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||||
|
|||||||
@@ -1,3 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { commands, FavInfo } from '../../bindings.ts'
|
||||||
|
import { useStore } from '../../store.ts'
|
||||||
|
import FavPanel from './components/FavPanel.vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const favInfo = ref<FavInfo>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.userInfo,
|
||||||
|
async () => {
|
||||||
|
if (store.userInfo === undefined) {
|
||||||
|
favInfo.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFavFoldersResult = await commands.getFavFolders(store.userInfo.mid)
|
||||||
|
if (getFavFoldersResult.status === 'error') {
|
||||||
|
console.error(getFavFoldersResult.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const favFolders = getFavFoldersResult.data
|
||||||
|
const getFavInfoResult = await commands.getFavInfo({ media_list_id: favFolders.list[0].id, pn: 1 })
|
||||||
|
if (getFavInfoResult.status === 'error') {
|
||||||
|
console.error(getFavInfoResult.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
favInfo.value = getFavInfoResult.data
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>FavPane</div>
|
<div v-if="favInfo !== undefined" class="h-full">
|
||||||
|
<FavPanel v-model:fav-info="favInfo" />
|
||||||
|
</div>
|
||||||
|
<n-empty v-else class="mt-2" description="请先登录" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { MediaInFav } from '../../../bindings.ts'
|
||||||
|
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
|
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||||
|
import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../../../utils.tsx'
|
||||||
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import { navDownloadButtonRefKey } from '../../../injection_keys.ts'
|
||||||
|
import { SearchType } from '../../SearchPane/SearchPane.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
media: MediaInFav
|
||||||
|
downloadEpisode?: (media: MediaInFav) => Promise<void>
|
||||||
|
checkboxChecked?: (media: MediaInFav) => boolean
|
||||||
|
handleCheckboxClick?: (media: MediaInFav) => void
|
||||||
|
handleContextMenu?: (media: MediaInFav) => void
|
||||||
|
search?: (input: string, searchType: SearchType) => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||||
|
const rootDivRef = ref<HTMLDivElement>()
|
||||||
|
const downloadButtonRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
const openInBrowserHref = computed<string | undefined>(() => {
|
||||||
|
if (props.media.type === 2) {
|
||||||
|
return `https://www.bilibili.com/video/${props.media.bvid}/`
|
||||||
|
} else if (props.media.type === 12) {
|
||||||
|
return `https://www.bilibili.com/audio/au${props.media.id}`
|
||||||
|
} else if (props.media.type === 24) {
|
||||||
|
return `https://www.bilibili.com/bangumi/play/ep${props.media.id}`
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const hint = computed<string | undefined>(() => {
|
||||||
|
if (props.media.type === 12) {
|
||||||
|
return '不支持音乐下载'
|
||||||
|
} else if (props.media.type === 24) {
|
||||||
|
return '下载请点击左下角放大镜按钮'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleDownloadClick() {
|
||||||
|
if (props.downloadEpisode === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await props.downloadEpisode(props.media)
|
||||||
|
playDownloadAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
function playDownloadAnimation() {
|
||||||
|
if (rootDivRef.value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = downloadButtonRef.value
|
||||||
|
const to = navDownloadButtonRef?.value
|
||||||
|
|
||||||
|
if (from instanceof Element && to !== undefined) {
|
||||||
|
if (isElementInViewport(rootDivRef.value)) {
|
||||||
|
// 只有卡片在视口内才播放动画
|
||||||
|
playTaskToQueueAnimation(from, to)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function searchInSearchPane() {
|
||||||
|
if (props.search === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.media.type === 2) {
|
||||||
|
props.search(props.media.bvid, 'Normal')
|
||||||
|
} else if (props.media.type === 24) {
|
||||||
|
props.search(`ep${props.media.id}`, 'Bangumi')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ playDownloadAnimation, media: props.media })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-200px relative p-3 rounded-lg"
|
||||||
|
ref="rootDivRef"
|
||||||
|
:title="hint"
|
||||||
|
@contextmenu="handleContextMenu?.(media)">
|
||||||
|
<SimpleCheckbox
|
||||||
|
v-if="media.attr === 0 && handleCheckboxClick !== undefined && checkboxChecked !== undefined"
|
||||||
|
class="absolute top-6 left-6 z-1 backdrop-blur-2"
|
||||||
|
:checked="checkboxChecked(media)"
|
||||||
|
:on-click="() => handleCheckboxClick?.(media)" />
|
||||||
|
<div v-if="media.type === 24" class="absolute top-6 right-6 z-1 bg-[#ff6699] text-white px-1 rounded">番剧</div>
|
||||||
|
<div v-else-if="media.type === 12" class="absolute top-6 right-6 z-1 bg-green-5 text-white px-1 rounded">音乐</div>
|
||||||
|
<img
|
||||||
|
class="w-200px h-125px rounded-lg object-cover lazyload"
|
||||||
|
:data-src="`${ensureHttps(media.cover)}@672w_378h_1c.webp`"
|
||||||
|
:key="media.cover"
|
||||||
|
alt=""
|
||||||
|
draggable="false" />
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col h-45px mt-2">
|
||||||
|
<span class="line-clamp-2" :title="media.title">{{ media.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center whitespace-nowrap text-gray text-12px w-full overflow-hidden">
|
||||||
|
<a
|
||||||
|
v-if="media.type !== 24"
|
||||||
|
class="min-w-0 color-inherit no-underline hover:text-sky-5 mr-1"
|
||||||
|
:href="`https://space.bilibili.com/${media.upper.mid}`"
|
||||||
|
target="_blank"
|
||||||
|
draggable="false">
|
||||||
|
<div class="truncate text-ellipsis" :title="media.upper.name">{{ media.upper.name }}</div>
|
||||||
|
</a>
|
||||||
|
<div v-else class="truncate text-ellipsis">{{ media.intro }}</div>
|
||||||
|
|
||||||
|
<span class="ml-auto flex-shrink-0" title="收藏时间">
|
||||||
|
<n-time unix type="date" :time="media.fav_time" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-1 items-center">
|
||||||
|
<a
|
||||||
|
:href="openInBrowserHref"
|
||||||
|
target="_blank"
|
||||||
|
draggable="false"
|
||||||
|
title="在浏览器中打开"
|
||||||
|
class="p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6">
|
||||||
|
<PhGoogleChromeLogo :size="24" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="search !== undefined"
|
||||||
|
title="在下载器内搜索"
|
||||||
|
class="cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||||
|
@click="searchInSearchPane">
|
||||||
|
<PhMagnifyingGlass :size="24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="props.downloadEpisode !== undefined"
|
||||||
|
ref="downloadButtonRef"
|
||||||
|
title="一键下载"
|
||||||
|
class="ml-auto cursor-pointer p-1 rounded-lg flex items-center justify-between text-gray-6 hover:bg-sky-5 hover:text-white active:bg-sky-6"
|
||||||
|
@click="handleDownloadClick">
|
||||||
|
<PhDownloadSimple :size="24" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
|
import { commands, FavFolders, Folder, FavInfo, MediaInFav } from '../../../bindings.ts'
|
||||||
|
import { SelectionArea } from '@viselect/vue'
|
||||||
|
import { useEpisodeDropdown, useEpisodeSelection } from '../../../utils.tsx'
|
||||||
|
import { SelectOption } from 'naive-ui'
|
||||||
|
import { searchPaneRefKey } from '../../../injection_keys.ts'
|
||||||
|
import FavCard from './FavCard.vue'
|
||||||
|
|
||||||
|
const favInfo = defineModel<FavInfo>('favInfo', { required: true })
|
||||||
|
|
||||||
|
const searchPaneRef = inject(searchPaneRefKey)
|
||||||
|
|
||||||
|
const favFolders = ref<FavFolders>()
|
||||||
|
const selectedMediaListId = ref<number>(favInfo.value.info.id)
|
||||||
|
const currentPage = ref<number>(1)
|
||||||
|
|
||||||
|
const selectedFolder = computed<Folder | undefined>(() => {
|
||||||
|
return favFolders.value?.list.find((folder) => folder.id === selectedMediaListId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectOptions = computed<SelectOption[]>(() => {
|
||||||
|
if (favFolders.value === undefined) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return favFolders.value.list.map((folder) => ({
|
||||||
|
label: folder.title,
|
||||||
|
value: folder.id,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageCount = computed<number>(() => {
|
||||||
|
if (selectedFolder.value === undefined) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return Math.ceil(selectedFolder.value.media_count / 36)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const result = await commands.getFavFolders(favInfo.value.info.mid)
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
favFolders.value = result.data
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedMediaListId.value,
|
||||||
|
() => {
|
||||||
|
if (favFolders.value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getFav(1)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function getFav(page: number) {
|
||||||
|
currentPage.value = page
|
||||||
|
const result = await commands.getFavInfo({ media_list_id: selectedMediaListId.value, pn: page })
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
favInfo.value = result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
const favCardRefs = ref<InstanceType<typeof FavCard>[]>([])
|
||||||
|
const favCardRefsMap = computed<Map<number, InstanceType<typeof FavCard>>>(() => {
|
||||||
|
const map = new Map<number, InstanceType<typeof FavCard>>()
|
||||||
|
favCardRefs.value.forEach((card) => map.set(card.media.id, card))
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const { selectedIds, updateSelectedIds, unselectAll } = useEpisodeSelection()
|
||||||
|
const selectionAreaRef = ref<InstanceType<typeof SelectionArea>>()
|
||||||
|
const checkedIds = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
watch(favInfo, async () => {
|
||||||
|
selectedIds.value.clear()
|
||||||
|
checkedIds.value.clear()
|
||||||
|
selectionAreaRef.value?.selection?.clearSelection()
|
||||||
|
selectionAreaRef.value?.$el.scrollTo({ top: 0, behavior: 'instant' })
|
||||||
|
})
|
||||||
|
|
||||||
|
const { dropdownX, dropdownY, dropdownShowing, dropdownOptions, showDropdown } = useEpisodeDropdown(
|
||||||
|
() => {
|
||||||
|
selectedIds.value.forEach((aid) => checkedIds.value.add(aid))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
selectedIds.value.forEach((aid) => checkedIds.value.delete(aid))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
favInfo.value.medias
|
||||||
|
?.filter((media) => media.attr === 0 && media.type === 2)
|
||||||
|
.forEach((media) => selectedIds.value.add(media.id))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { downloadEpisode, checkboxChecked, handleCheckboxClick, handleContextMenu } = useFavCard(
|
||||||
|
async (media: MediaInFav) => {
|
||||||
|
await downloadNormalEpisode(media.id)
|
||||||
|
},
|
||||||
|
(media: MediaInFav) => {
|
||||||
|
return checkedIds.value.has(media.id)
|
||||||
|
},
|
||||||
|
(media: MediaInFav) => {
|
||||||
|
const checked = checkedIds.value.has(media.id)
|
||||||
|
if (checked) {
|
||||||
|
checkedIds.value.delete(media.id)
|
||||||
|
} else {
|
||||||
|
checkedIds.value.add(media.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(media: MediaInFav) => {
|
||||||
|
if (selectedIds.value.has(media.id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedIds.value.add(media.id)
|
||||||
|
const selection = selectionAreaRef.value?.selection
|
||||||
|
if (selection) {
|
||||||
|
selection.clearSelection()
|
||||||
|
selection.select(`[data-key="${media.id}"]`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function downloadNormalEpisode(aid: number) {
|
||||||
|
// 获取普通视频信息,用于创建下载任务
|
||||||
|
const result = await commands.getNormalInfo({ Aid: aid })
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 创建下载任务
|
||||||
|
await commands.createDownloadTasks({ Normal: { info: result.data, aid_cid_pairs: [[aid, null]] } })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadCheckedEpisodes() {
|
||||||
|
for (const aid of checkedIds.value) {
|
||||||
|
// 创建下载任务
|
||||||
|
await downloadNormalEpisode(aid)
|
||||||
|
// 播放下载动画
|
||||||
|
const card = favCardRefsMap.value.get(aid)
|
||||||
|
if (card !== undefined) {
|
||||||
|
card.playDownloadAnimation()
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFavCard(
|
||||||
|
downloadEpisode: (media: MediaInFav) => Promise<void>,
|
||||||
|
checkboxChecked: (media: MediaInFav) => boolean,
|
||||||
|
handleCheckboxClick: (media: MediaInFav) => void,
|
||||||
|
handleContextMenu: (media: MediaInFav) => void,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
downloadEpisode,
|
||||||
|
checkboxChecked,
|
||||||
|
handleCheckboxClick,
|
||||||
|
handleContextMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full select-none overflow-auto">
|
||||||
|
<SelectionArea
|
||||||
|
ref="selectionAreaRef"
|
||||||
|
class="selection-container flex flex-col flex-1 px-2 overflow-auto"
|
||||||
|
:options="{ selectables: '.selectable', features: { deselectOnBlur: true } }"
|
||||||
|
@contextmenu="showDropdown"
|
||||||
|
@move="updateSelectedIds"
|
||||||
|
@start="unselectAll">
|
||||||
|
<div class="animate-pulse text-violet">左键拖动进行框选,右键打开菜单</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<template v-for="media in favInfo.medias" :key="media.id">
|
||||||
|
<FavCard
|
||||||
|
v-if="media.attr === 0 && media.type === 2"
|
||||||
|
ref="favCardRefs"
|
||||||
|
:data-key="media.id"
|
||||||
|
:class="[
|
||||||
|
'selectable border border-solid border-transparent',
|
||||||
|
selectedIds.has(media.id) ? 'selected shadow-md' : 'hover:bg-gray-1',
|
||||||
|
]"
|
||||||
|
:media="media"
|
||||||
|
:download-episode="downloadEpisode"
|
||||||
|
:checkbox-checked="checkboxChecked"
|
||||||
|
:handle-checkbox-click="handleCheckboxClick"
|
||||||
|
:handle-context-menu="handleContextMenu"
|
||||||
|
:search="searchPaneRef?.search" />
|
||||||
|
|
||||||
|
<FavCard
|
||||||
|
v-else-if="media.attr === 0 && media.type === 24"
|
||||||
|
ref="favCardRefs"
|
||||||
|
class="border border-solid border-transparent hover:border-gray-3"
|
||||||
|
:media="media"
|
||||||
|
:search="searchPaneRef?.search" />
|
||||||
|
|
||||||
|
<FavCard
|
||||||
|
v-else
|
||||||
|
ref="favCardRefs"
|
||||||
|
class="border border-solid border-transparent hover:border-gray-3"
|
||||||
|
:media="media" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</SelectionArea>
|
||||||
|
|
||||||
|
<div class="flex gap-2 m-2 box-border">
|
||||||
|
<n-pagination :page-count="pageCount" :page="currentPage" @update:page="getFav($event)" />
|
||||||
|
<n-select class="w-40%" size="small" v-model:value="selectedMediaListId" :options="selectOptions" />
|
||||||
|
<n-button class="ml-auto" size="small" type="primary" @click="downloadCheckedEpisodes">下载勾选视频</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-dropdown
|
||||||
|
placement="bottom-start"
|
||||||
|
trigger="manual"
|
||||||
|
:x="dropdownX"
|
||||||
|
:y="dropdownY"
|
||||||
|
:options="dropdownOptions"
|
||||||
|
:show="dropdownShowing"
|
||||||
|
:on-clickoutside="() => (dropdownShowing = false)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.selection-container .selected {
|
||||||
|
@apply bg-[rgb(204,232,255)];
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
commands,
|
commands,
|
||||||
GetBangumiInfoParams,
|
GetBangumiInfoParams,
|
||||||
GetCheeseInfoParams,
|
GetCheeseInfoParams,
|
||||||
|
GetFavInfoParams,
|
||||||
GetNormalInfoParams,
|
GetNormalInfoParams,
|
||||||
GetUserVideoInfoParams,
|
GetUserVideoInfoParams,
|
||||||
SearchParams,
|
SearchParams,
|
||||||
@@ -14,13 +15,14 @@ import {
|
|||||||
} from '../../bindings.ts'
|
} from '../../bindings.ts'
|
||||||
import NormalSeasonPanel from './components/NormalSeasonPanel.vue'
|
import NormalSeasonPanel from './components/NormalSeasonPanel.vue'
|
||||||
import NormalSinglePanel from './components/NormalSinglePanel.vue'
|
import NormalSinglePanel from './components/NormalSinglePanel.vue'
|
||||||
import { extractBvid, extractAid, extractEpId, extractSeasonId, extractUid } from '../../utils.tsx'
|
|
||||||
import { useStore } from '../../store.ts'
|
|
||||||
import BangumiPanel from './components/BangumiPanel.vue'
|
import BangumiPanel from './components/BangumiPanel.vue'
|
||||||
import CheesePanel from './components/CheesePanel.vue'
|
import CheesePanel from './components/CheesePanel.vue'
|
||||||
|
import { extractBvid, extractAid, extractEpId, extractSeasonId, extractUid, extractMediaListId } from '../../utils.tsx'
|
||||||
|
import { useStore } from '../../store.ts'
|
||||||
import UserVideoPanel from './components/UserVideoPanel.vue'
|
import UserVideoPanel from './components/UserVideoPanel.vue'
|
||||||
|
import FavPanel from '../FavPane/components/FavPanel.vue'
|
||||||
|
|
||||||
export type SearchType = 'Auto' | 'Normal' | 'Bangumi' | 'Cheese' | 'UserVideo'
|
export type SearchType = 'Auto' | 'Normal' | 'Bangumi' | 'Cheese' | 'UserVideo' | 'Fav'
|
||||||
|
|
||||||
const searchTypeOptions: SelectProps['options'] = [
|
const searchTypeOptions: SelectProps['options'] = [
|
||||||
{ label: '自动', value: 'Auto' },
|
{ label: '自动', value: 'Auto' },
|
||||||
@@ -28,6 +30,7 @@ const searchTypeOptions: SelectProps['options'] = [
|
|||||||
{ label: '番剧', value: 'Bangumi' },
|
{ label: '番剧', value: 'Bangumi' },
|
||||||
{ label: '课程', value: 'Cheese' },
|
{ label: '课程', value: 'Cheese' },
|
||||||
{ label: 'UP投稿', value: 'UserVideo' },
|
{ label: 'UP投稿', value: 'UserVideo' },
|
||||||
|
{ label: '收藏夹', value: 'Fav' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
@@ -48,8 +51,10 @@ const searchLabel = computed(() => {
|
|||||||
return '链接 / ep... / ss...'
|
return '链接 / ep... / ss...'
|
||||||
} else if (searchTypeSelected.value === 'UserVideo') {
|
} else if (searchTypeSelected.value === 'UserVideo') {
|
||||||
return '个人空间链接 / uid...'
|
return '个人空间链接 / uid...'
|
||||||
|
} else if (searchTypeSelected.value === 'Fav') {
|
||||||
|
return '收藏夹链接 / fid...'
|
||||||
}
|
}
|
||||||
return '链接 / av... / BV... / ep... / ss... / uid...'
|
return '链接 / av... / BV... / ep... / ss... / uid... / fid...'
|
||||||
})
|
})
|
||||||
|
|
||||||
async function search(input: string, searchType: SearchType) {
|
async function search(input: string, searchType: SearchType) {
|
||||||
@@ -74,6 +79,8 @@ async function search(input: string, searchType: SearchType) {
|
|||||||
await searchCheese(input, isUrl)
|
await searchCheese(input, isUrl)
|
||||||
} else if (searchType === 'UserVideo') {
|
} else if (searchType === 'UserVideo') {
|
||||||
await searchUserVideo(input, isUrl)
|
await searchUserVideo(input, isUrl)
|
||||||
|
} else if (searchType === 'Fav') {
|
||||||
|
await searchFav(input, isUrl)
|
||||||
} else {
|
} else {
|
||||||
message.error('未知的搜索类型')
|
message.error('未知的搜索类型')
|
||||||
}
|
}
|
||||||
@@ -89,6 +96,7 @@ async function searchAuto(input: string, isUrl: boolean) {
|
|||||||
const epId = extractEpId(input)
|
const epId = extractEpId(input)
|
||||||
const seasonId = extractSeasonId(input)
|
const seasonId = extractSeasonId(input)
|
||||||
const uid = extractUid(input)
|
const uid = extractUid(input)
|
||||||
|
const mediaListId = extractMediaListId(input)
|
||||||
|
|
||||||
if (bvid !== undefined) {
|
if (bvid !== undefined) {
|
||||||
params = { Normal: { Bvid: bvid } }
|
params = { Normal: { Bvid: bvid } }
|
||||||
@@ -98,6 +106,8 @@ async function searchAuto(input: string, isUrl: boolean) {
|
|||||||
params = { Bangumi: { EpId: epId } }
|
params = { Bangumi: { EpId: epId } }
|
||||||
} else if (seasonId !== undefined) {
|
} else if (seasonId !== undefined) {
|
||||||
params = { Bangumi: { SeasonId: seasonId } }
|
params = { Bangumi: { SeasonId: seasonId } }
|
||||||
|
} else if (mediaListId !== undefined) {
|
||||||
|
params = { Fav: { media_list_id: mediaListId, pn: 1 } }
|
||||||
} else if (uid !== undefined) {
|
} else if (uid !== undefined) {
|
||||||
params = { UserVideo: { mid: uid, pn: 1 } }
|
params = { UserVideo: { mid: uid, pn: 1 } }
|
||||||
}
|
}
|
||||||
@@ -123,6 +133,11 @@ async function searchAuto(input: string, isUrl: boolean) {
|
|||||||
if (!isNaN(uid)) {
|
if (!isNaN(uid)) {
|
||||||
params = { UserVideo: { mid: uid, pn: 1 } }
|
params = { UserVideo: { mid: uid, pn: 1 } }
|
||||||
}
|
}
|
||||||
|
} else if (input.toLowerCase().startsWith('fid')) {
|
||||||
|
const mediaListId = parseInt(input.substring(3), 10)
|
||||||
|
if (!isNaN(mediaListId)) {
|
||||||
|
params = { Fav: { media_list_id: mediaListId, pn: 1 } }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params === undefined) {
|
if (params === undefined) {
|
||||||
@@ -276,6 +291,39 @@ async function searchUserVideo(input: string, isUrl: boolean) {
|
|||||||
searchResult.value = result.data
|
searchResult.value = result.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function searchFav(input: string, isUrl: boolean) {
|
||||||
|
let params: GetFavInfoParams | undefined
|
||||||
|
|
||||||
|
if (isUrl) {
|
||||||
|
const mediaListId = extractMediaListId(input)
|
||||||
|
if (mediaListId !== undefined) {
|
||||||
|
params = { media_list_id: mediaListId, pn: 1 }
|
||||||
|
}
|
||||||
|
} else if (input.toLowerCase().startsWith('fid')) {
|
||||||
|
const mediaListId = parseInt(input.substring(3), 10)
|
||||||
|
if (!isNaN(mediaListId)) {
|
||||||
|
params = { media_list_id: mediaListId, pn: 1 }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const mediaListId = parseInt(input, 10)
|
||||||
|
if (!isNaN(mediaListId)) {
|
||||||
|
params = { media_list_id: mediaListId, pn: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params === undefined) {
|
||||||
|
message.error('解析输入失败,请输入正确的收藏夹链接或ID(如 fid...)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await commands.search({ Fav: params })
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
searchResult.value = result.data
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({ search })
|
defineExpose({ search })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -316,6 +364,7 @@ defineExpose({ search })
|
|||||||
<BangumiPanel v-else-if="'Bangumi' in searchResult" :bangumi-result="searchResult.Bangumi" />
|
<BangumiPanel v-else-if="'Bangumi' in searchResult" :bangumi-result="searchResult.Bangumi" />
|
||||||
<CheesePanel v-else-if="'Cheese' in searchResult" :cheese-result="searchResult.Cheese" />
|
<CheesePanel v-else-if="'Cheese' in searchResult" :cheese-result="searchResult.Cheese" />
|
||||||
<UserVideoPanel v-else-if="'UserVideo' in searchResult" v-model:user-video-result="searchResult.UserVideo" />
|
<UserVideoPanel v-else-if="'UserVideo' in searchResult" v-model:user-video-result="searchResult.UserVideo" />
|
||||||
|
<FavPanel v-else-if="'Fav' in searchResult" :fav-info="searchResult.Fav" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -73,6 +73,18 @@ export function extractUid(url: string): number | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractMediaListId(url: string): number | undefined {
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
const params = new URLSearchParams(parsedUrl.search)
|
||||||
|
const fid = params.get('fid')
|
||||||
|
if (fid !== null) {
|
||||||
|
const mediaListId = parseInt(fid, 10)
|
||||||
|
if (!isNaN(mediaListId)) {
|
||||||
|
return mediaListId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useEpisodeDropdown(onCheck: () => void, onUncheck: () => void, onSelectAll: () => void) {
|
export function useEpisodeDropdown(onCheck: () => void, onUncheck: () => void, onSelectAll: () => void) {
|
||||||
const dropdownX = ref<number>(0)
|
const dropdownX = ref<number>(0)
|
||||||
const dropdownY = ref<number>(0)
|
const dropdownY = ref<number>(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user