import { ToastType, uiFeedbackService } from '../UiFeedbackService.ts';
import { CoverThemeExtractor } from './CoverTheme.ts';
import { SeekReactiveIndicator } from './SeekReactiveIndicator.ts';
import { pushPageHistory } from '../pageHistory.ts';

/**
 * 音乐轨道接口
 */
interface MusicTrack {
    id: string;
    name: string;
    singer: string;
    cover: string;
    musicSrc: string;
    lyric: string;
    durationSeconds: number;
}

/**
 * 后端完整音乐模型(MusicResponse)
 */
interface RawMusicResponse {
    id: string;
    title: string;
    artist: string;
    coverUrl: string;
    musicUrl: string;
    lyricContent: string;
    duration: string | number;
}

/**
 * 歌词行接口
 */
interface LyricLine {
    time: number;
    text: string;
}

/**
 * 播放器状态接口
 */
interface PlayerState {
    mode: PlayMode;
    trackId: string | null;
    time: number;
    showLyrics: boolean;
    lyricsOnBackground: boolean;
}

/**
 * 播放模式类型
 */
type PlayMode = 'order' | 'random' | 'single';

/**
 * 跳转方向类型
 */
type SkipDirection = 'prev' | 'next';

/**
 * DOM 元素缓存接口
 */
interface MusicPlayerElements {
    mini: HTMLElement;
    miniCover: HTMLElement;
    miniStatus: HTMLElement;
    miniProgress: SVGCircleElement;
    panel: HTMLElement;
    panelContent: HTMLElement | null;
    backdrop: HTMLElement;
    close: HTMLElement;
    title: HTMLElement;
    artist: HTMLElement;
    cover: HTMLElement;
    play: HTMLElement;
    prev: HTMLElement;
    next: HTMLElement;
    seek: HTMLInputElement;
    seekWrapper: HTMLElement;
    seekProgress: HTMLElement;
    seekBuffered: HTMLElement;
    timeCurrent: HTMLElement;
    timeTotal: HTMLElement;
    mode: HTMLElement;
    lrcToggle: HTMLElement;
    listToggle: HTMLElement;
    list: HTMLElement;
    listItems: HTMLElement;
    listCloseMobile: HTMLElement | null;
    listContent: HTMLElement;
    listEmpty: HTMLElement;
    searchInput: HTMLInputElement | null;
    searchClear: HTMLElement | null;
    scrollToCurrent: HTMLElement | null;
    lyricsContainer: HTMLElement;
    lyricsInner: HTMLElement;
    bgLyrics: HTMLElement;
    bgLyricsInner: HTMLElement;
    loading: HTMLElement;
}

/**
 * 扩展 Window 接口以支持单例模式
 */
declare global {
    interface Window {
        __music_player_instance?: MusicPlayer;
    }
}

/**
 * 音乐播放器组件
 * 功能包括:音乐播放控制、歌词显示、播放列表管理、状态持久化等
 */
export class MusicPlayer {
    /**
     * 音频播放器实例
     */
    private readonly audio!: HTMLAudioElement;

    /**
     * 播放列表
     */
    private playlist: MusicTrack[] = [];

    /**
     * 当前播放歌曲的索引
     */
    private currentIndex: number = 0;

    /**
     * 是否正在播放
     */
    private isPlaying: boolean = false;

    /**
     * 播放模式(顺序、随机、单曲循环)
     */
    private playMode: PlayMode = 'order';

    /**
     * 已解析的歌词行数组
     */
    private lyrics: LyricLine[] = [];

    /**
     * 是否在面板内显示歌词
     */
    private showLyrics: boolean = false;

    /**
     * 是否在背景显示歌词
     */
    private lyricsOnBackground: boolean = false;

    /**
     * 是否显示播放列表
     */
    private showList!: boolean;

    /**
     * 是否正在拖动进度条
     */
    private isSeeking: boolean = false;

    /**
     * 当前高亮的歌词行索引
     */
    private currentLyricIndex: number = -1;

    /**
     * 设备是否支持悬停(非触摸设备)
     */
    private readonly _supportsHover!: boolean;

    /**
     * 移动端布局断点
     */
    private readonly _mobileBreakpoint: number = 768;

    /**
     * 当前是否为桌面端布局,用于避免键盘触发 resize 时误切换列表状态
     */
    private _isDesktopLayout!: boolean;

    /**
     * 是否正在拖动面板(移动端手势)
     */
    private _isPanelDragging: boolean = false;

    /**
     * 面板拖动开始时的 Y 坐标
     */
    private _panelDragStartY: number = 0;

    /**
     * 是否正在显示加载动画
     */
    private _isLoading: boolean = false;

    /**
     * 音频加载失败的重试次数
     */
    private _retryCount: number = 0;

    /**
     * 最大重试次数
     */
    private readonly _maxRetries: number = 3;

    /**
     * 重试超时时间(毫秒)
     */
    private readonly _retryTimeout: number = 10000;

    /**
     * 重试计时器
     */
    private _retryTimer: number | null = null;

    /**
     * 是否正在处理 hash 变化(防止循环触发)
     */
    private _isHandlingHashChange: boolean = false;

    /**
     * 上次自动保存状态的时间戳
     */
    private _lastAutoSaveTime: number = 0;

    /**
     * 当前音频文件是否已加载
     */
    private _isAudioLoaded: boolean = false;

    /**
     * 已预加载的歌曲索引
     */
    private _preloadedIndex: number = -1;

    /**
     * 预加载的音频对象
     */
    private _preloadAudio: HTMLAudioElement | null = null;

    /**
     * 随机模式下预先确定的下一首歌曲索引
     */
    private _nextRandomIndex: number = -1;

    /**
     * 当前显示的封面 URL(用于避免重复设置相同封面)
     */
    private _currentCoverUrl: string = '';

    /**
     * 是否发生过用户主动交互(用于满足浏览器自动播放策略)
     */
    private _hasUserInteracted: boolean = false;

    /**
     * 待恢复的播放时间(秒)
     */
    private _pendingRestoreTime: number = 0;

    /**
     * 待恢复播放时间对应的轨道 ID
     */
    private _pendingRestoreTrackId: string | null = null;

    /**
     * 迷你播放器圆形进度条的周长
     */
    private readonly circumference: number = 213.6;

    /**
     * 进度指示器的频谱呼吸效果控制器
     */
    private readonly _seekReactiveIndicator!: SeekReactiveIndicator;

    /**
     * 封面主题色提取器
     */
    private readonly _coverTheme!: CoverThemeExtractor;

    /**
     * 主题色提取任务令牌,防止快速切歌时旧封面结果覆盖新封面
     */
    private _coverThemeToken: number = 0;

    /**
     * DOM 元素缓存
     */
    private el!: MusicPlayerElements;

    /**
     * 构造函数,实现单例模式
     * 确保整个应用只有一个播放器实例
     */
    constructor() {
        if (window.__music_player_instance) {
            return window.__music_player_instance;
        }
        window.__music_player_instance = this;

        this.audio = new Audio();
        this.audio.crossOrigin = 'anonymous';
        this._seekReactiveIndicator = new SeekReactiveIndicator(this.audio);
        this._coverTheme = new CoverThemeExtractor();
        this._isDesktopLayout = window.innerWidth > this._mobileBreakpoint;
        this.showList = this._isDesktopLayout;
        this._supportsHover = window.matchMedia('(hover: hover) and (pointer: fine)').matches;

        this.init().then();
    }

    /**
     * 初始化播放器
     * 包括元素缓存、事件绑定、数据加载和 URL hash 监听
     */
    private async init(): Promise<void> {
        this.initElements();
        this.bindEvents();
        await this.fetchMusic();

        window.addEventListener('hashchange', () => {
            if (this._isHandlingHashChange) {
                return;
            }
            const isOpen = window.location.hash === '#music-player';
            const panelIsOpen = this.el.panel.classList.contains('music-player__panel--open');
            if (isOpen && !panelIsOpen) {
                this._isHandlingHashChange = true;
                this.togglePanel(true);
                this._isHandlingHashChange = false;
            } else if (!isOpen && panelIsOpen) {
                this._isHandlingHashChange = true;
                this.togglePanel(false);
                this._isHandlingHashChange = false;
            }
        });

        if (window.location.hash === '#music-player') {
            this.togglePanel(true);
        }
    }

    /**
     * 初始化并缓存所有 DOM 元素
     * 减少重复的 DOM 查询操作,提高性能
     */
    private initElements(): void {
        this.el = {
            mini: document.getElementById('mp-mini')!,
            miniCover: document.getElementById('mp-mini-cover')!,
            miniStatus: document.getElementById('mp-mini-status')!,
            miniProgress: document.getElementById(
                'mp-mini-progress'
            )! as unknown as SVGCircleElement,
            panel: document.getElementById('mp-panel')!,
            panelContent: this.el?.panel
                ? this.el.panel.querySelector('.music-player__content')
                : null,
            backdrop: document.getElementById('mp-backdrop')!,
            close: document.getElementById('mp-close')!,
            title: document.getElementById('mp-title')!,
            artist: document.getElementById('mp-artist')!,
            cover: document.getElementById('mp-cover')!,
            play: document.getElementById('mp-play')!,
            prev: document.getElementById('mp-prev')!,
            next: document.getElementById('mp-next')!,
            seek: document.getElementById('mp-seek')! as HTMLInputElement,
            seekWrapper: document.getElementById('mp-seek-wrapper')!,
            seekProgress: document.getElementById('mp-seek-progress')!,
            seekBuffered: document.getElementById('mp-seek-buffered')!,
            timeCurrent: document.getElementById('mp-time-current')!,
            timeTotal: document.getElementById('mp-time-total')!,
            mode: document.getElementById('mp-mode')!,
            lrcToggle: document.getElementById('mp-lrc-toggle')!,
            listToggle: document.getElementById('mp-list-toggle')!,
            list: document.getElementById('mp-list')!,
            listItems: document.getElementById('mp-list-items')!,
            listCloseMobile: document.getElementById('mp-list-close-mobile'),
            listContent: document.getElementById('mp-list-content')!,
            listEmpty: document.getElementById('mp-list-empty')!,
            searchInput: document.getElementById('mp-search') as HTMLInputElement | null,
            searchClear: document.getElementById('mp-search-clear'),
            scrollToCurrent: document.getElementById('mp-scroll-to-current'),
            lyricsContainer: document.getElementById('mp-lyrics-container')!,
            lyricsInner: document.getElementById('mp-lyrics-inner')!,
            bgLyrics: document.getElementById('mp-bg-lyrics')!,
            bgLyricsInner: document.getElementById('mp-bg-lyrics-inner')!,
            loading: document.getElementById('mp-loading')!,
        };

        const panel = document.getElementById('mp-panel');
        if (panel) {
            this.el.panelContent = panel.querySelector('.music-player__content');
        }

        this.el.mini.setAttribute('aria-label', '音乐播放器,点击播放');
        this.el.play.setAttribute('aria-label', '播放');
        this.el.prev.setAttribute('aria-label', '上一首');
        this.el.next.setAttribute('aria-label', '下一首');

        if (this.showList) {
            this.toggleList(true);
        }

        this._seekReactiveIndicator.attach(this.el.seekWrapper);
    }

    /**
     * 绑定所有事件监听器
     * 包括:播放控制、进度条、键盘快捷键、触摸手势、音频事件等
     */
    private bindEvents(): void {
        let clickTimer: number | null = null;
        let doubleClickHandled = false;
        let lastClickTime = 0;

        this.el.mini.addEventListener('click', () => {
            this._hasUserInteracted = true;
            const now = Date.now();

            if (!this._supportsHover) {
                if (now - lastClickTime < 300) {
                    lastClickTime = 0;
                    return;
                }
                lastClickTime = now;
                this.togglePlay();
                return;
            }

            if (clickTimer) {
                clearTimeout(clickTimer);
            }

            if (doubleClickHandled) {
                doubleClickHandled = false;
                return;
            }

            clickTimer = window.setTimeout(() => {
                if (!doubleClickHandled) {
                    this.togglePlay();
                }
                clickTimer = null;
            }, 250);
        });

        this.el.mini.addEventListener('dblclick', (e) => {
            this._hasUserInteracted = true;
            if (!this._supportsHover) {
                return;
            }

            e.preventDefault();
            doubleClickHandled = true;

            if (clickTimer) {
                clearTimeout(clickTimer);
                clickTimer = null;
            }

            this.togglePanel(true);

            setTimeout(() => {
                doubleClickHandled = false;
            }, 500);
        });

        let touchTimer: number | null = null;

        const startTouch = () => {
            touchTimer = window.setTimeout(() => {
                this.togglePanel(true);
                touchTimer = null;
                lastClickTime = 0;
            }, 600);
        };

        const endTouch = () => {
            if (touchTimer) {
                clearTimeout(touchTimer);
                touchTimer = null;
            }
        };

        this.el.mini.addEventListener('touchstart', startTouch, { passive: true });
        this.el.mini.addEventListener('touchend', endTouch);
        this.el.mini.addEventListener('touchmove', endTouch);

        this.el.close.addEventListener('click', () => this.togglePanel(false));
        this.el.backdrop.addEventListener('click', () => this.togglePanel(false));

        this.el.play.addEventListener('click', () => this.togglePlay());
        this.el.prev.addEventListener('click', () => {
            this._hasUserInteracted = true;
            this.skip('prev');
        });
        this.el.next.addEventListener('click', () => {
            this._hasUserInteracted = true;
            this.skip('next');
        });

        this.el.seek.addEventListener('input', (e) => {
            const target = e.target as HTMLInputElement;
            const pct = Number(target.value) / 100;
            this.el.timeCurrent.textContent = this.formatTime(this.audio.duration * pct);
            this.el.seekProgress.style.width = `${pct * 100}%`;
            this.isSeeking = true;
        });

        this.el.seek.addEventListener('change', (e) => {
            const target = e.target as HTMLInputElement;
            const pct = Number(target.value) / 100;
            if (this.audio.duration) {
                this.audio.currentTime = this.audio.duration * pct;
            }
            this.isSeeking = false;
        });

        // 监听缓冲进度
        this.audio.addEventListener('progress', () => this.updateBufferedProgress());

        this.el.mode.addEventListener('click', () => this.cycleMode());

        this.el.lrcToggle.addEventListener('click', () => {
            if (!this.showLyrics && !this.lyricsOnBackground) {
                this.showLyrics = true;
                this.lyricsOnBackground = false;
            } else if (this.showLyrics && !this.lyricsOnBackground) {
                this.showLyrics = false;
                this.lyricsOnBackground = true;
            } else {
                this.showLyrics = false;
                this.lyricsOnBackground = false;
            }

            this.updateLyricsDisplay();
            this.saveState();
        });

        this.el.listToggle.addEventListener('click', () => {
            this.toggleList(!this.showList);
        });

        if (this.el.listCloseMobile) {
            this.el.listCloseMobile.addEventListener('click', () => {
                this.toggleList(false);
            });
        }

        if (this.el.searchInput) {
            this.el.searchInput.addEventListener('input', (e) => {
                const target = e.target as HTMLInputElement;
                const value = target.value.trim();
                this.renderList(value);

                if (this.el.searchClear) {
                    this.el.searchClear.style.display = value ? 'flex' : 'none';
                }
            });

            this.el.searchInput.addEventListener('focus', () => this.syncViewportHeight());
        }

        if (this.el.searchClear) {
            const searchClear = this.el.searchClear;
            searchClear.addEventListener('click', () => {
                if (this.el.searchInput) {
                    this.el.searchInput.value = '';
                    this.el.searchInput.focus();
                }
                searchClear.style.display = 'none';
                this.renderList('');
            });
        }

        if (this.el.scrollToCurrent) {
            this.el.scrollToCurrent.addEventListener('click', () => {
                this.scrollToCurrentTrack();
            });
        }

        this.bindPanelGestures();
        this.bindKeyboardShortcuts();

        this.syncViewportHeight();
        window.addEventListener('resize', () => this.handleWindowResize());

        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', () => this.syncViewportHeight());
            window.visualViewport.addEventListener('scroll', () => this.syncViewportHeight());
        }

        this.audio.addEventListener('timeupdate', () => this.onTimeUpdate());
        this.audio.addEventListener('ended', () => this.onEnded());
        this.audio.addEventListener('durationchange', () => {
            this.el.timeTotal.textContent = this.formatTime(this.audio.duration);
        });
        this.audio.addEventListener('play', () => this.updatePlayState(true));
        this.audio.addEventListener('pause', () => this.updatePlayState(false));

        this.audio.addEventListener('loadstart', () => this.showLoading());
        this.audio.addEventListener('canplay', () => this.hideLoading());
        this.audio.addEventListener('waiting', () => this.showLoading());
        this.audio.addEventListener('playing', () => this.hideLoading());
        this.audio.addEventListener('stalled', () => this.showLoading());

        this.audio.addEventListener('error', (e) => this.handleAudioError(e));

        if ('mediaSession' in navigator) {
            navigator.mediaSession.setActionHandler('play', () => this.play());
            navigator.mediaSession.setActionHandler('pause', () => this.pause());
            navigator.mediaSession.setActionHandler('previoustrack', () => {
                this.skip('prev');
            });
            navigator.mediaSession.setActionHandler('nexttrack', () => {
                this.skip('next');
            });
        }
    }

    /**
     * 从服务器获取推荐音乐列表
     */
    private async fetchMusic(): Promise<void> {
        try {
            const res = await fetch('/Music/Recommend/v2');
            if (!res.ok) {
                uiFeedbackService.toast('Failed to load music');
                return;
            }
            const source = (await res.json()) as RawMusicResponse[];
            this.playlist = source.map((x) => this.normalizeTrack(x));

            if (this.playlist.length > 0) {
                this.renderList();
                this.restoreState();
            }
        } catch (e) {
            console.error(e);
            this.el.title.textContent = '加载失败';
        }
    }

    /**
     * 将后端完整模型转换为前端播放模型
     */
    private normalizeTrack(source: RawMusicResponse): MusicTrack {
        return {
            id: source.id || '',
            name: source.title || '未知标题',
            singer: source.artist || '未知歌手',
            cover: source.coverUrl || '',
            musicSrc: source.musicUrl || '',
            lyric: source.lyricContent || '',
            durationSeconds: this.parseDurationToSeconds(source.duration),
        };
    }

    /**
     * 解析后端时长为秒
     */
    private parseDurationToSeconds(duration: string | number | undefined): number {
        if (typeof duration === 'number') {
            return duration > 0 ? duration : 0;
        }
        if (!duration || typeof duration !== 'string') {
            return 0;
        }

        const parts = duration.split(':');
        if (parts.length !== 3) {
            return 0;
        }

        const hour = Number(parts[0]);
        const minute = Number(parts[1]);
        const second = Number(parts[2]);
        if (!Number.isFinite(hour) || !Number.isFinite(minute) || !Number.isFinite(second)) {
            return 0;
        }

        return hour * 3600 + minute * 60 + second;
    }

    /**
     * 渲染播放列表
     * @param filterText 搜索过滤文本,为空则显示全部
     */
    private renderList(filterText: string = ''): void {
        const filtered = filterText
            ? this.playlist.filter(
                  (track) =>
                      track.name.toLowerCase().includes(filterText.toLowerCase()) ||
                      track.singer.toLowerCase().includes(filterText.toLowerCase())
              )
            : this.playlist;

        if (filtered.length === 0) {
            this.el.listItems.style.display = 'none';
            this.el.listEmpty.style.display = 'flex';
            return;
        }

        this.el.listItems.style.display = 'block';
        this.el.listEmpty.style.display = 'none';

        this.el.listItems.innerHTML = filtered
            .map((track) => {
                const originalIndex = this.playlist.indexOf(track);
                const isPlaying = originalIndex === this.currentIndex;
                const playingIcon = this.isPlaying ? 'fa-volume-up' : 'fa-pause';
                const activeClass = isPlaying ? 'music-player__item--active' : '';
                const displayStyle = isPlaying ? '' : 'display: none;';

                return `
            <div class="music-player__item ${activeClass}" data-index="${originalIndex}">
                <div class="music-player__item-index">${originalIndex + 1}</div>
                <div class="music-player__item-meta">
                    <span class="music-player__item-title">
                        ${this.highlightText(track.name, filterText)}
                    </span>
                    <span class="music-player__item-artist">
                        ${this.highlightText(track.singer, filterText)}
                    </span>
                </div>
                <div class="music-player__item-playing" style="${displayStyle}">
                    <i class="fa ${playingIcon}"></i>
                </div>
            </div>
        `;
            })
            .join('');

        this.el.listItems.querySelectorAll('.music-player__item').forEach((item) => {
            item.addEventListener('click', () => {
                const htmlItem = item as HTMLElement;
                const index = parseInt(htmlItem.dataset.index || '0');
                this.loadTrack(index, true);
            });
        });
    }

    /**
     * 高亮搜索匹配的文本
     * @param text 原始文本
     * @param search 搜索关键词
     * @returns 包含高亮标记的 HTML 字符串
     */
    private highlightText(text: string, search: string): string {
        if (!search) {
            return text;
        }
        const escapedSearch = search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const regex = new RegExp(`(${escapedSearch})`, 'gi');
        return text.replace(regex, '<mark class="music-player__highlight">$1</mark>');
    }

    /**
     * 更新列表中活动项的状态
     * 仅更新样式和图标,不重新渲染整个列表
     */
    private updateActiveListItem(): void {
        const items = this.el.listItems.querySelectorAll('.music-player__item');
        const playingIcon = this.isPlaying ? 'fa-volume-up' : 'fa-pause';

        items.forEach((item) => {
            const htmlItem = item as HTMLElement;
            const index = parseInt(htmlItem.dataset.index || '0');
            const isActive = index === this.currentIndex;
            const playingIndicator = item.querySelector('.music-player__item-playing');
            const playingIconEl = item.querySelector('.music-player__item-playing i');

            if (isActive) {
                htmlItem.classList.add('music-player__item--active');
                if (playingIndicator) {
                    (playingIndicator as HTMLElement).style.display = '';
                }
                if (playingIconEl) {
                    playingIconEl.className = `fa ${playingIcon}`;
                }
            } else {
                htmlItem.classList.remove('music-player__item--active');
                if (playingIndicator) {
                    (playingIndicator as HTMLElement).style.display = 'none';
                }
            }
        });
    }

    /**
     * 滚动到当前播放的歌曲
     * 清除搜索过滤,定位到当前播放项并添加高亮动画
     */
    private scrollToCurrentTrack(): void {
        if (this.el.searchInput && this.el.searchInput.value) {
            this.el.searchInput.value = '';
            if (this.el.searchClear) {
                this.el.searchClear.style.display = 'none';
            }
            this.renderList('');
        }

        const activeItem = this.el.list.querySelector('.music-player__item--active');
        if (activeItem && this.el.listContent) {
            const htmlActiveItem = activeItem as HTMLElement;
            activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });

            htmlActiveItem.style.animation = 'music-item-highlight 0.6s ease';
            setTimeout(() => {
                htmlActiveItem.style.animation = '';
            }, 600);
        }
    }

    /**
     * 加载指定索引的音乐轨道
     * @param index 轨道索引
     * @param autoPlay 是否自动播放,默认 true
     * @param skipSave 是否跳过保存状态,默认 false
     */
    private loadTrack(index: number, autoPlay: boolean = true, skipSave: boolean = false): void {
        if (!this.playlist[index]) {
            return;
        }

        this.currentIndex = index;
        const track = this.playlist[index];

        this._retryCount = 0;
        if (this._retryTimer) {
            clearTimeout(this._retryTimer);
            this._retryTimer = null;
        }

        // 清理预加载状态(如果不是从预加载切换过来的)
        if (this._preloadedIndex !== index) {
            if (this._preloadAudio) {
                this._preloadAudio.src = '';
                this._preloadAudio = null;
            }
            this._preloadedIndex = -1;
        }

        // 清除随机索引,因为已经加载了新歌曲
        this._nextRandomIndex = -1;

        // 延迟加载:不立即设置 audio.src,只在播放时加载
        this._isAudioLoaded = false;
        this.audio.pause();
        this.audio.removeAttribute('src');
        this.audio.load();

        if (this._pendingRestoreTrackId !== track.id) {
            this._pendingRestoreTime = 0;
            this._pendingRestoreTrackId = null;
        }

        this.el.title.textContent = track.name;
        this.el.artist.textContent = track.singer;

        // 仅在封面 URL 变化时才更新,避免重复请求
        if (this._currentCoverUrl !== track.cover) {
            const coverUrl = `url('${track.cover}')`;
            this.el.cover.style.backgroundImage = coverUrl;
            this.el.miniCover.style.backgroundImage = coverUrl;
            this._currentCoverUrl = track.cover;
            this.applyCoverTheme(track.cover);
        }

        // 优化列表渲染:只在有搜索条件或列表为空时才重新渲染
        const currentSearch = this.el.searchInput ? this.el.searchInput.value.trim() : '';
        if (currentSearch || this.el.listItems.children.length === 0) {
            this.renderList(currentSearch);
        } else {
            // 只更新高亮状态,不重新渲染整个列表
            this.updateActiveListItem();
        }

        this.parseLyrics(track.lyric);
        this.currentLyricIndex = -1;

        this.el.timeTotal.textContent = this.formatTime(track.durationSeconds);

        const initialTime = this._pendingRestoreTrackId === track.id ? this._pendingRestoreTime : 0;

        if (initialTime > 0 && track.durationSeconds > 0) {
            const safeTime = Math.min(initialTime, track.durationSeconds);
            this.el.timeCurrent.textContent = this.formatTime(safeTime);
            this.el.seek.value = ((safeTime / track.durationSeconds) * 100).toString();
            const offset =
                this.circumference - (safeTime / track.durationSeconds) * this.circumference;
            this.el.miniProgress.style.strokeDashoffset = offset.toString();
        } else if (initialTime > 0) {
            this.el.timeCurrent.textContent = this.formatTime(initialTime);
            this.el.seek.value = '0';
            this.el.miniProgress.style.strokeDashoffset = this.circumference.toString();
        } else {
            this.el.timeCurrent.textContent = '0:00';
            this.el.seek.value = '0';
            this.el.miniProgress.style.strokeDashoffset = this.circumference.toString();
        }

        if (autoPlay) {
            this.play();
        } else {
            this.updatePlayState(false);
        }

        if ('mediaSession' in navigator) {
            navigator.mediaSession.metadata = new MediaMetadata({
                title: track.name,
                artist: track.singer,
                artwork: [{ src: String(track.cover), sizes: '512x512', type: 'image/jpeg' }],
            });
        }

        if (!skipSave) {
            this.saveState();
        }

        const isPanelOpen = this.el.panel.classList.contains('music-player__panel--open');
        if (window.innerWidth > 768 && isPanelOpen) {
            requestAnimationFrame(() => {
                const activeItem = this.el.list.querySelector('.music-player__item--active');
                if (activeItem && this.el.listContent) {
                    activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
            });
        }
    }

    /**
     * 根据封面异步提取并应用主题色
     * 提取失败(CORS、灰白封面等)时回退到默认主题
     * @param coverUrl 封面地址
     */
    private applyCoverTheme(coverUrl: string): void {
        const token = ++this._coverThemeToken;
        this._coverTheme.extract(coverUrl).then((colors) => {
            if (token !== this._coverThemeToken) {
                return;
            }
            this._coverTheme.apply(colors);
        });
    }

    /**
     * 播放音频
     * 延迟加载:只在真正播放时才加载音频文件
     */
    private play(): void {
        if (!this._hasUserInteracted) {
            return;
        }

        // 如果音频还未加载,先加载
        if (!this._isAudioLoaded) {
            const track = this.playlist[this.currentIndex];
            if (track) {
                this.audio.src = track.musicSrc;

                this._isAudioLoaded = true;

                const pendingTime =
                    this._pendingRestoreTrackId === track.id ? this._pendingRestoreTime : 0;
                if (pendingTime > 0) {
                    const onLoadedMetadata = () => {
                        try {
                            const maxDuration = this.audio.duration || track.durationSeconds;
                            if (maxDuration > 0) {
                                this.audio.currentTime = Math.min(pendingTime, maxDuration);
                            }
                            this._lastAutoSaveTime = Date.now();
                        } catch (e) {
                            console.warn('Failed to restore time on play:', e);
                        }
                        this._pendingRestoreTime = 0;
                        this._pendingRestoreTrackId = null;
                        this.audio.removeEventListener('loadedmetadata', onLoadedMetadata);
                    };
                    this.audio.addEventListener('loadedmetadata', onLoadedMetadata);
                }
            }
        }

        const p = this.audio.play();
        if (p) {
            p.catch((e) => {
                if (e instanceof Error && e.name === 'NotAllowedError') {
                    return;
                }
                console.warn('Playback prevented:', e);
            });
        }
    }

    /**
     * 暂停音频
     */
    private pause(): void {
        this.audio.pause();
    }

    /**
     * 切换播放/暂停状态
     */
    private togglePlay(): void {
        this._hasUserInteracted = true;
        if (this.audio.paused) {
            this.play();
        } else {
            this.pause();
        }
    }

    /**
     * 更新播放状态的 UI
     * 包括按钮图标、动画、列表项状态等
     * @param isPlaying 是否正在播放
     */
    private updatePlayState(isPlaying: boolean): void {
        this.isPlaying = isPlaying;
        const iconClass = isPlaying ? 'fa-pause' : 'fa-play';

        this.el.play.innerHTML = `<i class="fa ${iconClass}"></i>`;
        this.el.miniStatus.innerHTML = `<i class="fa ${iconClass}"></i>`;

        const miniLabel = isPlaying
            ? '音乐播放器,正在播放,点击暂停'
            : '音乐播放器,已暂停,点击播放';
        this.el.mini.setAttribute('aria-label', miniLabel);
        this.el.play.setAttribute('aria-label', isPlaying ? '暂停' : '播放');

        if (isPlaying) {
            this.el.cover.classList.add('music-player__cover--rotating');
            this.el.miniCover.classList.add('music-player__cover--rotating');
            this.el.mini.classList.add('music-player__mini--playing');
            this._seekReactiveIndicator.start();
        } else {
            this.el.cover.classList.remove('music-player__cover--rotating');
            this.el.miniCover.classList.remove('music-player__cover--rotating');
            this.el.mini.classList.remove('music-player__mini--playing');
            this._seekReactiveIndicator.stop();
        }

        const activeItem = this.el.list.querySelector('.music-player__item--active');
        if (activeItem) {
            const playingIndicator = activeItem.querySelector('.music-player__item-playing i');
            if (playingIndicator) {
                playingIndicator.className = isPlaying ? 'fa fa-volume-up' : 'fa fa-pause';
            }
        }

        if ('mediaSession' in navigator) {
            navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
        }
    }

    /**
     * 跳转到上一首或下一首
     * 根据播放模式决定跳转逻辑(顺序、随机)
     * @param direction 跳转方向 'prev' 或 'next'
     */
    private skip(direction: SkipDirection): void {
        let nextIndex = this.currentIndex;

        if (this.playMode === 'random') {
            if (direction === 'next' && this._nextRandomIndex !== -1) {
                // 如果是下一首且已经预先确定了随机索引,使用它
                nextIndex = this._nextRandomIndex;
                // 使用后清除
                this._nextRandomIndex = -1;
            } else if (this.playlist.length > 1) {
                // 否则生成一个新的随机索引
                do {
                    nextIndex = Math.floor(Math.random() * this.playlist.length);
                } while (nextIndex === this.currentIndex);
                // 清除旧的预测
                this._nextRandomIndex = -1;
            }
        } else {
            if (direction === 'next') {
                nextIndex = (this.currentIndex + 1) % this.playlist.length;
            } else {
                nextIndex = (this.currentIndex - 1 + this.playlist.length) % this.playlist.length;
            }
        }
        this.loadTrack(nextIndex, true);
    }

    /**
     * 获取下一首歌曲的索引
     * 根据播放模式返回不同的索引
     * @returns 下一首歌曲的索引
     */
    private getNextTrackIndex(): number {
        if (this.playMode === 'single') {
            return this.currentIndex;
        }

        if (this.playMode === 'random') {
            // 随机模式:如果已经预先确定了下一首,返回它
            if (this._nextRandomIndex !== -1) {
                return this._nextRandomIndex;
            }

            // 否则,现在生成一个随机索引
            if (this.playlist.length > 1) {
                let nextIndex: number;
                do {
                    nextIndex = Math.floor(Math.random() * this.playlist.length);
                } while (nextIndex === this.currentIndex);
                this._nextRandomIndex = nextIndex;
                return nextIndex;
            }
            return this.currentIndex;
        }

        // 顺序播放
        return (this.currentIndex + 1) % this.playlist.length;
    }

    /**
     * 音频播放结束事件处理
     * 根据播放模式决定是否循环或跳转下一首
     */
    private onEnded(): void {
        if (this.playMode === 'single') {
            this.audio.currentTime = 0;
            this.play();
        } else {
            this.skip('next');
        }
    }

    /**
     * 音频时间更新事件处理
     * 更新进度条、时间显示、歌词同步,定期自动保存进度
     * 智能预加载
     */
    private onTimeUpdate(): void {
        if (!this.isSeeking) {
            // 如果音频未加载,但有待恢复的进度,使用待恢复的时间显示UI
            const currentTrack = this.playlist[this.currentIndex];
            const shouldUsePendingTime =
                !this._isAudioLoaded &&
                this._pendingRestoreTime > 0 &&
                this._pendingRestoreTrackId === currentTrack?.id;

            const cur = shouldUsePendingTime
                ? this._pendingRestoreTime
                : this.audio.currentTime || 0;
            const dur =
                shouldUsePendingTime && currentTrack
                    ? currentTrack.durationSeconds
                    : this.audio.duration || 1;
            const pct = (cur / dur) * 100;

            this.el.seek.value = isFinite(pct) ? pct.toString() : '0';
            this.el.seekProgress.style.width = isFinite(pct) ? `${pct}%` : '0%';
            this.el.timeCurrent.textContent = this.formatTime(cur);

            if (isFinite(dur) && dur > 0) {
                const offset = this.circumference - (cur / dur) * this.circumference;
                this.el.miniProgress.style.strokeDashoffset = offset.toString();
            }
        }

        this.syncLyrics(this.audio.currentTime);

        const now = Date.now();
        if (!this._lastAutoSaveTime) {
            this._lastAutoSaveTime = now;
        }
        if (now - this._lastAutoSaveTime >= 5000) {
            this.saveState();
            this._lastAutoSaveTime = now;
        }

        // 智能预加载:当歌曲播放到剩余10秒时,预加载下一首
        this.handlePreload();
    }

    /**
     * 处理音频预加载
     * 当歌曲接近结束时,预加载下一首
     * 随机播放模式下会提前确定下一首随机歌曲
     */
    private handlePreload(): void {
        const cur = this.audio.currentTime || 0;
        const dur = this.audio.duration || 0;
        const remaining = dur - cur;

        // 剩余时间大于10秒,或者时长无效,不预加载
        if (remaining > 10 || !isFinite(remaining) || remaining <= 0) {
            return;
        }

        const nextIndex = this.getNextTrackIndex();

        // 已经预加载过该歌曲,跳过
        if (nextIndex === this._preloadedIndex) {
            return;
        }

        // 单曲循环且已加载,跳过
        if (nextIndex === this.currentIndex && this._isAudioLoaded) {
            return;
        }

        const nextTrack = this.playlist[nextIndex];
        if (!nextTrack) {
            return;
        }

        // 开始预加载
        if (this._preloadAudio) {
            this._preloadAudio.src = '';
            this._preloadAudio = null;
        }

        this._preloadAudio = new Audio();
        this._preloadAudio.preload = 'auto';
        this._preloadAudio.src = nextTrack.musicSrc;
        this._preloadedIndex = nextIndex;
    }

    /**
     * 循环切换播放模式
     * 顺序 → 随机 → 单曲循环 → 顺序
     */
    private cycleMode(): void {
        const modes: PlayMode[] = ['order', 'random', 'single'];
        const modeNames: Record<PlayMode, string> = {
            order: '顺序播放',
            random: '随机播放',
            single: '单曲循环',
        };
        const icons: Record<PlayMode, string> = {
            order: 'fa-sort-amount-asc',
            random: 'fa-random',
            single: 'fa-retweet',
        };

        let idx = modes.indexOf(this.playMode);
        idx = (idx + 1) % modes.length;
        this.playMode = modes[idx];

        const newTitle = modeNames[this.playMode];
        this.el.mode.innerHTML = `<i class="fa ${icons[this.playMode]}"></i>`;

        this.el.mode.setAttribute('data-original-title', newTitle);
        this.el.mode.removeAttribute('title');
        uiFeedbackService.showTooltip(this.el.mode, newTitle);

        // 清理预加载状态,因为播放模式变了
        if (this._preloadAudio) {
            this._preloadAudio.src = '';
            this._preloadAudio = null;
        }
        this._preloadedIndex = -1;
        this._nextRandomIndex = -1;

        this.saveState();
    }

    /**
     * 切换播放列表的显示/隐藏
     * @param show 是否显示列表
     */
    private toggleList(show: boolean): void {
        this.showList = show;
        this.el.list.classList.toggle('music-player__list--visible', this.showList);
        this.el.listToggle.classList.toggle('music-player__text-btn--active', this.showList);

        if (this.el.listCloseMobile) {
            if (window.innerWidth <= this._mobileBreakpoint) {
                this.el.listCloseMobile.style.display = show ? 'flex' : 'none';
            } else {
                this.el.listCloseMobile.style.display = 'none';
            }
        }

        if (this.showList) {
            const activeItem = this.el.list.querySelector('.music-player__item--active');
            if (activeItem) {
                setTimeout(() => {
                    activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }, 100);
            }
        }

        if (this.el.panelContent) {
            this.el.panelContent.classList.toggle('music-player__content--list-open', show);
        }
    }

    /**
     * 同步移动端真实可视高度,避免软键盘弹出后 fixed 面板仍按布局视口计算
     */
    private syncViewportHeight(): void {
        const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
        this.el.panel.style.setProperty('--music-player-viewport-height', `${viewportHeight}px`);

        const keyboardOffset = window.visualViewport
            ? Math.max(0, window.innerHeight - window.visualViewport.height)
            : 0;
        const isKeyboardOpen =
            window.innerWidth <= this._mobileBreakpoint && this.showList && keyboardOffset > 80;

        this.el.panel.classList.toggle('music-player__panel--keyboard-open', isKeyboardOpen);
    }

    /**
     * 仅在桌面/移动断点发生变化时同步列表状态,避免手机键盘触发 resize 关闭列表
     */
    private handleWindowResize(): void {
        this.syncViewportHeight();

        const isDesktop = window.innerWidth > this._mobileBreakpoint;
        if (isDesktop === this._isDesktopLayout) {
            return;
        }

        this._isDesktopLayout = isDesktop;

        if (isDesktop && !this.showList) {
            this.toggleList(true);
        } else if (!isDesktop && this.showList) {
            this.toggleList(false);
        }
    }

    /**
     * 切换音乐面板的显示/隐藏
     * 同步处理 URL hash、滚动锁定等
     * @param show 是否显示面板
     */
    private togglePanel(show: boolean): void {
        if (show) {
            this.el.panel.classList.add('music-player__panel--open');
            const shouldSetHash =
                !this._isHandlingHashChange && window.location.hash !== '#music-player';
            if (shouldSetHash) {
                pushPageHistory('#music-player');
            }
            if (window.innerWidth <= this._mobileBreakpoint) {
                document.body.style.overflow = 'hidden';
            }
        } else {
            this.el.panel.classList.remove('music-player__panel--open');
            if (this.el.panelContent) {
                this.el.panelContent.style.transform = '';
            }
            const shouldClearHash =
                !this._isHandlingHashChange && window.location.hash === '#music-player';
            if (shouldClearHash) {
                pushPageHistory(window.location.pathname + window.location.search);
            }
            document.body.style.overflow = '';
        }
    }

    /**
     * 绑定键盘快捷键
     * 空格:播放/暂停
     * 左右箭头:快进/快退 5 秒
     * ESC:关闭面板
     */
    private bindKeyboardShortcuts(): void {
        document.addEventListener('keydown', (e) => {
            const isPanelOpen = this.el.panel.classList.contains('music-player__panel--open');
            if (!isPanelOpen) {
                return;
            }

            const target = e.target as HTMLElement;
            const tag = target.tagName ? target.tagName.toLowerCase() : '';
            if (tag === 'input' || tag === 'textarea') {
                return;
            }

            if (e.key === ' ') {
                e.preventDefault();
                this.togglePlay();
            }

            if (e.key === 'ArrowRight') {
                e.preventDefault();
                if (isFinite(this.audio.duration)) {
                    this.audio.currentTime = Math.min(
                        this.audio.currentTime + 5,
                        this.audio.duration
                    );
                }
            }

            if (e.key === 'ArrowLeft') {
                e.preventDefault();
                this.audio.currentTime = Math.max(this.audio.currentTime - 5, 0);
            }

            if (e.key === 'Escape') {
                e.preventDefault();
                this.togglePanel(false);
            }
        });
    }

    /**
     * 绑定面板手势操作(移动端)
     * 支持下滑关闭面板
     */
    private bindPanelGestures(): void {
        if (!this.el.panelContent) {
            return;
        }

        const onTouchStart = (e: TouchEvent) => {
            const isPanelOpen = this.el.panel.classList.contains('music-player__panel--open');
            if (window.innerWidth > 768 || !isPanelOpen || this.showList) {
                return;
            }
            this._isPanelDragging = true;
            this._panelDragStartY = e.touches[0].clientY;
            if (this.el.panelContent) {
                this.el.panelContent.style.transition = 'none';
            }
        };

        const onTouchMove = (e: TouchEvent) => {
            if (!this._isPanelDragging || !this.el.panelContent) {
                return;
            }
            const deltaY = e.touches[0].clientY - this._panelDragStartY;
            if (deltaY <= 0) {
                return;
            }
            this.el.panelContent.style.transform = `translateY(${deltaY}px)`;
        };

        const onTouchEnd = (e: TouchEvent) => {
            if (!this._isPanelDragging || !this.el.panelContent) {
                return;
            }
            this._isPanelDragging = false;

            const endY = e.changedTouches[0].clientY;
            const deltaY = Math.max(0, endY - this._panelDragStartY);
            this.el.panelContent.style.transition = '';

            if (deltaY > 120) {
                this.togglePanel(false);
                return;
            }

            this.el.panelContent.style.transform = '';
        };

        this.el.panelContent.addEventListener('touchstart', onTouchStart, {
            passive: true,
        });
        this.el.panelContent.addEventListener('touchmove', onTouchMove, {
            passive: true,
        });
        this.el.panelContent.addEventListener('touchend', onTouchEnd, {
            passive: true,
        });
        this.el.panelContent.addEventListener('touchcancel', onTouchEnd, {
            passive: true,
        });
    }

    /**
     * 保存播放器状态到 localStorage
     * 包括播放模式、当前轨道、播放进度、歌词显示状态等
     */
    private saveState(): void {
        const currentTrack = this.playlist[this.currentIndex];
        if (!currentTrack) {
            return;
        }

        // 如果音频已加载,使用实际播放时间;否则使用待恢复的时间(避免覆盖进度)
        let currentTime = 0;
        if (this._isAudioLoaded) {
            currentTime = this.audio.currentTime;
        } else if (
            this._pendingRestoreTrackId === currentTrack.id &&
            this._pendingRestoreTime > 0
        ) {
            currentTime = this._pendingRestoreTime;
        }

        const state: PlayerState = {
            mode: this.playMode,
            trackId: currentTrack.id,
            time: currentTime,
            showLyrics: this.showLyrics,
            lyricsOnBackground: this.lyricsOnBackground,
        };
        localStorage.setItem('dpz_music_player_state', JSON.stringify(state));
    }

    /**
     * 从 localStorage 恢复播放器状态
     * 恢复上次的播放模式、歌曲、进度和歌词显示状态
     */
    private restoreState(): void {
        try {
            const saved = localStorage.getItem('dpz_music_player_state');
            if (!saved) {
                this.loadTrack(0, false);
                return;
            }

            const state = JSON.parse(saved) as Partial<PlayerState>;

            // 恢复播放模式
            if (state.mode && ['order', 'random', 'single'].includes(state.mode)) {
                this.playMode = state.mode;
                const icons: Record<PlayMode, string> = {
                    order: 'fa-sort-amount-asc',
                    random: 'fa-random',
                    single: 'fa-retweet',
                };
                const modeNames: Record<PlayMode, string> = {
                    order: '顺序播放',
                    random: '随机播放',
                    single: '单曲循环',
                };
                this.el.mode.innerHTML = `<i class="fa ${icons[this.playMode]}"></i>`;
                this.el.mode.setAttribute('title', modeNames[this.playMode]);
                if (this.el.mode.hasAttribute('data-original-title')) {
                    this.el.mode.setAttribute('data-original-title', modeNames[this.playMode]);
                }
            }

            // 恢复歌词显示设置
            if (typeof state.showLyrics === 'boolean') {
                this.showLyrics = state.showLyrics;
            }
            if (typeof state.lyricsOnBackground === 'boolean') {
                this.lyricsOnBackground = state.lyricsOnBackground;
            }

            // 查找上次播放的歌曲
            let index = 0;
            if (state.trackId) {
                const foundIndex = this.playlist.findIndex((t) => t.id === state.trackId);
                if (foundIndex !== -1) {
                    index = foundIndex;
                }
            }

            // 恢复播放进度
            const savedTime = typeof state.time === 'number' ? state.time : 0;
            if (savedTime > 0) {
                this._pendingRestoreTime = savedTime;
                this._pendingRestoreTrackId = this.playlist[index]?.id || null;
            }

            this.loadTrack(index, false, true);
            this.updateLyricsDisplay();
        } catch (e) {
            console.error('Failed to restore music player state', e);
            this.loadTrack(0, false);
        }
    }

    /**
     * 格式化时间(秒)为 M:SS 格式
     * @param seconds 秒数
     * @returns 格式化后的时间字符串,如 "3:45"
     */
    private formatTime(seconds: number): string {
        if (!isFinite(seconds)) {
            return '0:00';
        }
        const m = Math.floor(seconds / 60);
        const s = Math.floor(seconds % 60);
        return `${m}:${s < 10 ? '0' : ''}${s}`;
    }

    /**
     * 显示加载动画
     */
    private showLoading(): void {
        if (this._isLoading) {
            return;
        }
        this._isLoading = true;
        if (this.el.loading) {
            this.el.loading.style.display = 'flex';
        }
    }

    /**
     * 隐藏加载动画
     */
    private hideLoading(): void {
        if (!this._isLoading) {
            return;
        }
        this._isLoading = false;
        if (this.el.loading) {
            this.el.loading.style.display = 'none';
        }
    }

    /**
     * 处理音频加载错误
     * 自动重试机制:失败后重试最多 3 次,每次重试间隔 1 秒
     * @param e 错误事件
     */
    private async handleAudioError(e: Event): Promise<void> {
        if (!this._isAudioLoaded || (!this.audio.currentSrc && !this.audio.src)) {
            return;
        }

        console.error('音频加载错误', e);

        const track = this.playlist[this.currentIndex];
        if (!track) {
            return;
        }

        if (this._retryTimer) {
            clearTimeout(this._retryTimer);
            this._retryTimer = null;
        }

        if (this._retryCount < this._maxRetries) {
            if (!this._hasUserInteracted) {
                return;
            }

            this._retryCount++;
            const retryMsg = `加载失败,正在重试 (${this._retryCount}/${this._maxRetries})...`;
            uiFeedbackService.toast(retryMsg, ToastType.Warning);

            this._retryTimer = window.setTimeout(() => {
                console.warn(`重试${this._retryCount}超时,尝试下一次重试`);
                if (this._retryCount < this._maxRetries) {
                    this.audio.src = track.musicSrc;
                    this._isAudioLoaded = true;
                    this.play();
                } else {
                    this.skipToNextAfterError(track);
                }
            }, this._retryTimeout);

            await new Promise((resolve) => setTimeout(resolve, 1000));

            this.audio.src = track.musicSrc;
            this._isAudioLoaded = true;
            this.play();
        } else {
            this.skipToNextAfterError(track);
        }
    }

    /**
     * 所有重试失败后自动跳到下一曲
     * @param track 失败的音乐轨道
     */
    private skipToNextAfterError(track: MusicTrack): void {
        this._retryCount = 0;
        if (this._retryTimer) {
            clearTimeout(this._retryTimer);
            this._retryTimer = null;
        }
        this.hideLoading();

        uiFeedbackService.toast(`《${track.name}》加载失败,自动播放下一曲`, ToastType.Error);

        setTimeout(() => {
            this.skip('next');
        }, 1000);
    }

    /**
     * 解析 LRC 格式的歌词字符串
     * 格式示例:[00:12.34]歌词内容
     * @param lrcString LRC 格式的歌词字符串
     */
    private parseLyrics(lrcString: string): void {
        this.lyrics = [];
        this.el.lyricsInner.innerHTML = '';
        this.el.bgLyricsInner.innerHTML = '';
        this.el.bgLyricsInner.style.transform = 'translateY(0)';

        if (!lrcString) {
            const html = '<div class="music-player__lyric-line">暂无歌词</div>';
            this.el.lyricsInner.innerHTML = html;
            this.el.bgLyricsInner.innerHTML = html;
            return;
        }

        const lines = lrcString.split('\n');
        const regex = /\[(\d{2}):(\d{2})\.(\d{2,3})](.*)/;

        for (const line of lines) {
            const match = regex.exec(line);
            if (match) {
                const min = parseInt(match[1]);
                const sec = parseInt(match[2]);
                const msStr = match[3];
                const ms = msStr.length === 3 ? parseInt(msStr) : parseInt(msStr) * 10;
                const time = min * 60 + sec + ms / 1000;
                const text = match[4].trim();

                if (text) {
                    this.lyrics.push({ time, text });
                }
            }
        }

        const noLyricsHtml = '<div class="music-player__lyric-line">纯音乐 / 暂无歌词</div>';
        const lyricsHtml =
            this.lyrics.length === 0
                ? noLyricsHtml
                : this.lyrics
                      .map(
                          (l, i) =>
                              `<div class="music-player__lyric-line" data-index="${i}">${l.text}</div>`
                      )
                      .join('');

        this.el.lyricsInner.innerHTML = lyricsHtml;
        this.el.bgLyricsInner.innerHTML = lyricsHtml;

        this.updateLyricsDisplay();
    }

    /**
     * 更新歌词显示状态
     * 根据用户设置切换面板内歌词、背景歌词或隐藏歌词
     */
    private updateLyricsDisplay(): void {
        const shouldShowInPanel = this.showLyrics && !this.lyricsOnBackground;
        this.el.lyricsContainer.style.display = shouldShowInPanel ? 'block' : 'none';
        this.el.cover.style.display = shouldShowInPanel ? 'none' : 'block';

        this.el.bgLyrics.classList.toggle(
            'music-player__bg-lyrics--visible',
            this.lyricsOnBackground
        );

        const isActive = this.showLyrics || this.lyricsOnBackground;
        this.el.lrcToggle.classList.toggle('music-player__text-btn--active', isActive);

        if (this.lyricsOnBackground) {
            this.syncLyrics(this.audio.currentTime || 0);
        } else if (isActive) {
            this.syncLyrics(this.audio.currentTime);
        }
    }

    /**
     * 更新缓冲进度显示
     */
    private updateBufferedProgress(): void {
        if (!this.audio.duration || !isFinite(this.audio.duration)) {
            return;
        }

        const buffered = this.audio.buffered;
        if (buffered.length > 0) {
            // 获取最后一个缓冲区间的结束位置
            const bufferedEnd = buffered.end(buffered.length - 1);
            const bufferedPercent = (bufferedEnd / this.audio.duration) * 100;
            this.el.seekBuffered.style.width = `${Math.min(bufferedPercent, 100)}%`;
        }
    }

    /**
     * 同步歌词到当前播放时间
     * 高亮当前歌词行,并自动滚动到可视区域
     * @param time 当前播放时间(秒)
     */
    private syncLyrics(time: number): void {
        const shouldSync = (this.showLyrics || this.lyricsOnBackground) && this.lyrics.length > 0;
        if (!shouldSync) {
            return;
        }

        let activeIdx = -1;
        for (let i = 0; i < this.lyrics.length; i++) {
            if (time >= this.lyrics[i].time) {
                activeIdx = i;
            } else {
                break;
            }
        }

        if (activeIdx === this.currentLyricIndex) {
            return;
        }

        this.currentLyricIndex = activeIdx;

        if (this.showLyrics && !this.lyricsOnBackground) {
            const lines = this.el.lyricsInner.children;
            for (const line of lines) {
                line.classList.remove('music-player__lyric-line--current');
            }

            if (activeIdx >= 0 && lines[activeIdx]) {
                const activeLine = lines[activeIdx] as HTMLElement;
                activeLine.classList.add('music-player__lyric-line--current');
                activeLine.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
        }

        if (this.lyricsOnBackground) {
            const container = this.el.bgLyricsInner;
            const lines = container.children;

            for (const line of lines) {
                line.classList.remove('music-player__lyric-line--current');
            }

            if (activeIdx >= 0 && lines[activeIdx]) {
                const activeLine = lines[activeIdx] as HTMLElement;
                activeLine.classList.add('music-player__lyric-line--current');

                const offset = activeLine.offsetTop;
                container.style.transform = `translateY(-${offset}px)`;
            }
        }
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个功能非常完整的Web音乐播放器类,让我为您详细解释一下这个代码的主要结构和功能:

整体架构

这是一个采用单例模式的音乐播放器类,确保整个应用只有一个播放器实例。播放器支持完整的音乐播放功能,包括播放控制、歌词显示、播放列表管理等。

核心功能模块

1. 数据接口定义

interface MusicTrack {
    id: string;
    name: string;
    singer: string;
    cover: string;
    musicSrc: string;
    lyric: string;
    durationSeconds: number;
}

定义了音乐轨道的标准格式,以及后端数据格式转换接口。

2. 播放器核心状态

  • 音频控制: 使用HTML5 Audio API
  • 播放列表管理: 支持顺序、随机、单曲循环三种模式
  • 进度跟踪: 实时更新播放进度和时间显示
  • 状态持久化: 自动保存和恢复播放状态到localStorage

3. 用户界面功能

迷你播放器

  • 显示当前歌曲封面和播放状态
  • 圆形进度条显示播放进度
  • 单击播放/暂停,双击打开完整面板

完整播放面板

  • 大封面显示和旋转动画
  • 完整的播放控制按钮
  • 进度条拖拽控制
  • 歌词显示(面板内或背景显示)
  • 播放列表管理

4. 高级特性

智能预加载

private handlePreload(): void {
    const remaining = dur - cur;
    if (remaining > 10) return; // 剩余10秒时开始预加载下一首
}

错误处理和重试机制

private async handleAudioError(): Promise<void> {
    if (this._retryCount < this._maxRetries) {
        // 最多重试3次,每次间隔1秒
        await new Promise(resolve => setTimeout(resolve, 1000));
        this.audio.src = track.musicSrc;
        this.play();
    }
}

歌词同步显示

  • 支持LRC格式歌词解析
  • 实时高亮当前歌词行
  • 支持面板内滚动和背景滚动两种模式

5. 移动端优化

响应式布局

  • 桌面端默认显示播放列表
  • 移动端可折叠列表显示
  • 软键盘适配处理

手势支持

  • 长按迷你播放器打开面板
  • 下滑手势关闭面板
  • 触摸友好的控件设计

视口高度同步

private syncViewportHeight(): void {
    const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
    this.el.panel.style.setProperty('--music-player-viewport-height', `${viewportHeight}px`);
}

6. 性能优化

DOM元素缓存

所有DOM元素在初始化时一次性缓存,避免重复查询:

private initElements(): void {
    this.el = {
        mini: document.getElementById('mp-mini')!,
        play: document.getElementById('mp-play')!,
        // ... 其他元素
    };
}

延迟加载

音频文件只在真正播放时才加载,而不是切换歌曲时立即加载。

智能列表渲染

只在必要时重新渲染播放列表,平时只更新活动项状态。

7. 辅助功能

键盘快捷键

  • 空格键: 播放/暂停
  • 左右箭头: 快退/快进5秒
  • ESC: 关闭面板

无障碍支持

  • ARIA标签支持
  • 语义化的按钮标签
  • 屏幕阅读器友好

媒体会话集成

支持系统级媒体控制(如耳机按钮、锁屏控制等)。

技术亮点

  1. 单例模式: 确保全局唯一实例
  2. 状态管理: 完整的状态持久化和恢复
  3. 错误恢复: 自动重试和错误处理机制
  4. 性能优化: DOM缓存、延迟加载、智能预加载
  5. 用户体验: 平滑动画、手势支持、键盘快捷键
  6. 跨平台兼容: 桌面和移动端自适应

这是一个企业级的音乐播放器实现,代码结构清晰,功能完整,考虑了各种边界情况和用户体验细节。

评论加载中...