Files
blog/themes/Ying/assets/js/shortcodes.js
T

794 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});