Compare commits

...

37 Commits

Author SHA1 Message Date
jxxghp
30351a02ee 升级版本号 2025-01-31 08:00:53 +08:00
jxxghp
7f918408a6 优化加载界面的样式,调整HTML和CSS以改善用户体验 2025-01-31 08:00:30 +08:00
jxxghp
82f69bcad0 修复在小屏幕下的返回按钮显示逻辑 2025-01-30 21:18:02 +08:00
jxxghp
83b25eabbb 优化对话框组件的样式和属性设置 2025-01-29 19:21:57 +08:00
jxxghp
47da6db51a 为filterParams添加默认排序选项 2025-01-29 19:07:43 +08:00
jxxghp
eee092a7fd fix #292 2025-01-29 18:57:22 +08:00
jxxghp
4c0f65fcbc fix https://github.com/jxxghp/MoviePilot/issues/3823 2025-01-29 18:51:06 +08:00
jxxghp
acbd979569 bangumi添加年份过滤选项 2025-01-28 09:23:45 +08:00
jxxghp
52b68c18bf 优化标签显示效果 2025-01-28 09:11:52 +08:00
jxxghp
c6a74a75da build 2025-01-28 08:23:28 +08:00
jxxghp
e39eb62f52 调整资源页面导入路径,修正TorrentCardListView和TorrentRowListView组件的引用 2025-01-28 08:22:44 +08:00
jxxghp
4ecec4865d 优化过滤选项,简化组件结构,添加评分滑块功能 2025-01-28 08:20:41 +08:00
jxxghp
589007a22a 优化主题设置逻辑,简化代码结构 2025-01-28 07:32:45 +08:00
jxxghp
4d4c9516c6 重构发现页面,添加豆瓣和TheMovieDb过滤选项,优化媒体卡组件 2025-01-27 21:08:52 +08:00
jxxghp
8491f26617 更新菜单项 2025-01-27 18:37:58 +08:00
jxxghp
fcb3768a76 更新菜单项图标,添加豆瓣和TheMovieDb的SVG和PNG图标 2025-01-27 18:16:57 +08:00
jxxghp
966bb769df 更新浏览和发现页面,重构相关组件,调整路由和菜单项 2025-01-27 18:05:02 +08:00
jxxghp
dc8f7caab0 更新 menu.ts 2025-01-27 12:25:42 +08:00
jxxghp
683346d652 添加发现页面及相关路由和菜单项 2025-01-27 11:25:43 +08:00
jxxghp
f5fe39b2d2 更新 App.vue 2025-01-26 22:28:30 +08:00
jxxghp
51beb53f51 更新 index.html 2025-01-26 22:27:41 +08:00
jxxghp
9d3f03c83a 更新 index.html 2025-01-26 22:14:10 +08:00
jxxghp
3eda1e4ef7 更新 index.html 2025-01-26 22:12:42 +08:00
jxxghp
7181f83d66 更新 Footer.vue 2025-01-26 22:10:10 +08:00
jxxghp
fffad6e1b8 更新 UserListView.vue 2025-01-26 22:08:32 +08:00
jxxghp
7f3906e5cb 更新 loader.css 2025-01-26 08:59:31 +08:00
jxxghp
f836d175f0 fix(App.vue): 优化页面加载时的背景移除逻辑,增加延迟以确保渲染完成 2025-01-26 08:48:22 +08:00
jxxghp
f49cafc0cc feat: 添加确保渲染完成的函数并优化加载背景移除逻辑 2025-01-26 08:42:09 +08:00
jxxghp
a3ecad3436 feat: 添加刷新状态控制,优化多个视图的显示逻辑 2025-01-25 19:34:39 +08:00
jxxghp
a019dbd44e refactor(SubscribeListView): 移除不必要的 VPullToRefresh 组件,简化订阅列表渲染逻辑 2025-01-25 19:15:02 +08:00
jxxghp
b316f960a1 Merge pull request #291 from InfinityPacer/v2 2025-01-25 07:43:42 +08:00
InfinityPacer
d049b26825 fix(LibraryCard): handle image loading errors with gradient 2025-01-25 03:07:14 +08:00
jxxghp
852579c6ee 更新 ForkSubscribeDialog.vue,调整 VCardSubtitle 的行数限制以改善文本显示 2025-01-23 17:24:17 +08:00
jxxghp
5adcfa1877 更新 ForkSubscribeDialog.vue 2025-01-22 19:10:27 +08:00
jxxghp
f74458629e 为 DashboardRender 组件添加 key 属性以优化渲染性能 2025-01-22 18:58:06 +08:00
jxxghp
798f9249f8 更新 package.json 版本号至 2.2.4 2025-01-22 18:48:28 +08:00
jxxghp
6b4383643f 为 ForkSubscribeDialog 组件添加用户关注功能,并在 DashboardRender 组件中实现组件重渲染 2025-01-22 13:19:09 +08:00
37 changed files with 806 additions and 147 deletions

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en"
style="overflow: hidden auto; background: var(--initial-loader-bg, #fff); min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));">
<head>
<meta http-equiv="pragma" content="no-cache">
@@ -31,7 +32,7 @@
<link rel="stylesheet" type="text/css" href="/loader.css" />
</head>
<body>
<body style="margin: 0;">
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
@@ -145,17 +146,15 @@
</div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
const loaderColor = localStorage.getItem('materio-initial-loader-bg') || '#FFFFFF'
const primaryColor = localStorage.getItem('materio-initial-loader-color') || '#9155FD'
if (loaderColor)
document.documentElement.style.setProperty('--initial-loader-bg', loaderColor)
if (primaryColor)
document.documentElement.style.setProperty('--initial-loader-color', primaryColor)
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
</html>

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "2.2.3",
"version": "2.2.6",
"private": true,
"bin": "dist/service.js",
"scripts": {

View File

@@ -1,16 +1,6 @@
body {
margin: 0;
}
html {
overflow: hidden auto;
background: var(--initial-loader-bg, #fff);
min-block-size: calc(100% + env(safe-area-inset-top) + env(safe-area-inset-bottom));
}
#loading-bg {
position: absolute;
z-index: 999;
position: fixed;
z-index: 9999;
display: block;
background: var(--initial-loader-bg, #fff);
block-size: 100vh;
@@ -82,4 +72,4 @@ html {
opacity: 1;
transform: rotate(1turn);
}
}
}

View File

@@ -10,8 +10,7 @@ export function useDefer(maxFrameCount = 1) {
const refreshFrameCount = () => {
requestAnimationFrame(() => {
frameCount.value++
if (frameCount.value < maxFrameCount)
refreshFrameCount()
if (frameCount.value < maxFrameCount) refreshFrameCount()
})
}
refreshFrameCount()
@@ -19,3 +18,9 @@ export function useDefer(maxFrameCount = 1) {
return frameCount.value >= showInFrameCount
}
}
export function ensureRenderComplete(callback: () => void) {
requestAnimationFrame(() => {
requestAnimationFrame(callback)
})
}

View File

@@ -1,15 +1,16 @@
<script lang="ts" setup>
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
const { global: globalTheme } = useTheme()
import { ensureRenderComplete, removeEl } from './@core/utils/dom'
// 生效主题
async function setTheme() {
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
}
const { global: globalTheme } = useTheme()
let themeValue = localStorage.getItem('theme') || 'light'
const autoTheme = checkPrefersColorSchemeIsDark() ? 'dark' : 'light'
globalTheme.name.value = themeValue === 'auto' ? autoTheme : themeValue
// 显示状态
const show = ref(false)
// ApexCharts 全局配置
declare global {
@@ -41,14 +42,20 @@ if (window.Apex) {
}
}
// 页面加载时,加载当前用户数据
onBeforeMount(async () => {
setTheme()
onMounted(() => {
ensureRenderComplete(() => {
nextTick(() => {
setTimeout(() => {
removeEl('#loading-bg')
show.value = true
}, 1500)
})
})
})
</script>
<template>
<VApp>
<VApp v-show="show">
<RouterView />
</VApp>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 273.42 35.52"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" y1="17.76" x2="273.42" y2="17.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset="0.56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><title>Asset 3</title><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M191.85,35.37h63.9A17.67,17.67,0,0,0,273.42,17.7h0A17.67,17.67,0,0,0,255.75,0h-63.9A17.67,17.67,0,0,0,174.18,17.7h0A17.67,17.67,0,0,0,191.85,35.37ZM10.1,35.42h7.8V6.92H28V0H0v6.9H10.1Zm28.1,0H46V8.25h.1L55.05,35.4h6L70.3,8.25h.1V35.4h7.8V0H66.45l-8.2,23.1h-.1L50,0H38.2ZM89.14.12h11.7a33.56,33.56,0,0,1,8.08,1,18.52,18.52,0,0,1,6.67,3.08,15.09,15.09,0,0,1,4.53,5.52,18.5,18.5,0,0,1,1.67,8.25,16.91,16.91,0,0,1-1.62,7.58,16.3,16.3,0,0,1-4.38,5.5,19.24,19.24,0,0,1-6.35,3.37,24.53,24.53,0,0,1-7.55,1.15H89.14Zm7.8,28.2h4a21.66,21.66,0,0,0,5-.55A10.58,10.58,0,0,0,110,26a8.73,8.73,0,0,0,2.68-3.35,11.9,11.9,0,0,0,1-5.08,9.87,9.87,0,0,0-1-4.52,9.17,9.17,0,0,0-2.63-3.18A11.61,11.61,0,0,0,106.22,8a17.06,17.06,0,0,0-4.68-.63h-4.6ZM133.09.12h13.2a32.87,32.87,0,0,1,4.63.33,12.66,12.66,0,0,1,4.17,1.3,7.94,7.94,0,0,1,3,2.72,8.34,8.34,0,0,1,1.15,4.65,7.48,7.48,0,0,1-1.67,5,9.13,9.13,0,0,1-4.43,2.82V17a10.28,10.28,0,0,1,3.18,1,8.51,8.51,0,0,1,2.45,1.85,7.79,7.79,0,0,1,1.57,2.62,9.16,9.16,0,0,1,.55,3.2,8.52,8.52,0,0,1-1.2,4.68,9.32,9.32,0,0,1-3.1,3A13.38,13.38,0,0,1,152.32,35a22.5,22.5,0,0,1-4.73.5h-14.5Zm7.8,14.15h5.65a7.65,7.65,0,0,0,1.78-.2,4.78,4.78,0,0,0,1.57-.65,3.43,3.43,0,0,0,1.13-1.2,3.63,3.63,0,0,0,.42-1.8A3.3,3.3,0,0,0,151,8.6a3.42,3.42,0,0,0-1.23-1.13A6.07,6.07,0,0,0,148,6.9a9.9,9.9,0,0,0-1.85-.18h-5.3Zm0,14.65h7a8.27,8.27,0,0,0,1.83-.2,4.67,4.67,0,0,0,1.67-.7,3.93,3.93,0,0,0,1.23-1.3,3.8,3.8,0,0,0,.47-1.95,3.16,3.16,0,0,0-.62-2,4,4,0,0,0-1.58-1.18,8.23,8.23,0,0,0-2-.55,15.12,15.12,0,0,0-2.05-.15h-5.9Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -91,7 +91,17 @@ async function drawImages(imageList: string[]) {
const img = new Image()
img.setAttribute('crossorigin', 'anonymous')
img.src = imgSrc
await new Promise(resolve => (img.onload = resolve))
try {
await new Promise<void>((resolve, reject) => {
img.onload = () => resolve()
img.onerror = () => reject(new Error(`Failed to load image: ${imgSrc}`))
})
} catch (error) {
console.error(error)
ctx.fillStyle = '#e5e7eb'
ctx.fillRect(MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1), MARGIN_HEIGHT, POSTER_WIDTH, POSTER_HEIGHT)
return
}
const x = MARGIN_WIDTH * index + POSTER_WIDTH * (index - 1)
const y = MARGIN_HEIGHT

View File

@@ -535,7 +535,7 @@ function onRemoveSubscribe() {
density="compact"
class="absolute bottom-1 right-1"
tile
v-if="!hover.isHovering && isImageLoaded && props.media?.source"
v-if="!hover.isHovering && isImageLoaded && props.media?.source && !imageLoadError"
>
<VImg cover :src="sourceIconDict[props.media?.source]" class="shadow-lg" />
</VAvatar>

View File

@@ -48,7 +48,6 @@ function goPlay(isHovering: boolean | null = false) {
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
'ring-1': isImageLoaded,
}"
@click.stop="goPlay(hover.isHovering)"
>
<VImg
aspect-ratio="2/3"
@@ -79,7 +78,7 @@ function goPlay(isHovering: boolean | null = false) {
v-show="hover.isHovering || imageLoadError"
class="w-full h-full flex flex-col flex-wrap justify-end align-left text-white absolute bottom-0 cursor-pointer pa-2 pb-5"
style="background: linear-gradient(rgba(45, 55, 72, 40%) 0%, rgba(45, 55, 72, 90%) 100%)"
@click.stop=""
@click.stop="goPlay(hover.isHovering)"
>
<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 ...">

View File

@@ -26,6 +26,58 @@ const processing = ref(false)
// 删除中
const deleting = ref(false)
// 是否折叠
const isExpanded = ref(false)
// follow用户列表
const followUsers = ref<string[]>([])
// 当前用户是否已follow
const isFollowed = computed(() => followUsers.value.includes(props.media?.share_uid || ''))
// 折叠展开
function toggleExpand() {
isExpanded.value = !isExpanded.value
}
// 加载follow用户列表
async function queryFollowUsers() {
try {
const result: { [key: string]: any } = await api.get('system/setting/FollowSubscribers')
followUsers.value = result.data?.value ?? []
} catch (error) {
console.log(error)
}
}
// follow用户
async function followUser() {
try {
const result: { [key: string]: any } = await api.post(`subscribe/follow?share_uid=${props.media?.share_uid}`)
if (result.success) {
queryFollowUsers()
}
} catch (error) {
console.log(error)
}
}
// unfollow用户
async function unfollowUser() {
try {
const result: { [key: string]: any } = await api.delete('subscribe/follow', {
params: {
share_uid: props.media?.share_uid,
},
})
if (result.success) {
queryFollowUsers()
}
} catch (error) {
console.log(error)
}
}
// 计算海报图片地址
const posterUrl = computed(() => {
const url = props.media?.poster
@@ -97,6 +149,10 @@ async function doDelete() {
doneNProgress()
}
}
onMounted(() => {
queryFollowUsers()
})
</script>
<template>
<VDialog max-width="40rem" scrollable>
@@ -123,11 +179,13 @@ async function doDelete() {
</div>
<div class="flex-grow">
<VCardItem>
<VCardTitle class="text-center text-md-left">
<VCardTitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-2 overflow-hidden text-ellipsis"
>
{{ props.media?.share_title }}
</VCardTitle>
<VCardSubtitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-2 overflow-hidden text-ellipsis ..."
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-4 overflow-hidden text-ellipsis"
>
{{ props.media?.share_comment }}
</VCardSubtitle>
@@ -144,9 +202,12 @@ async function doDelete() {
<span class="text-body-1"> {{ media?.keyword }}</span>
</VListItemTitle>
</VListItem>
<VListItem class="ps-0" v-if="media?.custom_words">
<VListItem class="ps-0" v-if="media?.custom_words" @click.stop="toggleExpand">
<VListItemTitle
class="text-center text-md-left break-words whitespace-break-spaces line-clamp-10 overflow-hidden text-ellipsis ..."
class="text-center text-md-left break-words whitespace-break-spaces"
:class="{
'line-clamp-4 overflow-hidden text-ellipsis': !isExpanded,
}"
>
<span class="font-weight-medium">识别词</span>
<span class="text-body-1"> {{ media?.custom_words }}</span>
@@ -162,7 +223,7 @@ async function doDelete() {
prepend-icon="mdi-heart"
:loading="processing"
>
添加到我的订阅
订阅
</VBtn>
<VBtn
v-if="props.media?.share_uid && props.media?.share_uid === globalSettings.USER_UNIQUE_ID"
@@ -175,6 +236,24 @@ async function doDelete() {
>
取消分享
</VBtn>
<VBtn
v-else-if="isFollowed && props.media?.share_uid"
color="warning"
@click="unfollowUser"
prepend-icon="mdi-account-remove"
class="ms-2"
>
取消关注
</VBtn>
<VBtn
v-else-if="props.media?.share_uid"
@click="followUser"
color="info"
prepend-icon="mdi-account-plus"
class="ms-2"
>
关注
</VBtn>
</div>
<div class="text-xs mt-2" v-if="props.media?.count">
<VIcon icon="mdi-fire" /> {{ props.media?.count?.toLocaleString() }} 次复用

View File

@@ -99,7 +99,7 @@ onMounted(() => {
})
</script>
<template>
<VDialog max-width="80rem" scrollable z-index="1010" :fullscreen="!display.mdAndUp.value">
<VDialog max-width="80rem" scrollable :fullscreen="!display.mdAndUp.value">
<VCard :title="`浏览 - ${props.site?.name}`">
<DialogCloseBtn @click="emit('close')" />
<VDivider />

View File

@@ -6,10 +6,21 @@ import { type PropType } from 'vue'
const elementProps = defineProps({
config: Object as PropType<RenderProps>,
})
// key
const componentKey = ref(0)
onActivated(() => {
componentKey.value++
})
</script>
<template>
<Component :is="elementProps.config?.component" v-if="!elementProps.config?.html" v-bind="elementProps.config?.props">
<Component
:key="componentKey"
:is="elementProps.config?.component"
v-if="!elementProps.config?.html"
v-bind="elementProps.config?.props"
>
{{ elementProps.config?.text }}
<template v-for="(content, name) in elementProps.config?.slots || []" :key="name" v-slot:[name]="{ _props }">
<slot :name="name" v-bind="_props">
@@ -23,6 +34,7 @@ const elementProps = defineProps({
/>
</Component>
<Component
:key="componentKey"
:is="elementProps.config?.component"
v-if="elementProps.config?.html"
v-bind="elementProps.config?.props"

View File

@@ -64,7 +64,7 @@ onMounted(() => {
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Back Button -->
<IconBtn v-if="appMode && display.mdAndDown.value" class="ms-n2" @click="goBack">
<IconBtn v-if="appMode" class="ms-n2" @click="goBack">
<VIcon icon="mdi-arrow-left" size="32" />
</IconBtn>
<!-- 👉 Search Bar -->

View File

@@ -64,4 +64,3 @@ const activeState = computed(() => {
background-color: transparent !important;
}
</style>
}

View File

@@ -17,7 +17,6 @@ import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import { CronVuetify } from '@vue-js-cron/vuetify'
// 4. 工具函数和其他辅助模块
import { removeEl } from './@core/utils/dom'
import { fetchGlobalSettings } from './api'
import { isPWA } from './@core/utils/navigator'
@@ -114,5 +113,4 @@ initializeApp().then(() => {
},
})
.mount('#app')
.$nextTick(() => removeEl('#loading-bg'))
})

View File

@@ -1,5 +1,4 @@
<script setup lang="ts">
import api from '@/api'
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
import PersonCardListView from '@/views/discover/PersonCardListView.vue'

50
src/pages/discover.vue Normal file
View File

@@ -0,0 +1,50 @@
<script setup lang="ts">
import { DiscoverTabs } from '@/router/menu'
import router from '@/router'
import TheMovieDbView from '@/views/discover/TheMovieDbView.vue'
import DoubanView from '@/views/discover/DoubanView.vue'
import BangumiView from '@/views/discover/BangumiView.vue'
const route = useRoute()
const activeTab = ref(route.query.tab)
function jumpTab(tab: string) {
router.push('/subscribe/discover?tab=' + tab)
}
</script>
<template>
<div>
<VTabs v-model="activeTab" show-arrows>
<VTab v-for="item in DiscoverTabs" :value="item.tab" @to="jumpTab(item.tab)">
<div class="min-w-24">
{{ item.title }}
</div>
</VTab>
</VTabs>
<VWindow v-model="activeTab" class="mt-5 disable-tab-transition" :touch="false">
<VWindowItem value="themoviedb">
<transition name="fade-slide" appear>
<div>
<TheMovieDbView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="douban">
<transition name="fade-slide" appear>
<div>
<DoubanView />
</div>
</transition>
</VWindowItem>
<VWindowItem value="bangumi">
<transition name="fade-slide" appear>
<div>
<BangumiView />
</div>
</transition>
</VWindowItem>
</VWindow>
</div>
</template>

View File

@@ -35,7 +35,7 @@ onMounted(async () => {
<div v-if="downloaders.length > 0">
<VTabs v-model="activeTab">
<VTab v-for="item in downloaders" :value="item.name" @to="jumpTab(item.name)">
<span class="mx-5">{{ item.name }}</span>
<span class="min-w-24">{{ item.name }}</span>
</VTab>
</VTabs>

View File

@@ -17,16 +17,6 @@ const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>
linkurl: '/browse/bangumi/calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送',
},
{
apipath: 'tmdb/movies',
linkurl: '/browse/tmdb/movies?title=TMDB热门电影',
title: 'TMDB热门电影',
},
{
apipath: 'tmdb/tvs?with_original_language=zh|en|ja|ko',
linkurl: '/browse/tmdb/tvs??with_original_language=zh|en|ja|ko&title=TMDB热门电视剧',
title: 'TMDB热门电视剧',
},
{
apipath: 'douban/movie_hot',
linkurl: '/browse/douban/movie_hot?title=豆瓣热门电影',
@@ -42,16 +32,6 @@ const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>
linkurl: '/browse/douban/tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫',
},
{
apipath: 'douban/movies',
linkurl: '/browse/douban/movies?title=豆瓣最新电影',
title: '豆瓣最新电影',
},
{
apipath: 'douban/tvs',
linkurl: '/browse/douban/tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧',
},
{
apipath: 'douban/movie_top250',
linkurl: '/browse/douban/movie_top250?title=电影TOP250',
@@ -68,7 +48,6 @@ const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>
title: '豆瓣全球剧集榜',
},
])
</script>
<template>

View File

@@ -2,8 +2,8 @@
import NoDataFound from '@/components/NoDataFound.vue'
import api from '@/api'
import type { Context } from '@/api/types'
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
import TorrentCardListView from '@/views/torrent/TorrentCardListView.vue'
import TorrentRowListView from '@/views/torrent/TorrentRowListView.vue'
import { useDisplay } from 'vuetify'
// APP
@@ -139,27 +139,28 @@ onUnmounted(() => {
<TorrentCardListView v-else :items="dataList" />
</div>
<!-- 视图切换 -->
<VFab
v-if="viewType === 'list'"
class="mb-12"
icon="mdi-view-grid"
location="bottom"
size="x-large"
absolute
app
appear
@click="setViewType('card')"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-else
icon="mdi-view-list"
location="bottom"
size="x-large"
fixed
app
appear
@click="setViewType('list')"
:class="{ 'mb-12': appMode }"
/>
<div v-if="isRefreshed">
<VFab
v-if="viewType === 'list'"
icon="mdi-view-grid"
location="bottom"
size="x-large"
absolute
app
appear
@click="setViewType('card')"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-else
icon="mdi-view-list"
location="bottom"
size="x-large"
fixed
app
appear
@click="setViewType('list')"
:class="{ 'mb-12': appMode }"
/>
</div>
</template>

View File

@@ -18,12 +18,18 @@ function jumpTab(tab: string) {
<template>
<div>
<VTabs v-model="activeTab">
<VTabs v-model="activeTab" show-arrows>
<VTab v-if="subType == '电影'" v-for="item in SubscribeMovieTabs" :value="item.tab" @to="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
<div class="flex align-center min-w-24">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</div>
</VTab>
<VTab v-if="subType == '电视剧'" v-for="item in SubscribeTvTabs" :value="item.tab" @to="jumpTab(item.tab)">
<span class="mx-5">{{ item.title }}</span>
<div class="flex align-center min-w-24">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</div>
</VTab>
</VTabs>

View File

@@ -35,6 +35,14 @@ const router = createRouter({
requiresAuth: true,
},
},
{
path: '/discover',
component: () => import('../pages/discover.vue'),
meta: {
keepAlive: true,
requiresAuth: true,
},
},
{
path: '/resource',
component: () => import('../pages/resource.vue'),

View File

@@ -8,6 +8,13 @@ export const SystemNavMenus = [
admin: false,
footer: true,
},
{
title: '搜索结果',
icon: 'mdi-magnify',
to: '/resource',
header: '开始',
admin: false,
},
{
title: '推荐',
icon: 'mdi-star-outline',
@@ -17,11 +24,12 @@ export const SystemNavMenus = [
footer: true,
},
{
title: '资源搜索',
icon: 'mdi-magnify',
to: '/resource',
title: '索',
icon: 'mdi-apple-safari',
to: '/discover',
header: '发现',
admin: false,
footer: false,
},
{
title: '电影',
@@ -169,12 +177,12 @@ export const SubscribeMovieTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-movie-open-outline',
icon: 'mdi-heart',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-movie-open-outline',
icon: 'mdi-fire',
},
]
@@ -183,17 +191,17 @@ export const SubscribeTvTabs = [
{
title: '我的订阅',
tab: 'mysub',
icon: 'mdi-television',
icon: 'mdi-heart',
},
{
title: '热门订阅',
tab: 'popular',
icon: 'mdi-television',
icon: 'mdi-fire',
},
{
title: '订阅分享',
tab: 'share',
icon: 'mdi-television',
icon: 'mdi-share-variant',
},
]
@@ -210,3 +218,22 @@ export const PluginTabs = [
icon: 'mdi-store',
},
]
// 发现标签页
export const DiscoverTabs = [
{
title: 'TheMovieDb',
tab: 'themoviedb',
icon: 'themoviedb',
},
{
title: '豆瓣',
tab: 'douban',
icon: 'douban',
},
{
title: 'Bangumi',
tab: 'bangumi',
icon: 'bangumi',
},
]

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
// 过滤参数
const filterParams = reactive({
'type': 2,
'cat': null,
'sort': 'rank', // date/rank
'year': null,
})
// Bangumi cat字典
/**
* 0 为 其他
1 为 TV
2 为 OVA
3 为 Movie
5 为 WEB
*/
const bangumiCatDict = {
'0': '其他',
'1': 'TV',
'2': 'OVA',
'3': 'Movie',
'5': 'WEB',
}
// Bangumi排序字典
const bangumiSortDict = {
'rank': '排名',
'date': '日期',
}
// 年份字典自动生成最近10年
const yearDict: Record<number, number> = {}
const currentYear = new Date().getFullYear()
for (let i = 0; i < 10; i++) {
yearDict[currentYear - i] = currentYear - i
}
// 当前Key
const currentKey = ref(0)
// 类型和过滤参数变化后重新刷新列表
watch([filterParams], () => {
currentKey.value++
})
</script>
<template>
<div class="px-3">
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>类别</VLabel>
</div>
<VChipGroup v-model="filterParams.cat">
<VChip
:color="filterParams.cat == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in bangumiCatDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>排序</VLabel>
</div>
<VChipGroup v-model="filterParams.sort">
<VChip
:color="filterParams.sort == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in bangumiSortDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>年份</VLabel>
</div>
<VChipGroup v-model="filterParams.year">
<VChip
:color="filterParams.year == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in yearDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
</div>
<div>
<MediaCardListView :key="currentKey" apipath="bangumi/subjects" :params="filterParams" />
</div>
</template>

View File

@@ -0,0 +1,202 @@
<script setup lang="ts">
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
// 电影或者电视剧 movies/tvs
const type = ref('movies')
// 过滤参数
const filterParams = reactive({
'sort': 'U',
'tags': '',
})
// 豆瓣风格类型
const doubanCategory = ref('')
// 地区
const doubanZone = ref('')
// 年代
const doubanYear = ref('')
// 豆瓣风格字典
const categoryDict = {
'喜剧': '喜剧',
'爱情': '爱情',
'动作': '动作',
'科幻': '科幻',
'动画': '动画',
'悬疑': '悬疑',
'犯罪': '犯罪',
'惊悚': '惊悚',
'冒险': '冒险',
'音乐': '音乐',
'历史': '历史',
'奇幻': '奇幻',
'恐怖': '恐怖',
'战争': '战争',
'传记': '传记',
'歌舞': '歌舞',
'武侠': '武侠',
'情色': '情色',
'灾难': '灾难',
'西部': '西部',
'纪录片': '纪录片',
'短片': '短片',
}
// 地区字典
const zoneDict = {
'华语': '华语',
'欧美': '欧美',
'韩国': '韩国',
'日本': '日本',
'中国大陆': '中国大陆',
'美国': '美国',
'中国香港': '中国香港',
'中国台湾': '中国台湾',
'英国': '英国',
'法国': '法国',
'德国': '德国',
'意大利': '意大利',
'西班牙': '西班牙',
'印度': '印度',
'泰国': '泰国',
'俄罗斯': '俄罗斯',
'加拿大': '加拿大',
'澳大利亚': '澳大利亚',
'爱尔兰': '爱尔兰',
'瑞典': '瑞典',
'巴西': '巴西',
'丹麦': '丹麦',
}
// 年代字典
const yearDict: Record<string, string> = {
'2020年代': '2020年代',
'2010年代': '2010年代',
'2000年代': '2000年代',
'90年代': '90年代',
'80年代': '80年代',
'70年代': '70年代',
'60年代': '60年代',
}
// 往年代字典中追加当前年份及往前5年的字典
const currentYear = new Date().getFullYear()
for (let i = 0; i < 6; i++) {
yearDict[`${currentYear - i}`] = `${currentYear - i}`
}
// 豆瓣过滤参数
const doubanSortDict = {
'U': '综合排序',
'R': '首播时间',
'T': '近期热度',
'S': '高分优先',
}
// 风格、年代、地区变化时,以,分隔拼接到tags参数
watch([doubanCategory, doubanZone, doubanYear], () => {
filterParams.tags = [doubanCategory.value, doubanZone.value, doubanYear.value].filter(Boolean).join(',')
})
// 当前Key
const currentKey = ref(0)
// 类型和过滤参数变化后重新刷新列表
watch([type, filterParams], () => {
if (!type.value) {
type.value = 'movies'
}
if (!filterParams.sort) {
filterParams.sort = 'U'
}
currentKey.value++
})
</script>
<template>
<div class="px-3">
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>类型</VLabel>
</div>
<VChipGroup v-model="type">
<VChip :color="type == 'movies' ? 'primary' : ''" filter tile value="movies">电影</VChip>
<VChip :color="type == 'tvs' ? 'primary' : ''" filter tile value="tvs">电视剧</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>排序</VLabel>
</div>
<VChipGroup v-model="filterParams.sort">
<VChip
:color="filterParams.sort == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in doubanSortDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>风格</VLabel>
</div>
<VChipGroup v-model="doubanCategory">
<VChip
:color="doubanCategory == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in categoryDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>地区</VLabel>
</div>
<VChipGroup v-model="doubanZone">
<VChip
:color="doubanZone == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in zoneDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>年代</VLabel>
</div>
<VChipGroup v-model="doubanYear">
<VChip
:color="doubanYear == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in yearDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
</div>
<div>
<MediaCardListView :key="currentKey" :apipath="`douban/${type}`" :params="filterParams" />
</div>
</template>

View File

@@ -27,6 +27,7 @@ const isRefreshed = ref(false)
// 数据列表
const dataList = ref<MediaInfo[]>([])
const currData = ref<MediaInfo[]>([])
// 拼装参数
function getParams() {
let params = {
@@ -77,7 +78,6 @@ async function fetchData({ done }: { done: any }) {
} else {
// 加载一次
// 设置加载中
loading.value = true
// 请求API
currData.value = await api.get(props.apipath, {
@@ -115,7 +115,11 @@ async function fetchData({ done }: { done: any }) {
<div v-if="dataList.length > 0" class="grid gap-4 grid-media-card mx-3" tabindex="0">
<MediaCard v-for="data in dataList" :key="data.tmdb_id || data.douban_id" :media="data" />
</div>
<NoDataFound v-if="dataList.length === 0 && isRefreshed" error-code="404" error-title="没有数据"
error-description="无法获取到媒体信息" />
<NoDataFound
v-if="dataList.length === 0 && isRefreshed"
error-code="404"
error-title="没有数据"
error-description="无法获取到媒体信息"
/>
</VInfiniteScroll>
</template>

View File

@@ -0,0 +1,178 @@
<script setup lang="ts">
import MediaCardListView from '@/views/discover/MediaCardListView.vue'
// 电影或者电视剧 movies/tvs
const type = ref('movies')
// 过滤参数
const filterParams = reactive({
sort_by: 'popularity.desc',
with_genres: '',
with_original_language: '',
with_keywords: '',
with_watch_providers: '',
vote_average: 0,
vote_count: 10,
release_date: '',
})
// TMDB 电影排序字典
const tmdbSortDict = {
'popularity.desc': '热度降序',
'popularity.asc': '热度升序',
'release_date.desc': '上映日期降序',
'release_date.asc': '上映日期升序',
'vote_average.desc': '评分降序',
'vote_average.asc': '评分升序',
}
// TMDB 电视剧排序字典
const tmdbTvSortDict = {
'popularity.desc': '热度降序',
'popularity.asc': '热度升序',
'first_air_date.desc': '首播日期降序',
'first_air_date.asc': '首播日期升序',
'vote_average.desc': '评分降序',
'vote_average.asc': '评分升序',
}
// TMDB风格字典
const tmdbGenreDict = {
'28': '动作',
'12': '冒险',
'16': '动画',
'35': '喜剧',
'80': '犯罪',
'99': '纪录片',
'18': '剧情',
'10751': '家庭',
'14': '奇幻',
'36': '历史',
'27': '恐怖',
'10402': '音乐',
'9648': '悬疑',
'10749': '爱情',
'878': '科幻',
'10770': '电视电影',
'53': '惊悚',
'10752': '战争',
'37': '西部',
}
// TMDB原始语言字典主要语言
const tmdbLanguageDict = {
'zh': '中文',
'en': '英语',
'ja': '日语',
'ko': '韩语',
'fr': '法语',
'de': '德语',
'es': '西班牙语',
'it': '意大利语',
'ru': '俄语',
'pt': '葡萄牙语',
'ar': '阿拉伯语',
'hi': '印地语',
'th': '泰语',
}
// 当前Key
const currentKey = ref(0)
// 类型和过滤参数变化后重新刷新列表
watch([type, filterParams], () => {
if (!type.value) {
type.value = 'movies'
}
if (!filterParams.sort_by) {
filterParams.sort_by = 'popularity.desc'
}
currentKey.value++
})
</script>
<template>
<div class="px-3">
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>类型</VLabel>
</div>
<VChipGroup v-model="type">
<VChip :color="type == 'movies' ? 'primary' : ''" filter tile value="movies">电影</VChip>
<VChip :color="type == 'tvs' ? 'primary' : ''" filter tile value="tvs">电视剧</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>排序</VLabel>
</div>
<VChipGroup v-model="filterParams.sort_by">
<VChip
:color="filterParams.sort_by == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in type == 'movies' ? tmdbSortDict : tmdbTvSortDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>风格</VLabel>
</div>
<VChipGroup v-model="filterParams.with_genres">
<VChip
:color="filterParams.with_genres == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in tmdbGenreDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>语言</VLabel>
</div>
<VChipGroup v-model="filterParams.with_original_language">
<VChip
:color="filterParams.with_original_language == key ? 'primary' : ''"
filter
tile
:value="key"
v-for="(value, key) in tmdbLanguageDict"
:key="key"
>
{{ value }}
</VChip>
</VChipGroup>
</div>
<div class="flex justify-start align-center">
<div class="mr-5">
<VLabel>评分</VLabel>
</div>
<VSlider v-model="filterParams.vote_average" thumb-label max="10" min="0" class="align-center" hide-details>
<template v-slot:append>
<VTextField
width="5rem"
v-model="filterParams.vote_count"
density="compact"
type="number"
hide-details
single-line
/>
</template>
</VSlider>
</div>
</div>
<div>
<MediaCardListView :key="currentKey" :apipath="`tmdb/${type}`" :params="filterParams" />
</div>
</template>

View File

@@ -397,9 +397,12 @@ onMounted(async () => {
<template>
<div>
<VTabs v-model="activeTab">
<VTabs v-model="activeTab" show-arrows>
<VTab v-for="item in PluginTabs" :value="item.tab">
<span class="mx-5">{{ item.title }}</span>
<div class="flex align-center min-w-24">
<VIcon size="20" start :icon="item.icon" />
{{ item.title }}
</div>
</VTab>
</VTabs>
@@ -507,7 +510,7 @@ onMounted(async () => {
</VWindow>
</div>
<div>
<div v-if="isRefreshed">
<!-- 插件搜索图标 -->
<VFab
icon="mdi-magnify"

View File

@@ -113,6 +113,9 @@ const progressText = ref('请稍候 ...')
// 进度值
const progressValue = ref(0)
// 是否已刷新
const isRefreshed = ref(false)
// 删除确认对话框
const deleteConfirmDialog = ref(false)
@@ -160,7 +163,8 @@ watch(
)
// 搜索监听
watch([() => search.value, () => isComposing.value],
watch(
[() => search.value, () => isComposing.value],
debounce(async () => {
if (!isComposing.value) {
console.log('search: ' + search.value)
@@ -181,7 +185,7 @@ async function fetchData(page = currentPage.value, count = itemsPerPage.value) {
title: search.value,
},
})
isRefreshed.value = true
dataList.value = result.data?.list
totalItems.value = result.data?.total
searchHintList.value = ['失败', '成功', ...new Set(dataList.value.map(item => item.title || ''))].filter(
@@ -502,7 +506,7 @@ onMounted(fetchData)
</VCard>
<!-- 底部操作按钮 -->
<div>
<div v-if="isRefreshed">
<VFab
v-if="selected.length > 0"
icon="mdi-trash-can-outline"

View File

@@ -111,6 +111,7 @@ onActivated(() => {
/>
<!-- 新增站点按钮 -->
<VFab
v-if="isRefreshed"
icon="mdi-plus"
location="bottom"
size="x-large"

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import draggable from 'vuedraggable'
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'
import api from '@/api'
import type { Subscribe } from '@/api/types'
import NoDataFound from '@/components/NoDataFound.vue'
@@ -110,12 +109,6 @@ async function fetchData() {
// 刷新状态
const loading = ref(false)
// 下拉刷新
async function onRefresh({ done }: { done: any }) {
await fetchData()
done('ok')
}
onMounted(async () => {
await loadSubscribeOrderConfig()
await fetchData()
@@ -138,29 +131,27 @@ onActivated(async () => {
<template>
<LoadingBanner v-if="!isRefreshed" class="mt-12" />
<VPullToRefresh v-model="loading" @load="onRefresh">
<draggable
v-if="displayList.length > 0"
v-model="displayList"
@end="saveSubscribeOrder"
handle=".cursor-move"
item-key="id"
tag="div"
:component-data="{ class: 'mx-3 grid gap-4 grid-subscribe-card p-1' }"
>
<template #item="{ element }">
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
</template>
</draggable>
<NoDataFound
v-if="displayList.length === 0 && isRefreshed"
error-code="404"
error-title="没有订阅"
error-description="请通过搜索添加电影电视剧订阅"
/>
</VPullToRefresh>
<draggable
v-if="displayList.length > 0"
v-model="displayList"
@end="saveSubscribeOrder"
handle=".cursor-move"
item-key="id"
tag="div"
:component-data="{ class: 'mx-3 grid gap-4 grid-subscribe-card p-1' }"
>
<template #item="{ element }">
<SubscribeCard :key="element.id" :media="element" @remove="fetchData" @save="fetchData" />
</template>
</draggable>
<NoDataFound
v-if="displayList.length === 0 && isRefreshed"
error-code="404"
error-title="没有订阅"
error-description="请通过搜索添加电影电视剧订阅"
/>
<!-- 底部操作按钮 -->
<div>
<div v-if="isRefreshed">
<VFab
v-if="store.state.auth.superUser"
icon="mdi-clipboard-edit"

View File

@@ -68,6 +68,7 @@ onActivated(() => {
/>
<VFab
v-if="isRefreshed"
icon="mdi-plus"
location="bottom"
size="x-large"
@@ -85,7 +86,6 @@ onActivated(() => {
oper="add"
max-width="50rem"
persistent
z-index="1010"
@save="onUserAdd"
@close="addUserDialog = false"
/>

View File

@@ -403,7 +403,7 @@ watch(
</VRow>
<!-- 双重验证弹窗 -->
<VDialog v-model="otpDialog" max-width="45rem" persistent z-index="1010">
<VDialog v-model="otpDialog" max-width="45rem" persistent scrollable>
<!-- 开启双重验证弹窗内容 -->
<VCard>
<DialogCloseBtn @click="otpDialog = false" />