/** * Shortcodes JavaScript - 实况照片、语音、音乐卡片 * 移植自 Hugo-Theme-Amigo */ // ============================================================ // Global Audio Manager - 全局音频/视频管理器 // 确保所有音频控件互斥播放,PJAX 切换时保存进度 // ============================================================ var AudioManager = { currentPlaying: null, currentType: null, savedStates: {}, 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 all music cards document.querySelectorAll('.amigo-music-card[data-playing="true"]').forEach(function(card) { if (card.__audio) { try { card.__audio.pause(); } catch (e) {} } card.setAttribute('data-playing', 'false'); var playBtn = card.querySelector('.amigo-music-card__play'); if (playBtn) playBtn.setAttribute('aria-pressed', '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 // ============================================================ 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'); var audio = null; var isPlaying = false; if (!audioSrc) return; audio = new Audio(audioSrc); card.__audio = audio; // Load metadata audio.addEventListener('loadedmetadata', function() { var duration = formatTime(audio.duration); if (durationTime) durationTime.textContent = duration; }); // Update progress audio.addEventListener('timeupdate', function() { if (currentTime) currentTime.textContent = formatTime(audio.currentTime); if (seekInput && audio.duration) { var progress = (audio.currentTime / audio.duration) * 100; seekInput.value = (audio.currentTime / audio.duration * 1000).toString(); card.style.setProperty('--music-progress', progress + '%'); } }); // Play/Pause function togglePlay() { if (isPlaying) { audio.pause(); card.setAttribute('data-playing', 'false'); if (playBtn) playBtn.setAttribute('aria-pressed', 'false'); isPlaying = false; if (AudioManager.currentPlaying === card) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } } else { AudioManager.registerPlay(card, 'music'); audio.play(); card.setAttribute('data-playing', 'true'); if (playBtn) playBtn.setAttribute('aria-pressed', 'true'); isPlaying = true; } } if (playBtn) { playBtn.addEventListener('click', function(e) { e.stopPropagation(); togglePlay(); }); } card.addEventListener('click', togglePlay); // Seek if (seekInput) { seekInput.addEventListener('input', function() { if (audio.duration) { var progress = (parseFloat(seekInput.value) / 1000) * 100; audio.currentTime = (parseFloat(seekInput.value) / 1000) * audio.duration; card.style.setProperty('--music-progress', progress + '%'); } }); } // Ended audio.addEventListener('ended', function() { card.setAttribute('data-playing', 'false'); if (playBtn) playBtn.setAttribute('aria-pressed', 'false'); isPlaying = false; if (AudioManager.currentPlaying === card) { AudioManager.currentPlaying = null; AudioManager.currentType = null; } }); }); } 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() { // Save playback state before cleanup AudioManager.saveState(); cleanupVoiceBubbles(); cleanupMusicCards(); cleanupLivePhotos(); AudioManager.currentPlaying = null; AudioManager.currentType = null; } // ============================================================ // Initialize all shortcodes // ============================================================ function initAllShortcodes() { initLivePhotoShortcodes(); initVoiceBubbles(); initMusicCards(); // Restore playback state after initialization setTimeout(function() { AudioManager.restoreState(); }, 50); } // 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); });