Lab主页点歌卡片教程

歌单列表 重启微信自动消失 重新搜索点歌即可

设置好后 – 如果首页不显示 – 就重启微信√

图片[1]-Lab主页点歌卡片教程-苹果签主题官网


教程如下 ↓ :

注意事项:

首页卡片Widget选项里 – 要按照我图片的修改

底部的内容位置
(要求默认 显示不全就是你改动过 恢复默认数值就行)
内容缩放: 100
内容X位移: 0

内容Y位移: 0

显示不全 就让AI修改一下宽度 高度200不动
卡片偏移 就自己慢慢调 内容X位移+内容Y位移+内容缩放
一般是不需要动的 实在解决不了的再调整
显示正常的什么都不用改 内容位置也不需要改 默认就行

图片[2]-Lab主页点歌卡片教程-苹果签主题官网

每个手机比例不一样

显示不全 就发给AI修改下(插件打开都有显示)

图片[3]-Lab主页点歌卡片教程-苹果签主题官网


↑ 只改宽度 – 高度200不变

长摁下方 – 文字内容

选择全选 – 进行复制

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>至简のiMessage</title>
<style>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
    user-select: none;
}
body {
    background: transparent;
    font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.music-card {
    width: 398px;
    height: 200px;
    border-radius: 20px;
    padding: 12px 16px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    overflow: hidden;
    position: relative;
}
@media (prefers-color-scheme: light) {
    .music-card { background: #fff; color: #000; }
    #progressBar { background: #eee; }
    .playlist-pop { background: #fff; border:1px solid #eee; }
    .playlist-item:hover { background: #f5f5f5; }
    .add-music-btn, .small-btn { background: #f0f0f0; color: #000; }
    .delete-btn { color: #999; }
    .delete-btn:hover { color: #e53e3e; }
    .clear-all-btn { color:#999; border-top:1px solid #eee; }
    .lyric-area { background: rgba(0,0,0,0.04); color: #444; }
    .lyric-line.active { color: #007aff; }
    .source-toggle { background: rgba(0,0,0,0.06); color:#333; }
    .toggle-hint { background: rgba(0,0,0,0.7); color: #fff; }
}
@media (prefers-color-scheme: dark) {
    .music-card { background: #202020; color: #fff; }
    #progressBar { background: #333; }
    .playlist-pop { background: #1a1a1a; border:1px solid #333; }
    .playlist-item:hover { background: #2a2a2a; }
    .add-music-btn, .small-btn { background: #2c2c2c; color: #fff; }
    .delete-btn { color: #888; }
    .delete-btn:hover { color: #f56565; }
    .clear-all-btn { color:#aaa; border-top:1px solid #333; }
    .lyric-area { background: rgba(255,255,255,0.05); color: #ccc; }
    .lyric-line.active { color: #ff9f0a; }
    .source-toggle { background: rgba(255,255,255,0.1); color:#ddd; }
    .toggle-hint { background: rgba(0,0,0,0.75); color: #f0f0f0; }
    #searchDialog { background: #202020; color: #fff; }
}
.top-row {
    display: flex;
    gap: 12px;
    align-items: center;
}
.cover {
    width: 56px;
    height: 56px;
    border-radius: 12px;
    object-fit: cover;
    cursor: pointer;
    background: #666;
}
.info {
    flex: 1;
    min-width: 0;
}
.song-name {
    font-size: 15px;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.song-artist {
    font-size: 11px;
    opacity: 0.7;
    margin-top: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.progress-row {
    margin: 6px 0 4px;
}
.time-row {
    display: flex;
    justify-content: space-between;
    font-size: 10px;
    margin-bottom: 4px;
}
#progressBar {
    width: 100%;
    height: 5px;
    -webkit-appearance: none;
    border-radius: 3px;
    outline: none;
    cursor: pointer;
}
#progressBar::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: inherit;
    border: 2px solid #888;
    cursor: pointer;
}
.lyric-area {
    height: 48px;
    overflow-y: auto;
    font-size: 11px;
    line-height: 1.4;
    text-align: center;
    border-radius: 10px;
    padding: 6px 4px;
    margin: 6px 0 4px;
    scroll-behavior: smooth;
}
.lyric-line {
    transition: all 0.1s;
    white-space: pre-wrap;
    word-break: break-word;
    padding: 2px 0;
}
.lyric-line.active {
    font-weight: 600;
    transform: scale(1.02);
}
.control-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 4px;
}
.ctrl-buttons {
    display: flex;
    gap: 16px;
    align-items: center;
}
.ctrl-btn {
    background: none;
    border: none;
    color: inherit;
    cursor: pointer;
    display: inline-flex;
}
.ctrl-btn svg {
    width: 22px;
    height: 22px;
    fill: currentColor;
}
.play-btn svg {
    width: 30px;
    height: 30px;
}
.small-btn {
    border: none;
    border-radius: 999px;
    padding: 4px 12px;
    font-size: 11px;
    cursor: pointer;
    transition: all 0.2s ease;
}
.small-btn:hover {
    opacity: 0.8;
    transform: scale(0.96);
}
.btn-group {
    display: flex;
    gap: 6px;
}
.playlist-pop {
    position: absolute;
    bottom: 48px;
    left: 12px;
    width: 220px;
    max-height: 150px;
    border-radius: 12px;
    padding: 6px;
    overflow-y: auto;
    display: none;
    z-index: 99;
    background: inherit;
    border: 1px solid;
}
.playlist-item {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 5px 8px;
    font-size: 12px;
    border-radius: 8px;
    cursor: pointer;
}
.playlist-item.active { font-weight: 600; background: rgba(128,128,128,0.2); }
.delete-btn {
    font-size: 18px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0 4px;
}
.clear-all-btn {
    width: 100%;
    text-align: center;
    padding: 6px;
    font-size: 11px;
    background: none;
    border: none;
    border-top: 1px solid;
    margin-top: 4px;
    cursor: pointer;
}
input[type="file"] { display: none; }
#searchDialog {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 260px;
    border-radius: 20px;
    padding: 16px;
    z-index: 1000;
    box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
@media (prefers-color-scheme: light) {
    #searchDialog { background: #fff; color: #000; }
}
#searchKeyword {
    width: 100%;
    padding: 10px;
    border-radius: 40px;
    border: 1px solid #ccc;
    margin: 10px 0;
    background: inherit;
    color: inherit;
}
.dialog-buttons { display: flex; justify-content: flex-end; gap: 12px; }

/* 右上角切换源新样式 (按钮+浮动提示) */
.source-wrapper {
    margin-left: auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    position: relative;
}
.source-toggle {
    font-size: 11px;
    font-weight: 500;
    padding: 5px 10px;
    border-radius: 30px;
    cursor: pointer;
    backdrop-filter: blur(2px);
    transition: all 0.2s ease;
    white-space: nowrap;
    background: rgba(128,128,128,0.15);
    margin-left: 0;
}
.source-toggle:hover {
    opacity: 0.8;
    transform: scale(0.96);
}
.toggle-hint {
    font-size: 10px;
    padding: 4px 8px;
    margin-top: 6px;
    border-radius: 40px;
    white-space: nowrap;
    background: rgba(0,0,0,0.7);
    color: #fff;
    backdrop-filter: blur(4px);
    font-weight: 500;
    letter-spacing: 0.5px;
    animation: floatHint 0.8s infinite ease-in-out;
    box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
@keyframes floatHint {
    0% { transform: translateY(0px); }
    50% { transform: translateY(-3px); }
    100% { transform: translateY(0px); }
}
</style>
</head>
<body>
<div class="music-card">
    <div class="top-row">
        <img id="cover" class="cover" src="">
        <div class="info">
            <div class="song-name" id="songName">未播放</div>
            <div class="song-artist" id="songArtist">点击 🔍 搜歌</div>
            <div class="progress-row">
                <div class="time-row"><span id="curTime">00:00</span><span id="totalTime">00:00</span></div>
                <input type="range" id="progressBar" min="0" max="100" value="0">
            </div>
        </div>
        <!-- 音源切换按钮 + 动态提示(默认隐藏) -->
        <div class="source-wrapper">
            <div id="sourceToggle" class="source-toggle">🎵 QQ音乐</div>
            <div id="toggleHint" class="toggle-hint" style="display: none;">↑点击切换音源</div>
        </div>
    </div>
    <div id="lyricArea" class="lyric-area"><div class="lyric-line">✨ 暂无歌词,点击“📄 歌词”获取</div></div>
    <div class="control-bar">
        <div class="ctrl-buttons">
            <button class="ctrl-btn" id="playlistBtn">☰</button>
            <button class="ctrl-btn" id="prevBtn"><svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg></button>
            <button class="ctrl-btn play-btn" id="playBtn"><svg id="playIcon" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg></button>
            <button class="ctrl-btn" id="nextBtn"><svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg></button>
        </div>
        <div class="btn-group">
            <button class="small-btn" id="lyricBtn">📄 歌词</button>
            <button class="small-btn" id="searchBtn">🔍 搜歌</button>
            <button class="small-btn" id="addMusicBtn">📂 添加</button>
        </div>
    </div>
    <div class="playlist-pop" id="playlistPop"></div>
    <input type="file" id="musicPick" accept="audio/*" multiple>
    <input type="file" id="lrcPick" accept=".lrc,.txt">
    <input type="file" id="coverPick" accept="image/*" style="display:none">
</div>
<div id="searchDialog" style="display:none;">
    <div style="font-weight:600;">🎵 搜索歌曲</div>
    <input type="text" id="searchKeyword" placeholder="歌名,例如:晴天">
    <div class="dialog-buttons"><button id="searchCancelBtn">取消</button><button id="searchConfirmBtn">搜索</button></div>
</div>
<audio id="audioPlayer">
<script>
// DOM 元素
const audio = document.getElementById('audioPlayer');
const cover = document.getElementById('cover');
const songNameSpan = document.getElementById('songName');
const artistSpan = document.getElementById('songArtist');
const curTimeSpan = document.getElementById('curTime');
const totalTimeSpan = document.getElementById('totalTime');
const progressBar = document.getElementById('progressBar');
const playBtn = document.getElementById('playBtn');
const playIcon = document.getElementById('playIcon');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const playlistBtn = document.getElementById('playlistBtn');
const playlistPop = document.getElementById('playlistPop');
const searchBtn = document.getElementById('searchBtn');
const searchDialog = document.getElementById('searchDialog');
const searchKeyword = document.getElementById('searchKeyword');
const searchCancel = document.getElementById('searchCancelBtn');
const searchConfirm = document.getElementById('searchConfirmBtn');
const addMusicBtn = document.getElementById('addMusicBtn');
const musicPick = document.getElementById('musicPick');
const lyricBtn = document.getElementById('lyricBtn');
const lrcPick = document.getElementById('lrcPick');
const lyricArea = document.getElementById('lyricArea');
const coverPick = document.getElementById('coverPick');
const sourceToggle = document.getElementById('sourceToggle');
const toggleHint = document.getElementById('toggleHint');

// 全局数据
let playList = [];
let nowIndex = 0;
let isPlaying = false;
let currentLyrics = [];
let artistRestoreTimer = null;
let hintTimeout = null;           // 提示自动隐藏定时器
let globalListenerRemover = null; // 用于移除全局交互监听

// 音乐源切换 (qq / wyy)
let currentMusicSource = 'qq';

// ========== 性能优化核心:歌词缓存,避免频繁 innerHTML 重建 ==========
let cachedLyricLines = [];      // 存储当前歌词行的DOM元素(仅当歌词非空时有效)
let cachedNoLyricDiv = null;    // 无歌词时的占位div(用于复用)
let currentLyricVersion = 0;    // 用于标记歌词是否变化

// 重建歌词DOM(仅在歌词数据变化时调用一次)
function rebuildLyricDOM() {
    lyricArea.innerHTML = '';
    cachedLyricLines = [];
    if (!currentLyrics.length) {
        // 无歌词:显示占位符(完全保留原文字样式)
        if (!cachedNoLyricDiv) {
            cachedNoLyricDiv = document.createElement('div');
            cachedNoLyricDiv.className = 'lyric-line';
            cachedNoLyricDiv.innerText = '🎤 暂无歌词,点击“📄 歌词”上传或获取';
        }
        lyricArea.appendChild(cachedNoLyricDiv);
        return;
    }
    // 有歌词:批量构建行
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < currentLyrics.length; i++) {
        const lineDiv = document.createElement('div');
        lineDiv.className = 'lyric-line';
        lineDiv.innerText = currentLyrics[i].text;
        fragment.appendChild(lineDiv);
        cachedLyricLines.push(lineDiv);
    }
    lyricArea.appendChild(fragment);
}

// 仅更新高亮行和滚动(高频调用,极轻量)
function updateLyricHighlight(currentTime) {
    if (!currentLyrics.length) return;
    let activeIndex = -1;
    for (let i = 0; i < currentLyrics.length; i++) {
        const line = currentLyrics[i];
        const nextTime = (i + 1 < currentLyrics.length) ? currentLyrics[i+1].time : Infinity;
        if (currentTime >= line.time && currentTime < nextTime) {
            activeIndex = i;
            break;
        }
    }
    // 处理最后一句之后的情况
    if (activeIndex === -1 && currentLyrics.length && currentTime >= currentLyrics[currentLyrics.length-1].time) {
        activeIndex = currentLyrics.length - 1;
    }
    // 更新 DOM 中的 active 类
    for (let i = 0; i < cachedLyricLines.length; i++) {
        const lineDiv = cachedLyricLines[i];
        if (i === activeIndex) {
            if (!lineDiv.classList.contains('active')) {
                lineDiv.classList.add('active');
                // 滚动到可视区
                lineDiv.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
        } else {
            if (lineDiv.classList.contains('active')) lineDiv.classList.remove('active');
        }
    }
}

// 原 renderLyrics 的替代:根据歌词状态做轻量渲染(保留原函数名但内部优化,完全兼容旧调用)
function renderLyrics(currentTime = 0) {
    // 确保歌词区域与 currentLyrics 同步(若版本未变则只更新高亮,否则重建)
    // 外部调用时如果是歌词数据变化了,会先修改 currentLyrics 然后调用本函数,因此需要检测是否需要重建
    // 简便方式:对比 currentLyrics 与现有缓存的行数是否匹配(歌词内容变更时行数会变)
    let needRebuild = false;
    if (!currentLyrics.length) {
        // 当前无歌词,但 lyricArea 内如果没有占位符或者有歌词行残留则重建
        const hasNoLyricDiv = lyricArea.children.length === 1 && lyricArea.children[0] === cachedNoLyricDiv;
        if (!hasNoLyricDiv || !cachedNoLyricDiv) needRebuild = true;
    } else {
        // 有歌词,但缓存的行数不匹配或 lyricArea 内容不是由 cachedLyricLines 构成
        if (cachedLyricLines.length !== currentLyrics.length) needRebuild = true;
        else {
            // 可选:检查第一个歌词文本是否一致,简单防错
            if (cachedLyricLines.length > 0 && cachedLyricLines[0].innerText !== currentLyrics[0].text) needRebuild = true;
        }
    }
    if (needRebuild) {
        rebuildLyricDOM();
    }
    // 更新高亮
    if (currentLyrics.length) {
        updateLyricHighlight(currentTime);
    }
}

// 外部设置歌词时调用 (完全保留原逻辑,只是内部会触发重建)
function setLyricsForCurrent(song, lyricsArray) {
    song.lyrics = lyricsArray;
    if (playList[nowIndex] === song) {
        currentLyrics = lyricsArray || [];
        renderLyrics(audio.currentTime);
    }
}

// ----- 其余所有原有函数完全保留,只有上面歌词渲染部分被优化 -----
// 以下所有代码均从原版复制,未改动任何文字描述、提示、按钮文案

function formatTime(sec) {
    if (isNaN(sec)) return '00:00';
    const m = Math.floor(sec / 60);
    const s = Math.floor(sec % 60);
    return `${m.toString().padStart(2,'0')}:${s.toString().padStart(2,'0')}`;
}

function parseLRC(lrcStr) {
    const lines = lrcStr.split(/\r?\n/);
    const list = [];
    for (const line of lines) {
        const match = line.match(/\[(\d{2}):(\d{2}(?:\.\d+)?)\](.*)/);
        if (match) {
            const min = parseInt(match[1]);
            const sec = parseFloat(match[2]);
            const time = min * 60 + sec;
            const text = match[3].trim();
            if (text) list.push({ time, text });
        }
    }
    list.sort((a,b) => a.time - b.time);
    return list;
}
function escapeHtml(str) { return str.replace(/[&<>]/g, function(m){ return {'&':'&','<':'<','>':'>'}[m];}); }

function renderPlaylist() {
    playlistPop.innerHTML = '';
    playList.forEach((song, idx) => {
        const div = document.createElement('div');
        div.className = `playlist-item ${idx === nowIndex ? 'active' : ''}`;
        const nameSpan = document.createElement('span');
        nameSpan.innerText = `${song.name} - ${song.artist}`;
        nameSpan.style.flex = '1';
        nameSpan.style.cursor = 'pointer';
        nameSpan.onclick = (e) => { e.stopPropagation(); loadSong(idx); autoPlay(); closePop(); };
        const delBtn = document.createElement('button');
        delBtn.innerText = '✕';
        delBtn.className = 'delete-btn';
        delBtn.onclick = (e) => { e.stopPropagation(); deleteSong(idx); };
        div.appendChild(nameSpan);
        div.appendChild(delBtn);
        playlistPop.appendChild(div);
    });
    if (playList.length) {
        const clearBtn = document.createElement('button');
        clearBtn.innerText = '🗑️ 清空所有歌曲';
        clearBtn.className = 'clear-all-btn';
        clearBtn.onclick = () => clearAllSongs();
        playlistPop.appendChild(clearBtn);
    }
}
function closePop() { playlistPop.style.display = 'none'; }

function deleteSong(idx) {
    if (idx < 0 || idx >= playList.length) return;
    playList.splice(idx, 1);
    
    if (playList.length === 0) {
        nowIndex = 0;
        isPlaying = false;
        audio.src = '';
        cover.src = '';
        songNameSpan.innerText = '未播放';
        artistSpan.innerText = '点击 🔍 搜歌';
        currentLyrics = [];
        renderLyrics();
        renderPlaylist();
        return;
    }
    if (idx < nowIndex) nowIndex--;
    else if (idx === nowIndex) {
        if (nowIndex >= playList.length) nowIndex = playList.length - 1;
        loadSong(nowIndex);
        if (isPlaying) autoPlay();
    } else {
        loadSong(nowIndex);
    }
    renderPlaylist();
}

function clearAllSongs() {
    playList = [];
    nowIndex = 0;
    isPlaying = false;
    audio.src = '';
    cover.src = '';
    songNameSpan.innerText = '未播放';
    artistSpan.innerText = '点击 🔍 搜歌';
    currentLyrics = [];
    renderLyrics();
    renderPlaylist();
    if (playlistPop) playlistPop.style.display = 'none';
}

function loadSong(idx) {
    if (!playList.length) return;
    nowIndex = (idx + playList.length) % playList.length;
    const song = playList[nowIndex];
    audio.src = song.src;
    songNameSpan.innerText = song.name;
    if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
    artistSpan.innerText = song.artist;
    if (song.coverBase64) cover.src = song.coverBase64;
    else if (song.coverUrl) cover.src = song.coverUrl;
    else cover.src = '';
    currentLyrics = song.lyrics || [];
    renderLyrics(0);
    renderPlaylist();
}

function autoPlay() {
    if (!audio.src) return;
    audio.play().catch(e=>console.log);
    isPlaying = true;
    playIcon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
    if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
    const currentArtist = playList[nowIndex] ? playList[nowIndex].artist : '';
    artistSpan.innerText = '🎵 正在播放';
    artistRestoreTimer = setTimeout(() => {
        artistSpan.innerText = currentArtist;
    }, 1000);
}
function pausePlay() {
    audio.pause();
    isPlaying = false;
    playIcon.innerHTML = '<path d="M8 5v14l11-7z"/>';
    if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
    const currentArtist = playList[nowIndex] ? playList[nowIndex].artist : '';
    artistSpan.innerText = '⏸ 已暂停';
    artistRestoreTimer = setTimeout(() => {
        artistSpan.innerText = currentArtist;
    }, 1000);
}

audio.ontimeupdate = () => {
    if(audio.duration) {
        const percent = (audio.currentTime / audio.duration) * 100;
        progressBar.value = percent;
        curTimeSpan.innerText = formatTime(audio.currentTime);
        totalTimeSpan.innerText = formatTime(audio.duration);
        if (currentLyrics.length) renderLyrics(audio.currentTime);
    }
};
progressBar.oninput = () => { if(audio.duration) audio.currentTime = (progressBar.value/100) * audio.duration; };
audio.onended = () => nextBtn.click();

prevBtn.onclick = () => { if(playList.length) { loadSong(nowIndex-1); autoPlay(); } };
nextBtn.onclick = () => { if(playList.length) { loadSong(nowIndex+1); autoPlay(); } };
playBtn.onclick = () => {
    if(!playList.length) return;
    if(isPlaying) pausePlay();
    else autoPlay();
};
playlistBtn.onclick = (e) => { e.stopPropagation(); playlistPop.style.display = playlistPop.style.display === 'block' ? 'none' : 'block'; };
document.addEventListener('click', (e) => { if(!document.querySelector('.music-card').contains(e.target)) closePop(); });

cover.onclick = () => coverPick.click();
coverPick.onchange = async (e) => {
    const file = e.target.files[0];
    if (!file || !playList[nowIndex]) return;
    const reader = new FileReader();
    reader.onload = async (res) => {
        const imgUrl = res.target.result;
        cover.src = imgUrl;
        playList[nowIndex].coverBase64 = imgUrl;
        if (playList[nowIndex].fileName && window.TM && playList[nowIndex].isLocal) {
            const b64 = await TM.fileToBase64(file);
            await TM.saveFile(playList[nowIndex].coverFileName, b64, 'image');
        }
    };
    reader.readAsDataURL(file);
};

addMusicBtn.onclick = () => musicPick.click();
musicPick.onchange = async (e) => {
    if (!window.TM) { alert("当前环境不支持存储, 仅临时播放"); }
    for (let f of e.target.files) {
        const rawName = f.name.replace(/\.\w+$/, '');
        const parts = rawName.split(' - ');
        const songTitle = parts[0] || rawName;
        const artist = parts[1] || '本地音乐';
        let uri = URL.createObjectURL(f);
        if (window.TM) {
            const fileName = 'mus_'+Date.now()+'_'+f.name;
            const b64 = await TM.fileToBase64(f);
            await TM.saveFile(fileName, b64, 'audio');
            uri = await TM.loadFile('audio', fileName);
            playList.push({
                name: songTitle, artist, src: uri, coverBase64: '', coverUrl: '',
                isLocal: true, fileName, coverFileName: 'cover_'+fileName+'.jpg', lyrics: null
            });
        } else {
            playList.push({
                name: songTitle, artist, src: uri, coverBase64: '', coverUrl: '',
                isLocal: false, fileName: null, lyrics: null
            });
        }
    }
    renderPlaylist();
    if (playList.length === 1) { loadSong(0); autoPlay(); }
    musicPick.value = '';
};

lyricBtn.onclick = () => {
    if (!playList.length) { alert("请先播放歌曲"); return; }
    if (confirm("自动获取网络歌词?\n(取消则上传本地.lrc文件)")) {
        const song = playList[nowIndex];
        fetchLyricsFromNet(song.name, song.artist).then(lyrics => {
            if (lyrics && lyrics.length) {
                setLyricsForCurrent(song, lyrics);
                alert("歌词已加载");
            } else alert("未找到歌词,可手动上传");
        });
    } else lrcPick.click();
};
lrcPick.onchange = async (e) => {
    const file = e.target.files[0];
    if (!file || !playList[nowIndex]) return;
    const text = await file.text();
    const lyrics = parseLRC(text);
    if (lyrics.length) {
        setLyricsForCurrent(playList[nowIndex], lyrics);
        if (playList[nowIndex].fileName && window.TM && playList[nowIndex].isLocal) {
            const lrcName = playList[nowIndex].fileName.replace(/\.\w+$/, '.lrc');
            if (lrcName) await TM.saveFile(lrcName, btoa(unescape(encodeURIComponent(text))), 'audio');
        }
        alert("歌词已加载");
    } else alert("无效LRC格式");
    lrcPick.value = '';
};
async function fetchLyricsFromNet(title, artist) {
    try {
        const api = `https://lrclib.net/api/get?track_name=${encodeURIComponent(title)}&artist_name=${encodeURIComponent(artist)}`;
        const resp = await fetch(api);
        const data = await resp.json();
        if (data.syncedLyrics) return parseLRC(data.syncedLyrics);
        else return null;
    } catch(e) { return null; }
}

// 搜索对话框
searchDialog.style.display = 'none';
searchBtn.onclick = () => {
    hideToggleHintAndCleanup();   // 立即隐藏浮窗提示并清理监听
    searchDialog.style.display = 'block'; 
    searchKeyword.value = ''; 
    searchKeyword.focus();
};
searchCancel.onclick = () => { searchDialog.style.display = 'none'; };
searchConfirm.onclick = async () => {
    const kw = searchKeyword.value.trim();
    if (!kw) return alert("输入歌名");
    searchDialog.style.display = 'none';
    await searchSongBySource(kw);
};
searchKeyword.addEventListener('keypress', e => { if(e.key === 'Enter') searchConfirm.click(); });

async function searchSongBySource(songName) {
    let apiUrl = '';
    if (currentMusicSource === 'qq') {
        apiUrl = `https://api.iosnn.cn/apis.php?api=hbqq&name=${encodeURIComponent(songName)}`;
    } else {
        apiUrl = `https://api.iosnn.cn/apis.php?api=hbqq&name=${encodeURIComponent(songName)}`;
    }
    
    try {
        const resp = await fetch(apiUrl);
        const data = await resp.json();
        
        if (data.code !== 200 || !data.music_url || !data.title) {
            throw new Error(data.msg || data.message || "接口未返回有效歌曲");
        }
        
        const musicUrl = data.music_url;
        const coverUrl = (data.cover && data.cover.startsWith('http')) ? data.cover : '';
        let lyricsArray = null;
        const rawLrc = data.lrc || data.lyric || null;
        if (rawLrc && typeof rawLrc === 'string' && rawLrc.trim().length > 0) {
            lyricsArray = parseLRC(rawLrc);
        }
        if (!lyricsArray || lyricsArray.length === 0) {
            const netLyrics = await fetchLyricsFromNet(data.title, data.singer);
            if (netLyrics && netLyrics.length) lyricsArray = netLyrics;
        }
        
        const newSong = {
            name: data.title,
            artist: data.singer || '未知歌手',
            src: musicUrl,
            coverUrl: coverUrl,
            coverBase64: '',
            isLocal: false,
            lyrics: lyricsArray,
            fileName: null,
            coverFileName: null
        };
        
        playList.push(newSong);
        renderPlaylist();
        const idx = playList.length - 1;
        loadSong(idx);
        autoPlay();
    } catch (err) {
        alert(`搜歌失败 (${currentMusicSource === 'qq' ? 'QQ音乐' : '网易云'}): ` + err.message);
        console.error(err);
    }
}

async function initLocal() {
    if (!window.TM) return;
    const audioFiles = await TM.listFiles('audio');
    const imgFiles = await TM.listFiles('image');
    for (let f of audioFiles) {
        if (f.startsWith('mus_')) {
            const uri = await TM.loadFile('audio', f);
            let rawName = f.replace(/^mus_\d+_/, '').replace(/\.\w+$/, '');
            let title = rawName;
            let artist = '本地音乐';
            const dashIndex = rawName.lastIndexOf(' - ');
            if (dashIndex !== -1) {
                title = rawName.substring(0, dashIndex);
                artist = rawName.substring(dashIndex + 3);
            }
            const coverFileName = `cover_${f}.jpg`;
            let coverBase64 = imgFiles.includes(coverFileName) ? await TM.loadFile('image', coverFileName) : '';
            let lyricsArr = null;
            const lrcName = f.replace(/\.\w+$/, '.lrc');
            if (audioFiles.includes(lrcName)) {
                const lrcUri = await TM.loadFile('audio', lrcName);
                if (lrcUri) {
                    const lrcResp = await fetch(lrcUri);
                    const lrcText = await lrcResp.text();
                    lyricsArr = parseLRC(lrcText);
                }
            }
            playList.push({
                name: title,
                artist: artist,
                src: uri,
                coverBase64: coverBase64,
                coverUrl: '',
                fileName: f,
                coverFileName: coverFileName,
                lyrics: lyricsArr,
                isLocal: true
            });
        }
    }
    renderPlaylist();
    if (playList.length) loadSong(0);
}

function showToggleHint(keepGlobalListener = false) {
    if (hintTimeout) clearTimeout(hintTimeout);
    toggleHint.style.display = 'block';
    hintTimeout = setTimeout(() => {
        hideToggleHintAndCleanup();
    }, 2000);
    if (!keepGlobalListener || !globalListenerRemover) {
        if (globalListenerRemover) globalListenerRemover();
        const handleUserInteraction = (e) => {
            let target = e.target;
            let isSourceWrapper = false;
            while (target && target !== document.body) {
                if (target.classList && (target.classList.contains('source-wrapper') || 
                    target.id === 'sourceToggle' || target.id === 'toggleHint')) {
                    isSourceWrapper = true;
                    break;
                }
                target = target.parentElement;
            }
            if (!isSourceWrapper) hideToggleHintAndCleanup();
        };
        document.addEventListener('click', handleUserInteraction, true);
        document.addEventListener('touchstart', handleUserInteraction, true);
        globalListenerRemover = () => {
            document.removeEventListener('click', handleUserInteraction, true);
            document.removeEventListener('touchstart', handleUserInteraction, true);
        };
    }
}
function hideToggleHintAndCleanup() {
    if (hintTimeout) { clearTimeout(hintTimeout); hintTimeout = null; }
    toggleHint.style.display = 'none';
    if (globalListenerRemover) { globalListenerRemover(); globalListenerRemover = null; }
}
function showInitialHint() {
    hideToggleHintAndCleanup();
    showToggleHint(false);
}
function updateSourceToggleUI() {
    if (currentMusicSource === 'qq') {
        sourceToggle.innerHTML = '🎵 QQ音乐';
        sourceToggle.style.background = 'rgba(0,122,255,0.15)';
    } else {
        sourceToggle.innerHTML = '☁️ 网易云';
        sourceToggle.style.background = 'rgba(236, 72, 54, 0.18)';
    }
}
function toggleMusicSource() {
    if (currentMusicSource === 'qq') {
        currentMusicSource = 'wyy';
    } else {
        currentMusicSource = 'qq';
    }
    updateSourceToggleUI();
    if (globalListenerRemover) {
        if (hintTimeout) clearTimeout(hintTimeout);
        toggleHint.style.display = 'block';
        hintTimeout = setTimeout(() => {
            hideToggleHintAndCleanup();
        }, 2000);
    } else {
        showToggleHint(false);
    }
    const tip = currentMusicSource === 'qq' ? '已切换至 QQ音乐 搜歌' : '已切换至 网易云音乐 搜歌';
    const oldHint = artistSpan.innerText;
    artistSpan.innerText = tip;
    if (artistRestoreTimer) clearTimeout(artistRestoreTimer);
    artistRestoreTimer = setTimeout(() => {
        if (playList[nowIndex]) artistSpan.innerText = playList[nowIndex].artist;
        else artistSpan.innerText = oldHint;
        artistRestoreTimer = null;
    }, 2000);
}
sourceToggle.addEventListener('click', (e) => {
    e.stopPropagation();
    toggleMusicSource();
});

window.addEventListener('DOMContentLoaded', () => {
    updateSourceToggleUI();
    initLocal();
    setTimeout(() => {
        showInitialHint();
    }, 100);
});
</script>
</body>
</html>

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容