mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-05 07:41:03 +08:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d3b69ca34 | ||
|
|
fdcc4a44c8 | ||
|
|
5de0494538 | ||
|
|
2045f833e4 | ||
|
|
cc4f89aac1 | ||
|
|
1c2f2c17d4 | ||
|
|
ace7a6621f | ||
|
|
d02fe55a1e | ||
|
|
9b753a8f5b | ||
|
|
11e82582b8 | ||
|
|
419358863e | ||
|
|
1d0d7f9975 | ||
|
|
c5f564372b | ||
|
|
a50f0cd727 | ||
|
|
96f6f55138 | ||
|
|
6a45c8b358 | ||
|
|
165937596e | ||
|
|
fb976f043b | ||
|
|
ecb9c4e51a | ||
|
|
9e8c3b495c | ||
|
|
24a37fc33c | ||
|
|
d09a21114d | ||
|
|
6e2b12501f | ||
|
|
2a56e116cf | ||
|
|
6de4f238d8 | ||
|
|
1b426c5957 | ||
|
|
82454a650c | ||
|
|
227b6bd7ef | ||
|
|
9554025daf | ||
|
|
0eb5d607bf | ||
|
|
750f4bc276 | ||
|
|
d0aada1d3d | ||
|
|
8a4848387c | ||
|
|
6904fc7da3 | ||
|
|
28c55a05e6 | ||
|
|
562c829267 | ||
|
|
b200ed242d | ||
|
|
815cfe55df | ||
|
|
40a1094d74 | ||
|
|
346650c091 | ||
|
|
7f74715f51 | ||
|
|
b6fcee517d | ||
|
|
4f62551f6b | ||
|
|
3980249271 | ||
|
|
e3b11b1130 | ||
|
|
f866f23af1 | ||
|
|
c793bc24f0 | ||
|
|
591a46d559 | ||
|
|
2852f26702 |
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -31,8 +31,8 @@
|
|||||||
"volar.preview.port": 3000,
|
"volar.preview.port": 3000,
|
||||||
"volar.completion.preferredTagNameCase": "pascal",
|
"volar.completion.preferredTagNameCase": "pascal",
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": true,
|
"source.fixAll.eslint": "explicit",
|
||||||
"source.fixAll.stylelint": true
|
"source.fixAll.stylelint": "explicit"
|
||||||
},
|
},
|
||||||
"eslint.alwaysShowStatus": true,
|
"eslint.alwaysShowStatus": true,
|
||||||
"eslint.format.enable": true,
|
"eslint.format.enable": true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.4.9",
|
"version": "1.5.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -28,7 +28,12 @@ app.use(
|
|||||||
|
|
||||||
// 处理根路径的请求
|
// 处理根路径的请求
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'index.html')) // 指向你的前端入口文件
|
res.sendFile(path.join(__dirname, 'index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理所有其他请求,重定向到前端入口文件
|
||||||
|
app.get('*', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'index.html'))
|
||||||
})
|
})
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import type { ThemeSwitcherTheme } from '@layouts/types'
|
import type { ThemeSwitcherTheme } from '@layouts/types'
|
||||||
|
|
||||||
@@ -20,26 +20,30 @@ const {
|
|||||||
{ initialValue: savedTheme.value },
|
{ initialValue: savedTheme.value },
|
||||||
)
|
)
|
||||||
|
|
||||||
function changeTheme() {
|
function updateTheme() {
|
||||||
const nextTheme = getNextThemeName()
|
const autoTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
const theme = currentThemeName.value === 'auto' ? autoTheme : currentThemeName.value
|
||||||
globalTheme.name.value = nextTheme
|
globalTheme.name.value = theme
|
||||||
savedTheme.value = nextTheme
|
savedTheme.value = theme
|
||||||
localStorage.setItem('theme', nextTheme)
|
|
||||||
// 修改载入时背景色
|
// 修改载入时背景色
|
||||||
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
|
||||||
|
|
||||||
themeTransition()
|
themeTransition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update icon if theme is changed from other sources
|
// 监听系统主题变化
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => globalTheme.name.value,
|
() => currentThemeName.value,
|
||||||
(val) => {
|
() => updateTheme(),
|
||||||
currentThemeName.value = val
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function changeTheme() {
|
||||||
|
const nextTheme = getNextThemeName()
|
||||||
|
currentThemeName.value = nextTheme
|
||||||
|
localStorage.setItem('theme', nextTheme)
|
||||||
|
}
|
||||||
|
|
||||||
// Apply saved theme on page load
|
// Apply saved theme on page load
|
||||||
// onMounted(() => {
|
// onMounted(() => {
|
||||||
// globalTheme.name.value = savedTheme.value
|
// globalTheme.name.value = savedTheme.value
|
||||||
|
|||||||
21
src/@core/utils/dom.ts
Normal file
21
src/@core/utils/dom.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export function removeEl(selector: string) {
|
||||||
|
if (selector) {
|
||||||
|
const el = document.querySelector(selector)
|
||||||
|
el?.parentNode?.removeChild(el)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDefer(maxFrameCount = 1) {
|
||||||
|
const frameCount = ref(0)
|
||||||
|
const refreshFrameCount = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
frameCount.value++
|
||||||
|
if (frameCount.value < maxFrameCount)
|
||||||
|
refreshFrameCount()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
refreshFrameCount()
|
||||||
|
return function (showInFrameCount: number) {
|
||||||
|
return frameCount.value >= showInFrameCount
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,3 +109,41 @@ export function formatBytes(bytes: number, decimals = 2) {
|
|||||||
|
|
||||||
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化剧集列表
|
||||||
|
export function formatEp(nums: number[]): string {
|
||||||
|
if (!nums.length)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
if (nums.length === 1)
|
||||||
|
return nums[0].toString()
|
||||||
|
|
||||||
|
// 将数组升序排序
|
||||||
|
nums.sort((a, b) => a - b)
|
||||||
|
const formattedRanges: string[] = []
|
||||||
|
let start = nums[0]
|
||||||
|
let end = nums[0]
|
||||||
|
|
||||||
|
for (let i = 1; i < nums.length; i++) {
|
||||||
|
if (nums[i] === end + 1) {
|
||||||
|
end = nums[i]
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (start === end)
|
||||||
|
formattedRanges.push(start.toString())
|
||||||
|
|
||||||
|
else
|
||||||
|
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||||
|
|
||||||
|
start = end = nums[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start === end)
|
||||||
|
formattedRanges.push(start.toString())
|
||||||
|
|
||||||
|
else
|
||||||
|
formattedRanges.push(`${start.toString()}-${end.toString()}`)
|
||||||
|
|
||||||
|
return formattedRanges.join('、')
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function isToday(date: Date) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算时间差,返回xx天xx小时xx分钟
|
// 计算时间差,返回xx天/xx小时/xx分钟/xx秒
|
||||||
export function calculateTimeDifference(inputTime: string): string {
|
export function calculateTimeDifference(inputTime: string): string {
|
||||||
if (!inputTime)
|
if (!inputTime)
|
||||||
return ''
|
return ''
|
||||||
@@ -64,6 +64,38 @@ export function calculateTimeDifference(inputTime: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算时间差,返回xx天xx小时xx分钟
|
||||||
|
export function calculateTimeDiff(inputTime: string): string {
|
||||||
|
if (!inputTime)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
// 使用当前时区
|
||||||
|
const inputDate = new Date(inputTime)
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
const timeDifference = currentDate.getTime() - inputDate.getTime()
|
||||||
|
const secondsDifference = Math.floor(timeDifference / 1000)
|
||||||
|
|
||||||
|
const days = Math.floor(secondsDifference / 86400)
|
||||||
|
const hours = Math.floor(secondsDifference % 86400 / 3600)
|
||||||
|
const minutes = Math.floor(secondsDifference % 86400 % 3600 / 60)
|
||||||
|
const secones = Math.floor(secondsDifference % 60)
|
||||||
|
|
||||||
|
if (days > 0)
|
||||||
|
return `${days}天${hours}小时${minutes}分钟`
|
||||||
|
|
||||||
|
else if (hours > 0)
|
||||||
|
return `${hours}小时${minutes}分钟`
|
||||||
|
|
||||||
|
else if (minutes > 0)
|
||||||
|
return `${minutes}分钟`
|
||||||
|
|
||||||
|
else if (secones > 0)
|
||||||
|
return `${secones}秒`
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
// 判断一个数组subArray是不是在另一个数组mainArray中
|
// 判断一个数组subArray是不是在另一个数组mainArray中
|
||||||
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
export function isContained(subArray: any[], mainArray: any[]): boolean {
|
||||||
return subArray.every(element => mainArray.includes(element))
|
return subArray.every(element => mainArray.includes(element))
|
||||||
|
|||||||
10
src/App.vue
10
src/App.vue
@@ -3,9 +3,15 @@ import { useToast } from 'vue-toast-notification'
|
|||||||
import { useTheme } from 'vuetify'
|
import { useTheme } from 'vuetify'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
|
function setTheme() {
|
||||||
|
const { global: globalTheme } = useTheme()
|
||||||
|
let theme = localStorage.getItem('theme') || 'light'
|
||||||
|
if (theme === 'auto')
|
||||||
|
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
globalTheme.name.value = theme
|
||||||
|
}
|
||||||
// 第一时间应用主题
|
// 第一时间应用主题
|
||||||
const { global: globalTheme } = useTheme()
|
setTheme()
|
||||||
globalTheme.name.value = localStorage.getItem('theme') || 'light'
|
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ export interface Subscribe {
|
|||||||
|
|
||||||
// 当前优先级
|
// 当前优先级
|
||||||
current_priority: number
|
current_priority: number
|
||||||
|
|
||||||
|
// 保存目录
|
||||||
|
save_path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 历史记录
|
// 历史记录
|
||||||
@@ -648,6 +651,13 @@ export interface TorrentInfo {
|
|||||||
|
|
||||||
// 促销描述
|
// 促销描述
|
||||||
volume_factor: string
|
volume_factor: string
|
||||||
|
|
||||||
|
// 免费时间
|
||||||
|
freedate: string
|
||||||
|
|
||||||
|
// 剩余免费时间
|
||||||
|
freedate_diff: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 识别元数据
|
// 识别元数据
|
||||||
@@ -914,3 +924,26 @@ export interface FileItem {
|
|||||||
children: FileItem[]
|
children: FileItem[]
|
||||||
modify_time: number
|
modify_time: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 媒体服务器播放条目
|
||||||
|
export interface MediaServerPlayItem {
|
||||||
|
id?: string | number
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
type?: string
|
||||||
|
image?: string
|
||||||
|
link?: string
|
||||||
|
percent?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 媒体服务器媒体库
|
||||||
|
export interface MediaServerLibrary {
|
||||||
|
server: string
|
||||||
|
id?: string | number
|
||||||
|
name: string
|
||||||
|
path?: string
|
||||||
|
type?: string
|
||||||
|
image?: string
|
||||||
|
image_list?: string[]
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|||||||
BIN
src/assets/images/misc/emby.png
Normal file
BIN
src/assets/images/misc/emby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/jellyfin.png
Normal file
BIN
src/assets/images/misc/jellyfin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
BIN
src/assets/images/misc/plex.png
Normal file
BIN
src/assets/images/misc/plex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Axios } from 'axios'
|
import type { Axios } from 'axios'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import List from './filebrowser/List.vue'
|
||||||
|
|
||||||
import Toolbar from './filebrowser/Toolbar.vue'
|
import Toolbar from './filebrowser/Toolbar.vue'
|
||||||
import Tree from './filebrowser/Tree.vue'
|
import Tree from './filebrowser/Tree.vue'
|
||||||
import List from './filebrowser/List.vue'
|
|
||||||
import type { EndPoints } from '@/api/types'
|
import type { EndPoints } from '@/api/types'
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
@@ -70,10 +70,12 @@ const storagesArray = computed(() => {
|
|||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
function loadingChanged(loading: number) {
|
function loadingChanged(loading: number) {
|
||||||
if (loading)
|
if (loading) {
|
||||||
loading++
|
loading++
|
||||||
else if (loading > 0)
|
}
|
||||||
|
else if (loading > 0) {
|
||||||
loading--
|
loading--
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function storageChanged(storage: string) {
|
function storageChanged(storage: string) {
|
||||||
@@ -92,56 +94,58 @@ function sortChanged(s: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onBeforeMount(() => {
|
onMounted(() => {
|
||||||
activeStorage.value = props.storage ?? 'local'
|
activeStorage.value = props.storage ?? 'local'
|
||||||
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
axiosInstance.value = props.axios ?? axios.create(props.axiosconfig)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VCard class="mx-auto" :loading="loading > 0">
|
<VCard class="mx-auto" :loading="loading > 0 || !path">
|
||||||
<Toolbar
|
<div v-if="path">
|
||||||
:path="props.path"
|
<Toolbar
|
||||||
:storages="storagesArray"
|
:path="path"
|
||||||
:storage="activeStorage"
|
:storages="storagesArray"
|
||||||
:endpoints="props.endpoints"
|
:storage="activeStorage"
|
||||||
:axios="axiosInstance"
|
:endpoints="endpoints"
|
||||||
@storagechanged="storageChanged"
|
:axios="axiosInstance"
|
||||||
@pathchanged="pathChanged"
|
@storagechanged="storageChanged"
|
||||||
@foldercreated="refreshPending = true"
|
@pathchanged="pathChanged"
|
||||||
@sortchanged="sortChanged"
|
@foldercreated="refreshPending = true"
|
||||||
/>
|
@sortchanged="sortChanged"
|
||||||
<VRow no-gutters>
|
/>
|
||||||
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
<VRow no-gutters>
|
||||||
<Tree
|
<VCol v-if="tree" sm="auto" class="d-none d-md-block">
|
||||||
:path="props.path"
|
<Tree
|
||||||
:storage="activeStorage"
|
:path="path"
|
||||||
:icons="fileIcons"
|
:storage="activeStorage"
|
||||||
:endpoints="endpoints"
|
:icons="fileIcons"
|
||||||
:axios="axiosInstance"
|
:endpoints="endpoints"
|
||||||
:refreshpending="refreshPending"
|
:axios="axiosInstance"
|
||||||
@pathchanged="pathChanged"
|
:refreshpending="refreshPending"
|
||||||
@loading="loadingChanged"
|
@pathchanged="pathChanged"
|
||||||
@refreshed="refreshPending = false"
|
@loading="loadingChanged"
|
||||||
/>
|
@refreshed="refreshPending = false"
|
||||||
</VCol>
|
/>
|
||||||
<VDivider v-if="tree" vertical />
|
</VCol>
|
||||||
<VCol>
|
<VDivider v-if="tree" vertical />
|
||||||
<List
|
<VCol>
|
||||||
:path="props.path"
|
<List
|
||||||
:storage="activeStorage"
|
:path="path"
|
||||||
:icons="fileIcons"
|
:storage="activeStorage"
|
||||||
:endpoints="endpoints"
|
:icons="fileIcons"
|
||||||
:axios="axiosInstance"
|
:endpoints="endpoints"
|
||||||
:refreshpending="refreshPending"
|
:axios="axiosInstance"
|
||||||
:sort="sort"
|
:refreshpending="refreshPending"
|
||||||
@pathchanged="pathChanged"
|
:sort="sort"
|
||||||
@loading="loadingChanged"
|
@pathchanged="pathChanged"
|
||||||
@refreshed="refreshPending = false"
|
@loading="loadingChanged"
|
||||||
@filedeleted="refreshPending = true"
|
@refreshed="refreshPending = false"
|
||||||
@renamed="refreshPending = true"
|
@filedeleted="refreshPending = true"
|
||||||
/>
|
@renamed="refreshPending = true"
|
||||||
</VCol>
|
/>
|
||||||
</VRow>
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
89
src/components/cards/BackdropCard.vue
Normal file
89
src/components/cards/BackdropCard.vue
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片地址
|
||||||
|
const getImgUrl = computed(() => {
|
||||||
|
const image = props.media?.image || ''
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
class="ring-gray-500"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
'ring-1': imageLoaded,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<VImg
|
||||||
|
:src="getImgUrl"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white text-shadow font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
<span class="text-shadow">{{ props.media?.subtitle }}</span>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
<div class="w-full absolute bottom-0">
|
||||||
|
<VProgressLinear
|
||||||
|
v-if="props.media?.percent"
|
||||||
|
:model-value="props.media?.percent"
|
||||||
|
bg-color="success"
|
||||||
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-shadow{
|
||||||
|
text-shadow:1px 1px #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
src/components/cards/LibraryCard.vue
Normal file
202
src/components/cards/LibraryCard.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { MediaServerLibrary } from '@/api/types'
|
||||||
|
import plex from '@images/misc/plex.png'
|
||||||
|
import emby from '@images/misc/emby.png'
|
||||||
|
import jellyfin from '@images/misc/jellyfin.png'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerLibrary>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
|
|
||||||
|
// 图片地址
|
||||||
|
const imgUrl = ref('')
|
||||||
|
|
||||||
|
// 图片是否加载完成
|
||||||
|
const imageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片是否加载错误
|
||||||
|
const imageError = ref(false)
|
||||||
|
|
||||||
|
// 图片加载完成响应
|
||||||
|
function imageLoadHandler() {
|
||||||
|
imageLoaded.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载错误
|
||||||
|
function imageErrorHandler() {
|
||||||
|
imageError.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认图片
|
||||||
|
function getDefaultImage() {
|
||||||
|
if (props.media?.server === 'plex')
|
||||||
|
return plex
|
||||||
|
else if (props.media?.server === 'emby')
|
||||||
|
return emby
|
||||||
|
else if (props.media?.server === 'jellyfin')
|
||||||
|
return jellyfin
|
||||||
|
else
|
||||||
|
return plex
|
||||||
|
}
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成图片代理路径
|
||||||
|
function getImgUrl(url: string) {
|
||||||
|
if (!url)
|
||||||
|
return getDefaultImage()
|
||||||
|
else
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(url)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据多张图片生成媒体库封面
|
||||||
|
async function drawImages(imageList: string[]) {
|
||||||
|
// 图片
|
||||||
|
const IMAGES = imageList
|
||||||
|
if (IMAGES.length === 0)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 为所有图片添加system/img前缀
|
||||||
|
for (let i = 0; i < IMAGES.length; i++)
|
||||||
|
IMAGES[i] = `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(IMAGES[i])}`
|
||||||
|
|
||||||
|
// canvas
|
||||||
|
const canvas = canvasRef.value
|
||||||
|
if (!canvas)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 画布参数
|
||||||
|
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||||
|
const POSTER_HEIGHT = canvas.height * 0.75 - 8
|
||||||
|
const MARGIN_WIDTH = 4
|
||||||
|
const MARGIN_HEIGHT = 4
|
||||||
|
const REFLECTION_HEIGHT = POSTER_HEIGHT / 2
|
||||||
|
const REFLECTION_SHOW_HEIGHT = canvas.height / 4
|
||||||
|
|
||||||
|
// 获取画布上下文
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx)
|
||||||
|
return getDefaultImage()
|
||||||
|
|
||||||
|
// 设置背景色为黑色
|
||||||
|
ctx.fillStyle = '#000000'
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||||
|
|
||||||
|
// 绘制图片
|
||||||
|
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||||
|
if (!canvas)
|
||||||
|
return
|
||||||
|
|
||||||
|
if (!ctx)
|
||||||
|
return
|
||||||
|
|
||||||
|
const img = new Image()
|
||||||
|
img.setAttribute('crossorigin', 'anonymous')
|
||||||
|
img.src = imgSrc
|
||||||
|
await new Promise(resolve => img.onload = resolve)
|
||||||
|
|
||||||
|
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||||
|
const y = MARGIN_HEIGHT
|
||||||
|
|
||||||
|
ctx.drawImage(img, x, y, POSTER_WIDTH, POSTER_HEIGHT)
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(0, canvas.height)
|
||||||
|
ctx.scale(1, -1)
|
||||||
|
ctx.drawImage(
|
||||||
|
img,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
img.width,
|
||||||
|
img.height,
|
||||||
|
x,
|
||||||
|
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||||
|
POSTER_WIDTH,
|
||||||
|
REFLECTION_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
const gradient = ctx.createLinearGradient(
|
||||||
|
0,
|
||||||
|
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
||||||
|
0,
|
||||||
|
REFLECTION_HEIGHT,
|
||||||
|
)
|
||||||
|
|
||||||
|
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||||
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||||
|
ctx.fillStyle = gradient
|
||||||
|
ctx.fillRect(x, 0, POSTER_WIDTH, REFLECTION_SHOW_HEIGHT)
|
||||||
|
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制多张图片
|
||||||
|
const loopCount = Math.min(4, IMAGES.length)
|
||||||
|
for (let i = 0; i < loopCount; i++)
|
||||||
|
await drawImageWithReflection(IMAGES[i], i + 1)
|
||||||
|
|
||||||
|
// 转换为图片地址
|
||||||
|
return canvas.toDataURL('image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||||
|
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||||
|
else
|
||||||
|
imgUrl.value = getImgUrl(props.media?.image || '')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover
|
||||||
|
v-bind="props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
>
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
}"
|
||||||
|
@click="goPlay"
|
||||||
|
>
|
||||||
|
<template #image>
|
||||||
|
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||||
|
<VImg
|
||||||
|
:src="imgUrl"
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
cover
|
||||||
|
@load="imageLoadHandler"
|
||||||
|
@error="imageErrorHandler"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<VCardText
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<h1 class="mb-1 text-white font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.name }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</template>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
@@ -16,6 +16,11 @@ const props = defineProps({
|
|||||||
height: String,
|
height: String,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 订阅规则
|
||||||
|
const subscribeRules = ref({
|
||||||
|
show_edit_dialog: false,
|
||||||
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@ async function addSubscribe(season = 0) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 弹出订阅编辑弹窗
|
// 弹出订阅编辑弹窗
|
||||||
if (result.success && seasonsSelected.value.length <= 1) {
|
if (result.success && seasonsSelected.value.length <= 1 && subscribeRules.value.show_edit_dialog) {
|
||||||
subscribeId.value = result.data.id
|
subscribeId.value = result.data.id
|
||||||
subscribeEditDialog.value = true
|
subscribeEditDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -223,7 +228,7 @@ async function handleCheckSubscribe() {
|
|||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function handleCheckExists() {
|
async function handleCheckExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: props.media?.tmdb_id,
|
tmdbid: props.media?.tmdb_id,
|
||||||
title: props.media?.title,
|
title: props.media?.title,
|
||||||
@@ -269,7 +274,7 @@ async function checkSeasonsNotExists() {
|
|||||||
// 开始处理
|
// 开始处理
|
||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', props.media)
|
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', props.media)
|
||||||
if (result) {
|
if (result) {
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已入库 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
@@ -302,6 +307,20 @@ async function getMediaSeasons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询订阅弹窗规则
|
||||||
|
async function querySubscribeRules() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
subscribeRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 爱心订阅按钮响应
|
// 爱心订阅按钮响应
|
||||||
function handleSubscribe() {
|
function handleSubscribe() {
|
||||||
if (isSubscribed.value)
|
if (isSubscribed.value)
|
||||||
@@ -373,6 +392,7 @@ function handleSearch() {
|
|||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
handleCheckSubscribe()
|
handleCheckSubscribe()
|
||||||
handleCheckExists()
|
handleCheckExists()
|
||||||
|
querySubscribeRules()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 计算图片地址
|
// 计算图片地址
|
||||||
|
|||||||
102
src/components/cards/PosterCard.vue
Normal file
102
src/components/cards/PosterCard.vue
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import noImage from '@images/no-image.jpeg'
|
||||||
|
|
||||||
|
// 输入参数
|
||||||
|
const props = defineProps({
|
||||||
|
media: Object as PropType<MediaServerPlayItem>,
|
||||||
|
width: String,
|
||||||
|
height: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片加载状态
|
||||||
|
const isImageLoaded = ref(false)
|
||||||
|
|
||||||
|
// 图片加载失败
|
||||||
|
const imageLoadError = ref(false)
|
||||||
|
|
||||||
|
// 角标颜色
|
||||||
|
function getChipColor(type: string) {
|
||||||
|
if (type === '电影')
|
||||||
|
return 'border-blue-500 bg-blue-600'
|
||||||
|
else if (type === '电视剧')
|
||||||
|
return ' bg-indigo-500 border-indigo-600'
|
||||||
|
else
|
||||||
|
return 'border-purple-600 bg-purple-600'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算图片地址
|
||||||
|
const getImgUrl = computed(() => {
|
||||||
|
if (imageLoadError.value)
|
||||||
|
return noImage
|
||||||
|
const image = props.media?.image || ''
|
||||||
|
return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(image)}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 跳转播放
|
||||||
|
function goPlay() {
|
||||||
|
if (props.media?.link)
|
||||||
|
window.open(props.media?.link, '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VHover v-bind="props">
|
||||||
|
<template #default="hover">
|
||||||
|
<VCard
|
||||||
|
v-bind="hover.props"
|
||||||
|
:height="props.height"
|
||||||
|
:width="props.width"
|
||||||
|
class="outline-none shadow ring-gray-500 rounded-lg"
|
||||||
|
:class="{
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
|
'ring-1': isImageLoaded,
|
||||||
|
}"
|
||||||
|
@click.stop="goPlay"
|
||||||
|
>
|
||||||
|
<VImg
|
||||||
|
aspect-ratio="2/3"
|
||||||
|
:src="getImgUrl"
|
||||||
|
class="object-cover aspect-w-2 aspect-h-3"
|
||||||
|
:class="hover.isHovering ? 'on-hover' : ''"
|
||||||
|
cover
|
||||||
|
@load="isImageLoaded = true"
|
||||||
|
@error="imageLoadError = true"
|
||||||
|
>
|
||||||
|
<template #placeholder>
|
||||||
|
<div class="w-full h-full">
|
||||||
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- 类型角标 -->
|
||||||
|
<VChip
|
||||||
|
v-show="isImageLoaded"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
:class="getChipColor(props.media?.type || '')"
|
||||||
|
class="absolute left-2 top-2 bg-opacity-80 shadow-md text-white font-bold"
|
||||||
|
>
|
||||||
|
{{ props.media?.type }}
|
||||||
|
</VChip>
|
||||||
|
<!-- 详情 -->
|
||||||
|
<VCardText
|
||||||
|
v-show="hover.isHovering || imageLoadError"
|
||||||
|
class="w-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ props.media?.subtitle }}</span>
|
||||||
|
<h1 class="mb-1 text-white font-extrabold text-xl line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
|
{{ props.media?.title }}
|
||||||
|
</h1>
|
||||||
|
</VCardText>
|
||||||
|
</VImg>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
</VHover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.on-hover img {
|
||||||
|
@apply brightness-50;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -412,6 +412,23 @@ onMounted(() => {
|
|||||||
<div class="text-sm my-1">
|
<div class="text-sm my-1">
|
||||||
{{ item.raw.description }}
|
{{ item.raw.description }}
|
||||||
</div>
|
</div>
|
||||||
|
<VChip
|
||||||
|
v-if="item.raw?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="item.raw?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ item.raw?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in item.raw?.labels"
|
v-for="(label, index) in item.raw?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -195,6 +195,23 @@ onMounted(() => {
|
|||||||
v-if="torrent?.labels"
|
v-if="torrent?.labels"
|
||||||
class="pb-3 pt-0 pe-12"
|
class="pb-3 pt-0 pe-12"
|
||||||
>
|
>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in torrent?.labels"
|
v-for="(label, index) in torrent?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -150,6 +150,23 @@ onMounted(() => {
|
|||||||
v-if="torrent?.labels"
|
v-if="torrent?.labels"
|
||||||
class="pt-2"
|
class="pt-2"
|
||||||
>
|
>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.hit_and_run"
|
||||||
|
variant="elevated"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1 text-white bg-black"
|
||||||
|
>
|
||||||
|
H&R
|
||||||
|
</VChip>
|
||||||
|
<VChip
|
||||||
|
v-if="torrent?.freedate_diff"
|
||||||
|
variant="elevated"
|
||||||
|
color="secondary"
|
||||||
|
size="small"
|
||||||
|
class="me-1 mb-1"
|
||||||
|
>
|
||||||
|
{{ torrent?.freedate_diff }}
|
||||||
|
</VChip>
|
||||||
<VChip
|
<VChip
|
||||||
v-for="(label, index) in torrent?.labels"
|
v-for="(label, index) in torrent?.labels"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ async function updateSiteInfo() {
|
|||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="siteForm.limit_seconds"
|
v-model="siteForm.limit_count"
|
||||||
label="访问次数"
|
label="访问次数"
|
||||||
:rules="[numberValidator]"
|
:rules="[numberValidator]"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const subscribeForm = ref<Subscribe>({
|
|||||||
last_update: '',
|
last_update: '',
|
||||||
username: '',
|
username: '',
|
||||||
current_priority: 0,
|
current_priority: 0,
|
||||||
|
save_path: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// 提示框
|
// 提示框
|
||||||
@@ -322,6 +323,17 @@ watchEffect(() => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VTextField
|
||||||
|
v-model="subscribeForm.save_path"
|
||||||
|
label="保存路径"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol
|
<VCol
|
||||||
cols="12"
|
cols="12"
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const themes: ThemeSwitcherTheme[] = [
|
|||||||
name: 'purple',
|
name: 'purple',
|
||||||
icon: 'mdi-brightness-4',
|
icon: 'mdi-brightness-4',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'auto',
|
||||||
|
icon: 'mdi-brightness-auto',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import '@/@iconify/icons-bundle'
|
import '@/@iconify/icons-bundle'
|
||||||
import ToastPlugin from 'vue-toast-notification'
|
import ToastPlugin from 'vue-toast-notification'
|
||||||
import VuetifyUseDialog from 'vuetify-use-dialog'
|
import VuetifyUseDialog from 'vuetify-use-dialog'
|
||||||
|
import { removeEl } from './@core/utils/dom'
|
||||||
import App from '@/App.vue'
|
import App from '@/App.vue'
|
||||||
import vuetify from '@/plugins/vuetify'
|
import vuetify from '@/plugins/vuetify'
|
||||||
import { loadFonts } from '@/plugins/webfontloader'
|
import { loadFonts } from '@/plugins/webfontloader'
|
||||||
@@ -11,7 +12,6 @@ import '@core/scss/template/index.scss'
|
|||||||
import '@layouts/styles/index.scss'
|
import '@layouts/styles/index.scss'
|
||||||
import '@styles/styles.scss'
|
import '@styles/styles.scss'
|
||||||
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
import 'vue-toast-notification/dist/theme-bootstrap.css'
|
||||||
import { removeEl } from '@/util'
|
|
||||||
|
|
||||||
loadFonts()
|
loadFonts()
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,57 @@ import AnalyticsStorage from '@/views/dashboard/AnalyticsStorage.vue'
|
|||||||
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
import AnalyticsWeeklyOverview from '@/views/dashboard/AnalyticsWeeklyOverview.vue'
|
||||||
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
import AnalyticsCpu from '@/views/dashboard/AnalyticsCpu.vue'
|
||||||
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
||||||
|
import MediaServerLatest from '@/views/dashboard/MediaServerLatest.vue'
|
||||||
|
import MediaServerLibrary from '@/views/dashboard/MediaServerLibrary.vue'
|
||||||
|
import MediaServerPlaying from '@/views/dashboard/MediaServerPlaying.vue'
|
||||||
|
|
||||||
|
// 仪表盘配置
|
||||||
|
const dashboard_names = {
|
||||||
|
storage: '存储空间',
|
||||||
|
mediaStatistic: '媒体统计',
|
||||||
|
weeklyOverview: '最近入库',
|
||||||
|
speed: '实时速率',
|
||||||
|
scheduler: '后台任务',
|
||||||
|
cpu: 'CPU',
|
||||||
|
memory: '内存',
|
||||||
|
library: '我的媒体库',
|
||||||
|
playing: '继续观看',
|
||||||
|
latest: '最近添加',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹窗
|
||||||
|
const dialog = ref(false)
|
||||||
|
|
||||||
|
// 从localStorage中获取数据
|
||||||
|
const default_config = {
|
||||||
|
mediaStatistic: true,
|
||||||
|
scheduler: false,
|
||||||
|
speed: false,
|
||||||
|
storage: true,
|
||||||
|
weeklyOverview: false,
|
||||||
|
cpu: false,
|
||||||
|
memory: false,
|
||||||
|
library: true,
|
||||||
|
playing: true,
|
||||||
|
latest: true,
|
||||||
|
}
|
||||||
|
const config = ref(JSON.parse(localStorage.getItem('MP_DASHBOARD') || '{}'))
|
||||||
|
if (Object.keys(config.value).length === 0) {
|
||||||
|
config.value = default_config
|
||||||
|
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置项目
|
||||||
|
function setDashboardConfig() {
|
||||||
|
localStorage.setItem('MP_DASHBOARD', JSON.stringify(config.value))
|
||||||
|
dialog.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VRow class="match-height">
|
<VRow class="match-height">
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.storage"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -18,6 +64,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.mediaStatistic"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="8"
|
md="8"
|
||||||
>
|
>
|
||||||
@@ -25,6 +72,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.weeklyOverview"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -32,6 +80,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.speed"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -39,6 +88,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.scheduler"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="4"
|
md="4"
|
||||||
>
|
>
|
||||||
@@ -46,6 +96,7 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.cpu"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
>
|
>
|
||||||
@@ -53,10 +104,75 @@ import AnalyticsMemory from '@/views/dashboard/AnalyticsMemory.vue'
|
|||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
<VCol
|
<VCol
|
||||||
|
v-if="config.memory"
|
||||||
cols="12"
|
cols="12"
|
||||||
md="6"
|
md="6"
|
||||||
>
|
>
|
||||||
<AnalyticsMemory />
|
<AnalyticsMemory />
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.library"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerLibrary />
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.playing"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerPlaying />
|
||||||
|
</VCol>
|
||||||
|
|
||||||
|
<VCol
|
||||||
|
v-if="config.latest"
|
||||||
|
cols="12"
|
||||||
|
>
|
||||||
|
<MediaServerLatest />
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
|
<!-- 底部操作按钮 -->
|
||||||
|
<span class="fixed right-5 bottom-5">
|
||||||
|
<VBtn icon="mdi-view-dashboard-edit" class="me-2" color="primary" size="x-large" @click="dialog = true" />
|
||||||
|
</span>
|
||||||
|
<!-- 弹窗,根据配置生成选项 -->
|
||||||
|
<VDialog
|
||||||
|
v-model="dialog"
|
||||||
|
max-width="600"
|
||||||
|
scrollable
|
||||||
|
>
|
||||||
|
<VCard title="设置仪表板">
|
||||||
|
<VCardText>
|
||||||
|
<VRow>
|
||||||
|
<VCol
|
||||||
|
v-for="(item, key) in dashboard_names"
|
||||||
|
:key="key"
|
||||||
|
cols="12"
|
||||||
|
md="4"
|
||||||
|
>
|
||||||
|
<VCheckbox
|
||||||
|
v-model="config[key]"
|
||||||
|
:label="dashboard_names[key]"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
</VRow>
|
||||||
|
</VCardText>
|
||||||
|
<VCardActions>
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
@click="dialog = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</VBtn>
|
||||||
|
<VSpacer />
|
||||||
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
@click="setDashboardConfig"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</VBtn>
|
||||||
|
</VCardActions>
|
||||||
|
</VCard>
|
||||||
|
</vdialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
},
|
},
|
||||||
VList: {
|
VList: {
|
||||||
activeColor: 'primary',
|
color: 'primary',
|
||||||
},
|
},
|
||||||
VPagination: {
|
VPagination: {
|
||||||
activeColor: 'primary',
|
activeColor: 'primary',
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
export function removeEl(selector: string) {
|
|
||||||
if (selector) {
|
|
||||||
const el = document.querySelector(selector)
|
|
||||||
el?.parentNode?.removeChild(el)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './dom'
|
|
||||||
48
src/views/dashboard/MediaServerLatest.vue
Normal file
48
src/views/dashboard/MediaServerLatest.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import PosterCard from '@/components/cards/PosterCard.vue'
|
||||||
|
|
||||||
|
// 最近入库列表
|
||||||
|
const latestList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadLatest() {
|
||||||
|
try {
|
||||||
|
latestList.value = await api.get('mediaserver/latest')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLatest()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>最近添加</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="latestList.length > 0"
|
||||||
|
class="grid gap-4 grid-media-card mx-3 mb-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<PosterCard
|
||||||
|
v-for="data in latestList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-media-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(9.375rem, 1fr));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
50
src/views/dashboard/MediaServerLibrary.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import LibraryCard from '@/components/cards/LibraryCard.vue'
|
||||||
|
|
||||||
|
// 媒体库列表
|
||||||
|
const libraryList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadLibrary() {
|
||||||
|
try {
|
||||||
|
libraryList.value = await api.get('mediaserver/library')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadLibrary()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>我的媒体库</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="libraryList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<LibraryCard
|
||||||
|
v-for="data in libraryList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-backdrop-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
50
src/views/dashboard/MediaServerPlaying.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import api from '@/api'
|
||||||
|
import type { MediaServerPlayItem } from '@/api/types'
|
||||||
|
import BackdropCard from '@/components/cards/BackdropCard.vue'
|
||||||
|
|
||||||
|
// 继续播放列表
|
||||||
|
const playingList = ref<MediaServerPlayItem[]>([])
|
||||||
|
|
||||||
|
// 调用API查询
|
||||||
|
async function loadPlayingList() {
|
||||||
|
try {
|
||||||
|
playingList.value = await api.get('mediaserver/playing')
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadPlayingList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VCardTitle>继续观看</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="playingList.length > 0"
|
||||||
|
class="grid gap-4 grid-backdrop-card mx-3"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<BackdropCard
|
||||||
|
v-for="data in playingList"
|
||||||
|
:key="data.id"
|
||||||
|
:media="data"
|
||||||
|
height="10rem"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</VCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.grid-backdrop-card {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
|
||||||
|
padding-block-end: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -25,8 +25,8 @@ const mediaDetail = ref<MediaInfo>({} as MediaInfo)
|
|||||||
// 订阅编辑弹窗
|
// 订阅编辑弹窗
|
||||||
const subscribeEditDialog = ref(false)
|
const subscribeEditDialog = ref(false)
|
||||||
|
|
||||||
// 本地是否存在
|
// 本地是否存在,存在则包括Item信息
|
||||||
const isExists = ref(false)
|
const existsItemId = ref('')
|
||||||
|
|
||||||
// 是否已订阅
|
// 是否已订阅
|
||||||
const isSubscribed = ref(false)
|
const isSubscribed = ref(false)
|
||||||
@@ -46,6 +46,11 @@ const seasonsSubscribed = ref<{ [key: number]: boolean }>({})
|
|||||||
// 订阅编号
|
// 订阅编号
|
||||||
const subscribeId = ref<number>()
|
const subscribeId = ref<number>()
|
||||||
|
|
||||||
|
// 订阅规则
|
||||||
|
const subscribeRules = ref({
|
||||||
|
show_edit_dialog: false,
|
||||||
|
})
|
||||||
|
|
||||||
// 调用API查询详情
|
// 调用API查询详情
|
||||||
async function getMediaDetail() {
|
async function getMediaDetail() {
|
||||||
if (mediaProps.mediaid && mediaProps.type) {
|
if (mediaProps.mediaid && mediaProps.type) {
|
||||||
@@ -59,9 +64,8 @@ async function getMediaDetail() {
|
|||||||
return
|
return
|
||||||
|
|
||||||
// 检查存在状态
|
// 检查存在状态
|
||||||
if (mediaDetail.value.type === '电影')
|
checkExists()
|
||||||
checkMovieExists()
|
if (mediaDetail.value.type === '电视剧')
|
||||||
else
|
|
||||||
checkSeasonsNotExists()
|
checkSeasonsNotExists()
|
||||||
// 检查订阅状态
|
// 检查订阅状态
|
||||||
if (mediaDetail.value.type === '电影')
|
if (mediaDetail.value.type === '电影')
|
||||||
@@ -86,9 +90,9 @@ async function loadSeasonEpisodes(season: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前媒体是否已入库
|
// 查询当前媒体是否已入库
|
||||||
async function checkMovieExists() {
|
async function checkExists() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('media/exists', {
|
const result: { [key: string]: any } = await api.get('mediaserver/exists', {
|
||||||
params: {
|
params: {
|
||||||
tmdbid: mediaDetail.value.tmdb_id,
|
tmdbid: mediaDetail.value.tmdb_id,
|
||||||
title: mediaDetail.value.title,
|
title: mediaDetail.value.title,
|
||||||
@@ -99,7 +103,7 @@ async function checkMovieExists() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (result.success)
|
if (result.success)
|
||||||
isExists.value = true
|
existsItemId.value = result.data.item.id
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -133,11 +137,8 @@ async function checkSeasonsNotExists() {
|
|||||||
if (mediaDetail.value.type !== '电视剧')
|
if (mediaDetail.value.type !== '电视剧')
|
||||||
return
|
return
|
||||||
try {
|
try {
|
||||||
const result: NotExistMediaInfo[] = await api.post('download/notexists', mediaDetail.value)
|
const result: NotExistMediaInfo[] = await api.post('mediaserver/notexists', mediaDetail.value)
|
||||||
if (result) {
|
if (result) {
|
||||||
if (result.length === 0)
|
|
||||||
isExists.value = true
|
|
||||||
|
|
||||||
result.forEach((item) => {
|
result.forEach((item) => {
|
||||||
// 0-已入库 1-部分缺失 2-全部缺失
|
// 0-已入库 1-部分缺失 2-全部缺失
|
||||||
let state = 0
|
let state = 0
|
||||||
@@ -145,8 +146,6 @@ async function checkSeasonsNotExists() {
|
|||||||
state = 2
|
state = 2
|
||||||
else if (item.episodes.length < item.total_episode)
|
else if (item.episodes.length < item.total_episode)
|
||||||
state = 1
|
state = 1
|
||||||
if (state !== 2)
|
|
||||||
isExists.value = true
|
|
||||||
seasonsNotExisted.value[item.season] = state
|
seasonsNotExisted.value[item.season] = state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -188,7 +187,7 @@ async function addSubscribe(season = 0) {
|
|||||||
startNProgress()
|
startNProgress()
|
||||||
try {
|
try {
|
||||||
// 是否洗版
|
// 是否洗版
|
||||||
let best_version = isExists.value ? 1 : 0
|
let best_version = existsItemId.value ? 1 : 0
|
||||||
if (season)
|
if (season)
|
||||||
// 全部存在时洗版
|
// 全部存在时洗版
|
||||||
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
best_version = !seasonsNotExisted.value[season] ? 1 : 0
|
||||||
@@ -221,7 +220,7 @@ async function addSubscribe(season = 0) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 显示编辑弹窗
|
// 显示编辑弹窗
|
||||||
if (result.success) {
|
if (result.success && subscribeRules.value.show_edit_dialog) {
|
||||||
subscribeId.value = result.data.id
|
subscribeId.value = result.data.id
|
||||||
subscribeEditDialog.value = true
|
subscribeEditDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -283,6 +282,20 @@ async function removeSubscribe(season: number) {
|
|||||||
doneNProgress()
|
doneNProgress()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询订阅弹窗规则
|
||||||
|
async function querySubscribeRules() {
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
'system/setting/DefaultFilterRules',
|
||||||
|
)
|
||||||
|
if (result.data?.value)
|
||||||
|
subscribeRules.value = result.data?.value
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 订阅按钮响应
|
// 订阅按钮响应
|
||||||
function handleSubscribe(season = 0) {
|
function handleSubscribe(season = 0) {
|
||||||
if (isSubscribed.value)
|
if (isSubscribed.value)
|
||||||
@@ -403,8 +416,29 @@ function handleSearch(area: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 跳转播放页面
|
||||||
|
async function handlePlay() {
|
||||||
|
// 获取播放链接地址
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get(
|
||||||
|
`mediaserver/play/${existsItemId.value}`,
|
||||||
|
)
|
||||||
|
if (result?.success) {
|
||||||
|
// 打开链接地址
|
||||||
|
setTimeout(() => {
|
||||||
|
window.open(result.data.url, '_blank')
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
else { $toast.error(`获取播放链接失败:${result.message}!`) }
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
getMediaDetail()
|
getMediaDetail()
|
||||||
|
querySubscribeRules()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -438,7 +472,7 @@ onBeforeMount(() => {
|
|||||||
</VImg>
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-title">
|
<div class="media-title">
|
||||||
<div v-if="isExists" class="media-status">
|
<div v-if="existsItemId" class="media-status">
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full whitespace-nowrap transition !no-underline bg-green-500 bg-opacity-80 border border-green-500 !text-green-100 hover:bg-green-500 hover:bg-opacity-100 false overflow-hidden">
|
||||||
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
<div class="relative z-20 flex items-center false"><span>已入库</span></div>
|
||||||
</span>
|
</span>
|
||||||
@@ -458,7 +492,7 @@ onBeforeMount(() => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-actions">
|
<div class="media-actions">
|
||||||
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info">
|
<VBtn v-if="mediaDetail.tmdb_id || mediaDetail.douban_id" variant="tonal" color="info" class="mb-2">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-magnify" />
|
<VIcon icon="mdi-magnify" />
|
||||||
</template>
|
</template>
|
||||||
@@ -484,12 +518,18 @@ onBeforeMount(() => {
|
|||||||
</VList>
|
</VList>
|
||||||
</VMenu>
|
</VMenu>
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
<VBtn v-if="mediaDetail.type === '电影' || mediaDetail.douban_id" class="ms-2 mb-2" :color="getSubscribeColor" variant="tonal" @click="handleSubscribe(0)">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon :icon="getSubscribeIcon" />
|
<VIcon :icon="getSubscribeIcon" />
|
||||||
</template>
|
</template>
|
||||||
{{ isSubscribed ? '已订阅' : '订阅' }}
|
{{ isSubscribed ? '已订阅' : '订阅' }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
|
<VBtn v-if="existsItemId" class="ms-2 mb-2" variant="tonal" @click="handlePlay()">
|
||||||
|
<template #prepend>
|
||||||
|
<VIcon icon="mdi-play" />
|
||||||
|
</template>
|
||||||
|
在线播放
|
||||||
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-overview">
|
<div class="media-overview">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
import TorrentCard from '@/components/cards/TorrentCard.vue'
|
||||||
|
import { useDefer } from '@/@core/utils/dom'
|
||||||
|
|
||||||
interface SearchTorrent extends Context {
|
interface SearchTorrent extends Context {
|
||||||
more?: Array<Context>
|
more?: Array<Context>
|
||||||
@@ -48,7 +48,7 @@ const editionFilterOptions = ref<Array<string>>([])
|
|||||||
const resolutionFilterOptions = ref<Array<string>>([])
|
const resolutionFilterOptions = ref<Array<string>>([])
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref <Array<SearchTorrent>>([])
|
const dataList = ref<Array<SearchTorrent>>([])
|
||||||
|
|
||||||
// 分组后的数据列表
|
// 分组后的数据列表
|
||||||
const groupedDataList = ref<Map<string, Context[]>>()
|
const groupedDataList = ref<Map<string, Context[]>>()
|
||||||
@@ -69,7 +69,7 @@ function initOptions(data: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 计算分组后的列表
|
// 计算分组后的列表
|
||||||
watchEffect(() => {
|
onMounted(() => {
|
||||||
// 数据分组
|
// 数据分组
|
||||||
const groupMap = new Map<string, Context[]>()
|
const groupMap = new Map<string, Context[]>()
|
||||||
// 遍历数据
|
// 遍历数据
|
||||||
@@ -92,10 +92,12 @@ watchEffect(() => {
|
|||||||
groupedDataList.value = groupMap
|
groupedDataList.value = groupMap
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let defer = (_: number) => true
|
||||||
|
|
||||||
// 计算过滤后的列表
|
// 计算过滤后的列表
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// 清空列表
|
// 清空列表
|
||||||
dataList.value.splice(0)
|
dataList.value = []
|
||||||
// 匹配过滤函数
|
// 匹配过滤函数
|
||||||
const match = (filter: Array<string>, value: string | undefined) =>
|
const match = (filter: Array<string>, value: string | undefined) =>
|
||||||
filter.length === 0 || (value && filter.includes(value))
|
filter.length === 0 || (value && filter.includes(value))
|
||||||
@@ -126,10 +128,12 @@ watchEffect(() => {
|
|||||||
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
const firstData = _.cloneDeepWith(matchData[0]) as SearchTorrent
|
||||||
if (matchData.length > 1)
|
if (matchData.length > 1)
|
||||||
firstData.more = matchData.slice(1)
|
firstData.more = matchData.slice(1)
|
||||||
|
|
||||||
dataList.value.push(firstData)
|
dataList.value.push(firstData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
defer = useDefer(dataList.value.length)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -216,12 +220,9 @@ watchEffect(() => {
|
|||||||
</VRow>
|
</VRow>
|
||||||
</VCard>
|
</VCard>
|
||||||
<div class="grid gap-3 grid-torrent-card items-start">
|
<div class="grid gap-3 grid-torrent-card items-start">
|
||||||
<TorrentCard
|
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||||
v-for="(item, index) in dataList"
|
<TorrentCard v-if="defer(index)" :torrent="item" :more="item.more" />
|
||||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
</div>
|
||||||
:torrent="item"
|
|
||||||
:more="item.more"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Context } from '@/api/types'
|
import type { Context } from '@/api/types'
|
||||||
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
import TorrentItem from '@/components/cards/TorrentItem.vue'
|
||||||
|
import { useDefer } from '@/@core/utils/dom'
|
||||||
|
|
||||||
// 定义输入参数
|
// 定义输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -27,7 +28,7 @@ const filterForm = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref <Array<Context>>([])
|
const dataList = ref<Array<Context>>([])
|
||||||
|
|
||||||
// 获取站点过滤选项
|
// 获取站点过滤选项
|
||||||
const siteFilterOptions = ref<Array<string>>([])
|
const siteFilterOptions = ref<Array<string>>([])
|
||||||
@@ -59,10 +60,12 @@ function initOptions(data: Context) {
|
|||||||
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
optionValue(resolutionFilterOptions.value, meta_info?.resource_pix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defer = (_: number) => true
|
||||||
|
|
||||||
// 计算过滤后的列表
|
// 计算过滤后的列表
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
// 清空列表
|
// 清空列表
|
||||||
dataList.value.splice(0)
|
dataList.value = []
|
||||||
// 匹配过滤函数
|
// 匹配过滤函数
|
||||||
const match = (filter: Array<string>, value: string | undefined) =>
|
const match = (filter: Array<string>, value: string | undefined) =>
|
||||||
filter.length === 0 || (value && filter.includes(value))
|
filter.length === 0 || (value && filter.includes(value))
|
||||||
@@ -72,21 +75,22 @@ watchEffect(() => {
|
|||||||
if (
|
if (
|
||||||
// 站点过滤
|
// 站点过滤
|
||||||
match(filterForm.site, torrent_info.site_name)
|
match(filterForm.site, torrent_info.site_name)
|
||||||
// 促销状态过滤
|
// 促销状态过滤
|
||||||
&& match(filterForm.freeState, torrent_info.volume_factor)
|
&& match(filterForm.freeState, torrent_info.volume_factor)
|
||||||
// 季过滤
|
// 季过滤
|
||||||
&& match(filterForm.season, meta_info.season_episode)
|
&& match(filterForm.season, meta_info.season_episode)
|
||||||
// 制作组过滤
|
// 制作组过滤
|
||||||
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
&& match(filterForm.releaseGroup, meta_info.resource_team)
|
||||||
// 视频编码过滤
|
// 视频编码过滤
|
||||||
&& match(filterForm.videoCode, meta_info.video_encode)
|
&& match(filterForm.videoCode, meta_info.video_encode)
|
||||||
// 分辨率过滤
|
// 分辨率过滤
|
||||||
&& match(filterForm.resolution, meta_info.resource_pix)
|
&& match(filterForm.resolution, meta_info.resource_pix)
|
||||||
// 质量过滤
|
// 质量过滤
|
||||||
&& match(filterForm.edition, meta_info.edition)
|
&& match(filterForm.edition, meta_info.edition)
|
||||||
)
|
)
|
||||||
dataList.value.push(data)
|
dataList.value.push(data)
|
||||||
})
|
})
|
||||||
|
defer = useDefer(dataList.value.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 初始化过滤选项
|
// 初始化过滤选项
|
||||||
@@ -100,25 +104,18 @@ onMounted(() => {
|
|||||||
<template>
|
<template>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol>
|
<VCol>
|
||||||
<VList
|
<VList v-if="dataList.length === 0" lines="three" class="rounded">
|
||||||
lines="three"
|
<VListItem>
|
||||||
class="rounded"
|
|
||||||
>
|
|
||||||
<TorrentItem
|
|
||||||
v-for="(item, index) in dataList"
|
|
||||||
:key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`"
|
|
||||||
:torrent="item"
|
|
||||||
/>
|
|
||||||
<VListItem v-if="dataList.length === 0">
|
|
||||||
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
<VListItemTitle>没有附合当前过滤条件的资源。</VListItemTitle>
|
||||||
</VListItem>
|
</VListItem>
|
||||||
</VList>
|
</VList>
|
||||||
|
<div>
|
||||||
|
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
|
||||||
|
<TorrentItem v-if="defer(index)" :torrent="item" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</VCol>
|
</VCol>
|
||||||
<VCol
|
<VCol xl="2" md="3" class="d-none d-md-block">
|
||||||
xl="2"
|
|
||||||
md="3"
|
|
||||||
class="d-none d-md-block"
|
|
||||||
>
|
|
||||||
<VList lines="one" class="rounded">
|
<VList lines="one" class="rounded">
|
||||||
<VListSubheader v-if="siteFilterOptions.length > 0">
|
<VListSubheader v-if="siteFilterOptions.length > 0">
|
||||||
站点
|
站点
|
||||||
|
|||||||
@@ -3,42 +3,79 @@ import api from '@/api'
|
|||||||
import FileBrowser from '@/components/FileBrowser.vue'
|
import FileBrowser from '@/components/FileBrowser.vue'
|
||||||
|
|
||||||
const endpoints = {
|
const endpoints = {
|
||||||
list: { url: '/filebrowser/list?path={path}&sort={sort}', method: 'get' },
|
list: {
|
||||||
mkdir: { url: '/filebrowser/mkdir?path={path}', method: 'get' },
|
url: '/filebrowser/list?path={path}&sort={sort}',
|
||||||
delete: { url: '/filebrowser/delete?path={path}', method: 'get' },
|
method: 'get',
|
||||||
download: { url: '/filebrowser/download?path={path}', method: 'get' },
|
},
|
||||||
image: { url: '/filebrowser/image?path={path}', method: 'get' },
|
mkdir: {
|
||||||
rename: { url: '/filebrowser/rename?path={path}&new_name={newname}', method: 'get' },
|
url: '/filebrowser/mkdir?path={path}',
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
|
delete: {
|
||||||
|
url: '/filebrowser/delete?path={path}',
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
|
download: {
|
||||||
|
url: '/filebrowser/download?path={path}',
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
url: '/filebrowser/image?path={path}',
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
|
rename: {
|
||||||
|
url: '/filebrowser/rename?path={path}&new_name={newname}',
|
||||||
|
method: 'get',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取下载目录
|
// 读取下载目录
|
||||||
const path = ref('/')
|
const path: Ref<string | undefined> = ref()
|
||||||
|
|
||||||
// 调用API,加载当前系统环境设置
|
// 调用API,加载当前系统环境设置
|
||||||
async function loadSystemSettings() {
|
function loadSystemSettings(): Promise<string> {
|
||||||
try {
|
return new Promise((resolve, reject) => {
|
||||||
const result: { [key: string]: any } = await api.get('system/env')
|
api
|
||||||
if (result.success)
|
.get('system/env')
|
||||||
path.value = result.data?.DOWNLOAD_PATH || '/'
|
.then((result: any) => {
|
||||||
if (path.value && !path.value.endsWith('/'))
|
let path = '/'
|
||||||
path.value += '/'
|
if (result.success)
|
||||||
}
|
path = result.data?.DOWNLOAD_PATH || '/'
|
||||||
catch (error) {
|
|
||||||
console.log(error)
|
if (!path.endsWith('/'))
|
||||||
}
|
path += '/'
|
||||||
|
|
||||||
|
resolve(path)
|
||||||
|
})
|
||||||
|
.catch(error => reject(error))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function pathChanged(_path: string) {
|
function pathChanged(_path: string) {
|
||||||
path.value = _path
|
path.value = _path
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeMount(async () => {
|
onMounted(() => {
|
||||||
await loadSystemSettings()
|
loadSystemSettings()
|
||||||
|
.then((res) => {
|
||||||
|
path.value = res
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
path.value = '/'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<FileBrowser storages="local" :tree="false" :path="path" :endpoints="endpoints" :axios="api" @pathchanged="pathChanged" />
|
<FileBrowser
|
||||||
|
storages="local"
|
||||||
|
:tree="false"
|
||||||
|
:path="path"
|
||||||
|
:endpoints="endpoints"
|
||||||
|
:axios="api"
|
||||||
|
@pathchanged="pathChanged"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,20 +25,43 @@ const selected = ref<TransferHistory[]>([])
|
|||||||
|
|
||||||
// 表头
|
// 表头
|
||||||
const headers = [
|
const headers = [
|
||||||
{ title: '标题', key: 'title', sortable: false },
|
{
|
||||||
{ title: '目录', key: 'src', sortable: false },
|
title: '标题',
|
||||||
{ title: '转移方式', key: 'mode', sortable: false },
|
key: 'title',
|
||||||
{ title: '时间', key: 'date', sortable: false },
|
sortable: false,
|
||||||
{ title: '状态', key: 'status', sortable: false },
|
},
|
||||||
{ title: '失败原因', key: 'errmsg', sortable: false },
|
{
|
||||||
{ title: '', key: 'actions', sortable: false },
|
title: '目录',
|
||||||
|
key: 'src',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '转移方式',
|
||||||
|
key: 'mode',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '时间',
|
||||||
|
key: 'date',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
key: 'actions',
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// 数据列表
|
// 数据列表
|
||||||
const dataList = ref<TransferHistory[]>([])
|
const dataList = ref<TransferHistory[]>([])
|
||||||
|
|
||||||
// 搜索
|
// 搜索
|
||||||
const search = ref('')
|
const search = ref()
|
||||||
|
|
||||||
// 搜索提示词列表
|
// 搜索提示词列表
|
||||||
const searchHintList = ref<string[]>([])
|
const searchHintList = ref<string[]>([])
|
||||||
@@ -71,13 +94,7 @@ const deleteConfirmDialog = ref(false)
|
|||||||
const confirmTitle = ref('')
|
const confirmTitle = ref('')
|
||||||
|
|
||||||
// 获取订阅列表数据
|
// 获取订阅列表数据
|
||||||
async function fetchData({
|
async function fetchData({ page, itemsPerPage }: { page: number; itemsPerPage: number }) {
|
||||||
page,
|
|
||||||
itemsPerPage,
|
|
||||||
}: {
|
|
||||||
page: number
|
|
||||||
itemsPerPage: number
|
|
||||||
}) {
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
currentPage.value = page
|
currentPage.value = page
|
||||||
@@ -92,7 +109,9 @@ async function fetchData({
|
|||||||
|
|
||||||
dataList.value = result.data.list
|
dataList.value = result.data.list
|
||||||
totalItems.value = result.data.total
|
totalItems.value = result.data.total
|
||||||
searchHintList.value = [...new Set(dataList.value.map(item => item.title || ''))].filter(title => title !== '')
|
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
|
||||||
|
title => title !== '',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -110,11 +129,6 @@ function getIcon(type: string) {
|
|||||||
return 'mdi-help-circle'
|
return 'mdi-help-circle'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 计算颜色
|
|
||||||
function getStatusColor(status: boolean) {
|
|
||||||
return status ? 'success' : 'error'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转移方式字典
|
// 转移方式字典
|
||||||
const TransferDict: { [key: string]: string } = {
|
const TransferDict: { [key: string]: string } = {
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
@@ -136,7 +150,9 @@ async function removeHistory(item: TransferHistory) {
|
|||||||
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
|
async function remove(item: TransferHistory, deleteSrc: boolean, deleteDest: boolean) {
|
||||||
try {
|
try {
|
||||||
// 调用删除API
|
// 调用删除API
|
||||||
const result: { [key: string]: any } = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
const result: {
|
||||||
|
[key: string]: any
|
||||||
|
} = await api.delete(`history/transfer?deletesrc=${deleteSrc}&deletedest=${deleteDest}`, {
|
||||||
data: item,
|
data: item,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -154,6 +170,7 @@ async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
|
|||||||
deleteConfirmDialog.value = false
|
deleteConfirmDialog.value = false
|
||||||
if (!currentHistory.value)
|
if (!currentHistory.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
await remove(currentHistory.value, deleteSrc, deleteDest)
|
await remove(currentHistory.value, deleteSrc, deleteDest)
|
||||||
// 刷新
|
// 刷新
|
||||||
@@ -171,6 +188,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
|||||||
const total = selected.value.length
|
const total = selected.value.length
|
||||||
if (total === 0)
|
if (total === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 已处理条数
|
// 已处理条数
|
||||||
let handled = 0
|
let handled = 0
|
||||||
// 显示进度条
|
// 显示进度条
|
||||||
@@ -182,7 +200,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
|
|||||||
await remove(item, deleteSrc, deleteDest)
|
await remove(item, deleteSrc, deleteDest)
|
||||||
// 删除完成
|
// 删除完成
|
||||||
handled++
|
handled++
|
||||||
progressValue.value = handled / total * 100
|
progressValue.value = (handled / total) * 100
|
||||||
}
|
}
|
||||||
// 清空选中项
|
// 清空选中项
|
||||||
selected.value = []
|
selected.value = []
|
||||||
@@ -207,6 +225,7 @@ async function deleteConfirmHandler(deleteSrc: boolean, deleteDest: boolean) {
|
|||||||
async function removeHistoryBatch() {
|
async function removeHistoryBatch() {
|
||||||
if (selected.value.length === 0)
|
if (selected.value.length === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 清空当前操作记录
|
// 清空当前操作记录
|
||||||
currentHistory.value = undefined
|
currentHistory.value = undefined
|
||||||
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
|
confirmTitle.value = `确认删除 ${selected.value.length} 条记录 ?`
|
||||||
@@ -214,19 +233,47 @@ async function removeHistoryBatch() {
|
|||||||
deleteConfirmDialog.value = true
|
deleteConfirmDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算根路径
|
||||||
|
function getRootPath(path: string, type: string, category: string) {
|
||||||
|
if (!path)
|
||||||
|
return ''
|
||||||
|
|
||||||
|
let index = -2
|
||||||
|
if (type !== '电影')
|
||||||
|
index = -3
|
||||||
|
|
||||||
|
if (category)
|
||||||
|
index -= 1
|
||||||
|
|
||||||
|
if (path.includes('/'))
|
||||||
|
return path.split('/').slice(0, index).join('/')
|
||||||
|
else
|
||||||
|
return path.split('\\').slice(0, index).join('\\')
|
||||||
|
}
|
||||||
|
|
||||||
// 批量重新整理
|
// 批量重新整理
|
||||||
async function retransferBatch() {
|
async function retransferBatch() {
|
||||||
if (selected.value.length === 0)
|
if (selected.value.length === 0)
|
||||||
return
|
return
|
||||||
|
|
||||||
// 清空当前操作记录
|
// 清空当前操作记录
|
||||||
currentHistory.value = undefined
|
currentHistory.value = undefined
|
||||||
// 重新整理IDS
|
// 重新整理IDS
|
||||||
redoIds.value = selected.value.map(item => item.id)
|
redoIds.value = selected.value.map(item => item.id)
|
||||||
// 重新整理target
|
// 重新整理target
|
||||||
if (selected.value.length === 1)
|
if (selected.value.length === 1) {
|
||||||
redoTarget.value = selected.value[0].dest ?? ''
|
// 目的目录
|
||||||
else
|
const dest = selected.value[0].dest ?? ''
|
||||||
|
// 类型
|
||||||
|
const mediaType = selected.value[0].type ?? ''
|
||||||
|
// 分类
|
||||||
|
const category = selected.value[0].category ?? ''
|
||||||
|
// 计算根路径
|
||||||
|
redoTarget.value = getRootPath(dest, mediaType, category)
|
||||||
|
}
|
||||||
|
else {
|
||||||
redoTarget.value = ''
|
redoTarget.value = ''
|
||||||
|
}
|
||||||
// 打开识别弹窗
|
// 打开识别弹窗
|
||||||
redoDialog.value = true
|
redoDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -240,7 +287,7 @@ const dropdownItems = ref([
|
|||||||
prependIcon: 'mdi-redo-variant',
|
prependIcon: 'mdi-redo-variant',
|
||||||
click: (item: TransferHistory) => {
|
click: (item: TransferHistory) => {
|
||||||
redoIds.value = [item.id]
|
redoIds.value = [item.id]
|
||||||
redoTarget.value = item.dest ?? ''
|
redoTarget.value = getRootPath(item.dest ?? '', item.type ?? '', item.category ?? '')
|
||||||
redoDialog.value = true
|
redoDialog.value = true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -264,15 +311,17 @@ const dropdownItems = ref([
|
|||||||
<VCardItem>
|
<VCardItem>
|
||||||
<VCardTitle>
|
<VCardTitle>
|
||||||
<VRow>
|
<VRow>
|
||||||
<VCol> 历史记录 </VCol>
|
<VCol cols="4" md="6">
|
||||||
<VCol>
|
历史记录
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="8" md="6">
|
||||||
<VCombobox
|
<VCombobox
|
||||||
key="search_navbar"
|
key="search_navbar"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
:items="searchHintList"
|
:items="searchHintList"
|
||||||
class="text-disabled"
|
class="text-disabled"
|
||||||
density="compact"
|
density="compact"
|
||||||
label="搜索"
|
label="搜索标题、状态"
|
||||||
append-inner-icon="mdi-magnify"
|
append-inner-icon="mdi-magnify"
|
||||||
variant="solo-filled"
|
variant="solo-filled"
|
||||||
single-line
|
single-line
|
||||||
@@ -303,58 +352,52 @@ const dropdownItems = ref([
|
|||||||
@update:options="fetchData"
|
@update:options="fetchData"
|
||||||
>
|
>
|
||||||
<template #item.title="{ item }">
|
<template #item.title="{ item }">
|
||||||
<div class="d-flex">
|
<div class="d-flex align-center">
|
||||||
<VAvatar><VIcon :icon="getIcon(item.raw.type || '')" /></VAvatar>
|
<VAvatar>
|
||||||
|
<VIcon :icon="getIcon(item.value.type || '')" />
|
||||||
|
</VAvatar>
|
||||||
<div class="d-flex flex-column ms-1">
|
<div class="d-flex flex-column ms-1">
|
||||||
<span class="d-block whitespace-nowrap text-high-emphasis">
|
<span class="d-block whitespace-nowrap text-high-emphasis">
|
||||||
{{ item.raw.title }} {{ item.raw.seasons }}{{ item.raw.episodes }}
|
{{ item.value.title }} {{ item.value.seasons }}{{ item.value.episodes }}
|
||||||
</span>
|
</span>
|
||||||
<small>{{ item.raw.category }}</small>
|
<small>{{ item.value.category }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #item.src="{ item }">
|
<template #item.src="{ item }">
|
||||||
<small>{{ item.raw.src }} <br>=> {{ item.raw.dest }}</small>
|
<small>{{ item.value.src }} <br>=> {{ item.value.dest }}</small>
|
||||||
</template>
|
</template>
|
||||||
<template #item.mode="{ item }">
|
<template #item.mode="{ item }">
|
||||||
<VChip
|
<VChip variant="outlined" color="primary" size="small">
|
||||||
variant="outlined"
|
{{ TransferDict[item.value.mode] }}
|
||||||
color="primary"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
TransferDict[item.raw.mode]
|
|
||||||
}}
|
|
||||||
</VChip>
|
</VChip>
|
||||||
</template>
|
</template>
|
||||||
<template #item.status="{ item }">
|
<template #item.status="{ item }">
|
||||||
<VChip
|
<VChip v-if="item.value.status" color="success" size="small">
|
||||||
:color="getStatusColor(item.raw.status)"
|
成功
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{ item.raw.status ? "成功" : "失败" }}
|
|
||||||
</VChip>
|
</VChip>
|
||||||
|
<v-tooltip v-else :text="item.value.errmsg">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<VChip v-bind="props" color="error" size="small">
|
||||||
|
失败
|
||||||
|
</VChip>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #item.date="{ item }">
|
<template #item.date="{ item }">
|
||||||
<small>{{ item.raw.date }}</small>
|
<small>{{ item.value.date }}</small>
|
||||||
</template>
|
|
||||||
<template #item.errmsg="{ item }">
|
|
||||||
<small class="text-error">{{ item.raw.errmsg }}</small>
|
|
||||||
</template>
|
</template>
|
||||||
<template #item.actions="{ item }">
|
<template #item.actions="{ item }">
|
||||||
<IconBtn>
|
<IconBtn>
|
||||||
<VIcon icon="mdi-dots-vertical" />
|
<VIcon icon="mdi-dots-vertical" />
|
||||||
<VMenu
|
<VMenu activator="parent" close-on-content-click>
|
||||||
activator="parent"
|
|
||||||
close-on-content-click
|
|
||||||
>
|
|
||||||
<VList>
|
<VList>
|
||||||
<VListItem
|
<VListItem
|
||||||
v-for="(menu, i) in dropdownItems"
|
v-for="(menu, i) in dropdownItems"
|
||||||
:key="i"
|
:key="i"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
:base-color="menu.props.color"
|
:base-color="menu.props.color"
|
||||||
@click="menu.props.click(item.raw)"
|
@click="menu.props.click(item.value)"
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon :icon="menu.props.prependIcon" />
|
<VIcon :icon="menu.props.prependIcon" />
|
||||||
@@ -371,23 +414,9 @@ const dropdownItems = ref([
|
|||||||
</VDataTableServer>
|
</VDataTableServer>
|
||||||
</VCard>
|
</VCard>
|
||||||
<!-- 底部操作按钮 -->
|
<!-- 底部操作按钮 -->
|
||||||
<span
|
<span v-if="selected.length > 0" class="fixed right-5 bottom-5">
|
||||||
v-if="selected.length > 0"
|
<VBtn icon="mdi-redo-variant" class="me-2" color="primary" size="x-large" @click="retransferBatch" />
|
||||||
class="fixed right-5 bottom-5"
|
<VBtn icon="mdi-trash-can-outline" color="error" size="x-large" @click="removeHistoryBatch" />
|
||||||
>
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-redo-variant"
|
|
||||||
class="me-2"
|
|
||||||
color="primary"
|
|
||||||
size="x-large"
|
|
||||||
@click="retransferBatch"
|
|
||||||
/>
|
|
||||||
<VBtn
|
|
||||||
icon="mdi-trash-can-outline"
|
|
||||||
color="error"
|
|
||||||
size="x-large"
|
|
||||||
@click="removeHistoryBatch"
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
<!-- 底部弹窗 -->
|
<!-- 底部弹窗 -->
|
||||||
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
<VBottomSheet v-model="deleteConfirmDialog" inset>
|
||||||
@@ -396,33 +425,17 @@ const dropdownItems = ref([
|
|||||||
<VCardTitle class="pe-10">
|
<VCardTitle class="pe-10">
|
||||||
{{ confirmTitle }}
|
{{ confirmTitle }}
|
||||||
</VCardTitle>
|
</VCardTitle>
|
||||||
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
<div class="d-flex flex-column flex-lg-row justify-center my-3">
|
||||||
<VBtn
|
<VBtn color="primary" class="mb-2 mx-2" @click="deleteConfirmHandler(false, false)">
|
||||||
color="primary"
|
|
||||||
class="mb-2 mx-2"
|
|
||||||
@click="deleteConfirmHandler(false, false)"
|
|
||||||
>
|
|
||||||
仅删除历史记录
|
仅删除历史记录
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
<VBtn color="warning" class="mb-2 mx-2" @click="deleteConfirmHandler(true, false)">
|
||||||
color="warning"
|
|
||||||
class="mb-2 mx-2"
|
|
||||||
@click="deleteConfirmHandler(true, false)"
|
|
||||||
>
|
|
||||||
删除历史记录和源文件
|
删除历史记录和源文件
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
<VBtn color="info" class="mb-2 mx-2" @click="deleteConfirmHandler(false, true)">
|
||||||
color="info"
|
|
||||||
class="mb-2 mx-2"
|
|
||||||
@click="deleteConfirmHandler(false, true)"
|
|
||||||
>
|
|
||||||
删除历史记录和媒体库文件
|
删除历史记录和媒体库文件
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn
|
<VBtn color="error" class="mb-2 mx-2" @click="deleteConfirmHandler(true, true)">
|
||||||
color="error"
|
|
||||||
class="mb-2 mx-2"
|
|
||||||
@click="deleteConfirmHandler(true, true)"
|
|
||||||
>
|
|
||||||
删除历史记录、源文件和媒体库文件
|
删除历史记录、源文件和媒体库文件
|
||||||
</VBtn>
|
</VBtn>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,17 +446,19 @@ const dropdownItems = ref([
|
|||||||
v-model="redoDialog"
|
v-model="redoDialog"
|
||||||
:logids="redoIds"
|
:logids="redoIds"
|
||||||
:target="redoTarget"
|
:target="redoTarget"
|
||||||
@done="() => {
|
@done="
|
||||||
redoDialog = false
|
() => {
|
||||||
// 清空当前操作记录
|
redoDialog = false
|
||||||
currentHistory = undefined
|
// 清空当前操作记录
|
||||||
selected = []
|
currentHistory = undefined
|
||||||
// 刷新
|
selected = []
|
||||||
fetchData({
|
// 刷新
|
||||||
page: currentPage,
|
fetchData({
|
||||||
itemsPerPage,
|
page: currentPage,
|
||||||
})
|
itemsPerPage,
|
||||||
}"
|
})
|
||||||
|
}
|
||||||
|
"
|
||||||
@close="redoDialog = false"
|
@close="redoDialog = false"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ const currentRuleType = ref('SubscribeFilterRules')
|
|||||||
const defaultFilterRules = ref({
|
const defaultFilterRules = ref({
|
||||||
include: '',
|
include: '',
|
||||||
exclude: '',
|
exclude: '',
|
||||||
|
movie_size: '',
|
||||||
|
tv_size: '',
|
||||||
|
show_edit_dialog: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 导入代码弹窗
|
// 导入代码弹窗
|
||||||
@@ -503,6 +506,28 @@ onMounted(() => {
|
|||||||
label="排除(关键字、正则式)"
|
label="排除(关键字、正则式)"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.movie_size"
|
||||||
|
type="text"
|
||||||
|
label="电影文件大小(GB)"
|
||||||
|
placeholder="0-30"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="defaultFilterRules.tv_size"
|
||||||
|
type="text"
|
||||||
|
label="剧集单集文件大小(GB)"
|
||||||
|
placeholder="0-10"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="defaultFilterRules.show_edit_dialog"
|
||||||
|
label="订阅时编辑更多规则"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
</VRow>
|
</VRow>
|
||||||
</VForm>
|
</VForm>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang='ts' setup>
|
||||||
import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
|
import type { CalendarOptions, EventSourceInput } from '@fullcalendar/core'
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid'
|
import dayGridPlugin from '@fullcalendar/daygrid'
|
||||||
import interactionPlugin from '@fullcalendar/interaction'
|
import interactionPlugin from '@fullcalendar/interaction'
|
||||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||||
import FullCalendar from '@fullcalendar/vue3'
|
import FullCalendar from '@fullcalendar/vue3'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import type { MediaInfo, Rss, Subscribe, TmdbEpisode } from '@/api/types'
|
import type { MediaInfo, Subscribe, TmdbEpisode } from '@/api/types'
|
||||||
import api from '@/api'
|
import api from '@/api'
|
||||||
import { parseDate } from '@/@core/utils/formatters'
|
import { formatEp, parseDate } from '@/@core/utils/formatters'
|
||||||
|
|
||||||
// 日历属性
|
// 日历属性
|
||||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||||
@@ -33,7 +33,7 @@ const calendarOptions: Ref<CalendarOptions> = ref({
|
|||||||
events: [],
|
events: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
async function eventsHander(subscribe: Subscribe | Rss) {
|
async function eventsHander(subscribe: Subscribe) {
|
||||||
// 如果是电影直接返回
|
// 如果是电影直接返回
|
||||||
if (subscribe.type === '电影') {
|
if (subscribe.type === '电影') {
|
||||||
// 调用API查询TMDB详情
|
// 调用API查询TMDB详情
|
||||||
@@ -48,24 +48,52 @@ async function eventsHander(subscribe: Subscribe | Rss) {
|
|||||||
allDay: false,
|
allDay: false,
|
||||||
posterPath: subscribe.poster,
|
posterPath: subscribe.poster,
|
||||||
mediaType: subscribe.type,
|
mediaType: subscribe.type,
|
||||||
|
len: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 调用API查询集信息
|
// 调用API查询集信息
|
||||||
const episodes: TmdbEpisode[] = await api.get(
|
const episodes: TmdbEpisode[] = await api.get(
|
||||||
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
|
`tmdb/${subscribe.tmdbid}/${subscribe.season}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
return episodes.map((episode) => {
|
interface EpisodeInfo {
|
||||||
return {
|
title: string
|
||||||
title: subscribe.name,
|
subtitle: string
|
||||||
subtitle: `第 ${episode.episode_number} 集`,
|
start: Date | null
|
||||||
start: parseDate(episode.air_date || ''),
|
allDay: boolean
|
||||||
allDay: false,
|
posterPath: string | undefined
|
||||||
posterPath: subscribe.poster,
|
mediaType: string
|
||||||
mediaType: subscribe.type,
|
len: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EpisodesDictionary {
|
||||||
|
[key: string]: EpisodeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
const dictEpisode: EpisodesDictionary = {}
|
||||||
|
episodes.forEach((episode: TmdbEpisode) => {
|
||||||
|
const air_date = episode.air_date ?? ''
|
||||||
|
if (dictEpisode[air_date]) {
|
||||||
|
dictEpisode[air_date].subtitle += `,${episode.episode_number}`
|
||||||
|
dictEpisode[air_date].len++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
dictEpisode[air_date] = {
|
||||||
|
title: subscribe.name,
|
||||||
|
subtitle: `${episode.episode_number}`,
|
||||||
|
start: parseDate(episode.air_date || ''),
|
||||||
|
allDay: false,
|
||||||
|
posterPath: subscribe.poster,
|
||||||
|
mediaType: subscribe.type,
|
||||||
|
len: 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
for (const key in dictEpisode)
|
||||||
|
dictEpisode[key].subtitle = formatEp(dictEpisode[key].subtitle.split(',').map(Number))
|
||||||
|
|
||||||
|
return Object.values(dictEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +143,11 @@ onMounted(() => {
|
|||||||
</VImg>
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<VCardSubtitle class="pa-2 font-bold break-words whitespace-break-spaces">
|
<VCardSubtitle class="pa-1 px-2 font-bold break-words whitespace-break-spaces">
|
||||||
{{ arg.event.title }}
|
{{ arg.event.title }}
|
||||||
</VCardSubtitle>
|
</VCardSubtitle>
|
||||||
<VCardText class="pa-0 px-2">
|
<VCardText v-if="arg.event.extendedProps.subtitle" class="pa-0 px-2 break-words">
|
||||||
{{ arg.event.extendedProps.subtitle }}
|
第{{ arg.event.extendedProps.subtitle }}集
|
||||||
</VCardText>
|
</VCardText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,6 +170,15 @@ onMounted(() => {
|
|||||||
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<VChip
|
||||||
|
v-if="arg.event.extendedProps.len > 1"
|
||||||
|
variant="elevated"
|
||||||
|
color="primary"
|
||||||
|
size="x-small"
|
||||||
|
class="absolute right-0 top-0"
|
||||||
|
>
|
||||||
|
{{ arg.event.extendedProps.len }}
|
||||||
|
</VChip>
|
||||||
</VImg>
|
</VImg>
|
||||||
</template>
|
</template>
|
||||||
</VTooltip>
|
</VTooltip>
|
||||||
@@ -150,7 +187,7 @@ onMounted(() => {
|
|||||||
</FullCalendar>
|
</FullCalendar>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang='scss'>
|
||||||
.v-application .fc {
|
.v-application .fc {
|
||||||
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
|
--fc-today-bg-color: rgba(var(--v-theme-on-surface), 0.04);
|
||||||
--fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
--fc-border-color: rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
@@ -253,10 +290,10 @@ onMounted(() => {
|
|||||||
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary,
|
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary,
|
||||||
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary:hover,
|
.v-application .fc .fc-toolbar-chunk .fc-button-group .fc-button-primary:hover,
|
||||||
.v-application
|
.v-application
|
||||||
.fc
|
.fc
|
||||||
.fc-toolbar-chunk
|
.fc-toolbar-chunk
|
||||||
.fc-button-group
|
.fc-button-group
|
||||||
.fc-button-primary:not(.disabled):active {
|
.fc-button-primary:not(.disabled):active {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
||||||
@@ -281,19 +318,18 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v-application
|
.v-application
|
||||||
.fc
|
.fc
|
||||||
.fc-toolbar-chunk:last-child
|
.fc-toolbar-chunk:last-child
|
||||||
.fc-button-group
|
.fc-button-group
|
||||||
.fc-button:not(:last-child) {
|
.fc-button:not(:last-child) {
|
||||||
border-inline-end: 0.0625rem solid
|
border-inline-end: 0.0625rem solid rgba(var(--v-theme-primary), var(--v-overlay-scrim-opacity));
|
||||||
rgba(var(--v-theme-primary), var(--v-overlay-scrim-opacity));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-application
|
.v-application
|
||||||
.fc
|
.fc
|
||||||
.fc-toolbar-chunk:last-child
|
.fc-toolbar-chunk:last-child
|
||||||
.fc-button-group
|
.fc-button-group
|
||||||
.fc-button.fc-button-active {
|
.fc-button.fc-button-active {
|
||||||
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
background-color: rgba(var(--v-theme-primary), var(--v-activated-opacity));
|
||||||
color: rgb(var(--v-theme-primary));
|
color: rgb(var(--v-theme-primary));
|
||||||
}
|
}
|
||||||
@@ -359,8 +395,8 @@ onMounted(() => {
|
|||||||
.v-application .fc .fc-popover {
|
.v-application .fc .fc-popover {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity),
|
box-shadow: 0 4px 14px -4px var(--v-shadow-key-umbra-opacity),
|
||||||
0 4px 8px -4px var(--v-shadow-key-penumbra-opacity),
|
0 4px 8px -4px var(--v-shadow-key-penumbra-opacity),
|
||||||
0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
|
0 4px 8px -4px var(--v-shadow-key-ambient-opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-application .fc .fc-popover .fc-popover-header,
|
.v-application .fc .fc-popover .fc-popover-header,
|
||||||
@@ -400,11 +436,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.v-theme--dark
|
.v-theme--dark
|
||||||
.v-application
|
.v-application
|
||||||
.fc
|
.fc
|
||||||
.fc-toolbar-chunk
|
.fc-toolbar-chunk
|
||||||
.fc-button-group
|
.fc-button-group
|
||||||
.fc-drawerToggler-button {
|
.fc-drawerToggler-button {
|
||||||
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' stroke='rgba(232,232,241,0.68)' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round' class='css-i6dzq1'%3E%3Cpath d='M3 12h18M3 6h18M3 18h18'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user