mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 15:36:49 +08:00
fix ui
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "1.9.6-beta",
|
"version": "1.9.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -35,36 +35,28 @@ function imageErrorHandler() {
|
|||||||
|
|
||||||
// 默认图片
|
// 默认图片
|
||||||
function getDefaultImage() {
|
function getDefaultImage() {
|
||||||
if (props.media?.server === 'plex')
|
if (props.media?.server === 'plex') return plex
|
||||||
return plex
|
else if (props.media?.server === 'emby') return emby
|
||||||
else if (props.media?.server === 'emby')
|
else if (props.media?.server === 'jellyfin') return jellyfin
|
||||||
return emby
|
else return plex
|
||||||
else if (props.media?.server === 'jellyfin')
|
|
||||||
return jellyfin
|
|
||||||
else
|
|
||||||
return plex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转播放
|
// 跳转播放
|
||||||
function goPlay() {
|
function goPlay() {
|
||||||
if (props.media?.link)
|
if (props.media?.link) window.open(props.media?.link, '_blank')
|
||||||
window.open(props.media?.link, '_blank')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成图片代理路径
|
// 生成图片代理路径
|
||||||
function getImgUrl(url: string) {
|
function getImgUrl(url: string) {
|
||||||
if (!url)
|
if (!url) return getDefaultImage()
|
||||||
return getDefaultImage()
|
else return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
||||||
else
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}system/img/0?imgurl=${encodeURIComponent(url)}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据多张图片生成媒体库封面
|
// 根据多张图片生成媒体库封面
|
||||||
async function drawImages(imageList: string[]) {
|
async function drawImages(imageList: string[]) {
|
||||||
// 图片
|
// 图片
|
||||||
const IMAGES = imageList
|
const IMAGES = imageList
|
||||||
if (IMAGES.length === 0)
|
if (IMAGES.length === 0) return getDefaultImage()
|
||||||
return getDefaultImage()
|
|
||||||
|
|
||||||
// 为所有图片添加system/img前缀
|
// 为所有图片添加system/img前缀
|
||||||
for (let i = 0; i < IMAGES.length; i++)
|
for (let i = 0; i < IMAGES.length; i++)
|
||||||
@@ -72,8 +64,7 @@ async function drawImages(imageList: string[]) {
|
|||||||
|
|
||||||
// canvas
|
// canvas
|
||||||
const canvas = canvasRef.value
|
const canvas = canvasRef.value
|
||||||
if (!canvas)
|
if (!canvas) return getDefaultImage()
|
||||||
return getDefaultImage()
|
|
||||||
|
|
||||||
// 画布参数
|
// 画布参数
|
||||||
const POSTER_WIDTH = (canvas.width - 32) / 4
|
const POSTER_WIDTH = (canvas.width - 32) / 4
|
||||||
@@ -85,8 +76,7 @@ async function drawImages(imageList: string[]) {
|
|||||||
|
|
||||||
// 获取画布上下文
|
// 获取画布上下文
|
||||||
const ctx = canvas.getContext('2d')
|
const ctx = canvas.getContext('2d')
|
||||||
if (!ctx)
|
if (!ctx) return getDefaultImage()
|
||||||
return getDefaultImage()
|
|
||||||
|
|
||||||
// 设置背景色为黑色
|
// 设置背景色为黑色
|
||||||
ctx.fillStyle = '#000000'
|
ctx.fillStyle = '#000000'
|
||||||
@@ -94,16 +84,14 @@ async function drawImages(imageList: string[]) {
|
|||||||
|
|
||||||
// 绘制图片
|
// 绘制图片
|
||||||
async function drawImageWithReflection(imgSrc: string, index: number) {
|
async function drawImageWithReflection(imgSrc: string, index: number) {
|
||||||
if (!canvas)
|
if (!canvas) return
|
||||||
return
|
|
||||||
|
|
||||||
if (!ctx)
|
if (!ctx) return
|
||||||
return
|
|
||||||
|
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.setAttribute('crossorigin', 'anonymous')
|
img.setAttribute('crossorigin', 'anonymous')
|
||||||
img.src = imgSrc
|
img.src = imgSrc
|
||||||
await new Promise(resolve => img.onload = resolve)
|
await new Promise(resolve => (img.onload = resolve))
|
||||||
|
|
||||||
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
|
||||||
const y = MARGIN_HEIGHT
|
const y = MARGIN_HEIGHT
|
||||||
@@ -125,12 +113,7 @@ async function drawImages(imageList: string[]) {
|
|||||||
REFLECTION_HEIGHT,
|
REFLECTION_HEIGHT,
|
||||||
)
|
)
|
||||||
|
|
||||||
const gradient = ctx.createLinearGradient(
|
const gradient = ctx.createLinearGradient(0, REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT, 0, REFLECTION_HEIGHT)
|
||||||
0,
|
|
||||||
REFLECTION_SHOW_HEIGHT - REFLECTION_HEIGHT,
|
|
||||||
0,
|
|
||||||
REFLECTION_HEIGHT,
|
|
||||||
)
|
|
||||||
|
|
||||||
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
gradient.addColorStop(0, 'rgba(0, 0, 0, 1)')
|
||||||
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.3)')
|
||||||
@@ -142,8 +125,7 @@ async function drawImages(imageList: string[]) {
|
|||||||
|
|
||||||
// 绘制多张图片
|
// 绘制多张图片
|
||||||
const loopCount = Math.min(4, IMAGES.length)
|
const loopCount = Math.min(4, IMAGES.length)
|
||||||
for (let i = 0; i < loopCount; i++)
|
for (let i = 0; i < loopCount; i++) await drawImageWithReflection(IMAGES[i], i + 1)
|
||||||
await drawImageWithReflection(IMAGES[i], i + 1)
|
|
||||||
|
|
||||||
// 转换为图片地址
|
// 转换为图片地址
|
||||||
return canvas.toDataURL('image/png')
|
return canvas.toDataURL('image/png')
|
||||||
@@ -152,17 +134,12 @@ async function drawImages(imageList: string[]) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.media?.image_list && props.media?.image_list.length > 0)
|
if (props.media?.image_list && props.media?.image_list.length > 0)
|
||||||
imgUrl.value = await drawImages(props.media?.image_list || [])
|
imgUrl.value = await drawImages(props.media?.image_list || [])
|
||||||
else
|
else imgUrl.value = getImgUrl(props.media?.image || '')
|
||||||
imgUrl.value = getImgUrl(props.media?.image || '')
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VHover
|
<VHover v-bind="props" :height="props.height" :width="props.width">
|
||||||
v-bind="props"
|
|
||||||
:height="props.height"
|
|
||||||
:width="props.width"
|
|
||||||
>
|
|
||||||
<template #default="hover">
|
<template #default="hover">
|
||||||
<VCard
|
<VCard
|
||||||
v-bind="hover.props"
|
v-bind="hover.props"
|
||||||
@@ -175,13 +152,7 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<template #image>
|
<template #image>
|
||||||
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
<canvas ref="canvasRef" class="w-full h-full hidden" />
|
||||||
<VImg
|
<VImg :src="imgUrl" aspect-ratio="2/3" cover @load="imageLoadHandler" @error="imageErrorHandler">
|
||||||
:src="imgUrl"
|
|
||||||
aspect-ratio="2/3"
|
|
||||||
cover
|
|
||||||
@load="imageLoadHandler"
|
|
||||||
@error="imageErrorHandler"
|
|
||||||
>
|
|
||||||
<template #placeholder>
|
<template #placeholder>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
|
||||||
@@ -190,7 +161,7 @@ onMounted(async () => {
|
|||||||
<VCardText
|
<VCardText
|
||||||
class="w-full flex flex-col flex-wrap justify-end align-center text-white absolute bottom-0 cursor-pointer pa-2"
|
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 ...">
|
<h1 class="mb-1 text-white text-shadow font-bold line-clamp-2 overflow-hidden text-ellipsis ...">
|
||||||
{{ props.media?.name }}
|
{{ props.media?.name }}
|
||||||
</h1>
|
</h1>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
@@ -200,3 +171,9 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</VHover>
|
</VHover>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow: 1px 1px #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ watch(
|
|||||||
class="flex flex-col rounded-lg"
|
class="flex flex-col rounded-lg"
|
||||||
:class="{
|
:class="{
|
||||||
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
|
||||||
|
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
|
||||||
}"
|
}"
|
||||||
@click="editSubscribeDialog"
|
@click="editSubscribeDialog"
|
||||||
>
|
>
|
||||||
@@ -223,26 +224,29 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
<div v-if="imageLoaded">
|
<div v-if="imageLoaded">
|
||||||
<VCardText class="flex items-center">
|
<VCardText class="flex items-center">
|
||||||
<div
|
<div class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg">
|
||||||
class="h-auto w-12 flex-shrink-0 overflow-hidden rounded-md shadow-lg"
|
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail">
|
||||||
:class="{ 'transition transform-cpu duration-300 scale-105': hover.isHovering }"
|
<template #placeholder>
|
||||||
>
|
<div class="w-full h-full">
|
||||||
<VImg :src="props.media?.poster" aspect-ratio="2/3" cover @click.stop="viewMediaDetail" />
|
<VSkeletonLoader class="object-cover aspect-w-2 aspect-h-3" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VImg>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
<div class="flex flex-col justify-center overflow-hidden pl-2 xl:pl-4">
|
||||||
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
<div class="text-sm font-medium text-white sm:pt-1">{{ props.media?.year }}</div>
|
||||||
<div class="mr-2 min-w-0 text-lg font-bold text-white xl:text-xl">
|
<div class="mr-2 min-w-0 text-lg font-bold text-white">
|
||||||
{{ props.media?.name }}
|
{{ props.media?.name }}
|
||||||
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardText class="flex justify-space-between align-center flex-wrap">
|
<VCardText class="flex justify-space-between align-center flex-wrap">
|
||||||
<div class="d-flex align-center">
|
<div class="flex align-center">
|
||||||
<IconBtn
|
<IconBtn
|
||||||
v-if="props.media?.total_episode"
|
v-if="props.media?.total_episode"
|
||||||
v-bind="props"
|
v-bind="props"
|
||||||
icon="mdi-progress-clock"
|
icon="mdi-progress-download"
|
||||||
color="white"
|
color="white"
|
||||||
class="me-1"
|
class="me-1"
|
||||||
/>
|
/>
|
||||||
@@ -260,12 +264,14 @@ watch(
|
|||||||
<VIcon icon="mdi-download" class="me-1" />
|
<VIcon icon="mdi-download" class="me-1" />
|
||||||
{{ lastUpdateText }}
|
{{ lastUpdateText }}
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VProgressLinear
|
<div class="w-full absolute bottom-0">
|
||||||
v-if="getPercentage() > 0"
|
<VProgressLinear
|
||||||
:model-value="getPercentage()"
|
v-if="getPercentage() > 0"
|
||||||
bg-color="success"
|
:model-value="getPercentage()"
|
||||||
color="success"
|
bg-color="success"
|
||||||
/>
|
color="success"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</VCard>
|
</VCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,12 +27,10 @@ function slideNext(next: boolean) {
|
|||||||
if (run_to_left_px >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth)
|
if (run_to_left_px >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth)
|
||||||
run_to_left_px = slideview_content.value.scrollWidth - slideview_content.value.clientWidth
|
run_to_left_px = slideview_content.value.scrollWidth - slideview_content.value.clientWidth
|
||||||
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
const card_index = card_current - card_max
|
const card_index = card_current - card_max
|
||||||
run_to_left_px = card_index * card_width
|
run_to_left_px = card_index * card_width
|
||||||
if (run_to_left_px <= 0)
|
if (run_to_left_px <= 0) run_to_left_px = 0
|
||||||
run_to_left_px = 0
|
|
||||||
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
// console.log(`最多显示: ${card_max} 当前起点: ${card_current} 目标起点: ${card_index} 卡片宽度: ${card_width}`)
|
||||||
}
|
}
|
||||||
slideview_content.value.scrollTo({
|
slideview_content.value.scrollTo({
|
||||||
@@ -46,7 +44,7 @@ function slideNext(next: boolean) {
|
|||||||
function countMaxNumber() {
|
function countMaxNumber() {
|
||||||
slide_card_length = slideview_content.value.children.length
|
slide_card_length = slideview_content.value.children.length
|
||||||
card_width = slideview_content.value.firstElementChild.getBoundingClientRect().width
|
card_width = slideview_content.value.firstElementChild.getBoundingClientRect().width
|
||||||
slide_gap_px = (slideview_content.value.scrollWidth / slide_card_length) - card_width
|
slide_gap_px = slideview_content.value.scrollWidth / slide_card_length - card_width
|
||||||
card_width += slide_gap_px
|
card_width += slide_gap_px
|
||||||
card_max = Math.trunc(slideview_content.value.clientWidth / card_width)
|
card_max = Math.trunc(slideview_content.value.clientWidth / card_width)
|
||||||
countDisabled()
|
countDisabled()
|
||||||
@@ -55,16 +53,18 @@ function countMaxNumber() {
|
|||||||
// 修改分页切换按钮状态
|
// 修改分页切换按钮状态
|
||||||
function countDisabled() {
|
function countDisabled() {
|
||||||
slideview_scrollLeft.value = slideview_content.value.scrollLeft
|
slideview_scrollLeft.value = slideview_content.value.scrollLeft
|
||||||
card_current = slideview_content.value.scrollLeft === 0 ? 0 : Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
card_current =
|
||||||
if (slide_card_length * card_width <= slideview_content.value.clientWidth)
|
slideview_content.value.scrollLeft === 0
|
||||||
disabled.value = 3
|
? 0
|
||||||
else if (slideview_content.value.scrollLeft === 0)
|
: Math.trunc((slideview_content.value.scrollLeft + card_width / 2) / card_width)
|
||||||
disabled.value = 0
|
if (slide_card_length * card_width <= slideview_content.value.clientWidth) disabled.value = 3
|
||||||
else if (slideview_content.value.scrollLeft >= slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2)
|
else if (slideview_content.value.scrollLeft === 0) disabled.value = 0
|
||||||
|
else if (
|
||||||
|
slideview_content.value.scrollLeft >=
|
||||||
|
slideview_content.value.scrollWidth - slideview_content.value.clientWidth - 2
|
||||||
|
)
|
||||||
disabled.value = 2
|
disabled.value = 2
|
||||||
|
else disabled.value = 1
|
||||||
else
|
|
||||||
disabled.value = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件加载完成
|
// 组件加载完成
|
||||||
@@ -123,8 +123,7 @@ onActivated(() => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.slideview_content {
|
.slideview_content {
|
||||||
-ms-overflow-style: none !important;
|
-ms-overflow-style: none !important;
|
||||||
overflow-x: scroll !important;
|
overflow: scroll hidden !important;
|
||||||
overflow-y: hidden !important;
|
|
||||||
overscroll-behavior-x: contain !important;
|
overscroll-behavior-x: contain !important;
|
||||||
scrollbar-width: none !important;
|
scrollbar-width: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = inject('rankingPropsKey')
|
const props: any = inject('rankingPropsKey')
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="ms-1">
|
||||||
class="ms-1"
|
<RouterLink :to="props?.linkurl ? props?.linkurl : ''" class="slider-title">
|
||||||
>
|
<span>{{ props?.title }}</span>
|
||||||
<RouterLink
|
<VIcon icon="mdi-arrow-right-circle-outline" class="ms-1" />
|
||||||
:to="props.linkurl ? props.linkurl : ''"
|
|
||||||
class="slider-title"
|
|
||||||
>
|
|
||||||
<span>{{ props.title }}</span>
|
|
||||||
<VIcon
|
|
||||||
icon="mdi-arrow-right-circle-outline"
|
|
||||||
class="ms-1"
|
|
||||||
/>
|
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function getApiPath(paths: string[] | string) {
|
|||||||
<div v-if="title" class="mt-3 md:flex md:items-center md:justify-between">
|
<div v-if="title" class="mt-3 md:flex md:items-center md:justify-between">
|
||||||
<div class="min-w-0 flex-1 mx-0">
|
<div class="min-w-0 flex-1 mx-0">
|
||||||
<h2
|
<h2
|
||||||
class="mb-4 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0"
|
class="mb-4 ms-3 truncate text-2xl font-bold leading-7 text-gray-100 sm:overflow-visible sm:text-4xl sm:leading-9 md:mb-0"
|
||||||
data-testid="page-header"
|
data-testid="page-header"
|
||||||
>
|
>
|
||||||
<span class="text-moviepilot">{{ title }}</span>
|
<span class="text-moviepilot">{{ title }}</span>
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ const progressDialog = ref(false)
|
|||||||
// 加载中
|
// 加载中
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 已加载过
|
||||||
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
// 日历属性
|
// 日历属性
|
||||||
const calendarOptions: Ref<CalendarOptions> = ref({
|
const calendarOptions: Ref<CalendarOptions> = ref({
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
@@ -103,7 +106,7 @@ async function eventsHander(subscribe: Subscribe) {
|
|||||||
|
|
||||||
// 调用API查询所有订阅
|
// 调用API查询所有订阅
|
||||||
async function getSubscribes() {
|
async function getSubscribes() {
|
||||||
progressDialog.value = true
|
if (!isLoaded.value) progressDialog.value = true
|
||||||
try {
|
try {
|
||||||
// 订阅
|
// 订阅
|
||||||
loading.value = true
|
loading.value = true
|
||||||
@@ -111,6 +114,7 @@ async function getSubscribes() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
const subEvents = await Promise.all(subscribes.map(async sub => eventsHander(sub)))
|
const subEvents = await Promise.all(subscribes.map(async sub => eventsHander(sub)))
|
||||||
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
|
calendarOptions.value.events = subEvents.flat().filter(event => event.start) as EventSourceInput
|
||||||
|
isLoaded.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user