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
+2
@@ -16,6 +16,7 @@ declare module 'vue' {
|
|||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||||
NDialog: typeof import('naive-ui')['NDialog']
|
NDialog: typeof import('naive-ui')['NDialog']
|
||||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
@@ -31,6 +32,7 @@ declare module 'vue' {
|
|||||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
NPagination: typeof import('naive-ui')['NPagination']
|
NPagination: typeof import('naive-ui')['NPagination']
|
||||||
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
NProgress: typeof import('naive-ui')['NProgress']
|
NProgress: typeof import('naive-ui')['NProgress']
|
||||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||||
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
NRadioButton: typeof import('naive-ui')['NRadioButton']
|
||||||
|
|||||||
+6
-2
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppContent from './AppContent.vue'
|
import AppContent from './AppContent.vue'
|
||||||
import { GlobalThemeOverrides } from 'naive-ui'
|
import { GlobalThemeOverrides, zhCN, dateZhCN } from 'naive-ui'
|
||||||
|
|
||||||
const themeOverrides: GlobalThemeOverrides = {
|
const themeOverrides: GlobalThemeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
@@ -15,6 +15,10 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
Tabs: {
|
Tabs: {
|
||||||
tabGapSmallLine: '10px',
|
tabGapSmallLine: '10px',
|
||||||
tabPaddingSmallLine: '6px 8px',
|
tabPaddingSmallLine: '6px 8px',
|
||||||
|
tabTextColorActiveSegment: '#00AEECFF',
|
||||||
|
tabTextColorHoverSegment: '#00AEECFF',
|
||||||
|
tabColorSegment: '#DFF6FDFF',
|
||||||
|
colorSegment: '#FFFFFFFF',
|
||||||
},
|
},
|
||||||
Button: {
|
Button: {
|
||||||
paddingSmall: '0 8px',
|
paddingSmall: '0 8px',
|
||||||
@@ -35,7 +39,7 @@ const themeOverrides: GlobalThemeOverrides = {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :theme-overrides="themeOverrides">
|
<n-config-provider :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-modal-provider>
|
<n-modal-provider>
|
||||||
<n-notification-provider placement="bottom-right" :max="3">
|
<n-notification-provider placement="bottom-right" :max="3">
|
||||||
|
|||||||
+25
-8
@@ -11,6 +11,7 @@ import {
|
|||||||
PhClock,
|
PhClock,
|
||||||
PhHeart,
|
PhHeart,
|
||||||
PhDownload,
|
PhDownload,
|
||||||
|
PhPlayCircle,
|
||||||
} from '@phosphor-icons/vue'
|
} from '@phosphor-icons/vue'
|
||||||
import AboutDialog from './dialogs/AboutDialog.vue'
|
import AboutDialog from './dialogs/AboutDialog.vue'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
@@ -22,8 +23,9 @@ 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'
|
import BangumiFollowPane from './panes/BangumiFollow/BangumiFollowPane.vue'
|
||||||
|
import HistoryPane from './panes/HistoryPane/HistoryPane.vue'
|
||||||
|
|
||||||
export type CurrentNavName = 'search' | 'fav' | 'watch_later' | 'bangumi_follow' | 'download'
|
export type CurrentNavName = 'search' | 'fav' | 'history' | 'bangumi_follow' | 'watch_later' | 'download'
|
||||||
|
|
||||||
const currentPlatform = platform()
|
const currentPlatform = platform()
|
||||||
|
|
||||||
@@ -72,7 +74,7 @@ onMounted(() => {
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
收藏
|
收藏夹
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||||
@@ -84,13 +86,13 @@ onMounted(() => {
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
稍后再看
|
历史记录
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div
|
<div
|
||||||
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
class="flex cursor-pointer hover:text-sky-5 hover:bg-gray-2/70 rounded py-1 my-1 px-2"
|
||||||
@click="store.currentNavName = 'watch_later'"
|
@click="store.currentNavName = 'history'"
|
||||||
:class="{ 'text-sky-5': store.currentNavName === 'watch_later' }">
|
:class="{ 'text-sky-5': store.currentNavName === 'history' }">
|
||||||
<PhClock :weight="store.currentNavName === 'watch_later' ? 'fill' : 'regular'" size="28" />
|
<PhClock :weight="store.currentNavName === 'history' ? 'fill' : 'regular'" size="28" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
@@ -108,7 +110,19 @@ onMounted(() => {
|
|||||||
</n-tooltip>
|
</n-tooltip>
|
||||||
|
|
||||||
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
<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 = 'watch_later'"
|
||||||
|
:class="{ 'text-sky-5': store.currentNavName === 'watch_later' }">
|
||||||
|
<PhPlayCircle :weight="store.currentNavName === 'watch_later' ? 'fill' : 'regular'" size="28" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-tooltip>
|
||||||
|
|
||||||
|
<n-tooltip placement="right" trigger="hover" :show-arrow="false">
|
||||||
|
下载任务
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<n-badge :value="store.uncompletedProgressesCount" :offset="[-7, 7]">
|
<n-badge :value="store.uncompletedProgressesCount" :offset="[-7, 7]">
|
||||||
<div
|
<div
|
||||||
@@ -163,11 +177,14 @@ onMounted(() => {
|
|||||||
<FavPane class="absolute inset-0" v-show="store.currentNavName === 'fav'" />
|
<FavPane class="absolute inset-0" v-show="store.currentNavName === 'fav'" />
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<WatchLaterPane class="absolute inset-0" v-show="store.currentNavName === 'watch_later'" />
|
<HistoryPane class="absolute inset-0" v-show="store.currentNavName === 'history'" />
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<BangumiFollowPane class="absolute inset-0" v-show="store.currentNavName === 'bangumi_follow'" />
|
<BangumiFollowPane class="absolute inset-0" v-show="store.currentNavName === 'bangumi_follow'" />
|
||||||
</transition>
|
</transition>
|
||||||
|
<transition name="fade">
|
||||||
|
<WatchLaterPane class="absolute inset-0" v-show="store.currentNavName === 'watch_later'" />
|
||||||
|
</transition>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<DownloadPane class="absolute inset-0" v-show="store.currentNavName === 'download'" />
|
<DownloadPane class="absolute inset-0" v-show="store.currentNavName === 'download'" />
|
||||||
</transition>
|
</transition>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { commands, HistoryInfo } from '../../bindings.ts'
|
||||||
|
import { useStore } from '../../store.ts'
|
||||||
|
import HistoryPanel from './components/HistoryPanel.vue'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const historyInfo = ref<HistoryInfo>()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.userInfo,
|
||||||
|
async () => {
|
||||||
|
if (store.userInfo === undefined) {
|
||||||
|
historyInfo.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await commands.getHistoryInfo({
|
||||||
|
pn: 1,
|
||||||
|
keyword: '',
|
||||||
|
add_time_start: 0,
|
||||||
|
add_time_end: 0,
|
||||||
|
arc_max_duration: 0,
|
||||||
|
arc_min_duration: 0,
|
||||||
|
device_type: 'All',
|
||||||
|
})
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
historyInfo.value = result.data
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="historyInfo !== undefined" class="h-full">
|
||||||
|
<HistoryPanel v-model:history-info="historyInfo" />
|
||||||
|
</div>
|
||||||
|
<n-empty v-else class="mt-2" description="请先登录" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { EpisodeType, HistoryDetail } from '../../../bindings.ts'
|
||||||
|
import { SearchType } from '../../SearchPane/SearchPane.vue'
|
||||||
|
import { computed, inject, ref } from 'vue'
|
||||||
|
import { navDownloadButtonRefKey } from '../../../injection_keys.ts'
|
||||||
|
import { ensureHttps, isElementInViewport, playTaskToQueueAnimation } from '../../../utils.tsx'
|
||||||
|
import { PhDownloadSimple, PhGoogleChromeLogo, PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
|
import SimpleCheckbox from '../../../components/SimpleCheckbox.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
episodeType: EpisodeType
|
||||||
|
historyDetail: HistoryDetail
|
||||||
|
downloadEpisode?: (historyDetail: HistoryDetail) => Promise<void>
|
||||||
|
checkboxChecked?: (historyDetail: HistoryDetail) => boolean
|
||||||
|
handleCheckboxClick?: (historyDetail: HistoryDetail) => void
|
||||||
|
handleContextMenu?: (historyDetail: HistoryDetail) => 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.episodeType === 'Normal') {
|
||||||
|
return `https://www.bilibili.com/video/${props.historyDetail.history.bvid}/`
|
||||||
|
} else if (props.episodeType === 'Bangumi') {
|
||||||
|
return `https://www.bilibili.com/bangumi/play/ep${props.historyDetail.history.epid}`
|
||||||
|
} else if (props.episodeType === 'Cheese') {
|
||||||
|
return `https://www.bilibili.com/cheese/play/ep${props.historyDetail.history.epid}`
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const downloadHint = computed<string | undefined>(() => {
|
||||||
|
if (props.episodeType !== 'Normal') {
|
||||||
|
return '下载请点击左下角放大镜按钮'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const subTitle = computed<string>(() => {
|
||||||
|
if (props.episodeType === 'Normal') {
|
||||||
|
return ''
|
||||||
|
} else if (props.historyDetail.long_title !== '') {
|
||||||
|
return props.historyDetail.long_title
|
||||||
|
} else if (props.historyDetail.show_title !== '') {
|
||||||
|
return props.historyDetail.show_title
|
||||||
|
} else if (props.historyDetail.new_desc !== '') {
|
||||||
|
return props.historyDetail.new_desc
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const timeFormat = computed(() => {
|
||||||
|
function isToday(date: Date): boolean {
|
||||||
|
const today = new Date()
|
||||||
|
return (
|
||||||
|
date.getDate() === today.getDate() &&
|
||||||
|
date.getMonth() === today.getMonth() &&
|
||||||
|
date.getFullYear() === today.getFullYear()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isYesterday(date: Date): boolean {
|
||||||
|
const yesterday = new Date()
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
return (
|
||||||
|
date.getDate() === yesterday.getDate() &&
|
||||||
|
date.getMonth() === yesterday.getMonth() &&
|
||||||
|
date.getFullYear() === yesterday.getFullYear()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(props.historyDetail.view_at * 1000)
|
||||||
|
|
||||||
|
if (isToday(date)) {
|
||||||
|
return "'今天' HH:mm"
|
||||||
|
}
|
||||||
|
if (isYesterday(date)) {
|
||||||
|
return "'昨天' HH:mm"
|
||||||
|
} else {
|
||||||
|
return 'MM-dd HH:mm'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleDownloadClick() {
|
||||||
|
if (props.downloadEpisode === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await props.downloadEpisode(props.historyDetail)
|
||||||
|
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.episodeType === 'Normal') {
|
||||||
|
props.search(props.historyDetail.history.bvid, 'Normal')
|
||||||
|
} else if (props.episodeType === 'Bangumi') {
|
||||||
|
props.search(`ep${props.historyDetail.history.epid}`, 'Bangumi')
|
||||||
|
} else if (props.episodeType === 'Cheese') {
|
||||||
|
props.search(`ep${props.historyDetail.history.epid}`, 'Cheese')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ playDownloadAnimation, historyDetail: props.historyDetail })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col w-200px relative p-3 rounded-lg"
|
||||||
|
ref="rootDivRef"
|
||||||
|
:title="downloadHint"
|
||||||
|
@contextmenu="handleContextMenu?.(historyDetail)">
|
||||||
|
<SimpleCheckbox
|
||||||
|
v-if="episodeType === 'Normal' && handleCheckboxClick !== undefined && checkboxChecked !== undefined"
|
||||||
|
class="absolute top-6 left-6 z-1 backdrop-blur-2"
|
||||||
|
:checked="checkboxChecked(historyDetail)"
|
||||||
|
:on-click="() => handleCheckboxClick?.(historyDetail)" />
|
||||||
|
<div v-if="historyDetail.badge !== ''" class="absolute top-6 right-6 z-1 bg-[#ff6699] text-white px-1 rounded">
|
||||||
|
{{ historyDetail.badge }}
|
||||||
|
</div>
|
||||||
|
<img
|
||||||
|
class="w-200px h-125px rounded-lg object-cover lazyload"
|
||||||
|
:data-src="`${ensureHttps(historyDetail.cover)}@672w_378h_1c.webp`"
|
||||||
|
:key="historyDetail.cover"
|
||||||
|
alt=""
|
||||||
|
draggable="false" />
|
||||||
|
|
||||||
|
<div class="w-full flex flex-col h-45px mt-2">
|
||||||
|
<span class="line-clamp-2" :title="historyDetail.title">{{ historyDetail.title }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center whitespace-nowrap text-gray text-12px w-full overflow-hidden">
|
||||||
|
<a
|
||||||
|
v-if="episodeType === 'Normal'"
|
||||||
|
class="min-w-0 color-inherit no-underline hover:text-sky-5 mr-1"
|
||||||
|
:href="`https://space.bilibili.com/${historyDetail.author_mid}`"
|
||||||
|
target="_blank"
|
||||||
|
draggable="false">
|
||||||
|
<div class="truncate text-ellipsis" :title="historyDetail.author_name">{{ historyDetail.author_name }}</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
v-else
|
||||||
|
class="min-w-0 color-inherit no-underline hover:text-sky-5 mr-1"
|
||||||
|
:href="openInBrowserHref"
|
||||||
|
target="_blank"
|
||||||
|
draggable="false">
|
||||||
|
<div class="truncate text-ellipsis" :title="subTitle">{{ subTitle }}</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span class="ml-auto flex-shrink-0" title="上次观看时间">
|
||||||
|
<n-time unix :format="timeFormat" :time="historyDetail.view_at" />
|
||||||
|
</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,359 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { commands, DeviceType, HistoryDetail, HistoryInfo } from '../../../bindings.ts'
|
||||||
|
import { computed, inject, ref, watch } from 'vue'
|
||||||
|
import { searchPaneRefKey } from '../../../injection_keys.ts'
|
||||||
|
import HistoryCard from './HistoryCard.vue'
|
||||||
|
import { useEpisodeDropdown, useEpisodeSelection } from '../../../utils.tsx'
|
||||||
|
import { SelectionArea } from '@viselect/vue'
|
||||||
|
import FloatLabelInput from '../../../components/FloatLabelInput.vue'
|
||||||
|
import { PhMagnifyingGlass } from '@phosphor-icons/vue'
|
||||||
|
|
||||||
|
const historyInfo = defineModel<HistoryInfo>('historyInfo', { required: true })
|
||||||
|
|
||||||
|
const searchPaneRef = inject(searchPaneRefKey)
|
||||||
|
|
||||||
|
const currentPage = ref<number>(1)
|
||||||
|
const pageCount = computed<number>(() => Math.ceil(historyInfo.value.page.total / 20))
|
||||||
|
|
||||||
|
const searching = ref<boolean>(false)
|
||||||
|
|
||||||
|
const searchInput = ref<string>('')
|
||||||
|
|
||||||
|
type DurationTabName = 'all' | '<10' | '10-30' | '30-60' | '>60'
|
||||||
|
const durationTabName = ref<DurationTabName>('all')
|
||||||
|
let addTimeStart: number = 0
|
||||||
|
let addTimeEnd: number = 0
|
||||||
|
|
||||||
|
const datePickerRange = ref<[number, number]>(getInitRange())
|
||||||
|
type StartTimeTabName = 'all' | 'today' | 'yesterday' | 'week' | 'date-picker'
|
||||||
|
const startTimeTabName = ref<StartTimeTabName>('all')
|
||||||
|
let arcMinDuration: number = 0
|
||||||
|
let arcMaxDuration: number = 0
|
||||||
|
|
||||||
|
const selectedDeviceType = ref<DeviceType>('All')
|
||||||
|
|
||||||
|
watch(durationTabName, () => {
|
||||||
|
if (durationTabName.value === 'all') {
|
||||||
|
arcMinDuration = 0
|
||||||
|
arcMaxDuration = 0
|
||||||
|
} else if (durationTabName.value === '<10') {
|
||||||
|
arcMinDuration = 0
|
||||||
|
arcMaxDuration = 10 * 60
|
||||||
|
} else if (durationTabName.value === '10-30') {
|
||||||
|
arcMinDuration = 10 * 60
|
||||||
|
arcMaxDuration = 30 * 60
|
||||||
|
} else if (durationTabName.value === '30-60') {
|
||||||
|
arcMinDuration = 30 * 60
|
||||||
|
arcMaxDuration = 60 * 60
|
||||||
|
} else if (durationTabName.value === '>60') {
|
||||||
|
arcMinDuration = 60 * 60
|
||||||
|
arcMaxDuration = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(startTimeTabName, () => {
|
||||||
|
const tabName = startTimeTabName.value
|
||||||
|
if (tabName === 'date-picker') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabName === 'all') {
|
||||||
|
addTimeStart = 0
|
||||||
|
addTimeEnd = 0
|
||||||
|
} else if (tabName === 'today') {
|
||||||
|
const now = new Date()
|
||||||
|
addTimeStart = Math.floor(new Date(now.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||||
|
addTimeEnd = 0
|
||||||
|
} else if (tabName === 'yesterday') {
|
||||||
|
const now = new Date()
|
||||||
|
const yesterday = new Date(now.setDate(now.getDate() - 1))
|
||||||
|
addTimeStart = Math.floor(new Date(yesterday.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||||
|
addTimeEnd = Math.floor(new Date(yesterday.setHours(23, 59, 59, 0)).getTime() / 1000)
|
||||||
|
} else if (tabName === 'week') {
|
||||||
|
const now = new Date()
|
||||||
|
const weekAgo = new Date(now.setDate(now.getDate() - 7))
|
||||||
|
addTimeStart = Math.floor(new Date(weekAgo.setHours(0, 0, 0, 0)).getTime() / 1000)
|
||||||
|
addTimeEnd = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
getHistory(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(selectedDeviceType, () => getHistory(1))
|
||||||
|
|
||||||
|
async function getHistory(page: number) {
|
||||||
|
currentPage.value = page
|
||||||
|
searching.value = true
|
||||||
|
|
||||||
|
const result = await commands.getHistoryInfo({
|
||||||
|
pn: page,
|
||||||
|
keyword: searchInput.value,
|
||||||
|
add_time_start: addTimeStart,
|
||||||
|
add_time_end: addTimeEnd,
|
||||||
|
arc_min_duration: arcMinDuration,
|
||||||
|
arc_max_duration: arcMaxDuration,
|
||||||
|
device_type: selectedDeviceType.value,
|
||||||
|
})
|
||||||
|
if (result.status === 'error') {
|
||||||
|
console.error(result.error)
|
||||||
|
searching.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
historyInfo.value = result.data
|
||||||
|
searching.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const historyCardRefs = ref<InstanceType<typeof HistoryCard>[]>([])
|
||||||
|
const historyCardRefsMap = computed<Map<number, InstanceType<typeof HistoryCard>>>(() => {
|
||||||
|
const map = new Map<number, InstanceType<typeof HistoryCard>>()
|
||||||
|
historyCardRefs.value.forEach((card) => map.set(card.historyDetail.kid, card))
|
||||||
|
return map
|
||||||
|
})
|
||||||
|
|
||||||
|
const { selectedIds, updateSelectedIds, unselectAll } = useEpisodeSelection()
|
||||||
|
const selectionAreaRef = ref<InstanceType<typeof SelectionArea>>()
|
||||||
|
const checkedIds = ref<Set<number>>(new Set())
|
||||||
|
|
||||||
|
watch(historyInfo, () => {
|
||||||
|
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
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
historyInfo.value.list?.forEach((detail) => selectedIds.value.add(detail.kid))
|
||||||
|
dropdownShowing.value = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const { downloadEpisode, checkboxChecked, handleCheckboxClick, handleContextMenu } = useFavCard(
|
||||||
|
async (historyDetail: HistoryDetail) => {
|
||||||
|
await downloadNormalEpisode(historyDetail.kid)
|
||||||
|
},
|
||||||
|
(historyDetail: HistoryDetail) => {
|
||||||
|
return checkedIds.value.has(historyDetail.kid)
|
||||||
|
},
|
||||||
|
(historyDetail: HistoryDetail) => {
|
||||||
|
const checked = checkedIds.value.has(historyDetail.kid)
|
||||||
|
if (checked) {
|
||||||
|
checkedIds.value.delete(historyDetail.kid)
|
||||||
|
} else {
|
||||||
|
checkedIds.value.add(historyDetail.kid)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(historyDetail: HistoryDetail) => {
|
||||||
|
if (selectedIds.value.has(historyDetail.kid)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedIds.value.clear()
|
||||||
|
selectedIds.value.add(historyDetail.kid)
|
||||||
|
const selection = selectionAreaRef.value?.selection
|
||||||
|
if (selection) {
|
||||||
|
selection.clearSelection()
|
||||||
|
selection.select(`[data-key="${historyDetail.kid}"]`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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 = historyCardRefsMap.value.get(aid)
|
||||||
|
if (card !== undefined) {
|
||||||
|
card.playDownloadAnimation()
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTimePickerConfirm(range: [number, number] | null) {
|
||||||
|
if (range === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addTimeStart = Math.floor(new Date(range[0]).setHours(0, 0, 0, 0) / 1000)
|
||||||
|
addTimeEnd = Math.floor(new Date(range[1]).setHours(23, 59, 59, 0) / 1000)
|
||||||
|
|
||||||
|
startTimeTabName.value = 'date-picker'
|
||||||
|
getHistory(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitRange(): [number, number] {
|
||||||
|
const now = new Date()
|
||||||
|
|
||||||
|
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||||||
|
lastMonthStart.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
const todayEnd = new Date(now)
|
||||||
|
todayEnd.setHours(23, 59, 59, 999)
|
||||||
|
|
||||||
|
return [lastMonthStart.getTime(), todayEnd.getTime()]
|
||||||
|
}
|
||||||
|
|
||||||
|
function useFavCard(
|
||||||
|
downloadEpisode: (historyDetail: HistoryDetail) => Promise<void>,
|
||||||
|
checkboxChecked: (historyDetail: HistoryDetail) => boolean,
|
||||||
|
handleCheckboxClick: (historyDetail: HistoryDetail) => void,
|
||||||
|
handleContextMenu: (historyDetail: HistoryDetail) => void,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
downloadEpisode,
|
||||||
|
checkboxChecked,
|
||||||
|
handleCheckboxClick,
|
||||||
|
handleContextMenu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full select-none overflow-auto">
|
||||||
|
<n-input-group class="box-border px-2 pt-2">
|
||||||
|
<FloatLabelInput
|
||||||
|
label="搜索标题/up主昵称"
|
||||||
|
size="small"
|
||||||
|
v-model:value="searchInput"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="getHistory(1)" />
|
||||||
|
<n-popover trigger="click" :show-arrow="false">
|
||||||
|
<template #trigger>
|
||||||
|
<n-select class="w-20%" :show="false" default-value="更多筛选" size="small" />
|
||||||
|
</template>
|
||||||
|
<div class="w-155">
|
||||||
|
<n-tabs class="w-111.5" type="segment" size="small" v-model:value="durationTabName">
|
||||||
|
<n-tab name="all">全部时长</n-tab>
|
||||||
|
<n-tab name="<10">10分钟以下</n-tab>
|
||||||
|
<n-tab name="10-30">10-30分钟</n-tab>
|
||||||
|
<n-tab name="30-60">30-60分钟</n-tab>
|
||||||
|
<n-tab name=">60">60分钟以上</n-tab>
|
||||||
|
</n-tabs>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<n-tabs
|
||||||
|
class="justify-between"
|
||||||
|
type="segment"
|
||||||
|
size="small"
|
||||||
|
v-model:value="startTimeTabName"
|
||||||
|
@before-leave="(tabName: StartTimeTabName) => tabName !== 'date-picker'">
|
||||||
|
<n-tab name="all">全部时间</n-tab>
|
||||||
|
<n-tab name="today">今天</n-tab>
|
||||||
|
<n-tab name="yesterday">昨天</n-tab>
|
||||||
|
<n-tab name="week">近一周</n-tab>
|
||||||
|
<n-tab class="cursor-default!" name="date-picker">
|
||||||
|
<n-date-picker
|
||||||
|
size="small"
|
||||||
|
class="ml-auto w-63.5 px-1"
|
||||||
|
v-model:value="datePickerRange"
|
||||||
|
type="daterange"
|
||||||
|
@confirm="handleTimePickerConfirm" />
|
||||||
|
</n-tab>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
<n-tabs class="w-111.5" type="segment" size="small" v-model:value="selectedDeviceType">
|
||||||
|
<n-tab name="All">全部设备</n-tab>
|
||||||
|
<n-tab name="PC">PC</n-tab>
|
||||||
|
<n-tab name="Mobile">手机</n-tab>
|
||||||
|
<n-tab name="TV">平板</n-tab>
|
||||||
|
<n-tab name="Pad">TV</n-tab>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
<n-button :loading="searching" type="primary" size="small" class="w-10%" @click="getHistory(1)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon size="22">
|
||||||
|
<PhMagnifyingGlass weight="bold" />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
|
||||||
|
<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="historyDetail in historyInfo.list" :key="historyDetail.kid">
|
||||||
|
<HistoryCard
|
||||||
|
v-if="historyDetail.badge === ''"
|
||||||
|
ref="historyCardRefs"
|
||||||
|
:data-key="historyDetail.kid"
|
||||||
|
:class="[
|
||||||
|
'selectable border border-solid border-transparent',
|
||||||
|
selectedIds.has(historyDetail.kid) ? 'selected shadow-md' : 'hover:bg-gray-1',
|
||||||
|
]"
|
||||||
|
episode-type="Normal"
|
||||||
|
:history-detail="historyDetail"
|
||||||
|
:download-episode="downloadEpisode"
|
||||||
|
:checkbox-checked="checkboxChecked"
|
||||||
|
:handle-checkbox-click="handleCheckboxClick"
|
||||||
|
:handle-context-menu="handleContextMenu"
|
||||||
|
:search="searchPaneRef?.search" />
|
||||||
|
|
||||||
|
<HistoryCard
|
||||||
|
v-else-if="historyDetail.badge === '课堂'"
|
||||||
|
ref="historyCardRefs"
|
||||||
|
class="border border-solid border-transparent hover:border-gray-3"
|
||||||
|
episode-type="Cheese"
|
||||||
|
:history-detail="historyDetail"
|
||||||
|
:search="searchPaneRef?.search" />
|
||||||
|
|
||||||
|
<HistoryCard
|
||||||
|
v-else
|
||||||
|
ref="historyCardRefs"
|
||||||
|
class="border border-solid border-transparent hover:border-gray-3"
|
||||||
|
episode-type="Bangumi"
|
||||||
|
:history-detail="historyDetail"
|
||||||
|
:search="searchPaneRef?.search" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</SelectionArea>
|
||||||
|
|
||||||
|
<div class="flex gap-2 m-2 box-border">
|
||||||
|
<n-pagination :page-count="pageCount" :page="currentPage" @update:page="getHistory($event)" />
|
||||||
|
<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