Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ declare module 'vue' {
CoverList: typeof import('./src/components/list/CoverList.vue')['default']
DbCacheManager: typeof import('./src/components/settings/custom/DbCacheManager.vue')['default']
DefaultLyrics: typeof import('./src/components/player/Lyrics/DefaultLyrics.vue')['default']
DetailCover: typeof import('./src/components/collection/DetailCover.vue')['default']
DeviceSelector: typeof import('./src/components/settings/custom/DeviceSelector.vue')['default']
DialogClose: typeof import('reka-ui')['DialogClose']
DialogContent: typeof import('reka-ui')['DialogContent']
Expand Down Expand Up @@ -117,6 +118,7 @@ declare module 'vue' {
IconLucideHeart: typeof import('~icons/lucide/heart')['default']
IconLucideHeartOff: typeof import('~icons/lucide/heart-off')['default']
IconLucideHistory: typeof import('~icons/lucide/history')['default']
IconLucideHourglass: typeof import('~icons/lucide/hourglass')['default']
IconLucideInfinity: typeof import('~icons/lucide/infinity')['default']
IconLucideKey: typeof import('~icons/lucide/key')['default']
IconLucideKeyRound: typeof import('~icons/lucide/key-round')['default']
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"rimraf": "^6.1.3",
"sortablejs": "^1.15.7",
"tsx": "^4.23.7",
"typescript": "^5.9.3",
"typescript": "^6.0.3",
"unocss": "^66.7.5",
"unplugin-auto-import": "^21.1.0",
"unplugin-icons": "^23.0.1",
Expand Down
267 changes: 130 additions & 137 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<script setup lang="ts">
import { useSettingsStore } from "@/stores/settings";
const settings = useSettingsStore();
watchEffect(() => {
const v = useSettingsStore().appearance.fontFamily;
document.documentElement.toggleAttribute("data-reduced-motion", settings.reducedMotion);
const v = settings.appearance.fontFamily;
const root = document.documentElement.style;
if (v) root.setProperty("--user-font", `${v}, var(--app-font)`);
else root.removeProperty("--user-font");
Expand Down
72 changes: 72 additions & 0 deletions src/components/collection/DetailCover.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<script setup lang="ts">
import type { SImgProps } from "@/components/ui/SImg.vue";
import defaultFallback from "@/assets/images/song.jpg";
const props = withDefaults(defineProps<SImgProps>(), {
fallback: defaultFallback,
alt: "",
});
const loaded = ref(false);
/** 缩略图本身失效时退回内置占位图,避免播放器持续显示破图 */
const fallbackSrc = ref(props.fallback || defaultFallback);
watch(
() => props.fallback,
(value) => {
fallbackSrc.value = value || defaultFallback;
},
);
watch(
() => props.src,
() => {
loaded.value = false;
},
);
/**
* 新封面解码完成后移除缩略图,忽略已被下一地址替换的加载结果
* @param event - 原生图片加载事件
*/
const handleLoad = async (event: Event): Promise<void> => {
const image = event.target as HTMLImageElement;
try {
await image.decode();
} catch {
return;
}
if (image.getAttribute("src") === props.src) loaded.value = true;
};
/** 兜底图加载失败时改用内置占位图 */
const handleFallbackError = (): void => {
if (fallbackSrc.value !== defaultFallback) fallbackSrc.value = defaultFallback;
};
</script>

<template>
<div class="relative isolate overflow-hidden">
<img
v-if="!loaded"
:src="fallbackSrc"
:alt="alt"
class="absolute inset-0 size-full object-cover"
decoding="async"
@error="handleFallbackError"
/>
<img
v-if="src"
:key="src"
:src="src"
:alt="alt"
data-cover-image
class="absolute inset-0 size-full object-cover"
:style="{ opacity: loaded ? 1 : 0 }"
loading="eager"
decoding="async"
@load="handleLoad"
@error="loaded = false"
/>
</div>
</template>
31 changes: 28 additions & 3 deletions src/components/list/CoverCard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { CoverItem } from "@/types/artist";
import artistFallback from "@/assets/images/artist.jpg";
import { captureCover } from "@/services/coverTransition";
export interface CoverCardProps {
/** 卡片数据 */
Expand All @@ -18,7 +19,15 @@ const props = withDefaults(defineProps<CoverCardProps>(), {
rounded: "rounded-xl",
});
defineEmits<{ click: [] }>();
const emit = defineEmits<{ click: [] }>();
/** 稳定的封面标识:页面重新挂载后仍能匹配,不能用组件实例 ID */
const coverKey = computed(() => `${props.type}:${props.item.id}`);
/** 记录封面后分发点击,由实际目标路由决定是否启用共享过渡 */
const handleClick = (): void => {
captureCover(props.item, coverKey.value);
emit("click");
};
const coverRounded = computed(() => (props.type === "artist" ? "rounded-full" : props.rounded));
const actualFallback = computed(() => (props.type === "artist" ? artistFallback : props.fallback));
Expand All @@ -28,18 +37,21 @@ const actualFallback = computed(() => (props.type === "artist" ? artistFallback
<div
class="cursor-pointer group rounded-xl transition-colors duration-300"
:class="type !== 'artist' ? 'hover:bg-primary/10' : ''"
@click="$emit('click')"
@click="handleClick"
>
<!-- 封面 -->
<div class="relative overflow-hidden group-hover:will-change-transform" :class="coverRounded">
<SImg
:data-cover-key="coverKey"
:src="item.cover"
:fallback="actualFallback"
:alt="item.title"
class="w-full aspect-square transition-[transform,filter] duration-300 ease-out group-hover:scale-108 group-hover:brightness-80"
class="cover-card-image w-full aspect-square transition-[transform,filter] duration-300 ease-out group-hover:scale-108 group-hover:brightness-80"
:class="coverRounded"
/>
<!-- 播放按钮 -->
<div
data-cover-action
class="absolute size-9 flex items-center justify-center rounded-full opacity-0 transition-[opacity,transform] duration-300 group-hover:opacity-100"
:class="
type === 'artist'
Expand Down Expand Up @@ -72,3 +84,16 @@ const actualFallback = computed(() => (props.type === "artist" ? artistFallback
</div>
</div>
</template>

<style scoped>
[data-cover-shared] {
transform: none !important;
filter: none !important;
transition: none;
}
[data-cover-shared] + [data-cover-action] {
opacity: 0 !important;
transition: none;
}
</style>
5 changes: 3 additions & 2 deletions src/components/player/FullPlayer/PlayerCover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const media = useMediaStore();
const status = useStatusStore();
const { isPlaying } = storeToRefs(status);
/** 加载中的歌曲使用队列当前项作为封面兜底。 */
/** 加载中的歌曲使用队列当前项作为封面兜底 */
const displayTrack = computed(() => media.track ?? status.currentTrack);
/** 高清封面缓存 */
Expand All @@ -32,6 +32,7 @@ watchEffect(async () => {

<template>
<div
data-player-cover="full"
:class="
fullscreen
? 'player-cover-fullscreen w-full h-full aspect-auto rounded-none bg-transparent overflow-hidden shrink-0'
Expand All @@ -43,7 +44,7 @@ watchEffect(async () => {
]
"
>
<SImg :src="coverSrc" class="size-full" />
<DetailCover :src="coverSrc" :fallback="displayTrack?.cover" class="size-full" />
</div>
</template>

Expand Down
3 changes: 2 additions & 1 deletion src/components/player/FullPlayer/PlayerData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ResourceNavigationTarget,
} from "@/utils/navigate";
import { getValidArtists } from "@shared/utils/track";
import { setPlayerExpanded } from "@/services/playerTransition";
const { t } = useI18n();
Expand Down Expand Up @@ -87,7 +88,7 @@ const isAlbumLinkable = computed(() =>
* @param target - 详情页目标
*/
const goToResource = (target: ResourceNavigationTarget | null): void => {
if (target && navigateToResource(target)) status.isPlayerExpanded = false;
if (target && navigateToResource(target)) setPlayerExpanded(false);
};
/** 来源标签 */
Expand Down
Loading
Loading