diff --git a/package.json b/package.json index 9f726742..18097f41 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "moviepilot", - "version": "1.4.8", + "version": "1.5.1", "private": true, "bin": "dist/service.js", "scripts": { @@ -28,6 +28,7 @@ "axios": "1.4.0", "axios-mock-adapter": "^1.21.4", "chart.js": "^4.1.2", + "colorthief": "^2.4.0", "express": "^4.18.2", "express-http-proxy": "^2.0.0", "jwt-decode": "^3.1.2", diff --git a/src/@core/utils/image.ts b/src/@core/utils/image.ts new file mode 100644 index 00000000..c2c3eacc --- /dev/null +++ b/src/@core/utils/image.ts @@ -0,0 +1,23 @@ +import ColorThief from 'colorthief' + +// 将 RGB 转换为十六进制 +function rgbStringToHex(rgbArray: number[]): string { + if (rgbArray.length !== 3 || rgbArray.some(isNaN)) + throw new Error('Invalid RGB string format') + + const [r, g, b] = rgbArray + + const toHex = (c: number): string => { + const hex = c.toString(16) + return hex.length === 1 ? `0${hex}` : hex + } + + return `#${toHex(r)}${toHex(g)}${toHex(b)}` +} + +// 提取主要颜色 +export async function getDominantColor(image: HTMLImageElement): Promise { + const colorThief = new ColorThief() + const dominantColor = colorThief.getColor(image) + return rgbStringToHex(dominantColor) +} diff --git a/src/@core/utils/navigator.ts b/src/@core/utils/navigator.ts new file mode 100644 index 00000000..fd914a66 --- /dev/null +++ b/src/@core/utils/navigator.ts @@ -0,0 +1,30 @@ +// 请求和获取剪贴板内容 +export async function getClipboardContent() { + if (navigator.clipboard && window.isSecureContext) { + return await navigator.clipboard.readText() + } + else { + const input = document.createElement('textarea') + document.body.appendChild(input) + input.select() + document.execCommand('paste') + const content = input.value + document.body.removeChild(input) + return content + } +} + +// 将内容复制到剪贴板,兼容非安全域场景 +export async function copyToClipboard(content: string) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(content) + } + else { + const input = document.createElement('textarea') + input.value = content + document.body.appendChild(input) + input.select() + document.execCommand('copy') + document.body.removeChild(input) + } +} diff --git a/src/api/types.ts b/src/api/types.ts index eac29aa0..242f1ac6 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -540,9 +540,6 @@ export interface Plugin { // 插件图标 plugin_icon?: string - // 主题色 - plugin_color?: string - // 插件版本 plugin_version?: string diff --git a/src/assets/images/logos/plugin.png b/src/assets/images/logos/plugin.png new file mode 100644 index 00000000..af049e27 Binary files /dev/null and b/src/assets/images/logos/plugin.png differ diff --git a/src/components/cards/PluginAppCard.vue b/src/components/cards/PluginAppCard.vue index 5cb7974e..562d6f28 100644 --- a/src/components/cards/PluginAppCard.vue +++ b/src/components/cards/PluginAppCard.vue @@ -2,6 +2,8 @@ import { useToast } from 'vue-toast-notification' import api from '@/api' import type { Plugin } from '@/api/types' +import noImage from '@images/logos/plugin.png' +import { getDominantColor } from '@/@core/utils/image' // 输入参数 const props = defineProps({ @@ -13,6 +15,12 @@ const props = defineProps({ // 定义触发的自定义事件 const emit = defineEmits(['install']) +// 背景颜色 +const backgroundColor = ref('#28A9E1') + +// 图片对象 +const imageRef = ref() + // 提示框 const $toast = useToast() @@ -25,6 +33,17 @@ const progressText = ref('正在安装插件...') // 图片是否加载完成 const isImageLoaded = ref(false) +// 图片是否加载失败 +const imageLoadError = ref(false) + +// 图片加载完成 +async function imageLoaded() { + isImageLoaded.value = true + const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement + // 从图片中提取背景色 + backgroundColor.value = await getDominantColor(imageElement) +} + // 安装插件 async function installPlugin() { try { @@ -61,11 +80,54 @@ async function installPlugin() { } // 计算图标路径 -const iconPath = computed(() => { - return props.plugin?.plugin_icon?.startsWith('http') - ? props.plugin?.plugin_icon - : `/plugin_icon/${props.plugin?.plugin_icon}` +const iconPath: Ref = computed(() => { + if (imageLoadError.value) + return noImage + // 如果是网络图片则使用代理后返回 + if (props.plugin?.plugin_icon?.startsWith('http')) + return `${import.meta.env.VITE_API_BASE_URL}system/img/${encodeURIComponent(props.plugin?.plugin_icon)}` + + return `/plugin_icon/${props.plugin?.plugin_icon}` }) + +// 访问插件页面 +function visitPluginPage() { + // 将raw.githubusercontent.com转换为项目地址 + let repoUrl = props.plugin?.repo_url + if (repoUrl) { + if (repoUrl.includes('raw.githubusercontent.com')) { + if (!repoUrl.endsWith('/')) + repoUrl += '/' + + if (repoUrl.split('/').length < 6) + repoUrl = `${repoUrl}main/` + + try { + const [user, repo] = repoUrl.split('/').slice(-4, -2) + repoUrl = `https://github.com/${user}/${repo}` + } + catch (error) { + return + } + } + } + else { + repoUrl = props.plugin?.author_url + } + window.open(repoUrl, '_blank') +} + +// 弹出菜单 +const dropdownItems = ref([ + { + title: '查看详情', + value: 1, + props: { + prependIcon: 'mdi-information-outline', + click: visitPluginPage, + }, + }, +])