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

550 lines
20 KiB
JavaScript
Raw Normal View History

2026-06-24 17:25:54 +08:00
/**
* Shortcodes JavaScript - 实况照片、语音、音乐卡片
* 移植自 Hugo-Theme-Amigo
*/
2026-06-24 21:02:53 +08:00
// ============================================================
// 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';
}
}
}
}
}
});
}
};
2026-06-24 17:25:54 +08:00
// ============================================================
// Live Photo
// ============================================================
function initLivePhotoShortcodes() {
2026-06-24 20:22:40 +08:00
document.querySelectorAll('.amigo-livephoto').forEach(function(livePhoto) {
2026-06-24 17:25:54 +08:00
if (livePhoto.__liveBound) return;
livePhoto.__liveBound = true;
2026-06-24 20:22:40 +08:00
var video = livePhoto.querySelector('.amigo-livephoto-video');
var posterImg = livePhoto.querySelector('.amigo-livephoto-img');
var soundBtn = livePhoto.querySelector('.amigo-livephoto-sound');
2026-06-24 17:25:54 +08:00
2026-06-24 20:22:40 +08:00
if (!video) return;
2026-06-24 17:25:54 +08:00
var HOVER_DELAY = 500;
var hoverTimer = null;
var isLoaded = false;
2026-06-24 20:22:40 +08:00
var isUserToggled = false;
2026-06-24 17:25:54 +08:00
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;
2026-06-24 20:22:40 +08:00
if (isMuted) {
video.setAttribute('muted', '');
livePhoto.classList.add('is-muted');
} else {
video.removeAttribute('muted');
livePhoto.classList.remove('is-muted');
}
2026-06-24 17:25:54 +08:00
}
function playVideo() {
ensureLoaded();
2026-06-24 21:02:53 +08:00
AudioManager.registerPlay(livePhoto, 'livephoto');
2026-06-24 17:25:54 +08:00
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) {}
2026-06-24 21:02:53 +08:00
if (AudioManager.currentPlaying === livePhoto) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
2026-06-24 17:25:54 +08:00
}
2026-06-24 20:22:40 +08:00
function togglePlay() {
2026-06-24 17:25:54 +08:00
if (livePhoto.classList.contains('is-playing')) {
2026-06-24 20:22:40 +08:00
isUserToggled = false;
2026-06-24 17:25:54 +08:00
stopVideo();
} else {
2026-06-24 20:22:40 +08:00
isUserToggled = true;
2026-06-24 17:25:54 +08:00
playVideo();
}
2026-06-24 20:22:40 +08:00
}
// Click to toggle
livePhoto.addEventListener('click', function() {
togglePlay();
2026-06-24 17:25:54 +08:00
});
2026-06-24 20:22:40 +08:00
// Sound button click
if (soundBtn) {
soundBtn.addEventListener('click', function(e) {
e.stopPropagation();
setMuted(!video.muted);
});
}
2026-06-24 17:25:54 +08:00
2026-06-24 20:22:40 +08:00
// Hover to play (desktop)
if ('ontouchstart' in window === false) {
livePhoto.addEventListener('mouseenter', function() {
if (isUserToggled) return;
hoverTimer = setTimeout(playVideo, HOVER_DELAY);
});
2026-06-24 17:25:54 +08:00
2026-06-24 20:22:40 +08:00
livePhoto.addEventListener('mouseleave', function() {
clearTimeout(hoverTimer);
if (!isUserToggled) {
stopVideo();
}
});
}
2026-06-24 17:25:54 +08:00
2026-06-24 21:02:53 +08:00
// Video ended
video.addEventListener('ended', function() {
livePhoto.classList.remove('is-playing');
isUserToggled = false;
if (AudioManager.currentPlaying === livePhoto) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
});
2026-06-24 20:22:40 +08:00
// Initialize muted state
setMuted(livePhoto.classList.contains('is-muted'));
2026-06-24 17:25:54 +08:00
});
}
// ============================================================
// 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);
2026-06-24 21:02:53 +08:00
bubble.__audio = audio;
2026-06-24 17:25:54 +08:00
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;
2026-06-24 20:22:40 +08:00
bubble.classList.remove('is-playing');
2026-06-24 17:25:54 +08:00
isPlaying = false;
2026-06-24 21:02:53 +08:00
if (AudioManager.currentPlaying === bubble) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
2026-06-24 17:25:54 +08:00
} else {
2026-06-24 21:02:53 +08:00
AudioManager.registerPlay(bubble, 'voice');
2026-06-24 17:25:54 +08:00
audio.play();
2026-06-24 20:22:40 +08:00
bubble.classList.add('is-playing');
2026-06-24 17:25:54 +08:00
isPlaying = true;
}
});
audio.addEventListener('ended', function() {
2026-06-24 20:22:40 +08:00
bubble.classList.remove('is-playing');
2026-06-24 17:25:54 +08:00
isPlaying = false;
2026-06-24 21:02:53 +08:00
if (AudioManager.currentPlaying === bubble) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
2026-06-24 17:25:54 +08:00
});
});
}
// ============================================================
// 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);
2026-06-24 20:22:40 +08:00
card.__audio = audio;
2026-06-24 17:25:54 +08:00
// 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) {
2026-06-24 20:22:40 +08:00
var progress = (audio.currentTime / audio.duration) * 100;
2026-06-24 17:25:54 +08:00
seekInput.value = (audio.currentTime / audio.duration * 1000).toString();
2026-06-24 20:22:40 +08:00
card.style.setProperty('--music-progress', progress + '%');
2026-06-24 17:25:54 +08:00
}
});
// Play/Pause
function togglePlay() {
if (isPlaying) {
audio.pause();
2026-06-24 20:22:40 +08:00
card.setAttribute('data-playing', 'false');
if (playBtn) playBtn.setAttribute('aria-pressed', 'false');
2026-06-24 17:25:54 +08:00
isPlaying = false;
2026-06-24 21:02:53 +08:00
if (AudioManager.currentPlaying === card) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
2026-06-24 17:25:54 +08:00
} else {
2026-06-24 21:02:53 +08:00
AudioManager.registerPlay(card, 'music');
2026-06-24 17:25:54 +08:00
audio.play();
2026-06-24 20:22:40 +08:00
card.setAttribute('data-playing', 'true');
if (playBtn) playBtn.setAttribute('aria-pressed', 'true');
2026-06-24 17:25:54 +08:00
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) {
2026-06-24 20:22:40 +08:00
var progress = (parseFloat(seekInput.value) / 1000) * 100;
2026-06-24 17:25:54 +08:00
audio.currentTime = (parseFloat(seekInput.value) / 1000) * audio.duration;
2026-06-24 20:22:40 +08:00
card.style.setProperty('--music-progress', progress + '%');
2026-06-24 17:25:54 +08:00
}
});
}
// Ended
audio.addEventListener('ended', function() {
2026-06-24 20:22:40 +08:00
card.setAttribute('data-playing', 'false');
if (playBtn) playBtn.setAttribute('aria-pressed', 'false');
2026-06-24 17:25:54 +08:00
isPlaying = false;
2026-06-24 21:02:53 +08:00
if (AudioManager.currentPlaying === card) {
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
}
2026-06-24 17:25:54 +08:00
});
});
}
function formatTime(seconds) {
var m = Math.floor(seconds / 60);
var s = Math.floor(seconds % 60);
return m + ':' + (s < 10 ? '0' : '') + s;
}
2026-06-24 20:22:40 +08:00
// ============================================================
// 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() {
2026-06-24 21:02:53 +08:00
// Save playback state before cleanup
AudioManager.saveState();
2026-06-24 20:22:40 +08:00
cleanupVoiceBubbles();
cleanupMusicCards();
cleanupLivePhotos();
2026-06-24 21:02:53 +08:00
AudioManager.currentPlaying = null;
AudioManager.currentType = null;
2026-06-24 20:22:40 +08:00
}
2026-06-24 17:25:54 +08:00
// ============================================================
// Initialize all shortcodes
// ============================================================
function initAllShortcodes() {
initLivePhotoShortcodes();
initVoiceBubbles();
initMusicCards();
2026-06-24 21:02:53 +08:00
// Restore playback state after initialization
setTimeout(function() {
AudioManager.restoreState();
}, 50);
2026-06-24 17:25:54 +08:00
}
// DOMContentLoaded
document.addEventListener('DOMContentLoaded', initAllShortcodes);
// Page load
window.addEventListener('load', initAllShortcodes);
2026-06-24 20:22:40 +08:00
// 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);
2026-06-24 17:25:54 +08:00
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);
2026-06-24 20:22:40 +08:00
document.addEventListener('turbo:load', initAllShortcodes);
document.addEventListener('turbo:before-render', cleanupAllShortcodes);
// History API based navigation
document.addEventListener('popstate', function() {
cleanupAllShortcodes();
setTimeout(initAllShortcodes, 10);
});