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:
@@ -10,7 +10,7 @@ use crate::{
|
|||||||
logger,
|
logger,
|
||||||
types::{
|
types::{
|
||||||
bangumi_follow_info::BangumiFollowInfo,
|
bangumi_follow_info::BangumiFollowInfo,
|
||||||
bangumi_info::EpInBangumi,
|
bangumi_info::{BangumiInfo, EpInBangumi},
|
||||||
create_download_task_params::CreateDownloadTaskParams,
|
create_download_task_params::CreateDownloadTaskParams,
|
||||||
fav_folders::FavFolders,
|
fav_folders::FavFolders,
|
||||||
fav_info::FavInfo,
|
fav_info::FavInfo,
|
||||||
@@ -120,6 +120,20 @@ pub async fn get_user_info(app: AppHandle, sessdata: String) -> CommandResult<Us
|
|||||||
Ok(user_info)
|
Ok(user_info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command(async)]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn get_bangumi_info(
|
||||||
|
app: AppHandle,
|
||||||
|
params: GetBangumiInfoParams,
|
||||||
|
) -> CommandResult<BangumiInfo> {
|
||||||
|
let bili_client = app.get_bili_client();
|
||||||
|
let bangumi_info = bili_client
|
||||||
|
.get_bangumi_info(params)
|
||||||
|
.await
|
||||||
|
.map_err(|err| CommandError::from("获取番剧视频信息失败", err))?;
|
||||||
|
Ok(bangumi_info)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command(async)]
|
#[tauri::command(async)]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_normal_info(
|
pub async fn get_normal_info(
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn run() {
|
|||||||
get_qrcode_status,
|
get_qrcode_status,
|
||||||
get_user_info,
|
get_user_info,
|
||||||
get_normal_info,
|
get_normal_info,
|
||||||
|
get_bangumi_info,
|
||||||
get_user_video_info,
|
get_user_video_info,
|
||||||
get_fav_folders,
|
get_fav_folders,
|
||||||
get_fav_info,
|
get_fav_info,
|
||||||
|
|||||||
+16
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
PhMagnifyingGlass,
|
PhMagnifyingGlass,
|
||||||
PhStar,
|
PhStar,
|
||||||
PhClock,
|
PhClock,
|
||||||
|
PhHeart,
|
||||||
PhDownload,
|
PhDownload,
|
||||||
} from '@phosphor-icons/vue'
|
} from '@phosphor-icons/vue'
|
||||||
import AboutDialog from './dialogs/AboutDialog.vue'
|
import AboutDialog from './dialogs/AboutDialog.vue'
|
||||||
@@ -20,8 +21,9 @@ import FavPane from './panes/FavPane/FavPane.vue'
|
|||||||
import WatchLaterPane from './panes/WatchLaterPane/WatchLaterPane.vue'
|
import WatchLaterPane from './panes/WatchLaterPane/WatchLaterPane.vue'
|
||||||
import DownloadPane from './panes/DownloadPane/DownloadPane.vue'
|
import DownloadPane from './panes/DownloadPane/DownloadPane.vue'
|
||||||
import { searchPaneRefKey, navDownloadButtonRefKey } from './injection_keys.ts'
|
import { searchPaneRefKey, navDownloadButtonRefKey } from './injection_keys.ts'
|
||||||
|
import BangumiFollowPane from './panes/BangumiFollow/BangumiFollowPane.vue'
|
||||||
|
|
||||||
export type CurrentNavName = 'search' | 'fav' | 'watch_later' | 'download'
|
export type CurrentNavName = 'search' | 'fav' | 'watch_later' | 'bangumi_follow' | 'download'
|
||||||
|
|
||||||
const currentPlatform = platform()
|
const currentPlatform = platform()
|
||||||
|
|
||||||
@@ -93,6 +95,18 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
|
追番追剧
|
||||||
|
<template #trigger>
|
||||||
|
<div
|
||||||
|
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||||
|
@click="store.currentNavName = 'bangumi_follow'"
|
||||||
|
:class="{ 'text-sky-5': store.currentNavName === 'bangumi_follow' }">
|
||||||
|
<PhHeart :weight="store.currentNavName === 'bangumi_follow' ? 'fill' : 'regular'" size="28" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
下载
|
下载
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
@@ -145,6 +159,7 @@ onMounted(() => {
|
|||||||
<SearchPane v-show="store.currentNavName === 'search'" ref="searchPaneRef" />
|
<SearchPane v-show="store.currentNavName === 'search'" ref="searchPaneRef" />
|
||||||
<FavPane v-show="store.currentNavName === 'fav'" />
|
<FavPane v-show="store.currentNavName === 'fav'" />
|
||||||
<WatchLaterPane v-show="store.currentNavName === 'watch_later'" />
|
<WatchLaterPane v-show="store.currentNavName === 'watch_later'" />
|
||||||
|
<BangumiFollowPane v-show="store.currentNavName === 'bangumi_follow'" />
|
||||||
<DownloadPane v-show="store.currentNavName === 'download'" />
|
<DownloadPane v-show="store.currentNavName === 'download'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ async getNormalInfo(params: GetNormalInfoParams) : Promise<Result<NormalInfo, Co
|
|||||||
else return { status: "error", error: e as any };
|
else return { status: "error", error: e as any };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async getBangumiInfo(params: GetBangumiInfoParams) : Promise<Result<BangumiInfo, CommandError>> {
|
||||||
|
try {
|
||||||
|
return { status: "ok", data: await TAURI_INVOKE("get_bangumi_info", { params }) };
|
||||||
|
} catch (e) {
|
||||||
|
if(e instanceof Error) throw e;
|
||||||
|
else return { status: "error", error: e as any };
|
||||||
|
}
|
||||||
|
},
|
||||||
async getUserVideoInfo(params: GetUserVideoInfoParams) : Promise<Result<UserVideoInfo, CommandError>> {
|
async getUserVideoInfo(params: GetUserVideoInfoParams) : Promise<Result<UserVideoInfo, CommandError>> {
|
||||||
try {
|
try {
|
||||||
return { status: "ok", data: await TAURI_INVOKE("get_user_video_info", { params }) };
|
return { status: "ok", data: await TAURI_INVOKE("get_user_video_info", { params }) };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps<{
|
const { size = 'medium' } = defineProps<{
|
||||||
|
size?: 'small' | 'medium'
|
||||||
checked: boolean
|
checked: boolean
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}>()
|
}>()
|
||||||
@@ -7,12 +8,21 @@ defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="w-7 h-7 flex items-center justify-center rounded cursor-pointer border border-solid border-white"
|
class="flex items-center justify-center rounded cursor-pointer border border-solid border-white"
|
||||||
:class="checked ? 'bg-sky-5' : 'bg-gray/50'"
|
:class="{
|
||||||
|
'bg-sky-5': checked,
|
||||||
|
'bg-gray/50': !checked,
|
||||||
|
'w-7 h-7': size === 'medium',
|
||||||
|
'w-5 h-5': size === 'small',
|
||||||
|
}"
|
||||||
@click="onClick">
|
@click="onClick">
|
||||||
<svg
|
<svg
|
||||||
v-show="checked"
|
v-show="checked"
|
||||||
class="w-5 h-5 text-white"
|
class="text-white"
|
||||||
|
:class="{
|
||||||
|
'w-5 h-5': size === 'medium',
|
||||||
|
'w-4 h-4': size === 'small',
|
||||||
|
}"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useStore } from '../../store.ts'
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { BangumiFollowInfo, commands } from '../../bindings.ts'
|
||||||
|
import BangumiFollowPanel from './components/BangumiFollowPanel.vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const bangumiFollowInfo = ref<BangumiFollowInfo>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.userInfo,
|
||||||
|
async () => {
|
||||||
|
if (store.userInfo === undefined) {
|
||||||
|
bangumiFollowInfo.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await commands.getBangumiFollowInfo({
|
||||||
|
vmid: store.userInfo.mid,
|
||||||
|
pn: 1,
|
||||||
|
type: 1,
|
||||||
|
follow_status: 0,
|
||||||
|
})
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bangumiFollowInfo.value = result.data
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="bangumiFollowInfo !== undefined" class="h-full">
|
||||||
|
<BangumiFollowPanel v-model:bangumi-follow-info="bangumiFollowInfo" />
|
||||||
|
</div>
|
||||||
|
<n-empty v-else class="mt-2" description="请先登录" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, ref } from 'vue'
|
||||||
|
import { navDownloadButtonRefKey, searchPaneRefKey } from '../../../injection_keys.ts'
|
||||||
|
import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../../../utils.tsx'
|
||||||
|
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
|
import { EpInBangumiFollow } from '../../../bindings.ts'
|
||||||
|
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||||
|
|
||||||
|
const searchPaneRef = inject(searchPaneRefKey)
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
ep: EpInBangumiFollow
|
||||||
|
downloadEpisode: (ep: EpInBangumiFollow) => Promise<void>
|
||||||
|
checkboxChecked: (ep: EpInBangumiFollow) => boolean
|
||||||
|
handleCheckboxClick: (ep: EpInBangumiFollow) => void
|
||||||
|
handleContextMenu: (ep: EpInBangumiFollow) => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const navDownloadButtonRef = inject(navDownloadButtonRefKey)
|
||||||
|
const rootDivRef = ref<HTMLDivElement>()
|
||||||
|
const downloadButtonRef = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
async function handleDownloadClick() {
|
||||||
|
if (props.downloadEpisode === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await props.downloadEpisode(props.ep)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ playDownloadAnimation, ep: props.ep })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col w-200px relative p-3 rounded-lg" ref="rootDivRef" @contextmenu="handleContextMenu(ep)">
|
||||||
|
<SimpleCheckbox
|
||||||
|
class="absolute top-5 left-5 z-1 backdrop-blur-2"
|
||||||
|
size="small"
|
||||||
|
:checked="checkboxChecked(ep)"
|
||||||
|
:on-click="() => handleCheckboxClick(ep)" />
|
||||||
|
<div class="flex">
|
||||||
|
<img
|
||||||
|
class="w-90px h-120px rounded-lg object-cover lazyload"
|
||||||
|
:data-src="`${ensureHttps(ep.cover)}@308w_410h_1c.webp`"
|
||||||
|
:key="ep.cover"
|
||||||
|
alt=""
|
||||||
|
draggable="false" />
|
||||||
|
<div class="flex flex-col ml-1">
|
||||||
|
<span class="line-clamp-2" :title="ep.title">{{ ep.title }}</span>
|
||||||
|
<span class="text-gray mt-auto">{{ ep.season_type_name }} · {{ ep.areas[0].name }}</span>
|
||||||
|
<span class="text-gray">{{ ep.new_ep.index_show }}</span>
|
||||||
|
<span class="text-gray">{{ ep.progress }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-1 items-center mt-2">
|
||||||
|
<a
|
||||||
|
:href="`https://www.bilibili.com/bangumi/play/ss${ep.season_id}`"
|
||||||
|
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
|
||||||
|
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="searchPaneRef?.search(`ss${props.ep.season_id}`, 'Bangumi')">
|
||||||
|
<PhMagnifyingGlass :size="24" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="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,220 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { BangumiFollowInfo, commands, EpInBangumiFollow } from '../../../bindings.ts'
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useStore } from '../../../store.ts'
|
||||||
|
import BangumiFollowCard from './BangumiFollowCard.vue'
|
||||||
|
import { useEpisodeDropdown, useEpisodeSelection } from '../../../utils.tsx'
|
||||||
|
import { SelectionArea } from '@viselect/vue'
|
||||||
|
import { SelectOption } from 'naive-ui'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const bangumiFollowInfo = defineModel<BangumiFollowInfo>('bangumiFollowInfo', { required: true })
|
||||||
|
|
||||||
|
const selectedType = ref<number>(1)
|
||||||
|
const selectTypeOptions: SelectOption[] = [
|
||||||
|
{ label: '追番', value: 1 },
|
||||||
|
{ label: '追剧', value: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedFollowStatus = ref<number>(0)
|
||||||
|
const selectFollowStatusOptions: SelectOption[] = [
|
||||||
|
{ label: '全部', value: 0 },
|
||||||
|
{ label: '想看', value: 1 },
|
||||||
|
{ label: '在看', value: 2 },
|
||||||
|
{ label: '看过', value: 3 },
|
||||||
|
]
|
||||||
|
|
||||||
|
const currentPage = ref<number>(1)
|
||||||
|
const pageCount = computed<number>(() => {
|
||||||
|
if (bangumiFollowInfo.value === undefined) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return Math.ceil(bangumiFollowInfo.value.total / 24)
|
||||||
|
})
|
||||||
|
|
||||||
|
const bangumiFollowCardRefs = ref<InstanceType<typeof BangumiFollowCard>[]>([])
|
||||||
|
const bangumiFollowCardRefsMap = computed<Map<number, InstanceType<typeof BangumiFollowCard>>>(() => {
|
||||||
|
const map = new Map<number, InstanceType<typeof BangumiFollowCard>>()
|
||||||
|
bangumiFollowCardRefs.value.forEach((card) => map.set(card.ep.season_id, card))
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const { selectedIds, updateSelectedIds, unselectAll } = useEpisodeSelection()
|
||||||
|
const selectionAreaRef = ref<InstanceType<typeof SelectionArea>>()
|
||||||
|
const checkedIds = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
watch(bangumiFollowInfo, () => {
|
||||||
|
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((seasonId) => checkedIds.value.add(seasonId))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
selectedIds.value.forEach((seasonId) => checkedIds.value.delete(seasonId))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
bangumiFollowInfo.value.list?.forEach((ep) => selectedIds.value.add(ep.season_id))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { downloadEpisode, checkboxChecked, handleCheckboxClick, handleContextMenu } = useBangumiFollowCard(
|
||||||
|
async (ep: EpInBangumiFollow) => {
|
||||||
|
await downloadSeason(ep.season_id)
|
||||||
|
},
|
||||||
|
(ep: EpInBangumiFollow) => {
|
||||||
|
return checkedIds.value.has(ep.season_id)
|
||||||
|
},
|
||||||
|
(ep: EpInBangumiFollow) => {
|
||||||
|
const checked = checkedIds.value.has(ep.season_id)
|
||||||
|
if (checked) {
|
||||||
|
checkedIds.value.delete(ep.season_id)
|
||||||
|
} else {
|
||||||
|
checkedIds.value.add(ep.season_id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(ep: EpInBangumiFollow) => {
|
||||||
|
if (selectedIds.value.has(ep.season_id)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedIds.value.add(ep.season_id)
|
||||||
|
const selection = selectionAreaRef.value?.selection
|
||||||
|
if (selection) {
|
||||||
|
selection.clearSelection()
|
||||||
|
selection.select(`[data-key="${ep.season_id}"]`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function getBangumiFollowInfo(page: number) {
|
||||||
|
if (store.userInfo === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage.value = page
|
||||||
|
|
||||||
|
const result = await commands.getBangumiFollowInfo({
|
||||||
|
vmid: store.userInfo.mid,
|
||||||
|
pn: page,
|
||||||
|
type: selectedType.value,
|
||||||
|
follow_status: selectedFollowStatus.value,
|
||||||
|
})
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bangumiFollowInfo.value = result.data
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadSeason(seasonId: number) {
|
||||||
|
// 获取番剧信息,用于创建下载任务
|
||||||
|
const result = await commands.getBangumiInfo({ SeasonId: seasonId })
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const info = result.data
|
||||||
|
// 创建下载任务
|
||||||
|
await commands.createDownloadTasks({
|
||||||
|
Bangumi: { info, ep_ids: info.episodes.map((ep) => ep.ep_id) },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadCheckedEpisodes() {
|
||||||
|
for (const seasonId of checkedIds.value) {
|
||||||
|
// 创建下载任务
|
||||||
|
await downloadSeason(seasonId)
|
||||||
|
// 播放下载动画
|
||||||
|
const card = bangumiFollowCardRefsMap.value.get(seasonId)
|
||||||
|
if (card !== undefined) {
|
||||||
|
card.playDownloadAnimation()
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBangumiFollowCard(
|
||||||
|
downloadEpisode: (ep: EpInBangumiFollow) => Promise<void>,
|
||||||
|
checkboxChecked: (ep: EpInBangumiFollow) => boolean,
|
||||||
|
handleCheckboxClick: (ep: EpInBangumiFollow) => void,
|
||||||
|
handleContextMenu: (ep: EpInBangumiFollow) => 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">
|
||||||
|
<BangumiFollowCard
|
||||||
|
v-for="ep in bangumiFollowInfo.list"
|
||||||
|
:key="ep.season_id"
|
||||||
|
ref="bangumiFollowCardRefs"
|
||||||
|
:data-key="ep.season_id"
|
||||||
|
:class="[
|
||||||
|
'selectable border border-solid border-transparent',
|
||||||
|
selectedIds.has(ep.season_id) ? 'selected shadow-md' : 'hover:bg-gray-1',
|
||||||
|
]"
|
||||||
|
:ep="ep"
|
||||||
|
:download-episode="downloadEpisode"
|
||||||
|
:checkbox-checked="checkboxChecked"
|
||||||
|
:handle-checkbox-click="handleCheckboxClick"
|
||||||
|
:handle-context-menu="handleContextMenu" />
|
||||||
|
</div>
|
||||||
|
</SelectionArea>
|
||||||
|
<div class="flex gap-2 m-2 box-border">
|
||||||
|
<n-pagination :page-count="pageCount" :page="currentPage" @update:page="getBangumiFollowInfo($event)" />
|
||||||
|
<n-select
|
||||||
|
class="w-20"
|
||||||
|
v-model:value="selectedType"
|
||||||
|
:options="selectTypeOptions"
|
||||||
|
size="small"
|
||||||
|
@update:value="getBangumiFollowInfo(1)" />
|
||||||
|
<n-select
|
||||||
|
class="w-20"
|
||||||
|
v-model:value="selectedFollowStatus"
|
||||||
|
:options="selectFollowStatusOptions"
|
||||||
|
size="small"
|
||||||
|
@update:value="getBangumiFollowInfo(1)" />
|
||||||
|
<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>
|
||||||
Reference in New Issue
Block a user