Compare commits

...

51 Commits

Author SHA1 Message Date
jxxghp
28b307fb98 back arrow 2024-06-14 22:51:50 +08:00
jxxghp
a1dc723445 add select-none 2024-06-14 19:50:28 +08:00
jxxghp
23f4a70693 add drag delay 2024-06-14 19:40:58 +08:00
jxxghp
be5b4b39e5 fix ui 2024-06-14 15:51:38 +08:00
jxxghp
cf706e0e30 fix ui 2024-06-14 15:36:25 +08:00
jxxghp
8bc80d2088 fix ui 2024-06-14 15:12:11 +08:00
jxxghp
b94f8c92f0 fix https://github.com/jxxghp/MoviePilot/issues/2335 2024-06-14 14:38:30 +08:00
jxxghp
c3be75bed1 fix loading ui 2024-06-14 12:30:15 +08:00
jxxghp
91c8d8077f fix search ui 2024-06-14 11:39:00 +08:00
jxxghp
f598eed149 fix ui 2024-06-14 08:27:42 +08:00
jxxghp
971bae3be0 更新 Footer.vue 2024-06-14 07:44:54 +08:00
jxxghp
9a6abf4d5a 更新 Footer.vue 2024-06-14 07:27:09 +08:00
jxxghp
d756077a48 更新 Footer.vue 2024-06-14 07:26:55 +08:00
jxxghp
a1fc87bb1e fix footer ui 2024-06-14 07:15:17 +08:00
jxxghp
07186d2ae1 fix app mode margin 2024-06-13 20:39:27 +08:00
jxxghp
d2164d9ada fix ui 2024-06-13 20:29:53 +08:00
jxxghp
7eacaf8fc5 fix ui 2024-06-13 19:52:31 +08:00
jxxghp
9aa2de526e fix 2024-06-13 19:24:04 +08:00
jxxghp
12dfc5b407 fix app mode ui 2024-06-13 19:11:00 +08:00
jxxghp
1fc964ec16 add app mode 2024-06-13 17:30:50 +08:00
jxxghp
7f2f7b100b 更新 AccountSettingNotification.vue 2024-06-13 07:11:03 +08:00
jxxghp
8292140f1f 更新 package.json 2024-06-13 07:07:45 +08:00
jxxghp
c26e610a23 更新 MediaDetailView.vue 2024-06-13 07:06:49 +08:00
jxxghp
c96cfe81ab Merge pull request #154 from Mattoids/main 2024-06-11 18:43:06 +08:00
liufei
bb1cc0b60e 修复 套件版本无法添加用户的问题 2024-06-11 17:59:49 +08:00
jxxghp
1e74073344 更新 SearchBarView.vue 2024-06-10 17:05:11 +08:00
jxxghp
d83d1dd888 fix 榜单 & 订阅弹窗 & 订阅重置 2024-06-10 09:36:42 +08:00
jxxghp
e34573e72f fix webpush仅限管理员 2024-06-08 12:35:05 +08:00
jxxghp
9d3f4879ef feat:增加域名设置 2024-06-08 10:56:29 +08:00
jxxghp
6317277a70 v1.9.4-1 2024-06-08 07:46:20 +08:00
jxxghp
a1130ec60b feat:捷径根据参数自动打开 2024-06-08 07:45:45 +08:00
jxxghp
a1a3ccf6fb fix 2024-06-07 20:24:02 +08:00
jxxghp
aedb8bee9c fix service worker 2024-06-07 20:22:59 +08:00
jxxghp
6620d1c8fe fix service-worker 2024-06-07 08:34:09 +08:00
jxxghp
0ecc7dfead remove defer 2024-06-06 14:07:25 +08:00
jxxghp
9f5859ee93 feat:订阅重置 2024-06-06 07:57:45 +08:00
jxxghp
d559e1717c fix service worker 2024-06-05 22:21:27 +08:00
jxxghp
e649be58a2 add webpush switch 2024-06-05 18:42:39 +08:00
jxxghp
157c37c862 add service worker 2024-06-05 18:12:07 +08:00
jxxghp
da910ac670 Merge pull request #151 from hotlcc/develop-20240604-2 2024-06-04 17:53:02 +08:00
Allen
3831363815 删除和整理场景路由参数未改变,reloadPage不会生效,需要fetchData刷新数据 2024-06-04 17:41:09 +08:00
jxxghp
94a6ea13bd rollback 2024-06-04 16:18:06 +08:00
jxxghp
06c1ad0f69 更新 main.ts 2024-06-04 16:14:25 +08:00
jxxghp
d6873781e8 更新 SearchBarView.vue 2024-06-04 15:46:36 +08:00
jxxghp
ab6c9647a7 Merge pull request #149 from hotlcc/develop-20240604-1 2024-06-04 15:42:36 +08:00
Allen
59b0350993 针对异形屏做了优化 2024-06-04 15:28:17 +08:00
jxxghp
df0be4c070 更新 package.json 2024-06-04 14:02:59 +08:00
jxxghp
87f3ef4353 Merge pull request #148 from hotlcc/develop-20240604-1 2024-06-04 14:00:49 +08:00
Allen
2611bbaea4 弹窗 VDialog 在低版本 iOS Safari 浏览器下宽度异常问题处理 2024-06-04 13:54:48 +08:00
jxxghp
7c0d8cf792 Merge pull request #147 from hotlcc/develop-20240604-1 2024-06-04 12:54:09 +08:00
Allen
2d17baccd2 低版本safari主菜单样式兼容性处理 2024-06-04 12:33:18 +08:00
51 changed files with 1771 additions and 1080 deletions

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=http://localhost:3001/api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

View File

@@ -1 +1,2 @@
VITE_API_BASE_URL=api/v1/
VITE_PUBLIC_VAPID_KEY=BH3w49sZA6jXUnE-yt4jO6VKh73lsdsvwoJ6Hx7fmPIDKoqGiUl2GEoZzy-iJfn4SfQQcx7yQdHf9RknwrL_lSM

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ node_modules
.DS_Store
dist
dist-ssr
dev-dist
*.local
/cypress/videos/

View File

@@ -15,7 +15,6 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="apple-touch-startup-image" href="/splash/apple-splash.jpg" />
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
<link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
@@ -37,7 +36,7 @@
<div id="loading-bg">
<div class="loading-logo">
<!-- Logo -->
<svg width="100px" height="100px" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
<svg width="10rem" height="10rem" viewBox="0 0 192 192" version="1.1" xmlns="http://www.w3.org/2000/svg"
style="fill-rule: evenodd; clip-rule: evenodd; stroke-linejoin: round; stroke-miterlimit: 2">
<g transform="matrix(1,0,0,1,-2606,-236)">
<g id="a2-c" transform="matrix(1,0,0,1,2606,236)">

View File

@@ -1,6 +1,6 @@
{
"name": "moviepilot",
"version": "1.9.3-3",
"version": "1.9.5-1",
"private": true,
"bin": "dist/service.js",
"scripts": {
@@ -42,7 +42,6 @@
"sass": "^1.59.3",
"tailwindcss": "^3.3.2",
"unplugin-vue-define-options": "^1.3.5",
"vite-plugin-pwa": "^0.19.8",
"vue": "^3.3.2",
"vue-router": "^4.2.0",
"vue-toast-notification": "^3",
@@ -92,6 +91,7 @@
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.8",
"vite-plugin-pages": "^0.32.1",
"vite-plugin-pwa": "^0.20.0",
"vite-plugin-vue-layouts": "^0.11.0",
"vite-plugin-vuetify": "2.0.3",
"vue-shepherd": "^3.0.0",

View File

@@ -3,10 +3,9 @@ body {
}
html {
overflow: hidden auto;
background: var(--initial-loader-bg, #fff);
min-block-size: calc(100% + env(safe-area-inset-top));
overflow-x: hidden;
overflow-y: auto;
}
#loading-bg {
@@ -20,8 +19,8 @@ html {
.loading-logo {
position: absolute;
inset-block-start: 40%;
inset-inline-start: calc(50% - 50px);
inset-block-start: 35%;
inset-inline-start: calc(50% - 5rem);
}
.loading {
@@ -83,4 +82,4 @@ html {
opacity: 1;
transform: rotate(1turn);
}
}
}

View File

@@ -1,80 +0,0 @@
{
"name": "MoviePilot",
"short_name": "MoviePilot",
"start_url": "./",
"display": "standalone",
"icons": [
{
"src": "./android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-192x192_maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "./android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "./android-chrome-512x512_maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#28243D",
"background_color": "#28243D",
"shortcuts": [
{
"name": "推荐",
"url": "./ranking",
"icons": [
{
"src": "./sparkles-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电影订阅",
"url": "./subscribe-movie",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "电视剧订阅",
"url": "./subscribe-tv",
"icons": [
{
"src": "./clock-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
},
{
"name": "设置",
"url": "./setting",
"icons": [
{
"src": "./cog-icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
]
}
]
}

2
public/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -8,7 +8,7 @@ const props = defineProps({
<template>
<div class="w-full text-center text-gray-500 text-sm flex flex-col items-center">
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="48" indeterminate color="primary" />
<VProgressCircular v-if="!props.text || !props.progress" class="mb-3" size="64" indeterminate color="primary" />
<VProgressCircular v-if="props.progress" class="mb-3" color="primary" :model-value="props.progress" size="64" />
<span>{{ props.text }}</span>
</div>

View File

@@ -3,5 +3,5 @@
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
/* stylelint-enable */
background-color: rgb(var(--v-theme-surface), 0.9);
background-color: rgb(var(--v-theme-surface), 0.8);
}

View File

@@ -5,14 +5,14 @@
/**
* 修复低版本Safari等浏览器数组不支持at函数的问题
*/
export function fixArrayAt() {
if (!Array.prototype.at) {
Array.prototype.at = function(index: number) {
if (index >= 0) {
return this[index]
} else {
return this[this.length + index]
}
}
;(function fixArrayAt() {
if (!Array.prototype.at) {
Array.prototype.at = function (index: number) {
if (index >= 0) {
return this[index]
} else {
return this[this.length + index]
}
}
}
}
})()

View File

@@ -8,8 +8,7 @@ dayjs.extend(relativeTime)
dayjs.locale(ZH_CN)
export function avatarText(value: string) {
if (!value)
return ''
if (!value) return ''
const nameArray = value.split(' ')
return nameArray.map(word => word.charAt(0).toUpperCase()).join('')
@@ -19,7 +18,9 @@ export function avatarText(value: string) {
export function kFormatter(num: number) {
const regex = /\B(?=(\d{3})+(?!\d))/g
return Math.abs(num) > 9999 ? `${Math.sign(num) * +((Math.abs(num) / 1000).toFixed(1))}k` : Math.abs(num).toFixed(0).replace(regex, ',')
return Math.abs(num) > 9999
? `${Math.sign(num) * +(Math.abs(num) / 1000).toFixed(1)}k`
: Math.abs(num).toFixed(0).replace(regex, ',')
}
/**
@@ -29,9 +30,11 @@ export function kFormatter(num: number) {
* @param {string} value date to format
* @param {Intl.DateTimeFormatOptions} formatting Intl object to format with
*/
export function formatDate(value: string, formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }) {
if (!value)
return value
export function formatDate(
value: string,
formatting: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' },
) {
if (!value) return value
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
}
@@ -46,21 +49,19 @@ export function formatDateToMonthShort(value: string, toTimeForCurrentDay = true
const date = new Date(value)
let formatting: Record<string, string> = { month: 'short', day: 'numeric' }
if (toTimeForCurrentDay && isToday(date))
formatting = { hour: 'numeric', minute: 'numeric' }
if (toTimeForCurrentDay && isToday(date)) formatting = { hour: 'numeric', minute: 'numeric' }
return new Intl.DateTimeFormat('en-US', formatting).format(new Date(value))
}
export const prefixWithPlus = (value: number) => value > 0 ? `+${value}` : value
export const prefixWithPlus = (value: number) => (value > 0 ? `+${value}` : value)
// 格式化为Sxx
export const formatSeason = (value: string) => value ? `S${value.padStart(2, '0')}` : ''
export const formatSeason = (value: string) => (value ? `S${value.padStart(2, '0')}` : '')
// 格式化为xx[TGMK]B
export function formatFileSize(bytes: number) {
if (bytes < 0)
throw new Error('字节数不能为负数。')
if (bytes < 0) throw new Error('字节数不能为负数。')
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
@@ -82,22 +83,18 @@ export function formatSeconds(seconds: number) {
let formattedTime = ''
if (hours > 0)
formattedTime += `${hours}小时`
if (hours > 0) formattedTime += `${hours}小时`
if (minutes > 0)
formattedTime += `${minutes}`
if (minutes > 0) formattedTime += `${minutes}`
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0)
formattedTime += `${remainingSeconds}`
if ((remainingSeconds > 0 || formattedTime === '') && hours <= 0) formattedTime += `${remainingSeconds}`
return formattedTime
}
// YYYY-MM-DD 转化为Date
export function parseDate(dateString: string): Date | null {
if (!dateString)
return null
if (!dateString) return null
const [year, month, day] = dateString.split('-').map(Number)
return new Date(year, month - 1, day)
@@ -105,8 +102,7 @@ export function parseDate(dateString: string): Date | null {
// 文件大小格式化
export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0)
return '0 bytes'
if (bytes === 0) return '0 bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
@@ -119,11 +115,9 @@ export function formatBytes(bytes: number, decimals = 2) {
// 格式化剧集列表
export function formatEp(nums: number[]): string {
if (!nums.length)
return ''
if (!nums.length) return ''
if (nums.length === 1)
return nums[0].toString()
if (nums.length === 1) return nums[0].toString()
// 将数组升序排序
nums.sort((a, b) => a - b)
@@ -134,44 +128,22 @@ export function formatEp(nums: number[]): string {
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()}`)
} 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()}`)
if (start === end) formattedRanges.push(start.toString())
else formattedRanges.push(`${start.toString()}-${end.toString()}`)
return formattedRanges.join('、')
}
// 将yyyy-mm-dd hh:mm:ss转换为时间差1小时前1天前
export function formatDateDifference(dateString: string): string {
// const timeDifference = dayjs().millisecond() - dayjs(dateString).millisecond()
// const secondsDifference = Math.floor(timeDifference / 1000)
// const minutesDifference = Math.floor(secondsDifference / 60)
// const hoursDifference = Math.floor(minutesDifference / 60)
// const daysDifference = Math.floor(hoursDifference / 24)
// if (daysDifference > 0)
// return `${daysDifference}天前`
// else if (hoursDifference > 0)
// return `${hoursDifference}小时前`
// else if (minutesDifference > 0)
// return `${minutesDifference}分钟前`
// else
// return '刚刚'
if (!dateString)
return ''
if (!dateString) return ''
return dayjs(dateString).fromNow()
}

View File

@@ -1,7 +1,6 @@
// 👉 IsEmpty
export function isEmpty(value: unknown): boolean {
if (value === null || value === undefined || value === '')
return true
if (value === null || value === undefined || value === '') return true
return !!(Array.isArray(value) && value.length === 0)
}
@@ -33,73 +32,6 @@ export function isToday(date: Date) {
)
}
/**
* 计算时间差返回xx天/xx小时/xx分钟/xx秒
*
* @deprecated 建议使用:@core/utils/formatters.ts formatDateDifference
*/
export function calculateTimeDifference(inputTime: string): string {
if (!inputTime)
return ''
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
const currentDate = new Date()
const timeDifference = currentDate.getTime() - inputDate.getTime()
const secondsDifference = Math.floor(timeDifference / 1000)
if (secondsDifference < 60) {
return `${secondsDifference}`
}
else if (secondsDifference < 3600) {
const minutes = Math.floor(secondsDifference / 60)
return `${minutes}分钟`
}
else if (secondsDifference < 86400) {
const hours = Math.floor(secondsDifference / 3600)
return `${hours}小时`
}
else {
const days = Math.floor(secondsDifference / 86400)
return `${days}`
}
}
// 计算时间差返回xx天xx小时xx分钟
export function calculateTimeDiff(inputTime: string): string {
if (!inputTime)
return ''
// 使用当前时区
const inputDate = new Date(inputTime.replaceAll(/-/g, '/'))
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中
export function isContained(subArray: any[], mainArray: any[]): boolean {
return subArray.every(element => mainArray.includes(element))
@@ -112,8 +44,7 @@ export function isIntersected(array1: any[], array2: any[]): boolean {
export function isNullOrEmptyObject(obj: any): boolean {
// 首先判断是否为 null 或 undefined
if (obj === null || obj === undefined)
return true
if (obj === null || obj === undefined) return true
// 然后判断是否为空对象
return !!(typeof obj === 'object' && Object.keys(obj).length === 0)
@@ -127,3 +58,10 @@ export function checkPrefersColorSchemeIsDark(): boolean {
return false
}
}
// 从URL中获取参数值
export function getQueryValue(key: string, url = window.location.href): string {
const reg = new RegExp(`[?&]${key}=([^&#]*)`, 'i')
const res = reg.exec(url)
return res ? res[1] : ''
}

View File

@@ -28,3 +28,17 @@ export async function copyToClipboard(content: string) {
document.body.removeChild(input)
}
}
// VAPID公钥转Uint8Array
export function urlBase64ToUint8Array(base64String: string) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = window.atob(base64)
const outputArray = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i)
}
return outputArray
}

View File

@@ -33,7 +33,10 @@ defineProps<{
.nav-link a {
display: flex;
align-items: center;
border-radius: 0 3.125rem 3.125rem 0 !important;
cursor: pointer;
margin-inline-end: 1.125em;
padding-inline: 1.375rem 1rem;
}
}
</style>

View File

@@ -18,3 +18,12 @@ defineProps<{
</div>
</li>
</template>
<style lang="scss">
.layout-vertical-nav {
.nav-section-title {
padding-left: 1.375rem;
padding-right: 1rem;
}
}
</style>

View File

@@ -720,6 +720,7 @@ export interface NotificationSwitch {
slack: boolean
synologychat: boolean
vocechat: boolean
webpush: boolean
}
// 文件浏览接口

View File

@@ -1,5 +1,6 @@
<script lang="ts" setup>
import { useToast } from 'vue-toast-notification'
import { useConfirm } from 'vuetify-use-dialog'
import SubscribeEditDialog from '../dialog/SubscribeEditDialog.vue'
import { formatDateDifference } from '@/@core/utils/formatters'
import { formatSeason } from '@/@core/utils/formatters'
@@ -15,6 +16,9 @@ const props = defineProps({
// 定义触发的自定义事件
const emit = defineEmits(['remove', 'save'])
// 确认框
const createConfirm = useConfirm()
// 提示框
const $toast = useToast()
@@ -34,8 +38,8 @@ function imageLoadHandler() {
// 根据 type 返回不同的图标
function getIcon() {
if (props.media?.type === '电影') return 'mdi-movie'
else if (props.media?.type === '电视剧') return 'mdi-television-classic'
if (props.media?.type === '电影') return 'mdi-movie-open'
else if (props.media?.type === '电视剧') return 'mdi-television-play'
else return 'mdi-help-circle'
}
@@ -50,12 +54,12 @@ function getPercentage() {
// 计算文本颜色
function getTextColor() {
return imageLoaded.value ? 'white' : ''
return 'white'
}
// 计算文本类
function getTextClass() {
return imageLoaded.value ? 'text-white' : ''
return 'text-white'
}
// 删除订阅
@@ -84,6 +88,27 @@ async function searchSubscribe() {
}
}
// 重置订阅
async function resetSubscribe() {
// 确认
try {
const isConfirmed = await createConfirm({
title: '确认',
content: `重置后 ${props.media?.name} 已下载记录将被清除,未入库的剧集将会重新下载,是否确认?`,
})
if (!isConfirmed) return
// 重置
const result: { [key: string]: any } = await api.get(`subscribe/reset/${props.media?.id}`)
// 提示
if (result.success) {
$toast.success(`${props.media?.name} 重置成功!`)
emit('save')
} else $toast.error(`${props.media?.name} 重置失败:${result.message}`)
} catch (e) {
console.log(e)
}
}
// 编辑订阅响应
async function editSubscribeDialog() {
subscribeEditDialog.value = true
@@ -124,8 +149,18 @@ const dropdownItems = ref([
},
},
{
title: '取消订阅',
title: '重置',
value: 4,
props: {
prependIcon: 'mdi-restore-alert',
click: resetSubscribe,
color: 'warning',
},
show: props.media?.type === '电视剧',
},
{
title: '取消订阅',
value: 5,
props: {
prependIcon: 'mdi-trash-can-outline',
color: 'error',
@@ -151,7 +186,7 @@ watch(
:key="props.media?.id"
class="flex flex-col"
:class="{
'outline-dashed outline-1': props.media?.best_version,
'outline-dashed outline-1': props.media?.best_version && imageLoaded,
'transition transform-cpu duration-300 scale-105 shadow-lg': hover.isHovering,
}"
@click="editSubscribeDialog"
@@ -159,73 +194,87 @@ watch(
<template #image>
<VImg
:src="props.media?.backdrop || props.media?.poster"
aspect-ratio="2/3"
aspect-ratio="3/2"
cover
class="brightness-50"
:class="{ 'brightness-50': imageLoaded }"
@load="imageLoadHandler"
/>
>
<template #placeholder>
<div class="w-full h-full">
<VSkeletonLoader class="object-cover aspect-w-3 aspect-h-2" />
</div>
</template>
</VImg>
</template>
<VCardItem>
<template #prepend>
<VIcon size="1.9rem" :color="getTextColor()" :icon="getIcon()" />
</template>
<VCardTitle :class="getTextClass()">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</VCardTitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
<VMenu activator="parent" close-on-content-click>
<VList>
<VListItem
v-for="(item, i) in dropdownItems"
:key="i"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
<div v-if="imageLoaded">
<VCardItem>
<template #prepend>
<VIcon size="1.9rem" :color="getTextColor()" :icon="getIcon()" />
</template>
<VCardTitle :class="getTextClass()">
{{ props.media?.name }}
{{ formatSeason(props.media?.season ? props.media?.season.toString() : '') }}
</VCardTitle>
<template #append>
<div class="me-n3">
<IconBtn>
<VIcon icon="mdi-dots-vertical" :color="getTextColor()" />
<VMenu activator="parent" close-on-content-click>
<VList>
<template v-for="(item, i) in dropdownItems" :key="i">
<VListItem
v-if="item.show !== false"
variant="plain"
:base-color="item.props.color"
@click="item.props.click"
>
<template #prepend>
<VIcon :icon="item.props.prependIcon" />
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</template>
<VListItemTitle v-text="item.title" />
</VListItem>
</VList>
</VMenu>
</IconBtn>
</VList>
</VMenu>
</IconBtn>
</div>
</template>
</VCardItem>
<VCardText>
<p class="clamp-text mb-0" :class="getTextClass()">
{{ props.media?.description }}
</p>
</VCardText>
<VCardText class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center">
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-clock"
:color="getTextColor()"
class="me-1"
/>
<span v-if="props.media?.season" class="text-subtitle-2 me-4" :class="getTextClass()"
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}</span
>
<IconBtn v-if="props.media?.username" icon="mdi-account" :color="getTextColor()" class="me-1" />
<span v-if="props.media?.username" class="text-subtitle-2 me-4" :class="getTextClass()">
{{ props.media?.username }}
</span>
</div>
</template>
</VCardItem>
<VCardText>
<p class="clamp-text mb-0" :class="getTextClass()">
{{ props.media?.description }}
</p>
</VCardText>
<VCardText class="d-flex justify-space-between align-center flex-wrap">
<div class="d-flex align-center">
<IconBtn
v-if="props.media?.total_episode"
v-bind="props"
icon="mdi-progress-clock"
:color="getTextColor()"
class="me-1"
/>
<span v-if="props.media?.season" class="text-subtitle-2 me-4" :class="getTextClass()"
>{{ (props.media?.total_episode || 0) - (props.media?.lack_episode || 0) }} /
{{ props.media?.total_episode }}</span
>
<IconBtn v-if="props.media?.username" icon="mdi-account" :color="getTextColor()" class="me-1" />
<span v-if="props.media?.username" class="text-subtitle-2 me-4" :class="getTextClass()">
{{ props.media?.username }}
</span>
</div>
</VCardText>
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-download" class="me-1" />
{{ lastUpdateText }}
</VCardText>
<VProgressLinear v-if="getPercentage() > 0" :model-value="getPercentage()" bg-color="success" color="success" />
</VCardText>
<VCardText v-if="lastUpdateText" class="absolute right-0 bottom-0 d-flex align-center p-2 text-gray-300">
<VIcon icon="mdi-download" class="me-1" />
{{ lastUpdateText }}
</VCardText>
<VProgressLinear
v-if="getPercentage() > 0"
:model-value="getPercentage()"
bg-color="success"
color="success"
/>
</div>
</VCard>
</template>
</VHover>

View File

@@ -11,6 +11,15 @@ import store from '@/store'
import api from '@/api'
import MediaInfoCard from '@/components/cards/MediaInfoCard.vue'
import ProgressDialog from '../dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 输入参数
const inProps = defineProps({
@@ -364,7 +373,14 @@ onMounted(() => {
</VCardText>
<VCardText v-else-if="dirs.length || files.length" class="p-0">
<VList subheader>
<VVirtualScroll class="virtual-scroll-div" :items="[...dirs, ...files]">
<VVirtualScroll
:items="[...dirs, ...files]"
:style="
appMode
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
"
>
<template #default="{ item }">
<VHover>
<template #default="hover">
@@ -497,14 +513,4 @@ onMounted(() => {
.v-toolbar {
background: rgb(var(--v-table-header-background));
}
.virtual-scroll-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.virtual-scroll-div {
block-size: calc(100vh - 17rem);
}
}
</style>

View File

@@ -11,6 +11,12 @@ import UserProfile from '@/layouts/components/UserProfile.vue'
import store from '@/store'
import { SystemNavMenus } from '@/router/menu'
import { NavMenu } from '@/@layouts/types'
import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
@@ -19,6 +25,11 @@ const superUser = store.state.auth.superUser
const getMenuList = (header: string) => {
return SystemNavMenus.filter((item: NavMenu) => item.header === header && (!item.admin || superUser))
}
// 返回上一页
function goBack() {
history.back()
}
</script>
<template>
@@ -27,9 +38,13 @@ const getMenuList = (header: string) => {
<template #navbar="{ toggleVerticalOverlayNavActive }">
<div class="d-flex h-100 align-center mx-1">
<!-- 👉 Vertical Nav Toggle -->
<IconBtn class="ms-n2 d-lg-none" @click="toggleVerticalOverlayNavActive(true)">
<IconBtn v-if="!appMode && display.mdAndDown.value" class="ms-n2" @click="toggleVerticalOverlayNavActive(true)">
<VIcon icon="mdi-menu" />
</IconBtn>
<!-- 👉 Back Button -->
<IconBtn v-if="appMode" class="ms-n2" @click="goBack">
<VIcon icon="mdi-arrow-left" size="32" />
</IconBtn>
<!-- 👉 Search Bar -->
<SearchBar />
<!-- 👉 Spacer -->

View File

@@ -1,3 +1,69 @@
<script setup lang="ts">
import { useDisplay } from 'vuetify'
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
const route = useRoute()
// 各按钮活动状态
const activeState = computed(() => {
return {
home: route.path === '/dashboard',
ranking: route.path === '/ranking',
movie: route.path === '/subscribe-movie',
tv: route.path === '/subscribe-tv',
apps: route.path === '/apps',
}
})
</script>
<template>
<div class="h-100 d-flex align-center justify-space-between" />
<div v-if="appMode" class="w-100" style="block-size: calc(3.5rem + env(safe-area-inset-bottom))">
<VBottomNavigation
grow
horizontal
color="primary"
class="footer-nav border-t"
style="block-size: calc(3.5rem + env(safe-area-inset-bottom))"
>
<VBtn to="/dashboard" :ripple="false">
<VIcon v-if="activeState.home" size="32">mdi-home</VIcon>
<VIcon v-else size="32">mdi-home-outline</VIcon>
</VBtn>
<VBtn to="/ranking" :ripple="false">
<VIcon v-if="activeState.ranking" size="32">mdi-star</VIcon>
<VIcon v-else size="32">mdi-star-outline</VIcon>
</VBtn>
<VBtn to="/subscribe-movie?tab=mysub" :ripple="false">
<VIcon v-if="activeState.movie" size="32">mdi-movie-open</VIcon>
<VIcon v-else size="32">mdi-movie-open-outline</VIcon>
</VBtn>
<VBtn to="/subscribe-tv?tab=mysub" :ripple="false">
<VIcon v-if="activeState.tv" size="32">mdi-television-play</VIcon>
<VIcon v-else size="32">mdi-television</VIcon>
</VBtn>
<VBtn to="/apps" :ripple="false">
<VIcon v-if="activeState.apps" size="32">mdi-dots-horizontal-circle</VIcon>
<VIcon v-else size="32">mdi-dots-horizontal</VIcon>
</VBtn>
</VBottomNavigation>
</div>
</template>
<style lang="scss">
.footer-nav {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.8);
padding-block-end: env(safe-area-inset-bottom);
}
.footer-nav .v-btn--variant-text .v-btn__overlay {
background-color: transparent !important;
}
</style>
}

View File

@@ -1,6 +1,9 @@
<script lang="ts" setup>
import * as Mousetrap from 'mousetrap'
import SearchBarView from '@/views/system/SearchBarView.vue'
import { useDisplay } from 'vuetify'
const display = useDisplay()
const searchDialog = ref(false)
@@ -20,7 +23,7 @@ function openSearchDialog() {
<IconBtn @click="openSearchDialog">
<VIcon icon="ri-search-line" />
</IconBtn>
<span class="d-none d-md-flex align-center text-disabled ms-2" @click="openSearchDialog">
<span v-if="display.lgAndUp.value" class="flex align-center text-disabled ms-2" @click="openSearchDialog">
<span class="me-3">搜索</span>
<span class="meta-key">K</span>
</span>

View File

@@ -8,6 +8,7 @@ import MessageView from '@/views/system/MessageView.vue'
import store from '@/store'
import api from '@/api'
import { useDisplay } from 'vuetify'
import { getQueryValue } from '@/@core/utils'
// 显示器宽度
const display = useDisplay()
@@ -75,6 +76,29 @@ async function sendMessage() {
onMounted(() => {
scrollMessageToEnd()
const shortcut = getQueryValue('shortcut')
if (shortcut) {
switch (shortcut) {
case 'nameTest':
nameTestDialog.value = true
break
case 'netTest':
netTestDialog.value = true
break
case 'logging':
loggingDialog.value = true
break
case 'ruleTest':
ruleTestDialog.value = true
break
case 'systemTest':
systemTestDialog.value = true
break
case 'message':
messageDialog.value = true
break
}
}
})
</script>

View File

@@ -6,6 +6,9 @@ import router from '@/router'
import avatar1 from '@images/avatars/avatar-1.png'
import api from '@/api'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useDisplay } from 'vuetify'
const display = useDisplay()
// Vuex Store
const store = useStore()
@@ -58,10 +61,20 @@ async function restart() {
}
}
// 是否精简模式
const isCompactMode = ref(localStorage.getItem('MP_APPMODE') != '0')
// 从Vuex Store中获取信息
const superUser = store.state.auth.superUser
const userName = store.state.auth.userName
const avatar = store.state.auth.avatar
// 监听精简模式切换
watch(isCompactMode, value => {
localStorage.setItem('MP_APPMODE', value ? '1' : '0')
//刷新页面
location.reload()
})
</script>
<template>
@@ -86,6 +99,17 @@ const avatar = store.state.auth.avatar
</VListItemTitle>
<VListItemSubtitle>{{ userName }}</VListItemSubtitle>
</VListItem>
<!-- Divider -->
<VDivider v-if="display.mdAndDown.value" class="my-2" />
<!-- 👉 AppMode -->
<VListItem v-if="display.mdAndDown.value">
<template #prepend>
<VSwitch class="me-2" v-model="isCompactMode"></VSwitch>
</template>
<VListItemTitle>App模式</VListItemTitle>
</VListItem>
<VDivider class="my-2" />
<!-- 👉 Profile -->
@@ -97,7 +121,7 @@ const avatar = store.state.auth.avatar
</VListItem>
<!-- 👉 FAQ -->
<VListItem href="https://github.com/jxxghp/MoviePilot/blob/main/README.md" target="_blank">
<VListItem href="https://wiki.movie-pilot.org" target="_blank">
<template #prepend>
<VIcon class="me-2" icon="mdi-help-circle-outline" size="22" />
</template>
@@ -105,7 +129,7 @@ const avatar = store.state.auth.avatar
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<VDivider v-if="superUser" class="my-2" />
<!-- 👉 restart -->
<VListItem v-if="superUser" @click="restart">
@@ -115,9 +139,6 @@ const avatar = store.state.auth.avatar
<VListItemTitle>重启</VListItemTitle>
</VListItem>
<!-- Divider -->
<VDivider class="my-2" />
<!-- 👉 Logout -->
<VListItem @click="logout">
<VBtn color="error" block>

View File

@@ -1,23 +1,19 @@
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import '@/@iconify/icons-bundle'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import '@/@core/utils/compatibility'
import './ace-config'
import VueApexCharts from 'vue3-apexcharts'
import { removeEl } from './@core/utils/dom'
import '@/@iconify/icons-bundle'
import '@/plugins/webfontloader'
import App from '@/App.vue'
import vuetify from '@/plugins/vuetify'
import { loadFonts } from '@/plugins/webfontloader'
import router from '@/router'
import store from '@/store'
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import { VAceEditor } from 'vue3-ace-editor'
import { createApp } from 'vue'
import { removeEl } from './@core/utils/dom'
import { PerfectScrollbarPlugin } from 'vue3-perfect-scrollbar'
import 'vue3-perfect-scrollbar/style.css'
import { VTreeview } from 'vuetify/labs/VTreeview'
import ToastPlugin from 'vue-toast-notification'
import VuetifyUseDialog from 'vuetify-use-dialog'
import VueApexCharts from 'vue3-apexcharts'
import DialogCloseBtn from '@/@core/components/DialogCloseBtn.vue'
import MediaCard from './components/cards/MediaCard.vue'
import PosterCard from './components/cards/PosterCard.vue'
@@ -27,13 +23,11 @@ import MediaInfoCard from './components/cards/MediaInfoCard.vue'
import TorrentCard from './components/cards/TorrentCard.vue'
import MediaIdSelector from './components/misc/MediaIdSelector.vue'
import PathField from './components/input/PathField.vue'
import { fixArrayAt } from '@/@core/utils/compatibility'
// 修复低版本Safari等浏览器数组不支持at函数的问题
fixArrayAt()
// 加载字体
loadFonts()
import '@core/scss/template/index.scss'
import '@layouts/styles/index.scss'
import '@styles/styles.scss'
import 'vue-toast-notification/dist/theme-bootstrap.css'
import 'vue3-perfect-scrollbar/style.css'
// 创建Vue实例
const app = createApp(App)

71
src/pages/appcenter.vue Normal file
View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { NavMenu } from '@/@layouts/types'
import { SystemNavMenus } from '@/router/menu'
import store from '@/store'
import draggable from 'vuedraggable'
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
// APP图标顺序
const appOrder = ref<string[]>([])
// 根据分类获取菜单列表
const getMenuList = () => {
return SystemNavMenus.filter((item: NavMenu) => !item.admin || superUser)
}
// APP列表
const appList = ref<NavMenu[]>(getMenuList())
// 保存APP图标顺序到localStorage
function saveAppsOrder() {
appOrder.value = appList.value.map(app => app.title)
localStorage.setItem('MP_APPS_ORDER', JSON.stringify(appOrder.value))
}
onMounted(() => {
const localOrder = localStorage.getItem('MP_APPS_ORDER')
if (localOrder) {
appOrder.value = JSON.parse(localOrder)
// 对appList进行排序
appList.value.sort((a, b) => {
const aIndex = appOrder.value.findIndex(item => item === a.title)
const bIndex = appOrder.value.findIndex(item => item === b.title)
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex)
})
}
})
</script>
<template>
<div class="ps ps--active-y mx-3 appcenter-grid" tabindex="0">
<draggable
v-model="appList"
item-key="title"
tag="VRow"
delay="500"
@end="saveAppsOrder"
:component-data="{ 'class': 'ma-0 mt-n1' }"
>
<template #item="{ element }">
<VCol cols="6" md="4" lg="3" class="text-center cursor-pointer shortcut-icon">
<VCard class="pa-4 select-none" :to="element.to" variant="flat">
<VAvatar size="64" variant="text">
<VIcon size="48" :icon="element.icon" color="primary" />
</VAvatar>
<h6 class="text-base font-weight-medium mt-2 mb-0">{{ element.full_title || element.title }}</h6>
</VCard>
</VCol>
</template>
</draggable>
</div>
</template>
<style type="scss">
.appcenter-grid .v-card {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.8);
}
</style>

View File

@@ -5,6 +5,13 @@ import { isNullOrEmptyObject } from '@/@core/utils'
import { DashboardItem } from '@/api/types'
import store from '@/store'
import DashboardElement from '@/components/misc/DashboardElement.vue'
import { useDisplay } from 'vuetify'
// APP
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 从Vuex Store中获取superuser信息
const superUser = store.state.auth.superUser
@@ -314,7 +321,16 @@ onBeforeMount(async () => {
</draggable>
<!-- 底部操作按钮 -->
<VFab icon="mdi-view-dashboard-edit" location="bottom" size="x-large" fixed app appear @click="dialog = true" />
<VFab
icon="mdi-view-dashboard-edit"
location="bottom"
size="x-large"
fixed
app
appear
@click="dialog = true"
:class="{ 'mb-12': appMode }"
/>
<!-- 弹窗根据配置生成选项 -->
<VDialog v-model="dialog" max-width="35rem" scrollable>

View File

@@ -8,6 +8,7 @@ import router from '@/router'
import logo from '@images/logo.png'
import { useTheme } from 'vuetify'
import { checkPrefersColorSchemeIsDark } from '@/@core/utils'
import { urlBase64ToUint8Array } from '@/@core/utils/navigator'
const { global: globalTheme } = useTheme()
@@ -89,11 +90,39 @@ async function setTheme() {
localStorage.setItem('materio-initial-loader-bg', globalTheme.current.value.colors.background)
}
async function afterLogin() {
// 订阅推送通知
async function subscribeForPushNotifications() {
if ('serviceWorker' in navigator && 'PushManager' in window) {
const registration = await navigator.serviceWorker.ready
// 获取订阅信息
const subscription = await registration.pushManager.getSubscription().then(function (subscription) {
if (subscription === null) {
const convertedVapidKey = urlBase64ToUint8Array(import.meta.env.VITE_PUBLIC_VAPID_KEY)
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey,
})
} else {
return subscription
}
})
// 发送订阅请求
try {
await api.post('/message/webpush/subscribe', subscription)
} catch (e) {
console.log(e)
}
}
}
// 登录后处理
async function afterLogin(superuser: boolean) {
// 生效主题配置
await setTheme()
// 跳转到首页或回原始页面
router.push(store.state.auth.originalPath ?? '/')
// 订阅推送通知
if (superuser) await subscribeForPushNotifications()
}
// 登录获取token事件
@@ -134,7 +163,7 @@ function login() {
store.dispatch('auth/updateAvatar', avatar)
// 登录后处理
afterLogin()
afterLogin(superuser)
})
.catch((error: any) => {
// 登录失败,显示错误提示

View File

@@ -1,82 +1,77 @@
<script setup lang="ts">
import MediaCardSlideView from '@/views/discover/MediaCardSlideView.vue'
const viewList = reactive<{apipath: string, linkurl: string, title: string}[]>([
const viewList = reactive<{ apipath: string; linkurl: string; title: string }[]>([
{
apipath: 'tmdb/trending',
linkurl: "/browse/tmdb/trending?title=流行趋势",
title: "流行趋势",
linkurl: '/browse/tmdb/trending?title=流行趋势',
title: '流行趋势',
},
{
apipath: "douban/showing",
linkurl: "/browse/douban/showing?title=正在热映",
title: "正在热映"
apipath: 'douban/showing',
linkurl: '/browse/douban/showing?title=正在热映',
title: '正在热映',
},
{
apipath: "bangumi/calendar",
linkurl: "/browse/bangumi/calendar?title=Bangumi每日放送",
title: "Bangumi每日放送"
apipath: 'bangumi/calendar',
linkurl: '/browse/bangumi/calendar?title=Bangumi每日放送',
title: 'Bangumi每日放送',
},
{
apipath: "tmdb/movies",
linkurl: "/browse/tmdb/movies?title=热门电影",
title: "热门电影"
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=热门电视剧",
title: "热门电视剧"
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=热门电影",
title: "热门电影"
apipath: 'douban/movie_hot',
linkurl: '/browse/douban/movie_hot?title=豆瓣热门电影',
title: '豆瓣热门电影',
},
{
apipath: "douban/tv_hot",
linkurl: "/browse/douban/tv_hot?title=热门电视剧",
title: "热门电视剧"
apipath: 'douban/tv_hot',
linkurl: '/browse/douban/tv_hot?title=豆瓣热门电视剧',
title: '豆瓣热门电视剧',
},
{
apipath: "douban/tv_animation",
linkurl: "/browse/douban/tv_animation?title=热门动漫",
title: "热门动漫"
apipath: 'douban/tv_animation',
linkurl: '/browse/douban/tv_animation?title=豆瓣热门动漫',
title: '豆瓣热门动漫',
},
{
apipath: "douban/movies",
linkurl: "/browse/douban/movies?title=最新电影",
title: "最新电影"
apipath: 'douban/movies',
linkurl: '/browse/douban/movies?title=豆瓣最新电影',
title: '豆瓣最新电影',
},
{
apipath: "douban/tvs",
linkurl: "/browse/douban/tvs?title=最新电视剧",
title: "最新电视剧"
apipath: 'douban/tvs',
linkurl: '/browse/douban/tvs?title=豆瓣最新电视剧',
title: '豆瓣最新电视剧',
},
{
apipath: "douban/movie_top250",
linkurl: "/browse/douban/movie_top250?title=电影TOP250",
title: "电影TOP250"
apipath: 'douban/movie_top250',
linkurl: '/browse/douban/movie_top250?title=电影TOP250',
title: '豆瓣电影TOP250',
},
{
apipath: "douban/tv_weekly_chinese",
linkurl: "/browse/douban/tv_weekly_chinese?title=国产剧集榜",
title: "国产剧集榜"
apipath: 'douban/tv_weekly_chinese',
linkurl: '/browse/douban/tv_weekly_chinese?title=豆瓣国产剧集榜',
title: '豆瓣国产剧集榜',
},
{
apipath: "douban/tv_weekly_global",
linkurl: "/browse/douban/tv_weekly_global?title=全球剧集榜",
title: "全球剧集榜"
}
apipath: 'douban/tv_weekly_global',
linkurl: '/browse/douban/tv_weekly_global?title=豆瓣全球剧集榜',
title: '豆瓣全球剧集榜',
},
])
</script>
<template>
<div>
<MediaCardSlideView
v-for="item in viewList"
:key="item.apipath"
v-bind="item"
/>
<MediaCardSlideView v-for="(item, index) in viewList" :key="index" v-bind="item" />
</div>
</template>

View File

@@ -5,6 +5,13 @@ import type { Context } from '@/api/types'
import store from '@/store'
import TorrentCardListView from '@/views/discover/TorrentCardListView.vue'
import TorrentRowListView from '@/views/discover/TorrentRowListView.vue'
import { useDisplay } from 'vuetify'
// APP
const display = useDisplay()
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 路由参数
const route = useRoute()
@@ -142,13 +149,25 @@ onUnmounted(() => {
<!-- 视图切换 -->
<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('card')"
@click="setViewType('list')"
:class="{ 'mb-12': appMode }"
/>
<VFab v-else icon="mdi-view-list" location="bottom" size="x-large" fixed app appear @click="setViewType('list')" />
</template>

View File

@@ -4,12 +4,12 @@
* webfontloader documentation: https://github.com/typekit/webfontloader
*/
export async function loadFonts() {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */'webfontloader')
;(async function loadFonts() {
const webFontLoader = await import(/* webpackChunkName: "webfontloader" */ 'webfontloader')
webFontLoader.load({
google: {
families: ['Inter:100,200,300,400,500,600,700&display=swap'],
},
})
}
})()

View File

@@ -1,5 +1,5 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { configureNProgress, doneNProgress, startNProgress } from '@/api/nprogress'
import { configureNProgress } from '@/api/nprogress'
import store from '@/store'
// Nprogress
@@ -137,6 +137,13 @@ const router = createRouter({
requiresAuth: true,
},
},
{
path: '/apps',
component: () => import('../pages/appcenter.vue'),
meta: {
requiresAuth: true,
},
},
],
},
{
@@ -161,17 +168,11 @@ router.beforeEach((to, from, next) => {
// 总是记录非login路由
if (to.fullPath != '/login') store.state.auth.originalPath = to.fullPath
const isAuthenticated = store.state.auth.token !== null
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
startNProgress()
next()
}
})
router.afterEach(() => {
doneNProgress()
})
export default router

View File

@@ -9,7 +9,7 @@ export const SystemNavMenus = [
},
{
title: '推荐',
icon: 'mdi-table-star',
icon: 'mdi-star-outline',
to: '/ranking',
header: '发现',
admin: false,
@@ -23,20 +23,23 @@ export const SystemNavMenus = [
},
{
title: '电影',
icon: 'mdi-movie-roll',
full_title: '电影订阅',
icon: 'mdi-movie-open-outline',
to: '/subscribe-movie?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '电视剧',
icon: 'mdi-television-classic',
full_title: '电视剧订阅',
icon: 'mdi-television',
to: '/subscribe-tv?tab=mysub',
header: '订阅',
admin: false,
},
{
title: '日历',
full_title: '订阅日历',
icon: 'mdi-calendar',
to: '/calendar',
header: '订阅',

74
src/service-worker.ts Normal file
View File

@@ -0,0 +1,74 @@
import { createHandlerBoundToURL, cleanupOutdatedCaches, precacheAndRoute } from 'workbox-precaching'
import { NavigationRoute, registerRoute } from 'workbox-routing'
import { clientsClaim } from 'workbox-core'
declare let self: ServiceWorkerGlobalScope
cleanupOutdatedCaches()
// self.__WB_MANIFEST is default injection point
precacheAndRoute(self.__WB_MANIFEST)
// to allow work offline
registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html'), { denylist: [/^\/api/] }))
// 通知选项
const options = {
icon: '/logo.png',
vibrate: [100, 50, 100],
actions: [{ action: 'close', title: '关闭' }],
}
// 监听 push 事件,显示通知
self.addEventListener('push', function (event) {
console.log('notification push')
if (!event.data) {
return
}
// 解析获取推送消息
let payload
try {
payload = event.data?.json()
} catch (err) {
console.log(err)
payload = {
title: event.data?.text(),
}
}
// 根据推送消息生成桌面通知并展现出来
try {
const content = {
body: payload.body || '',
icon: payload.icon || options.icon,
vibrate: [100, 50, 100],
data: { url: payload.url },
actions: options.actions,
}
event.waitUntil(self.registration.showNotification(payload.title, content))
} catch (e) {
console.error(e)
}
})
// 安装
self.addEventListener('install', function (e) {
console.log('worker install')
self.skipWaiting()
})
// 激活
self.addEventListener('activate', function (e) {
console.log('worker activate')
e.waitUntil(self.clients.claim())
})
// 监听通知点击事件
self.addEventListener('notificationclick', function (event) {
console.log('notification click')
const info = event.notification
if (event.action === 'close') {
info.close()
} else if (info.data?.url) {
event.waitUntil(self.clients.openWindow(info.data?.url))
}
})

View File

@@ -24,14 +24,11 @@
}
.v-dialog > .v-overlay__content {
inline-size: calc(100% - 1rem);
margin-block-start: calc(env(safe-area-inset-top) + 1rem);
max-block-size: calc(100% - env(safe-area-inset-top) - 1rem);
}
.v-dialog > .v-overlay__content{
inline-size: calc(100% - 1rem);
}
.v-dialog--fullscreen > .v-overlay__content{
inline-size: 100%;
margin-block-start: env(safe-area-inset-top);
@@ -65,7 +62,6 @@
color: transparent;
--tw-gradient-from: #818cf8;
--tw-gradient-to: rgba(129,140,248,0%);
--tw-gradient-stops: var(--tw-gradient-from),var(--tw-gradient-to);
--tw-gradient-to: #c084fc;
}
@@ -188,3 +184,28 @@
.v-tabs:not(.v-tabs-pill).v-tabs--horizontal {
border-block-end: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
}
.v-fab__container {
padding-block-end: env(safe-area-inset-bottom);
}
.v-overlay__content .v-list{
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(6px);
backdrop-filter: blur(6px);
background-color: rgb(var(--v-theme-surface), 0.9) !important;
}
.v-overlay__content .v-card:not(.bg-primary){
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
background-color: rgb(var(--v-theme-surface), 0.95) !important;
.v-list, .v-table {
/* stylelint-disable-next-line property-no-vendor-prefix */
-webkit-backdrop-filter: none;
backdrop-filter: none;
background-color: transparent !important;
}
}

View File

@@ -2,14 +2,7 @@
import api from '@/api'
import type { MediaStatistic } from '@/api/types'
const statistics = ref([
{
title: '',
stats: '',
icon: '',
color: '',
},
])
const statistics = ref<{ [key: string]: string }[]>([])
// 调用API加载媒体统计数据
async function loadMediaStatistic() {

View File

@@ -425,7 +425,7 @@ onBeforeMount(() => {
<div v-if="mediaDetail.tmdb_id || mediaDetail.douban_id || mediaDetail.bangumi_id" class="max-w-8xl mx-auto px-4">
<template v-if="mediaDetail.backdrop_path || mediaDetail.poster_path">
<div class="vue-media-back absolute left-0 top-0 w-full h-96">
<VImg class="h-96" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
<VImg class="h-96" position="top" :src="mediaDetail.backdrop_path || mediaDetail.poster_path" cover />
</div>
<div class="vue-media-back absolute left-0 top-0 w-full h-96" />
</template>
@@ -861,6 +861,7 @@ onBeforeMount(() => {
/>
<!-- 订阅编辑弹窗 -->
<SubscribeEditDialog
v-if="subscribeEditDialog"
v-model="subscribeEditDialog"
:subid="subscribeId"
@close="subscribeEditDialog = false"

View File

@@ -2,7 +2,6 @@
import _ from 'lodash'
import type { Context } from '@/api/types'
import TorrentCard from '@/components/cards/TorrentCard.vue'
import { useDefer } from '@/@core/utils/dom'
interface SearchTorrent extends Context {
more?: Array<Context>
@@ -73,13 +72,13 @@ const sortSeasonFilterOptions = computed(() => {
return seasonFilterOptions.value.sort((a, b) => {
// 按季,集降序排序
const parseSeasonEpisode = (str: string) => {
const seasonRangeMatch = str.match(/S(\d+)(?:-S(\d+))?/);
const episodeRangeMatch = str.match(/E(\d+)(?:-E(\d+))?/);
const seasonRangeMatch = str.match(/S(\d+)(?:-S(\d+))?/)
const episodeRangeMatch = str.match(/E(\d+)(?:-E(\d+))?/)
return {
seasonStart : seasonRangeMatch?.[1] ? parseInt(seasonRangeMatch[1]) : 0,
seasonEnd : seasonRangeMatch?.[2] ? parseInt(seasonRangeMatch[2]) : 0,
episodeStart : episodeRangeMatch?.[1] ? parseInt(episodeRangeMatch[1]) : 0,
episodeEnd : episodeRangeMatch?.[2] ? parseInt(episodeRangeMatch[2]) : 0
seasonStart: seasonRangeMatch?.[1] ? parseInt(seasonRangeMatch[1]) : 0,
seasonEnd: seasonRangeMatch?.[2] ? parseInt(seasonRangeMatch[2]) : 0,
episodeStart: episodeRangeMatch?.[1] ? parseInt(episodeRangeMatch[1]) : 0,
episodeEnd: episodeRangeMatch?.[2] ? parseInt(episodeRangeMatch[2]) : 0,
}
}
const parsedA = parseSeasonEpisode(a)
@@ -126,23 +125,18 @@ onMounted(() => {
groupedDataList.value = groupMap
})
let defer = (_: number) => true
// 计算过滤后的列表
watchEffect(() => {
// 清空列表
dataList.value = []
// 匹配过滤函数filter中有任一值包含value则返回true
const match = (filter: Array<string>, value: string | undefined): boolean =>
filter.length === 0 || filter.includes(value ?? '') || filter.some(v => value?.includes(v) ?? false)
const match = (filter: Array<string>, value: string | undefined) =>
filter.length === 0 || (value && filter.includes(value))
groupedDataList.value?.forEach(value => {
if (value.length > 0) {
const matchData = value.filter(data => {
const {
meta_info,
torrent_info,
} = data
const { meta_info, torrent_info } = data
// 季、制作组、视频编码
return (
// 站点过滤
@@ -169,7 +163,6 @@ watchEffect(() => {
}
}
})
defer = useDefer(dataList.value.length)
})
</script>
@@ -257,7 +250,7 @@ watchEffect(() => {
</VCard>
<div class="grid gap-3 grid-torrent-card items-start">
<div v-for="(item, index) in dataList" :key="`${index}_${item.torrent_info.title}_${item.torrent_info.site}`">
<TorrentCard v-if="defer(index)" :torrent="item" :more="item.more" />
<TorrentCard :torrent="item" :more="item.more" />
</div>
</div>
</template>

View File

@@ -1,7 +1,15 @@
<script lang="ts" setup>
import type { Context } from '@/api/types'
import TorrentItem from '@/components/cards/TorrentItem.vue'
import { useDefer } from '@/@core/utils/dom'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 定义输入参数
const props = defineProps({
@@ -124,7 +132,14 @@ onMounted(() => {
</VListItem>
</VList>
<VList v-if="dataList.length !== 0" lines="three" class="rounded p-0 torrent-list-vscroll shadow-lg">
<VVirtualScroll :items="dataList">
<VVirtualScroll
:items="dataList"
:style="
appMode
? 'height: calc(100vh - 7.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 6.5rem - env(safe-area-inset-bottom)'
"
>
<template #default="{ item }">
<TorrentItem :torrent="item" :key="`${item.torrent_info.title}_${item.torrent_info.site}`" />
</template>
@@ -132,7 +147,11 @@ onMounted(() => {
</VList>
</VCol>
<VCol xl="2" md="3" class="d-none d-md-block">
<VList lines="one" class="rounded torrent-list-vscroll shadow-lg">
<VList
lines="one"
class="rounded shadow-lg"
style="block-size: calc(100vh - 6.5rem - env(safe-area-inset-bottom))"
>
<VListSubheader> 排序 </VListSubheader>
<VListItem>
<VChipGroup column v-model="sortField">
@@ -257,15 +276,3 @@ onMounted(() => {
</VCol>
</VRow>
</template>
<style lang="scss">
.torrent-list-vscroll {
block-size: calc(100vh - 6rem);
overflow-y: auto;
}
@media (width <= 768px) {
.orrent-list-vscroll {
block-size: calc(100vh - 10rem);
}
}
</style>

View File

@@ -8,7 +8,6 @@ import PluginCard from '@/components/cards/PluginCard.vue'
import noImage from '@images/logos/plugin.png'
import { useDisplay } from 'vuetify'
import { isNullOrEmptyObject } from '@/@core/utils'
import { useDefer } from '@/@core/utils/dom'
import router from '@/router'
import { PluginTabs } from '@/router/menu'
@@ -17,8 +16,10 @@ const route = useRoute()
// 显示器宽度
const display = useDisplay()
// 延迟加载
let deferApp = (_: number) => true
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 当前标签
const activeTab = ref(route.query.tab)
@@ -280,8 +281,6 @@ const sortedUninstalledList = computed(() => {
}
})
deferApp = useDefer(ret_list.length)
if (isNullOrEmptyObject(PluginStatistics.value)) return ret_list
// 数据排序
if (!activeSort.value || activeSort.value === 'count') {
@@ -418,12 +417,7 @@ onBeforeMount(async () => {
</div>
<div v-if="isAppMarketLoaded" class="grid gap-4 grid-plugin-card items-start">
<template v-for="(data, index) in sortedUninstalledList" :key="`${data.id}_v${data.plugin_version}`">
<PluginAppCard
v-if="deferApp(index)"
:plugin="data"
:count="PluginStatistics[data.id || '0']"
@install="pluginInstalled"
/>
<PluginAppCard :plugin="data" :count="PluginStatistics[data.id || '0']" @install="pluginInstalled" />
</template>
</div>
<NoDataFound
@@ -449,6 +443,7 @@ onBeforeMount(async () => {
app
appear
@click="SearchDialog = true"
:class="{ 'mb-12': appMode }"
/>
<VDialog
v-if="SearchDialog"

View File

@@ -7,6 +7,15 @@ import ReorganizeDialog from '@/components/dialog/ReorganizeDialog.vue'
import ProgressDialog from '@/components/dialog/ProgressDialog.vue'
import { useRoute } from 'vue-router'
import router from '@/router'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 提示框
const $toast = useToast()
@@ -202,7 +211,7 @@ async function removeSingle(deleteSrc: boolean, deleteDest: boolean) {
// 删除
await remove(currentHistory.value, deleteSrc, deleteDest)
// 刷新
reloadPage()
fetchData()
}
// 批量删除记录
@@ -231,7 +240,7 @@ async function removeBatch(deleteSrc: boolean, deleteDest: boolean) {
// 隐藏进度条
progressDialog.value = false
// 重新获取数据
reloadPage()
fetchData()
}
// 响应删除操作
@@ -283,7 +292,7 @@ function transferDone() {
currentHistory.value = undefined
selected.value = []
// 刷新
reloadPage()
fetchData()
}
// 弹出菜单
@@ -384,8 +393,12 @@ onMounted(fetchData)
fixed-header
show-select
loading-text="加载中..."
class="data-table-div"
hover
:style="
appMode
? 'height: calc(100vh - 15.5rem - env(safe-area-inset-bottom) - 3.5rem)'
: 'height: calc(100vh - 14.5rem - env(safe-area-inset-bottom)'
"
>
<template #item.title="{ item }">
<div class="d-flex align-center">
@@ -473,10 +486,11 @@ onMounted(fetchData)
app
appear
@click="removeHistoryBatch"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-if="selected.length > 0"
class="mb-16"
:class="appMode ? 'mb-28' : 'mb-16'"
icon="mdi-redo-variant"
location="bottom"
size="x-large"
@@ -521,14 +535,4 @@ onMounted(fetchData)
.v-table th {
white-space: nowrap;
}
.data-table-div {
block-size: calc(100vh - 14rem);
}
@media (width <= 768px) {
.data-table-div {
block-size: calc(100vh - 17rem);
}
}
</style>

View File

@@ -170,7 +170,7 @@ onMounted(() => {
rel="noreferrer"
class="text-indigo-500 transition duration-300 hover:underline"
>
https://github.com/jxxghp/MoviePilot/blob/main/README.md
https://wiki.movie-pilot.org
</a>
</span>
</dd>

View File

@@ -160,7 +160,7 @@ async function addUser() {
return
}
try {
const result: { [key: string]: any } = await api.post('user', userForm)
const result: { [key: string]: any } = await api.post('user/', userForm)
if (result.success) {
$toast.success('用户新增成功!')
loadAllUsers()

View File

@@ -56,6 +56,10 @@ const NotificationChannels = [
title: 'VoceChat',
value: 'vocechat',
},
{
title: 'WebPush',
value: 'webpush',
},
]
// 提示框
@@ -232,7 +236,7 @@ onMounted(() => {
<VCol cols="12" md="4">
<VTextField
v-model="notificationSettings.WECHAT_APP_SECRET"
label="应用Secret"
label="应用 Secret"
hint="企业微信自建应用的Secret"
persistent-hint
/>
@@ -430,6 +434,7 @@ onMounted(() => {
<th scope="col">Slack</th>
<th scope="col">SynologyChat</th>
<th scope="col">VoceChat</th>
<th scope="col">WebPush</th>
</tr>
</thead>
<tbody>
@@ -452,6 +457,9 @@ onMounted(() => {
<td>
<VCheckbox v-model="message.vocechat" />
</td>
<td>
<VCheckbox v-model="message.webpush" />
</td>
</tr>
<tr v-if="messagemTypes.length === 0">
<td colspan="6" class="text-center">没有设置任何通知渠道</td>

View File

@@ -16,6 +16,11 @@ const downloaderTab = ref('qbittorrent')
// 媒体服务器选中标签页
const mediaserverTab = ref('emby')
// 系统设置项
const SystemSettings = ref({
APP_DOMAIN: '',
})
// 下载器设置项
const downloaderSettings = ref({
DOWNLOADER_MONITOR: true,
@@ -208,6 +213,33 @@ async function saveMediaServerSetting() {
}
}
// 加载系统设置
async function loadSystemSettings() {
try {
const result: { [key: string]: any } = await api.get('system/env')
if (result.success) {
const { APP_DOMAIN } = result.data
SystemSettings.value = {
APP_DOMAIN,
}
}
} catch (error) {
console.log(error)
}
}
// 调用API保存系统设置
async function saveSystemSetting() {
try {
const result: { [key: string]: any } = await api.post('system/env', SystemSettings.value)
if (result.success) $toast.success('保存设置成功')
else $toast.error('保存设置失败!')
} catch (error) {
console.log(error)
}
}
// 调用API接口重新加载模块
async function reloadModule() {
try {
@@ -223,11 +255,41 @@ async function reloadModule() {
onMounted(() => {
loadDownloaderSetting()
loadMediaServerSetting()
loadSystemSettings()
})
</script>
<template>
<VRow>
<VCol cols="12">
<VCard>
<VCardItem>
<VCardTitle>系统</VCardTitle>
<VCardSubtitle>设置服务使用的域名等信息</VCardSubtitle>
</VCardItem>
<VCardText>
<VForm>
<VRow>
<VCol cols="12" md="6">
<VTextField
v-model="SystemSettings.APP_DOMAIN"
label="访问域名"
hint="用于通知跳转格式http(s)://domain:port"
persistent-hint
/>
</VCol>
</VRow>
</VForm>
</VCardText>
<VCardText>
<VForm @submit.prevent="() => {}">
<div class="d-flex flex-wrap gap-4 mt-4">
<VBtn mtype="submit" @click="saveSystemSetting"> 保存 </VBtn>
</div>
</VForm>
</VCardText>
</VCard>
</VCol>
<VCol cols="12">
<VCard>
<VCardItem>

View File

@@ -5,6 +5,15 @@ import type { Site } from '@/api/types'
import SiteCard from '@/components/cards/SiteCard.vue'
import NoDataFound from '@/components/NoDataFound.vue'
import SiteAddEditDialog from '@/components/dialog/SiteAddEditDialog.vue'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 数据列表
const dataList = ref<Site[]>([])
@@ -67,7 +76,16 @@ onBeforeMount(fetchData)
error-description="已添加并支持的站点将会在这里显示"
/>
<!-- 新增站点按钮 -->
<VFab icon="mdi-plus" location="bottom" size="x-large" fixed app appear @click="siteAddDialog = true" />
<VFab
icon="mdi-plus"
location="bottom"
size="x-large"
fixed
app
appear
@click="siteAddDialog = true"
:class="{ 'mb-12': appMode }"
/>
<!-- 新增站点弹窗 -->
<SiteAddEditDialog
v-if="siteAddDialog"

View File

@@ -7,6 +7,15 @@ import SubscribeCard from '@/components/cards/SubscribeCard.vue'
import SubscribeEditDialog from '@/components/dialog/SubscribeEditDialog.vue'
import SubscribeHistoryDialog from '@/components/dialog/SubscribeHistoryDialog.vue'
import store from '@/store'
import { useDisplay } from 'vuetify'
// 显示器宽度
const display = useDisplay()
// APP
const appMode = computed(() => {
return localStorage.getItem('MP_APPMODE') != '0' && display.mdAndDown.value
})
// 输入参数
const props = defineProps({
@@ -97,13 +106,14 @@ onMounted(async () => {
app
appear
@click="subscribeEditDialog = true"
:class="{ 'mb-12': appMode }"
/>
<VFab
v-if="store.state.auth.superUser"
icon="mdi-history"
color="info"
location="bottom"
class="mb-16"
:class="appMode ? 'mb-28' : 'mb-16'"
size="x-large"
fixed
app

View File

@@ -281,8 +281,8 @@ onMounted(() => {
</VCardItem>
<DialogCloseBtn inner-class="absolute right-3 top-5 text-high-emphasis" @click="emit('close')" />
<VDivider />
<VCardText>
<VList lines="one" v-if="searchWord">
<VCardText class="p-0">
<VList lines="two" v-if="searchWord">
<!-- 搜索结果 -->
<VListSubheader v-if="searchWord"> 媒体 & 资源 </VListSubheader>
<VHover>
@@ -294,7 +294,7 @@ onMounted(() => {
v-bind="hover.props"
@click="searchMedia('media')"
>
<VListItemTitle>
<VListItemTitle class="break-words whitespace-break-spaces">
搜索 <span class="font-bold">{{ searchWord }} </span> 相关的电影电视剧 ...
</VListItemTitle>
<template #append>
@@ -306,7 +306,7 @@ onMounted(() => {
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-account-search" link v-bind="hover.props" @click="searchMedia('person')">
<VListItemTitle>
<VListItemTitle class="break-words whitespace-break-spaces">
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的演职人员 ...
</VListItemTitle>
<template #append>
@@ -318,7 +318,7 @@ onMounted(() => {
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-search-web" link v-bind="hover.props" @click="searchTorrent">
<VListItemTitle>
<VListItemTitle class="break-words whitespace-break-spaces">
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的站点资源 ...
</VListItemTitle>
<template #append>
@@ -330,7 +330,7 @@ onMounted(() => {
<VHover>
<template #default="hover">
<VListItem prepend-icon="mdi-history" link v-bind="hover.props" @click="searchHistory">
<VListItemTitle>
<VListItemTitle class="break-words whitespace-break-spaces">
搜索 <span class="font-bold">{{ searchWord }}</span> 相关的历史记录 ...
</VListItemTitle>
<template #append>
@@ -412,7 +412,7 @@ onMounted(() => {
<VChip
v-for="(word, index) in recentSearches"
:key="index"
class="me-2"
class="me-2 mb-1"
variant="tonal"
@click="searchWord = word"
label

View File

@@ -46,7 +46,8 @@
"esnext",
"dom",
"dom.iterable",
"scripthost"
"scripthost",
"WebWorker"
],
"skipLibCheck": true,
"types": [
@@ -63,11 +64,13 @@
"src/**/*.vue",
"themeConfig.ts",
"auto-imports.d.ts",
"components.d.ts"
"components.d.ts",
"src/service-worker.ts",
"public/service.js"
],
"exclude": [
"dist",
"node_modules",
"src/@iconify/*"
]
}
}

View File

@@ -4,8 +4,8 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { defineConfig } from 'vite'
import { VitePWA } from 'vite-plugin-pwa'
import vuetify from 'vite-plugin-vuetify'
import { VitePWA } from 'vite-plugin-pwa'
// https://vitejs.dev/config/
export default defineConfig({
@@ -13,8 +13,6 @@ export default defineConfig({
plugins: [
vue(),
vueJsx(),
// https://github.com/vuetifyjs/vuetify-loader/tree/next/packages/vite-plugin
vuetify({
styles: {
configFile: 'src/styles/variables/_vuetify.scss',
@@ -29,12 +27,100 @@ export default defineConfig({
vueTemplate: true,
}),
VitePWA({
registerType: 'autoUpdate',
injectRegister: 'script',
manifest: false,
registerType: 'autoUpdate',
strategies: 'injectManifest',
srcDir: 'src',
filename: 'service-worker.ts',
workbox: {
navigateFallbackDenylist: [
/.*\/api\/v\d+\/system\/logging.*/,
globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg}'],
navigateFallbackDenylist: [/.*\/api\/v\d+\/system\/logging.*/],
},
injectManifest: {
rollupFormat: 'iife',
},
devOptions: {
enabled: true,
type: 'module',
},
manifest: {
'name': 'MoviePilot',
'short_name': 'MoviePilot',
'start_url': './',
'display': 'standalone',
'icons': [
{
'src': './android-chrome-192x192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'any',
},
{
'src': './android-chrome-192x192_maskable.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
},
{
'src': './android-chrome-512x512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'any',
},
{
'src': './android-chrome-512x512_maskable.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
},
],
'theme_color': '#28243D',
'background_color': '#28243D',
'shortcuts': [
{
'name': '推荐',
'url': './ranking',
'icons': [
{
'src': './sparkles-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '电影订阅',
'url': './subscribe-movie?tab=mysub',
'icons': [
{
'src': './clock-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '电视剧订阅',
'url': './subscribe-tv?tab=mysub',
'icons': [
{
'src': './clock-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
{
'name': '设置',
'url': './setting',
'icons': [
{
'src': './cog-icon-192x192.png',
'sizes': '192x192',
'type': 'image/png',
},
],
},
],
},
}),
@@ -63,8 +149,6 @@ export default defineConfig({
},
optimizeDeps: {
exclude: ['vuetify'],
entries: [
'./src/**/*.vue',
],
entries: ['./src/**/*.vue'],
},
})

1418
yarn.lock

File diff suppressed because it is too large Load Diff