/** * Shortcodes JavaScript - 实况照片、语音、音乐卡片 * 移植自 Hugo-Theme-Amigo */ // ============================================================ // Global Audio Manager - 全局音频/视频管理器 // 确保所有音频控件互斥播放,PJAX 切换时保存进度 // ============================================================ // 持久化 AudioManager 对象本身(防止脚本重载时丢失方法) // 第一次加载:定义 AudioManager // 后续加载(PJAX 切换):合并新方法到已存在的对象 if (!window.AudioManager) { window.AudioManager = {}; } // 用一个 IIFE 来定义 AudioManager 的方法 (function() { var AM = window.AudioManager; // 一次性定义所有方法 AM.getAudioState = function() { if (!window.__amigoAudioState) { window.__amigoAudioState = { audio: null, src: null, isPlaying: false, currentTime: 0, attached: false }; } return window.__amigoAudioState; }; AM._getAudio = function() { return this.getAudioState().audio; }; AM._getSrc = function() { return this.getAudioState().src; }; AM._setSrc = function(src) { this.getAudioState().src = src; }; AM.init = function() { var state = this.getAudioState(); if (!state.audio) { state.audio = new Audio(); state.audio.preload = 'metadata'; if (!state.attached) { state.audio.style.display = 'none'; state.audio.setAttribute('data-amigo-music', 'true'); document.body.appendChild(state.audio); state.attached = true; } var self = this; state.audio.addEventListener('timeupdate', function() { self._onTimeUpdate(); }); state.audio.addEventListener('ended', function() { self._onEnded(); }); state.audio.addEventListener('loadedmetadata', function() { self._onLoadedMetadata(); }); } }; AM.saveToSession = function() { var audio = this._getAudio(); var src = this._getSrc(); console.log('[AudioManager] saveToSession called, audio:', !!audio, 'src:', src, 'isPlaying:', audio ? !audio.paused : false); if (!audio || !src) { console.log('[AudioManager] saveToSession skipped - no audio or src'); return; } var state = { src: src, currentTime: audio.currentTime, isPlaying: !audio.paused, name: this.globalMusicCard ? this.globalMusicCard.dataset.name : '', artist: this.globalMusicCard ? this.globalMusicCard.dataset.artist : '', savedAt: Date.now() }; console.log('[AudioManager] saving state:', JSON.stringify(state)); try { sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(state)); } catch (e) { console.warn('Failed to save music state:', e); } }; AM.restoreFromSession = function() { try { var stored = sessionStorage.getItem(this.STORAGE_KEY); if (!stored) return null; return JSON.parse(stored); } catch (e) { return null; } }; AM.clearSession = function() { sessionStorage.removeItem(this.STORAGE_KEY); }; AM.reset = function() { console.log('[AudioManager] reset called'); var audio = this._getAudio(); if (audio) { audio.pause(); audio.currentTime = 0; audio.src = ''; } this._setSrc(null); console.log('[AudioManager] reset done, src:', this._getSrc()); this.globalMusicCard = null; this.clearSession(); }; AM.restorePlayingState = function() { var state = this.restoreFromSession(); if (!state || !state.src) return; var audio = this._getAudio(); var currentSrc = this._getSrc(); if (currentSrc === state.src && audio && audio.src) { this._syncCardUI(state.src, state.isPlaying); return; } if (audio && !audio.paused && currentSrc !== state.src) { audio.pause(); } this._setSrc(state.src); if (!audio.src || audio.src !== state.src) { audio.src = state.src; } var targetCard = this._findCardBySrc(state.src); if (targetCard) { this.globalMusicCard = targetCard; this._updateUI(targetCard, state.isPlaying); } var self = this; var doRestore = function() { if (state.currentTime > 0 && state.currentTime < audio.duration) { audio.currentTime = state.currentTime; } self._onTimeUpdate(); if (state.isPlaying && audio.paused) { audio.play().catch(function() {}); } }; if (audio.readyState >= 1) { doRestore(); } else { audio.addEventListener('loadedmetadata', function onLoaded() { audio.removeEventListener('loadedmetadata', onLoaded); doRestore(); }); } }; AM._findCardBySrc = function(src) { var cards = document.querySelectorAll('.amigo-music-card'); for (var i = 0; i < cards.length; i++) { if (cards[i].dataset.src === src) { return cards[i]; } } return null; }; AM._syncCardUI = function(src, isPlaying) { var card = this._findCardBySrc(src); if (card) { this.globalMusicCard = card; this._updateUI(card, isPlaying); this._onTimeUpdate(); } }; AM.playMusic = function(card) { if (!card) return; var audioSrc = card.dataset.src; if (!audioSrc) return; console.log('[AudioManager] playMusic called:', audioSrc); var audio = this._getAudio(); var currentSrc = this._getSrc(); if (currentSrc === audioSrc && audio) { console.log('[AudioManager] Same song, toggling play/pause'); if (audio.paused) { audio.play().catch(function() {}); this.globalMusicCard = card; this._updateUI(card, true); } else { audio.pause(); this._updateUI(card, false); } return; } this.pauseAll(); this._setSrc(audioSrc); console.log('[AudioManager] set src:', audioSrc, 'state.src:', this._getSrc()); this.globalMusicCard = card; audio.src = audioSrc; this._updateUI(card, true); var self = this; audio.play().then(function() { console.log('[AudioManager] Playback started, saving session'); self.saveToSession(); }).catch(function(err) { console.warn('Play failed:', err); }); }; AM.seekTo = function(time) { var audio = this._getAudio(); if (audio) { audio.currentTime = time; this.saveToSession(); } }; AM._onTimeUpdate = function() { var audio = this._getAudio(); if (!audio || !this.globalMusicCard) return; if (audio.__seeking) return; var currentTime = audio.currentTime; var duration = audio.duration; if (isNaN(duration) || duration === 0) return; var progress = (currentTime / duration) * 100; this.globalMusicCard.style.setProperty('--music-progress', progress + '%'); var currentTimeEl = this.globalMusicCard.querySelector('.amigo-music-card__time--current'); if (currentTimeEl) currentTimeEl.textContent = formatTime(currentTime); var durationTimeEl = this.globalMusicCard.querySelector('.amigo-music-card__time--duration'); if (durationTimeEl && durationTimeEl.textContent === '--:--') { durationTimeEl.textContent = formatTime(duration); } if (Math.floor(currentTime) % 5 === 0) { this.saveToSession(); } }; AM._onLoadedMetadata = function() { var audio = this._getAudio(); if (!audio || !this.globalMusicCard) return; var duration = audio.duration; var durationTimeEl = this.globalMusicCard.querySelector('.amigo-music-card__time--duration'); if (durationTimeEl) durationTimeEl.textContent = formatTime(duration); }; AM._onEnded = function() { if (this.globalMusicCard) { this._updateUI(this.globalMusicCard, false); } this.globalMusicCard = null; this._setSrc(null); this.clearSession(); }; AM._updateUI = function(card, isPlaying) { if (!card) return; card.setAttribute('data-playing', isPlaying ? 'true' : 'false'); var playBtn = card.querySelector('.amigo-music-card__play'); if (playBtn) playBtn.setAttribute('aria-pressed', isPlaying ? 'true' : 'false'); if (isPlaying) { this.currentPlaying = card; this.currentType = 'music'; } else if (this.globalMusicCard === card) { this.currentPlaying = null; this.currentType = null; } }; AM.pauseAll = function() { document.querySelectorAll('.amigo-voice-bubble.is-playing').forEach(function(bubble) { if (bubble.__audio) { try { bubble.__audio.pause(); bubble.__audio.currentTime = 0; } catch (e) {} } bubble.classList.remove('is-playing'); }); var audio = this._getAudio(); if (audio && !audio.paused) { audio.pause(); if (this.globalMusicCard) { this._updateUI(this.globalMusicCard, false); } } document.querySelectorAll('.amigo-livephoto.is-playing').forEach(function(livePhoto) { var video = livePhoto.querySelector('.amigo-livephoto-video'); if (video) { try { video.pause(); video.currentTime = 0; } catch (e) {} } livePhoto.classList.remove('is-playing'); }); this.currentPlaying = null; this.currentType = null; }; AM.registerPlay = function(element, type) { if (this.currentPlaying && this.currentPlaying !== element) { // 触发其他播放的暂停 } this.currentPlaying = element; this.currentType = type; }; // 初始化基本字段 if (typeof AM.currentPlaying === 'undefined') AM.currentPlaying = null; if (typeof AM.currentType === 'undefined') AM.currentType = null; if (typeof AM.savedStates === 'undefined') AM.savedStates = {}; if (typeof AM.STORAGE_KEY === 'undefined') AM.STORAGE_KEY = 'amigo_music_state'; })(); // 保留对 window.AudioManager 的引用 var AudioManager = window.AudioManager; // ============================================================ // Live Photo // ============================================================ function initLivePhotoShortcodes() { document.querySelectorAll('.amigo-livephoto').forEach(function(livePhoto) { if (livePhoto.__liveBound) return; livePhoto.__liveBound = true; var video = livePhoto.querySelector('.amigo-livephoto-video'); var posterImg = livePhoto.querySelector('.amigo-livephoto-img'); var soundBtn = livePhoto.querySelector('.amigo-livephoto-sound'); if (!video) return; var HOVER_DELAY = 500; var hoverTimer = null; var isLoaded = false; var isUserToggled = false; function ensureLoaded() { if (isLoaded) return; isLoaded = true; var src = video.dataset.src || ''; if (src && !video.getAttribute('src')) { video.setAttribute('src', src); video.src = src; } try { video.load(); } catch (e) {} } function setMuted(isMuted) { video.muted = !!isMuted; if (isMuted) { video.setAttribute('muted', ''); livePhoto.classList.add('is-muted'); } else { video.removeAttribute('muted'); livePhoto.classList.remove('is-muted'); } } function playVideo() { ensureLoaded(); AudioManager.registerPlay(livePhoto, 'livephoto'); livePhoto.classList.add('is-playing'); try { video.play(); } catch (e) {} } function stopVideo() { livePhoto.classList.remove('is-playing'); try { video.pause(); video.currentTime = 0; } catch (e) {} if (AudioManager.currentPlaying === livePhoto) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } } function togglePlay() { if (livePhoto.classList.contains('is-playing')) { isUserToggled = false; stopVideo(); } else { isUserToggled = true; playVideo(); } } // Click to toggle livePhoto.addEventListener('click', function() { togglePlay(); }); // Sound button click if (soundBtn) { soundBtn.addEventListener('click', function(e) { e.stopPropagation(); setMuted(!video.muted); }); } // Hover to play (desktop) if ('ontouchstart' in window === false) { livePhoto.addEventListener('mouseenter', function() { if (isUserToggled) return; hoverTimer = setTimeout(playVideo, HOVER_DELAY); }); livePhoto.addEventListener('mouseleave', function() { clearTimeout(hoverTimer); if (!isUserToggled) { stopVideo(); } }); } // Video ended video.addEventListener('ended', function() { livePhoto.classList.remove('is-playing'); isUserToggled = false; if (AudioManager.currentPlaying === livePhoto) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } }); // Initialize muted state setMuted(livePhoto.classList.contains('is-muted')); }); } // ============================================================ // Voice Bubble // ============================================================ function initVoiceBubbles() { document.querySelectorAll('.amigo-voice-bubble').forEach(function(bubble) { if (bubble.__voiceBound) return; bubble.__voiceBound = true; var audioSrc = bubble.dataset.src; var audio = null; var isPlaying = false; var durationSpan = bubble.querySelector('.amigo-voice-duration'); // Load audio duration if (audioSrc) { audio = new Audio(audioSrc); bubble.__audio = audio; audio.addEventListener('loadedmetadata', function() { var duration = Math.round(audio.duration); if (durationSpan) { durationSpan.textContent = duration + '"'; } }); } bubble.addEventListener('click', function() { if (!audio) return; if (isPlaying) { audio.pause(); audio.currentTime = 0; bubble.classList.remove('is-playing'); isPlaying = false; if (AudioManager.currentPlaying === bubble) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } } else { AudioManager.registerPlay(bubble, 'voice'); audio.play(); bubble.classList.add('is-playing'); isPlaying = true; } }); audio.addEventListener('ended', function() { bubble.classList.remove('is-playing'); isPlaying = false; if (AudioManager.currentPlaying === bubble) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } }); }); } // ============================================================ // Music Card - PJAX 持久化版本 // 使用全局 AudioManager 管理音乐播放 // ============================================================ function initMusicCards() { document.querySelectorAll('.amigo-music-card').forEach(function(card) { if (card.__musicBound) return; card.__musicBound = true; var audioSrc = card.dataset.src; var playBtn = card.querySelector('.amigo-music-card__play'); var seekInput = card.querySelector('.amigo-music-card__seek'); var currentTime = card.querySelector('.amigo-music-card__time--current'); var durationTime = card.querySelector('.amigo-music-card__time--duration'); if (!audioSrc) return; // 不再创建单独的 Audio 对象,使用全局 AudioManager // card.__audio = null; // 检查是否应该显示播放状态 // (由 AudioManager 在 restorePlayingState 中处理) // Play/Pause - 使用 AudioManager function togglePlay(e) { if (e) e.stopPropagation(); AudioManager.playMusic(card); } if (playBtn) { playBtn.addEventListener('click', togglePlay); } card.addEventListener('click', togglePlay); // Seek - 使用 AudioManager if (seekInput) { seekInput.addEventListener('input', function() { var audio = AudioManager._getAudio(); if (audio && audio.duration) { var time = (parseFloat(seekInput.value) / 1000) * audio.duration; AudioManager.seekTo(time); } }); // 拖动时暂停更新 seekInput.addEventListener('mousedown', function() { var audio = AudioManager._getAudio(); if (audio) { audio.__seeking = true; } }); seekInput.addEventListener('mouseup', function() { var audio = AudioManager._getAudio(); if (audio) { audio.__seeking = false; } }); } }); } function formatTime(seconds) { var m = Math.floor(seconds / 60); var s = Math.floor(seconds % 60); return m + ':' + (s < 10 ? '0' : '') + s; } // ============================================================ // Cleanup functions for PJAX // ============================================================ function cleanupVoiceBubbles() { document.querySelectorAll('.amigo-voice-bubble').forEach(function(bubble) { if (bubble.__audio) { try { bubble.__audio.pause(); bubble.__audio.src = ''; bubble.__audio = null; } catch (e) {} } bubble.__voiceBound = false; }); } function cleanupMusicCards() { document.querySelectorAll('.amigo-music-card').forEach(function(card) { if (card.__audio) { try { card.__audio.pause(); card.__audio.src = ''; card.__audio = null; } catch (e) {} } card.__musicBound = false; }); } function cleanupLivePhotos() { document.querySelectorAll('.amigo-livephoto').forEach(function(livePhoto) { var video = livePhoto.querySelector('video'); if (video) { try { video.pause(); video.src = ''; } catch (e) {} } livePhoto.__liveBound = false; }); } function cleanupAllShortcodes() { // 保存当前播放状态到 sessionStorage(音乐不中断) AudioManager.saveToSession(); // 只清理语音气泡和实况照片(音乐由全局 AudioManager 管理) cleanupVoiceBubbles(); cleanupLivePhotos(); // 不再清理音乐卡片,因为音乐由全局 AudioManager 管理 // cleanupMusicCards(); // 已移除,音乐在 PJAX 切换时继续播放 } // ============================================================ // Initialize all shortcodes // ============================================================ // 防止 initAllShortcodes 在 PJAX 切换时被重复调用 var __shortcodesInitLock = false; function initAllShortcodes() { console.log('[AudioManager] initAllShortcodes called, lock:', __shortcodesInitLock); // 防止重复初始化 if (__shortcodesInitLock) return; __shortcodesInitLock = true; // 初始化 AudioManager(创建全局音频元素) AudioManager.init(); initLivePhotoShortcodes(); initVoiceBubbles(); initMusicCards(); // 检查 sessionStorage 中是否有保存的播放状态 // 如果有,恢复播放(无论是 PJAX 切换还是脚本重新加载) // 如果没有,重置播放器(首次加载或刷新页面) var savedState = AudioManager.restoreFromSession(); console.log('[AudioManager] initAllShortcodes - savedState:', savedState ? JSON.stringify(savedState) : null); if (savedState && savedState.src) { setTimeout(function() { AudioManager.restorePlayingState(); // 延迟释放锁,避免短时间内重复触发 setTimeout(function() { __shortcodesInitLock = false; }, 500); }, 100); } else { // 没有保存的状态,重置播放器到初始状态 AudioManager.reset(); __shortcodesInitLock = false; } } // DOMContentLoaded document.addEventListener('DOMContentLoaded', initAllShortcodes); // Page load window.addEventListener('load', initAllShortcodes); // If DOM is already ready (script loaded late), initialize immediately if (document.readyState === 'interactive' || document.readyState === 'complete') { initAllShortcodes(); } // PJAX support - cleanup before and re-initialize after page transition document.addEventListener('pjax:beforeSend', function() { window.__isPjaxNavigation = true; cleanupAllShortcodes(); }); document.addEventListener('pjax:complete', function() { initAllShortcodes(); }); document.addEventListener('pjax:success', function() { // pjax:success 在 pjax:complete 之前触发,已经被锁过滤 initAllShortcodes(); }); // Also listen for custom events that some PJAX libraries fire document.addEventListener('page:loaded', initAllShortcodes); document.addEventListener('page:change', initAllShortcodes); document.addEventListener('turbo:load', initAllShortcodes); document.addEventListener('turbo:before-render', cleanupAllShortcodes); // History API based navigation document.addEventListener('popstate', function() { cleanupAllShortcodes(); setTimeout(initAllShortcodes, 10); });