/** * Shortcodes JavaScript - 实况照片、语音、音乐卡片 * 移植自 Hugo-Theme-Amigo */ // ============================================================ // Global Audio Manager - 全局音频/视频管理器 // 确保所有音频控件互斥播放,PJAX 切换时保存进度 // ============================================================ var AudioManager = { currentPlaying: null, currentType: null, savedStates: {}, // 全局音乐播放器 - PJAX 持久化 globalAudio: null, globalMusicSrc: null, globalMusicCard: null, // Session storage key STORAGE_KEY: 'amigo_music_state', // 获取当前页面的存储 key(基于 URL path) _getPageKey: function() { return window.location.pathname; }, init: function() { // 创建全局音频元素 if (!this.globalAudio) { this.globalAudio = new Audio(); this.globalAudio.preload = 'metadata'; // 监听全局音频事件 this.globalAudio.addEventListener('timeupdate', this._onTimeUpdate.bind(this)); this.globalAudio.addEventListener('ended', this._onEnded.bind(this)); this.globalAudio.addEventListener('loadedmetadata', this._onLoadedMetadata.bind(this)); } }, // 保存当前播放状态到 sessionStorage(按页面保存) saveToSession: function() { if (!this.globalAudio || !this.globalMusicSrc) return; var pageKey = this._getPageKey(); var allStates = this._loadAllStates(); allStates[pageKey] = { src: this.globalMusicSrc, currentTime: this.globalAudio.currentTime, isPlaying: !this.globalAudio.paused, name: this.globalMusicCard ? this.globalMusicCard.dataset.name : '', artist: this.globalMusicCard ? this.globalMusicCard.dataset.artist : '' }; try { sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(allStates)); } catch (e) { console.warn('Failed to save music state:', e); } }, // 加载所有页面的状态 _loadAllStates: function() { try { var stored = sessionStorage.getItem(this.STORAGE_KEY); if (!stored) return {}; return JSON.parse(stored); } catch (e) { return {}; } }, // 从 sessionStorage 恢复当前页面的播放状态 restoreFromSession: function() { var pageKey = this._getPageKey(); var allStates = this._loadAllStates(); var state = allStates[pageKey]; if (!state || !state.src) return null; return state; }, // 清除当前页面的播放状态 clearSession: function() { var pageKey = this._getPageKey(); var allStates = this._loadAllStates(); delete allStates[pageKey]; try { sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(allStates)); } catch (e) {} }, // 恢复播放状态 - 在 PJAX 切换后调用 restorePlayingState: function() { var state = this.restoreFromSession(); if (!state || !state.src) { // 当前页面没有保存的状态,暂停播放 if (this.globalAudio && !this.globalAudio.paused) { this.globalAudio.pause(); } return; } var self = this; // 找到对应的音乐卡片 var cards = document.querySelectorAll('.amigo-music-card'); var targetCard = null; for (var i = 0; i < cards.length; i++) { var card = cards[i]; if (card.dataset.src === state.src) { targetCard = card; break; } } if (!targetCard) { // 当前页面没有这个音乐卡片,暂停播放 if (this.globalAudio && !this.globalAudio.paused) { this.globalAudio.pause(); } return; } // 设置全局音频 this.globalMusicSrc = state.src; this.globalMusicCard = targetCard; this.globalAudio.src = state.src; // 更新 UI this._updateUI(targetCard, state.isPlaying); // 等待元数据加载完成 this.globalAudio.addEventListener('loadedmetadata', function onLoaded() { self.globalAudio.removeEventListener('loadedmetadata', onLoaded); if (state.currentTime > 0 && state.currentTime < self.globalAudio.duration) { self.globalAudio.currentTime = state.currentTime; } self._onTimeUpdate(); if (state.isPlaying) { self.globalAudio.play().catch(function() {}); } }); if (this.globalAudio.readyState >= 1) { if (state.currentTime > 0 && state.currentTime < this.globalAudio.duration) { this.globalAudio.currentTime = state.currentTime; } this._onTimeUpdate(); if (state.isPlaying) { this.globalAudio.play().catch(function() {}); } } }, // 播放指定音乐卡片 playMusic: function(card) { if (!card) return; var audioSrc = card.dataset.src; if (!audioSrc) return; // 如果是同一首歌,切换播放/暂停 if (this.globalMusicSrc === audioSrc && this.globalAudio) { if (this.globalAudio.paused) { this.globalAudio.play().catch(function() {}); this.globalMusicCard = card; this._updateUI(card, true); } else { this.globalAudio.pause(); this._updateUI(card, false); } return; } // 暂停其他所有音频 this.pauseAll(); // 设置新的音频源 this.globalMusicSrc = audioSrc; this.globalMusicCard = card; this.globalAudio.src = audioSrc; // 更新 UI this._updateUI(card, true); // 尝试播放 var self = this; this.globalAudio.play().then(function() { // 保存状态 self.saveToSession(); }).catch(function(err) { console.warn('Play failed:', err); }); }, // 跳转到指定时间 seekTo: function(time) { if (this.globalAudio) { this.globalAudio.currentTime = time; this.saveToSession(); } }, // 更新时间回调 _onTimeUpdate: function() { if (!this.globalAudio || !this.globalMusicCard) return; // 如果正在拖动进度条,跳过 UI 更新 if (this.globalAudio.__seeking) return; var currentTime = this.globalAudio.currentTime; var duration = this.globalAudio.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); } // 定期保存状态(每5秒) if (Math.floor(currentTime) % 5 === 0) { this.saveToSession(); } }, // 加载完成回调 _onLoadedMetadata: function() { if (!this.globalAudio || !this.globalMusicCard) return; var duration = this.globalAudio.duration; // 更新总时长显示 var durationTimeEl = this.globalMusicCard.querySelector('.amigo-music-card__time--duration'); if (durationTimeEl) { durationTimeEl.textContent = formatTime(duration); } }, // 播放结束回调 _onEnded: function() { if (this.globalMusicCard) { this._updateUI(this.globalMusicCard, false); } this.globalMusicCard = null; this.clearSession(); }, // 更新 UI 状态 _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; } }, pauseAll: function() { // Pause all voice bubbles 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'); }); // Pause global music if (this.globalAudio && !this.globalAudio.paused) { this.globalAudio.pause(); if (this.globalMusicCard) { this._updateUI(this.globalMusicCard, false); } } // Pause all live photos 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; }, registerPlay: function(element, type) { if (this.currentPlaying && this.currentPlaying !== element) { this.pauseAll(); } this.currentPlaying = element; this.currentType = type; }, saveState: function() { var states = []; // Save music card states document.querySelectorAll('.amigo-music-card').forEach(function(card, index) { var audio = card.__audio; if (audio && audio.src) { states.push({ type: 'music', src: card.dataset.src, currentTime: audio.currentTime, isPlaying: card.getAttribute('data-playing') === 'true', index: index }); } }); // Save live photo states document.querySelectorAll('.amigo-livephoto').forEach(function(livePhoto, index) { var video = livePhoto.querySelector('.amigo-livephoto-video'); if (video && video.src) { states.push({ type: 'livephoto', src: livePhoto.dataset.src, currentTime: video.currentTime, isPlaying: livePhoto.classList.contains('is-playing'), index: index }); } }); this.savedStates[window.location.pathname] = states; }, restoreState: function() { var path = window.location.pathname; var states = this.savedStates[path]; if (!states || states.length === 0) return; states.forEach(function(state) { if (state.type === 'music') { var cards = document.querySelectorAll('.amigo-music-card'); var card = cards[state.index]; if (card && card.__audio && card.dataset.src === state.src) { var audio = card.__audio; var playBtn = card.querySelector('.amigo-music-card__play'); var seekInput = card.querySelector('.amigo-music-card__seek'); var currentTimeEl = card.querySelector('.amigo-music-card__time--current'); audio.addEventListener('loadedmetadata', function onLoaded() { audio.currentTime = state.currentTime; audio.removeEventListener('loadedmetadata', onLoaded); if (currentTimeEl) { currentTimeEl.textContent = formatTime(state.currentTime); } if (seekInput && audio.duration) { var progress = (state.currentTime / audio.duration) * 100; seekInput.value = (state.currentTime / audio.duration * 1000).toString(); card.style.setProperty('--music-progress', progress + '%'); } if (state.isPlaying) { audio.play().catch(function() {}); card.setAttribute('data-playing', 'true'); if (playBtn) playBtn.setAttribute('aria-pressed', 'true'); AudioManager.currentPlaying = card; AudioManager.currentType = 'music'; } }); if (audio.readyState >= 1) { audio.currentTime = state.currentTime; if (currentTimeEl) { currentTimeEl.textContent = formatTime(state.currentTime); } if (seekInput && audio.duration) { var progress = (state.currentTime / audio.duration) * 100; seekInput.value = (state.currentTime / audio.duration * 1000).toString(); card.style.setProperty('--music-progress', progress + '%'); } if (state.isPlaying) { audio.play().catch(function() {}); card.setAttribute('data-playing', 'true'); if (playBtn) playBtn.setAttribute('aria-pressed', 'true'); AudioManager.currentPlaying = card; AudioManager.currentType = 'music'; } } } } else if (state.type === 'livephoto') { var photos = document.querySelectorAll('.amigo-livephoto'); var livePhoto = photos[state.index]; if (livePhoto && livePhoto.dataset.src === state.src) { var video = livePhoto.querySelector('.amigo-livephoto-video'); if (video) { var src = livePhoto.dataset.src || ''; if (src && !video.getAttribute('src')) { video.setAttribute('src', src); video.src = src; } video.addEventListener('loadedmetadata', function onLoaded() { video.currentTime = state.currentTime; video.removeEventListener('loadedmetadata', onLoaded); if (state.isPlaying) { video.play().catch(function() {}); livePhoto.classList.add('is-playing'); AudioManager.currentPlaying = livePhoto; AudioManager.currentType = 'livephoto'; } }); if (video.readyState >= 1) { video.currentTime = state.currentTime; if (state.isPlaying) { video.play().catch(function() {}); livePhoto.classList.add('is-playing'); AudioManager.currentPlaying = livePhoto; AudioManager.currentType = 'livephoto'; } } } } } }); } }; // ============================================================ // 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() { if (AudioManager.globalAudio && AudioManager.globalAudio.duration) { var time = (parseFloat(seekInput.value) / 1000) * AudioManager.globalAudio.duration; AudioManager.seekTo(time); } }); // 拖动时暂停更新 seekInput.addEventListener('mousedown', function() { if (AudioManager.globalAudio) { AudioManager.globalAudio.__seeking = true; } }); seekInput.addEventListener('mouseup', function() { if (AudioManager.globalAudio) { AudioManager.globalAudio.__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 // ============================================================ function initAllShortcodes() { // 初始化 AudioManager(创建全局音频元素,恢复播放状态) AudioManager.init(); initLivePhotoShortcodes(); initVoiceBubbles(); initMusicCards(); // 恢复 PJAX 切换前的播放状态 setTimeout(function() { AudioManager.restorePlayingState(); }, 100); } // 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', cleanupAllShortcodes); document.addEventListener('pjax:complete', initAllShortcodes); document.addEventListener('pjax:success', 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); });