mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-05 23:56:42 +08:00
refactor: optimize storage card accent colors and clean up unused directory UI logic
This commit is contained in:
@@ -4,25 +4,9 @@ import api from '@/api'
|
|||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { storageRemoteDict } from '@/api/constants'
|
import { storageRemoteDict } from '@/api/constants'
|
||||||
import { getCardAccentRgbFromImage } from '@/composables/useCardAccentColor'
|
|
||||||
import storage_png from '@images/misc/storage.png'
|
|
||||||
import alipan_png from '@images/misc/alipan.webp'
|
|
||||||
import u115_png from '@images/misc/u115.png'
|
|
||||||
import rclone_png from '@images/misc/rclone.png'
|
|
||||||
import alist_png from '@images/misc/openlist.svg'
|
|
||||||
import smb_png from '@images/misc/smb.png'
|
|
||||||
|
|
||||||
const DEFAULT_DIRECTORY_ACCENT_RGB = '145, 85, 253'
|
const DEFAULT_DIRECTORY_ACCENT_RGB = '145, 85, 253'
|
||||||
const STORAGE_ICON_MAP = {
|
const STORAGE_ACCENT_COLOR_MAP = {
|
||||||
local: storage_png,
|
|
||||||
alipan: alipan_png,
|
|
||||||
u115: u115_png,
|
|
||||||
rclone: rclone_png,
|
|
||||||
alist: alist_png,
|
|
||||||
smb: smb_png,
|
|
||||||
}
|
|
||||||
|
|
||||||
const STORAGE_FALLBACK_COLOR_MAP = {
|
|
||||||
local: '#FFB400',
|
local: '#FFB400',
|
||||||
alipan: '#00A7F2',
|
alipan: '#00A7F2',
|
||||||
u115: '#17B26A',
|
u115: '#17B26A',
|
||||||
@@ -35,7 +19,6 @@ const STORAGE_FALLBACK_COLOR_MAP = {
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const downloadAccentRgb = ref(DEFAULT_DIRECTORY_ACCENT_RGB)
|
const downloadAccentRgb = ref(DEFAULT_DIRECTORY_ACCENT_RGB)
|
||||||
const libraryAccentRgb = ref(DEFAULT_DIRECTORY_ACCENT_RGB)
|
const libraryAccentRgb = ref(DEFAULT_DIRECTORY_ACCENT_RGB)
|
||||||
let accentUpdateToken = 0
|
|
||||||
|
|
||||||
// 输入参数
|
// 输入参数
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -92,12 +75,8 @@ const transferSourceItems = computed(() => [
|
|||||||
{ title: t('directory.manualTransfer'), value: 'manual' },
|
{ title: t('directory.manualTransfer'), value: 'manual' },
|
||||||
])
|
])
|
||||||
|
|
||||||
function hasKnownStorageType(storageType?: string): storageType is keyof typeof STORAGE_ICON_MAP {
|
function hasKnownStorageType(storageType?: string): storageType is keyof typeof STORAGE_ACCENT_COLOR_MAP {
|
||||||
return !!storageType && Object.prototype.hasOwnProperty.call(STORAGE_ICON_MAP, storageType)
|
return !!storageType && Object.prototype.hasOwnProperty.call(STORAGE_ACCENT_COLOR_MAP, storageType)
|
||||||
}
|
|
||||||
|
|
||||||
function getStorageIcon(storageType?: string) {
|
|
||||||
return hasKnownStorageType(storageType) ? STORAGE_ICON_MAP[storageType] : storage_png
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hexToRgbString(hexColor: string) {
|
function hexToRgbString(hexColor: string) {
|
||||||
@@ -109,46 +88,18 @@ function hexToRgbString(hexColor: string) {
|
|||||||
return `${(colorValue >> 16) & 255}, ${(colorValue >> 8) & 255}, ${colorValue & 255}`
|
return `${(colorValue >> 16) & 255}, ${(colorValue >> 8) & 255}, ${colorValue & 255}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function rgbToHex(value: number) {
|
function getCustomStoragePaletteColor(storageType?: string) {
|
||||||
return Math.round(value).toString(16).padStart(2, '0')
|
const customStorageIndex = Math.max(Number(storageType?.match(/\d+$/)?.[0] ?? 1) - 1, 0)
|
||||||
|
const customStorageColors = ['#F97316', '#8B5CF6', '#06B6D4', '#84CC16', '#EC4899', '#14B8A6']
|
||||||
|
|
||||||
|
return customStorageColors[customStorageIndex % customStorageColors.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
function hslToHex(hue: number, saturation: number, lightness: number) {
|
function getStorageAccentColor(storageType?: string) {
|
||||||
const normalizedSaturation = saturation / 100
|
if (hasKnownStorageType(storageType)) return STORAGE_ACCENT_COLOR_MAP[storageType]
|
||||||
const normalizedLightness = lightness / 100
|
|
||||||
const chroma = (1 - Math.abs(2 * normalizedLightness - 1)) * normalizedSaturation
|
|
||||||
const secondComponent = chroma * (1 - Math.abs(((hue / 60) % 2) - 1))
|
|
||||||
const lightnessMatch = normalizedLightness - chroma / 2
|
|
||||||
let red = 0
|
|
||||||
let green = 0
|
|
||||||
let blue = 0
|
|
||||||
|
|
||||||
if (hue < 60) [red, green, blue] = [chroma, secondComponent, 0]
|
// 自定义存储没有固定品牌图标,使用离散调色板,保证连续 custom1/custom2 也能明显区分。
|
||||||
else if (hue < 120) [red, green, blue] = [secondComponent, chroma, 0]
|
return getCustomStoragePaletteColor(storageType)
|
||||||
else if (hue < 180) [red, green, blue] = [0, chroma, secondComponent]
|
|
||||||
else if (hue < 240) [red, green, blue] = [0, secondComponent, chroma]
|
|
||||||
else if (hue < 300) [red, green, blue] = [secondComponent, 0, chroma]
|
|
||||||
else [red, green, blue] = [chroma, 0, secondComponent]
|
|
||||||
|
|
||||||
return `#${rgbToHex((red + lightnessMatch) * 255)}${rgbToHex((green + lightnessMatch) * 255)}${rgbToHex((blue + lightnessMatch) * 255)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStableStorageColor(storageType?: string) {
|
|
||||||
const source = storageType || 'custom'
|
|
||||||
let hash = 0
|
|
||||||
|
|
||||||
for (let index = 0; index < source.length; index += 1) {
|
|
||||||
hash = Math.imul(31, hash) + source.charCodeAt(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hslToHex(Math.abs(hash) % 360, 66, 54)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStorageFallbackColor(storageType?: string) {
|
|
||||||
if (hasKnownStorageType(storageType)) return STORAGE_FALLBACK_COLOR_MAP[storageType]
|
|
||||||
|
|
||||||
// 自定义存储没有固定品牌图标,按类型生成稳定颜色,保证切换 custom1/custom2 时也有变化。
|
|
||||||
return getStableStorageColor(storageType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 目录卡片用下载存储和媒体库存储两端的图标主色生成轻渐变,体现整理链路的两个存储端点。
|
// 目录卡片用下载存储和媒体库存储两端的图标主色生成轻渐变,体现整理链路的两个存储端点。
|
||||||
@@ -157,50 +108,12 @@ const directoryAccentStyle = computed(() => ({
|
|||||||
'--app-card-accent-end-rgb': libraryAccentRgb.value,
|
'--app-card-accent-end-rgb': libraryAccentRgb.value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function loadStorageIconImage(storageType?: string) {
|
function updateDirectoryAccentColors() {
|
||||||
return new Promise<HTMLImageElement | null>(resolve => {
|
|
||||||
if (typeof Image === 'undefined') {
|
|
||||||
resolve(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const image = new Image()
|
|
||||||
|
|
||||||
image.onload = () => resolve(image)
|
|
||||||
image.onerror = () => resolve(null)
|
|
||||||
image.src = getStorageIcon(storageType)
|
|
||||||
|
|
||||||
if (image.complete) resolve(image)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getStorageAccentRgb(storageType?: string) {
|
|
||||||
const fallbackColor = getStorageFallbackColor(storageType)
|
|
||||||
|
|
||||||
if (!hasKnownStorageType(storageType)) return hexToRgbString(fallbackColor)
|
|
||||||
|
|
||||||
const image = await loadStorageIconImage(storageType)
|
|
||||||
|
|
||||||
return getCardAccentRgbFromImage(image, fallbackColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateDirectoryAccentColors() {
|
|
||||||
const currentToken = ++accentUpdateToken
|
|
||||||
const downloadStorage = props.directory.storage
|
const downloadStorage = props.directory.storage
|
||||||
const libraryStorage = props.directory.library_storage || props.directory.storage
|
const libraryStorage = props.directory.library_storage || props.directory.storage
|
||||||
|
|
||||||
downloadAccentRgb.value = hexToRgbString(getStorageFallbackColor(downloadStorage))
|
downloadAccentRgb.value = hexToRgbString(getStorageAccentColor(downloadStorage))
|
||||||
libraryAccentRgb.value = hexToRgbString(getStorageFallbackColor(libraryStorage))
|
libraryAccentRgb.value = hexToRgbString(getStorageAccentColor(libraryStorage))
|
||||||
|
|
||||||
const [downloadRgb, libraryRgb] = await Promise.all([
|
|
||||||
getStorageAccentRgb(downloadStorage),
|
|
||||||
getStorageAccentRgb(libraryStorage),
|
|
||||||
])
|
|
||||||
|
|
||||||
if (currentToken !== accentUpdateToken) return
|
|
||||||
|
|
||||||
downloadAccentRgb.value = downloadRgb
|
|
||||||
libraryAccentRgb.value = libraryRgb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监控模式下拉字典
|
// 监控模式下拉字典
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ html {
|
|||||||
--app-card-accent-rgb: var(--v-theme-primary);
|
--app-card-accent-rgb: var(--v-theme-primary);
|
||||||
--app-card-accent-end-rgb: var(--app-card-accent-rgb);
|
--app-card-accent-end-rgb: var(--app-card-accent-rgb);
|
||||||
--app-card-accent-start-opacity: 0.09;
|
--app-card-accent-start-opacity: 0.09;
|
||||||
--app-card-accent-end-opacity: 0.02;
|
--app-card-accent-end-opacity: 0.06;
|
||||||
--app-card-border-opacity: 0.2;
|
--app-card-border-opacity: 0.2;
|
||||||
--app-card-hover-border-opacity: 0.34;
|
--app-card-hover-border-opacity: 0.34;
|
||||||
--app-card-stripe-opacity: 0.78;
|
--app-card-stripe-opacity: 0.78;
|
||||||
@@ -115,7 +115,6 @@ html {
|
|||||||
.app-card-colorful:hover {
|
.app-card-colorful:hover {
|
||||||
border-color: rgba(var(--app-card-accent-rgb), var(--app-card-hover-border-opacity)) !important;
|
border-color: rgba(var(--app-card-accent-rgb), var(--app-card-hover-border-opacity)) !important;
|
||||||
box-shadow: var(--app-card-hover-shadow) !important;
|
box-shadow: var(--app-card-hover-shadow) !important;
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-card-colorful:focus-visible {
|
.app-card-colorful:focus-visible {
|
||||||
@@ -138,7 +137,7 @@ html[data-theme="transparent"] .app-card-colorful,
|
|||||||
backdrop-filter: blur(var(--transparent-blur, 10px));
|
backdrop-filter: blur(var(--transparent-blur, 10px));
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
--app-card-accent-start-opacity: 0.04;
|
--app-card-accent-start-opacity: 0.04;
|
||||||
--app-card-accent-end-opacity: 0.012;
|
--app-card-accent-end-opacity: 0.03;
|
||||||
--app-card-border-opacity: 0;
|
--app-card-border-opacity: 0;
|
||||||
--app-card-hover-border-opacity: 0;
|
--app-card-hover-border-opacity: 0;
|
||||||
--app-card-stripe-opacity: 0.42;
|
--app-card-stripe-opacity: 0.42;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ html[data-theme="transparent"] {
|
|||||||
linear-gradient(
|
linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(var(--app-card-accent-rgb), var(--app-card-accent-start-opacity, 0.04)),
|
rgba(var(--app-card-accent-rgb), var(--app-card-accent-start-opacity, 0.04)),
|
||||||
rgba(var(--app-card-accent-end-rgb, var(--app-card-accent-rgb)), var(--app-card-accent-end-opacity, 0.012)) 46%,
|
rgba(var(--app-card-accent-end-rgb, var(--app-card-accent-rgb)), var(--app-card-accent-end-opacity, 0.03)) 46%,
|
||||||
rgba(var(--v-theme-surface), 0) 76%
|
rgba(var(--v-theme-surface), 0) 76%
|
||||||
),
|
),
|
||||||
rgba(var(--v-theme-surface), var(--transparent-opacity-light)) !important;
|
rgba(var(--v-theme-surface), var(--transparent-opacity-light)) !important;
|
||||||
|
|||||||
Reference in New Issue
Block a user