Skip to content

Commit 3dfbb45

Browse files
committed
fix: 修复随机播放全部时首曲固定并调整播放参数 (Fixed #337)
1 parent d30a5f1 commit 3dfbb45

14 files changed

Lines changed: 129 additions & 24 deletions

File tree

‎src/components/list/DownloadList.vue‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ const isPlaying = (task: DownloadTask): boolean =>
7777
/** 在可播放队列中定位并播放 */
7878
const playTask = (task: DownloadTask): void => {
7979
const index = playableTasks.value.findIndex((item) => item.taskId === task.taskId);
80-
if (index >= 0) void player.playFrom(playableTasks.value.map(toLocalTrack), index);
80+
if (index >= 0)
81+
void player.playFrom(playableTasks.value.map(toLocalTrack), { startIndex: index });
8182
};
8283
8384
/** 序号位点击:正在播放则切换,否则播放 */
@@ -114,7 +115,7 @@ const confirmDelete = async (task: DownloadTask): Promise<void> => {
114115
/** 播放全部已完成(供下载页顶栏调用) */
115116
const playAll = (): void => {
116117
if (playableTasks.value.length > 0) {
117-
void player.playFrom(playableTasks.value.map(toLocalTrack), 0);
118+
void player.playFrom(playableTasks.value.map(toLocalTrack));
118119
}
119120
};
120121

‎src/components/list/SongList.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ const onTrackDblClick = (item: Track, index: number): void => {
283283
void player.playNow(item, props.playbackContext);
284284
return;
285285
}
286-
void player.playFrom(sortedItems.value, index, props.playbackContext);
286+
void player.playFrom(sortedItems.value, { startIndex: index, context: props.playbackContext });
287287
};
288288
289289
const emit = defineEmits<{

‎src/composables/home/useDailyRecommend.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export const useDailyRecommend = () => {
117117
toast.warning(t("home.hero.empty"));
118118
return;
119119
}
120-
await player.playFrom(current.tracks, current.featuredIndex);
120+
await player.playFrom(current.tracks, { startIndex: current.featuredIndex });
121121
};
122122

123123
/** 添加到队列:整组插入当前曲目之后 */

‎src/core/player/index.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -761,24 +761,32 @@ export const switchDevice = async (deviceId: string | null): Promise<void> => {
761761
}
762762
};
763763

764+
/** 列表播放选项 */
765+
export interface PlayFromOptions {
766+
/** 省略时顺序模式从首曲开始、随机模式随机选曲 */
767+
startIndex?: number;
768+
/** 队列中曲目共用的播放来源上下文 */
769+
context?: PlaybackContext;
770+
}
771+
764772
/**
765-
* 设置队列并从指定位置开始播放
773+
* 设置队列并开始播放,未指定起点时遵循随机模式
766774
* @param items - 歌曲列表
767-
* @param startIndex - 起始播放位置,默认 0
768-
* @param context - 队列中曲目共用的播放来源上下文
775+
* @param options - 起始位置和播放来源选项
769776
*/
770777
export const playFrom = async (
771778
items: readonly Track[],
772-
startIndex = 0,
773-
context?: PlaybackContext,
779+
{ startIndex, context }: PlayFromOptions = {},
774780
): Promise<void> => {
775781
if (items.length === 0) return;
776782
const status = useStatusStore();
777783
const media = useMediaStore();
778784
// 退出特殊模式
779785
status.heartMode = false;
780786
status.fmMode = false;
781-
const idx = Math.max(0, Math.min(startIndex, items.length - 1));
787+
const start =
788+
startIndex ?? (status.shuffleMode === "on" ? Math.floor(Math.random() * items.length) : 0);
789+
const idx = Math.max(0, Math.min(start, items.length - 1));
782790
const isSameTrack = media.track?.id === items[idx]?.id;
783791
queue.setQueue(items, context);
784792
status.playIndex = idx;
@@ -1294,10 +1302,12 @@ export const playFile = async (filePath: string): Promise<void> => {
12941302
export const playFiles = async (filePaths: string[]): Promise<void> => {
12951303
if (filePaths.length === 0) return;
12961304
const tracks = filePaths.map(createLocalTrack);
1297-
await playFrom(tracks, 0, {
1298-
originId: "local-files",
1299-
originType: "track",
1300-
originName: "本地文件",
1305+
await playFrom(tracks, {
1306+
context: {
1307+
originId: "local-files",
1308+
originType: "track",
1309+
originName: "本地文件",
1310+
},
13011311
});
13021312
};
13031313

‎src/core/player/preload.spec.ts‎

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import type { Track } from "@shared/types/player";
2+
import type { PlaybackContext, Track } from "@shared/types/player";
33

44
const mocks = vi.hoisted(() => {
55
const track = { id: "next", source: "netease", title: "Next", artists: [], duration: 1000 };
@@ -575,3 +575,97 @@ describe("交叉过渡的队列交接", () => {
575575
expect(mocks.status.state).toBe("stopped");
576576
});
577577
});
578+
579+
describe("列表播放起点", () => {
580+
const tracks = ["a", "b", "c"].map((id) => ({ ...mocks.track, id }) as Track);
581+
const context: PlaybackContext = {
582+
originId: "playlist",
583+
originType: "playlist",
584+
originName: "测试歌单",
585+
};
586+
587+
beforeEach(async () => {
588+
vi.resetModules();
589+
vi.clearAllMocks();
590+
vi.doUnmock("@/stores/queue");
591+
vi.doUnmock("@/stores/status");
592+
vi.doMock("localforage", () => ({
593+
default: { createInstance: () => ({ setItem: vi.fn().mockResolvedValue(null) }) },
594+
}));
595+
const { createPinia, setActivePinia } = await import("pinia");
596+
setActivePinia(createPinia());
597+
mocks.media.track = { ...mocks.track, id: "old" };
598+
mocks.consume.mockReturnValue(null);
599+
mocks.resolve.mockResolvedValue({
600+
source: "https://music/selected",
601+
fromCache: false,
602+
provider: "official",
603+
});
604+
mocks.load.mockResolvedValue({
605+
success: true,
606+
data: { detail: {}, mediaInfo: { duration: 1000 } },
607+
});
608+
Object.assign(window, {
609+
api: {
610+
player: {
611+
load: mocks.load,
612+
stop: vi.fn().mockResolvedValue({ success: true }),
613+
syncPlayMode: vi.fn(),
614+
},
615+
},
616+
});
617+
});
618+
619+
it.each([
620+
[0, "a"],
621+
[0.5, "b"],
622+
[0.99, "c"],
623+
] as const)("随机播放全部可从列表任意位置开始,随机值=%s", async (random, selectedId) => {
624+
const { useStatusStore } = await import("@/stores/status");
625+
const queue = await import("@/stores/queue");
626+
const { playFrom, nextTrack, setShuffleMode } = await import("./index");
627+
const status = useStatusStore();
628+
status.shuffleMode = "on";
629+
vi.spyOn(Math, "random").mockReturnValue(random);
630+
631+
await playFrom(tracks, { context });
632+
633+
expect(mocks.media.track.id).toBe(selectedId);
634+
expect(status.currentPlaybackContext).toEqual(context);
635+
expect(queue.queue.value[0].id).toBe(selectedId);
636+
expect(queue.queue.value.map(({ id }) => id).sort()).toEqual(["a", "b", "c"]);
637+
await nextTrack();
638+
expect(mocks.media.track.id).not.toBe(selectedId);
639+
setShuffleMode("off");
640+
expect(queue.queue.value.map(({ id }) => id)).toEqual(["a", "b", "c"]);
641+
expect(status.currentTrack?.id).toBe(mocks.media.track.id);
642+
expect(status.currentPlaybackContext).toEqual(context);
643+
});
644+
645+
it.each([0, 2])("随机模式手动选歌保留指定起点,索引=%s", async (index) => {
646+
const { useStatusStore } = await import("@/stores/status");
647+
const { playFrom } = await import("./index");
648+
const status = useStatusStore();
649+
status.shuffleMode = "on";
650+
vi.spyOn(Math, "random").mockReturnValue(0.5);
651+
652+
await playFrom(tracks, { startIndex: index, context });
653+
654+
expect(mocks.media.track.id).toBe(tracks[index].id);
655+
expect(status.currentTrack?.id).toBe(tracks[index].id);
656+
expect(status.currentPlaybackContext).toEqual(context);
657+
});
658+
659+
it("顺序播放全部从第一首开始,开启随机后继续当前歌曲", async () => {
660+
const { useStatusStore } = await import("@/stores/status");
661+
const { playFrom, setShuffleMode } = await import("./index");
662+
vi.spyOn(Math, "random").mockReturnValue(0.99);
663+
664+
await playFrom(tracks);
665+
expect(mocks.media.track.id).toBe("a");
666+
expect(useStatusStore().playIndex).toBe(0);
667+
setShuffleMode("on");
668+
expect(useStatusStore().currentTrack?.id).toBe("a");
669+
expect(mocks.load).toHaveBeenCalledOnce();
670+
});
671+
});

‎src/pages/Artist.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const playbackContext = computed<PlaybackContext>(() => ({
149149
150150
const handlePlayAll = () => {
151151
if (!artist.value?.tracks.length) return;
152-
player.playFrom(artist.value.tracks, 0, playbackContext.value);
152+
player.playFrom(artist.value.tracks, { context: playbackContext.value });
153153
};
154154
155155
/** 收藏歌手仅支持网易云 */

‎src/pages/Cloud.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const trackCount = computed(() => user.cloudCount || user.cloudTracks.length);
4343
4444
const handlePlayAll = (): void => {
4545
if (user.cloudTracks.length === 0) return;
46-
player.playFrom(user.cloudTracks, 0, playbackContext.value);
46+
player.playFrom(user.cloudTracks, { context: playbackContext.value });
4747
};
4848
4949
const songListRef = shallowRef<InstanceType<typeof SongList> | null>(null);

‎src/pages/Collection.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ const playbackContext = computed<PlaybackContext | undefined>(() => {
145145
146146
const handlePlayAll = () => {
147147
if (!collection.value?.tracks.length) return;
148-
player.playFrom(collection.value.tracks, 0, playbackContext.value);
148+
player.playFrom(collection.value.tracks, { context: playbackContext.value });
149149
};
150150
151151
const searchQuery = ref("");

‎src/pages/Daily.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const loading = ref(data.dailyRecommend.length === 0);
6666
/** 播放选中天的全部曲目 */
6767
const handlePlayAll = (): void => {
6868
const tracks = selectedDay.value?.tracks ?? [];
69-
if (tracks.length > 0) player.playFrom(tracks, 0);
69+
if (tracks.length > 0) player.playFrom(tracks);
7070
};
7171
7272
/** 按当前语言格式化日期的某个部分 */

‎src/pages/Folders.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const selectedTracks = computed<Track[]>(() => {
9292
9393
const handlePlayAll = (): void => {
9494
if (selectedTracks.value.length === 0) return;
95-
player.playFrom(selectedTracks.value, 0);
95+
player.playFrom(selectedTracks.value);
9696
};
9797
9898
/** 文件夹创建为本地歌单 */

0 commit comments

Comments
 (0)