|
|
@@ -1,5 +1,11 @@
|
|
|
const state = {
|
|
|
library: null,
|
|
|
+ libraries: {
|
|
|
+ local: null,
|
|
|
+ cloud: null,
|
|
|
+ },
|
|
|
+ activeSource: "local",
|
|
|
+ cloudLoadingStarted: false,
|
|
|
currentFolder: "",
|
|
|
currentQueue: [],
|
|
|
currentTrackIndex: -1,
|
|
|
@@ -26,12 +32,8 @@ const iconMarkup = {
|
|
|
};
|
|
|
|
|
|
function savePlaybackState() {
|
|
|
- const currentTrack = state.currentQueue[state.currentTrackIndex] || null;
|
|
|
const payload = {
|
|
|
currentFolder: state.currentFolder,
|
|
|
- currentQueue: state.currentQueue.map((track) => track.path),
|
|
|
- currentTrackPath: currentTrack ? currentTrack.path : "",
|
|
|
- currentTime: audio.currentTime || 0,
|
|
|
playMode: state.playMode,
|
|
|
activeAlbumPath: state.activeAlbumPath,
|
|
|
};
|
|
|
@@ -55,44 +57,12 @@ function restorePlaybackState() {
|
|
|
state.playMode = saved.playMode || (saved.shuffle ? "shuffle" : saved.loopMode === "one" ? "one" : saved.loopMode === "off" ? "once" : "list");
|
|
|
state.currentFolder = saved.currentFolder || "";
|
|
|
state.activeAlbumPath = saved.activeAlbumPath || "";
|
|
|
+ state.activeSource = saved.activeSource || (state.currentFolder.startsWith("cloud/") ? "cloud" : "local");
|
|
|
updateToggleStates();
|
|
|
+ syncLibraryTabs();
|
|
|
+ applyActiveLibrary();
|
|
|
setCurrentFolder(state.currentFolder);
|
|
|
|
|
|
- if (Array.isArray(saved.currentQueue) && saved.currentQueue.length) {
|
|
|
- state.currentQueue = saved.currentQueue
|
|
|
- .map((path) => state.library.all_tracks.find((track) => track.path === path))
|
|
|
- .filter(Boolean);
|
|
|
- }
|
|
|
-
|
|
|
- const trackPath = saved.currentTrackPath || "";
|
|
|
- if (trackPath) {
|
|
|
- const queueIndex = state.currentQueue.findIndex((track) => track.path === trackPath);
|
|
|
- if (queueIndex >= 0) {
|
|
|
- state.currentTrackIndex = queueIndex;
|
|
|
- } else {
|
|
|
- const single = state.library.all_tracks.find((track) => track.path === trackPath);
|
|
|
- if (single) {
|
|
|
- state.currentQueue = [single];
|
|
|
- state.currentTrackIndex = 0;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (state.currentQueue.length && state.currentTrackIndex >= 0) {
|
|
|
- const track = state.currentQueue[state.currentTrackIndex];
|
|
|
- audio.src = track.url;
|
|
|
- updateDock(track);
|
|
|
- renderQueue();
|
|
|
- syncPlayButton();
|
|
|
- audio.addEventListener(
|
|
|
- "loadedmetadata",
|
|
|
- () => {
|
|
|
- audio.currentTime = Number(saved.currentTime || 0);
|
|
|
- },
|
|
|
- { once: true }
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
if (state.activeAlbumPath) {
|
|
|
renderAlbumDetail(state.activeAlbumPath);
|
|
|
}
|
|
|
@@ -102,26 +72,71 @@ function restorePlaybackState() {
|
|
|
}
|
|
|
|
|
|
async function fetchLibrary() {
|
|
|
- const response = await fetch("/api/library");
|
|
|
- state.library = await response.json();
|
|
|
+ const response = await fetch("/api/library/local");
|
|
|
+ state.libraries.local = await response.json();
|
|
|
+ state.library = state.libraries.local;
|
|
|
+ applyActiveLibrary();
|
|
|
+ renderPlaylists();
|
|
|
+ restorePlaybackState();
|
|
|
+ if (!state.cloudLoadingStarted) {
|
|
|
+ state.cloudLoadingStarted = true;
|
|
|
+ queueMicrotask(() => fetchCloudLibrary());
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchCloudLibrary() {
|
|
|
+ try {
|
|
|
+ const response = await fetch("/api/library/cloud");
|
|
|
+ state.libraries.cloud = await response.json();
|
|
|
+ if (state.activeSource === "cloud") {
|
|
|
+ applyActiveLibrary();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("fetchCloudLibrary failed", error);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function applyActiveLibrary() {
|
|
|
+ const fallback = state.libraries.local;
|
|
|
+ state.library = state.libraries[state.activeSource] || fallback;
|
|
|
+ if (!state.library) return;
|
|
|
state.albums = buildAlbums(state.library.tree);
|
|
|
state.featuredAlbums = buildFeaturedAlbums(state.albums);
|
|
|
state.featuredIndex = 0;
|
|
|
-
|
|
|
document.getElementById("albumCount").textContent = state.albums.length;
|
|
|
document.getElementById("trackCount").textContent = state.library.all_tracks.length;
|
|
|
-
|
|
|
renderHome();
|
|
|
renderPlaylists();
|
|
|
- restorePlaybackState();
|
|
|
-
|
|
|
if (state.activeAlbumPath) {
|
|
|
renderAlbumDetail(state.activeAlbumPath);
|
|
|
+ } else {
|
|
|
+ showView("home");
|
|
|
}
|
|
|
-
|
|
|
startFeaturedCarousel();
|
|
|
}
|
|
|
|
|
|
+function syncLibraryTabs() {
|
|
|
+ document.querySelectorAll(".library-tab").forEach((button) => {
|
|
|
+ button.classList.toggle("is-active", button.dataset.source === state.activeSource);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function switchLibrary(source) {
|
|
|
+ if (source === state.activeSource) return;
|
|
|
+ state.activeSource = source;
|
|
|
+ state.activeAlbumPath = "";
|
|
|
+ state.detailTrackPage = 1;
|
|
|
+ state.currentFolder = source === "cloud" ? "cloud" : "";
|
|
|
+ syncLibraryTabs();
|
|
|
+ applyActiveLibrary();
|
|
|
+ setCurrentFolder(source === "cloud" ? "cloud" : "");
|
|
|
+ savePlaybackState();
|
|
|
+ if (source === "cloud" && !state.libraries.cloud && !state.cloudLoadingStarted) {
|
|
|
+ state.cloudLoadingStarted = true;
|
|
|
+ fetchCloudLibrary();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function buildAlbums(tree) {
|
|
|
const albums = [];
|
|
|
|
|
|
@@ -333,10 +348,8 @@ function renderAlbumDetail(path) {
|
|
|
|
|
|
document.getElementById("detailPlayBtn").onclick = () => startQueue(album.tracks, 0);
|
|
|
document.getElementById("detailPlayAllBtn").onclick = () => {
|
|
|
- state.currentQueue = [...album.tracks, ...state.currentQueue];
|
|
|
- state.currentTrackIndex = 0;
|
|
|
- renderQueue();
|
|
|
- playTrack(0);
|
|
|
+ appendToQueue(album.tracks);
|
|
|
+ showIconToast("已加入播放列表");
|
|
|
};
|
|
|
|
|
|
const container = document.getElementById("detailTrackList");
|
|
|
@@ -606,11 +619,25 @@ function startQueue(tracks, index) {
|
|
|
if (!tracks.length) return;
|
|
|
state.currentQueue = [...tracks];
|
|
|
state.currentTrackIndex = index;
|
|
|
+ state.activeSource = tracks[index]?.path?.startsWith("cloud/") ? "cloud" : "local";
|
|
|
+ syncLibraryTabs();
|
|
|
renderQueue();
|
|
|
playTrack(index);
|
|
|
savePlaybackState();
|
|
|
}
|
|
|
|
|
|
+function appendToQueue(tracks, options = {}) {
|
|
|
+ if (!tracks.length) return;
|
|
|
+ const { playNow = false, startIndex = 0 } = options;
|
|
|
+ const insertAt = state.currentQueue.length;
|
|
|
+ state.currentQueue.push(...tracks);
|
|
|
+ renderQueue();
|
|
|
+ savePlaybackState();
|
|
|
+ if (playNow) {
|
|
|
+ playTrack(insertAt + startIndex);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function updateDock(track) {
|
|
|
document.getElementById("nowTitle").textContent = track.name;
|
|
|
document.getElementById("nowMeta").textContent = track.path;
|
|
|
@@ -667,13 +694,7 @@ function syncPlayButton() {
|
|
|
}
|
|
|
|
|
|
async function getCachedAudioUrl(track) {
|
|
|
- if (track.path?.startsWith("cloud/")) {
|
|
|
- showIconToast("云端加载中");
|
|
|
- const response = await fetch(`/api/cloud-url/${track.path}`);
|
|
|
- if (!response.ok) throw new Error(`cloud url request failed: ${response.status}`);
|
|
|
- const data = await response.json();
|
|
|
- return data.url || track.url;
|
|
|
- }
|
|
|
+ if (track.path?.startsWith("cloud/")) return track.url;
|
|
|
if (!("caches" in window)) return track.url;
|
|
|
|
|
|
try {
|
|
|
@@ -701,6 +722,8 @@ async function playTrack(index) {
|
|
|
const requestId = ++playRequestId;
|
|
|
state.currentTrackIndex = index;
|
|
|
const track = state.currentQueue[index];
|
|
|
+ state.activeSource = track?.path?.startsWith("cloud/") ? "cloud" : "local";
|
|
|
+ syncLibraryTabs();
|
|
|
try {
|
|
|
const resolvedUrl = await getCachedAudioUrl(track);
|
|
|
if (requestId !== playRequestId) return;
|
|
|
@@ -764,6 +787,15 @@ function formatTime(seconds) {
|
|
|
}
|
|
|
|
|
|
audio.addEventListener("ended", nextTrack);
|
|
|
+audio.addEventListener("error", () => {
|
|
|
+ const currentTrack = state.currentQueue[state.currentTrackIndex];
|
|
|
+ console.error("audio error", currentTrack?.path, audio.error);
|
|
|
+ if (state.currentQueue.length > 1) {
|
|
|
+ nextTrack();
|
|
|
+ } else {
|
|
|
+ syncPlayButton();
|
|
|
+ }
|
|
|
+});
|
|
|
audio.addEventListener("play", syncPlayButton);
|
|
|
audio.addEventListener("pause", syncPlayButton);
|
|
|
audio.addEventListener("timeupdate", () => {
|
|
|
@@ -878,5 +910,9 @@ document.getElementById("createPlaylistBtn").onclick = async () => {
|
|
|
await fetchLibrary();
|
|
|
};
|
|
|
|
|
|
+document.querySelectorAll(".library-tab").forEach((button) => {
|
|
|
+ button.onclick = () => switchLibrary(button.dataset.source);
|
|
|
+});
|
|
|
+
|
|
|
updateToggleStates();
|
|
|
fetchLibrary();
|