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 切换时保存进度
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 持久化 AudioManager 对象本身(防止脚本重载时丢失方法)
|
|
|
|
|
// 第一次加载:定义 AudioManager
|
|
|
|
|
// 后续加载(PJAX 切换):合并新方法到已存在的对象
|
|
|
|
|
if (!window.AudioManager) {
|
|
|
|
|
window.AudioManager = {};
|
|
|
|
|
}
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 用一个 IIFE 来定义 AudioManager 的方法
|
|
|
|
|
(function() {
|
|
|
|
|
var AM = window.AudioManager;
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 一次性定义所有方法
|
|
|
|
|
AM.getAudioState = function() {
|
|
|
|
|
if (!window.__amigoAudioState) {
|
|
|
|
|
window.__amigoAudioState = {
|
|
|
|
|
audio: null,
|
|
|
|
|
src: null,
|
|
|
|
|
isPlaying: false,
|
|
|
|
|
currentTime: 0,
|
|
|
|
|
attached: false
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
return window.__amigoAudioState;
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._getAudio = function() {
|
|
|
|
|
return this.getAudioState().audio;
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._getSrc = function() {
|
|
|
|
|
return this.getAudioState().src;
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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,
|
2026-06-25 15:24:04 +08:00
|
|
|
name: this.globalMusicCard ? this.globalMusicCard.dataset.name : '',
|
2026-06-25 19:35:38 +08:00
|
|
|
artist: this.globalMusicCard ? this.globalMusicCard.dataset.artist : '',
|
|
|
|
|
savedAt: Date.now()
|
2026-06-25 15:24:04 +08:00
|
|
|
};
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
console.log('[AudioManager] saving state:', JSON.stringify(state));
|
|
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
try {
|
2026-06-25 19:35:38 +08:00
|
|
|
sessionStorage.setItem(this.STORAGE_KEY, JSON.stringify(state));
|
2026-06-25 15:24:04 +08:00
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to save music state:', e);
|
|
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM.restoreFromSession = function() {
|
2026-06-25 15:24:04 +08:00
|
|
|
try {
|
|
|
|
|
var stored = sessionStorage.getItem(this.STORAGE_KEY);
|
2026-06-25 19:35:38 +08:00
|
|
|
if (!stored) return null;
|
2026-06-25 15:24:04 +08:00
|
|
|
return JSON.parse(stored);
|
|
|
|
|
} catch (e) {
|
2026-06-25 19:35:38 +08:00
|
|
|
return null;
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM.clearSession = function() {
|
2026-06-25 15:47:04 +08:00
|
|
|
sessionStorage.removeItem(this.STORAGE_KEY);
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:47:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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() {
|
2026-06-25 15:24:04 +08:00
|
|
|
var state = this.restoreFromSession();
|
2026-06-25 19:35:38 +08:00
|
|
|
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);
|
2026-06-25 15:24:04 +08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
var self = this;
|
2026-06-25 19:35:38 +08:00
|
|
|
var doRestore = function() {
|
|
|
|
|
if (state.currentTime > 0 && state.currentTime < audio.duration) {
|
|
|
|
|
audio.currentTime = state.currentTime;
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
|
|
|
|
self._onTimeUpdate();
|
2026-06-25 19:35:38 +08:00
|
|
|
if (state.isPlaying && audio.paused) {
|
|
|
|
|
audio.play().catch(function() {});
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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];
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
return null;
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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) {
|
2026-06-25 15:24:04 +08:00
|
|
|
if (!card) return;
|
|
|
|
|
var audioSrc = card.dataset.src;
|
|
|
|
|
if (!audioSrc) return;
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
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() {});
|
2026-06-25 15:24:04 +08:00
|
|
|
this.globalMusicCard = card;
|
|
|
|
|
this._updateUI(card, true);
|
|
|
|
|
} else {
|
2026-06-25 19:35:38 +08:00
|
|
|
audio.pause();
|
2026-06-25 15:24:04 +08:00
|
|
|
this._updateUI(card, false);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.pauseAll();
|
2026-06-25 19:35:38 +08:00
|
|
|
this._setSrc(audioSrc);
|
|
|
|
|
console.log('[AudioManager] set src:', audioSrc, 'state.src:', this._getSrc());
|
2026-06-25 15:24:04 +08:00
|
|
|
this.globalMusicCard = card;
|
2026-06-25 19:35:38 +08:00
|
|
|
audio.src = audioSrc;
|
2026-06-25 15:24:04 +08:00
|
|
|
this._updateUI(card, true);
|
|
|
|
|
|
|
|
|
|
var self = this;
|
2026-06-25 19:35:38 +08:00
|
|
|
audio.play().then(function() {
|
|
|
|
|
console.log('[AudioManager] Playback started, saving session');
|
2026-06-25 15:24:04 +08:00
|
|
|
self.saveToSession();
|
|
|
|
|
}).catch(function(err) {
|
|
|
|
|
console.warn('Play failed:', err);
|
|
|
|
|
});
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM.seekTo = function(time) {
|
|
|
|
|
var audio = this._getAudio();
|
|
|
|
|
if (audio) {
|
|
|
|
|
audio.currentTime = time;
|
2026-06-25 15:24:04 +08:00
|
|
|
this.saveToSession();
|
|
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._onTimeUpdate = function() {
|
|
|
|
|
var audio = this._getAudio();
|
|
|
|
|
if (!audio || !this.globalMusicCard) return;
|
|
|
|
|
if (audio.__seeking) return;
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
var currentTime = audio.currentTime;
|
|
|
|
|
var duration = audio.duration;
|
2026-06-25 15:24:04 +08:00
|
|
|
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');
|
2026-06-25 19:35:38 +08:00
|
|
|
if (currentTimeEl) currentTimeEl.textContent = formatTime(currentTime);
|
2026-06-25 15:24:04 +08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._onLoadedMetadata = function() {
|
|
|
|
|
var audio = this._getAudio();
|
|
|
|
|
if (!audio || !this.globalMusicCard) return;
|
|
|
|
|
var duration = audio.duration;
|
2026-06-25 15:24:04 +08:00
|
|
|
var durationTimeEl = this.globalMusicCard.querySelector('.amigo-music-card__time--duration');
|
2026-06-25 19:35:38 +08:00
|
|
|
if (durationTimeEl) durationTimeEl.textContent = formatTime(duration);
|
|
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._onEnded = function() {
|
2026-06-25 15:24:04 +08:00
|
|
|
if (this.globalMusicCard) {
|
|
|
|
|
this._updateUI(this.globalMusicCard, false);
|
|
|
|
|
}
|
|
|
|
|
this.globalMusicCard = null;
|
2026-06-25 19:35:38 +08:00
|
|
|
this._setSrc(null);
|
2026-06-25 15:24:04 +08:00
|
|
|
this.clearSession();
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM._updateUI = function(card, isPlaying) {
|
2026-06-25 15:24:04 +08:00
|
|
|
if (!card) return;
|
|
|
|
|
card.setAttribute('data-playing', isPlaying ? 'true' : 'false');
|
|
|
|
|
var playBtn = card.querySelector('.amigo-music-card__play');
|
2026-06-25 19:35:38 +08:00
|
|
|
if (playBtn) playBtn.setAttribute('aria-pressed', isPlaying ? 'true' : 'false');
|
2026-06-25 15:24:04 +08:00
|
|
|
|
|
|
|
|
if (isPlaying) {
|
|
|
|
|
this.currentPlaying = card;
|
|
|
|
|
this.currentType = 'music';
|
|
|
|
|
} else if (this.globalMusicCard === card) {
|
|
|
|
|
this.currentPlaying = null;
|
|
|
|
|
this.currentType = null;
|
|
|
|
|
}
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-24 21:02:53 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM.pauseAll = function() {
|
2026-06-24 21:02:53 +08:00
|
|
|
document.querySelectorAll('.amigo-voice-bubble.is-playing').forEach(function(bubble) {
|
|
|
|
|
if (bubble.__audio) {
|
2026-06-25 19:35:38 +08:00
|
|
|
try { bubble.__audio.pause(); bubble.__audio.currentTime = 0; } catch (e) {}
|
2026-06-24 21:02:53 +08:00
|
|
|
}
|
|
|
|
|
bubble.classList.remove('is-playing');
|
|
|
|
|
});
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
var audio = this._getAudio();
|
|
|
|
|
if (audio && !audio.paused) {
|
|
|
|
|
audio.pause();
|
2026-06-25 15:24:04 +08:00
|
|
|
if (this.globalMusicCard) {
|
|
|
|
|
this._updateUI(this.globalMusicCard, false);
|
2026-06-24 21:02:53 +08:00
|
|
|
}
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
2026-06-24 21:02:53 +08:00
|
|
|
|
|
|
|
|
document.querySelectorAll('.amigo-livephoto.is-playing').forEach(function(livePhoto) {
|
|
|
|
|
var video = livePhoto.querySelector('.amigo-livephoto-video');
|
2026-06-25 19:35:38 +08:00
|
|
|
if (video) { try { video.pause(); video.currentTime = 0; } catch (e) {} }
|
2026-06-24 21:02:53 +08:00
|
|
|
livePhoto.classList.remove('is-playing');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.currentPlaying = null;
|
|
|
|
|
this.currentType = null;
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-24 21:02:53 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
AM.registerPlay = function(element, type) {
|
2026-06-24 21:02:53 +08:00
|
|
|
if (this.currentPlaying && this.currentPlaying !== element) {
|
2026-06-25 19:35:38 +08:00
|
|
|
// 触发其他播放的暂停
|
2026-06-24 21:02:53 +08:00
|
|
|
}
|
|
|
|
|
this.currentPlaying = element;
|
|
|
|
|
this.currentType = type;
|
2026-06-25 19:35:38 +08:00
|
|
|
};
|
2026-06-24 21:02:53 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 初始化基本字段
|
|
|
|
|
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';
|
|
|
|
|
})();
|
2026-06-24 21:02:53 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 保留对 window.AudioManager 的引用
|
|
|
|
|
var AudioManager = window.AudioManager;
|
2026-06-24 21:02:53 +08:00
|
|
|
|
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-06-25 15:24:04 +08:00
|
|
|
// Music Card - PJAX 持久化版本
|
|
|
|
|
// 使用全局 AudioManager 管理音乐播放
|
2026-06-24 17:25:54 +08:00
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
// 不再创建单独的 Audio 对象,使用全局 AudioManager
|
|
|
|
|
// card.__audio = null;
|
2026-06-24 17:25:54 +08:00
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
// 检查是否应该显示播放状态
|
|
|
|
|
// (由 AudioManager 在 restorePlayingState 中处理)
|
2026-06-24 17:25:54 +08:00
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
// Play/Pause - 使用 AudioManager
|
|
|
|
|
function togglePlay(e) {
|
|
|
|
|
if (e) e.stopPropagation();
|
|
|
|
|
AudioManager.playMusic(card);
|
2026-06-24 17:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (playBtn) {
|
2026-06-25 15:24:04 +08:00
|
|
|
playBtn.addEventListener('click', togglePlay);
|
2026-06-24 17:25:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
card.addEventListener('click', togglePlay);
|
|
|
|
|
|
2026-06-25 15:24:04 +08:00
|
|
|
// Seek - 使用 AudioManager
|
2026-06-24 17:25:54 +08:00
|
|
|
if (seekInput) {
|
|
|
|
|
seekInput.addEventListener('input', function() {
|
2026-06-25 19:35:38 +08:00
|
|
|
var audio = AudioManager._getAudio();
|
|
|
|
|
if (audio && audio.duration) {
|
|
|
|
|
var time = (parseFloat(seekInput.value) / 1000) * audio.duration;
|
2026-06-25 15:24:04 +08:00
|
|
|
AudioManager.seekTo(time);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 拖动时暂停更新
|
|
|
|
|
seekInput.addEventListener('mousedown', function() {
|
2026-06-25 19:35:38 +08:00
|
|
|
var audio = AudioManager._getAudio();
|
|
|
|
|
if (audio) {
|
|
|
|
|
audio.__seeking = true;
|
2026-06-25 15:24:04 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
seekInput.addEventListener('mouseup', function() {
|
2026-06-25 19:35:38 +08:00
|
|
|
var audio = AudioManager._getAudio();
|
|
|
|
|
if (audio) {
|
|
|
|
|
audio.__seeking = false;
|
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-25 15:24:04 +08:00
|
|
|
// 保存当前播放状态到 sessionStorage(音乐不中断)
|
|
|
|
|
AudioManager.saveToSession();
|
|
|
|
|
|
|
|
|
|
// 只清理语音气泡和实况照片(音乐由全局 AudioManager 管理)
|
2026-06-24 20:22:40 +08:00
|
|
|
cleanupVoiceBubbles();
|
|
|
|
|
cleanupLivePhotos();
|
2026-06-25 15:24:04 +08:00
|
|
|
|
|
|
|
|
// 不再清理音乐卡片,因为音乐由全局 AudioManager 管理
|
|
|
|
|
// cleanupMusicCards(); // 已移除,音乐在 PJAX 切换时继续播放
|
2026-06-24 20:22:40 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-24 17:25:54 +08:00
|
|
|
// ============================================================
|
|
|
|
|
// Initialize all shortcodes
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 防止 initAllShortcodes 在 PJAX 切换时被重复调用
|
|
|
|
|
var __shortcodesInitLock = false;
|
|
|
|
|
|
2026-06-24 17:25:54 +08:00
|
|
|
function initAllShortcodes() {
|
2026-06-25 19:35:38 +08:00
|
|
|
console.log('[AudioManager] initAllShortcodes called, lock:', __shortcodesInitLock);
|
|
|
|
|
// 防止重复初始化
|
|
|
|
|
if (__shortcodesInitLock) return;
|
|
|
|
|
__shortcodesInitLock = true;
|
|
|
|
|
|
2026-06-25 15:47:04 +08:00
|
|
|
// 初始化 AudioManager(创建全局音频元素)
|
2026-06-25 15:24:04 +08:00
|
|
|
AudioManager.init();
|
|
|
|
|
|
2026-06-24 17:25:54 +08:00
|
|
|
initLivePhotoShortcodes();
|
|
|
|
|
initVoiceBubbles();
|
|
|
|
|
initMusicCards();
|
2026-06-25 15:24:04 +08:00
|
|
|
|
2026-06-25 19:35:38 +08:00
|
|
|
// 检查 sessionStorage 中是否有保存的播放状态
|
|
|
|
|
// 如果有,恢复播放(无论是 PJAX 切换还是脚本重新加载)
|
|
|
|
|
// 如果没有,重置播放器(首次加载或刷新页面)
|
|
|
|
|
var savedState = AudioManager.restoreFromSession();
|
|
|
|
|
console.log('[AudioManager] initAllShortcodes - savedState:', savedState ? JSON.stringify(savedState) : null);
|
|
|
|
|
if (savedState && savedState.src) {
|
2026-06-25 15:47:04 +08:00
|
|
|
setTimeout(function() {
|
|
|
|
|
AudioManager.restorePlayingState();
|
2026-06-25 19:35:38 +08:00
|
|
|
// 延迟释放锁,避免短时间内重复触发
|
|
|
|
|
setTimeout(function() { __shortcodesInitLock = false; }, 500);
|
2026-06-25 15:47:04 +08:00
|
|
|
}, 100);
|
|
|
|
|
} else {
|
2026-06-25 19:35:38 +08:00
|
|
|
// 没有保存的状态,重置播放器到初始状态
|
2026-06-25 15:47:04 +08:00
|
|
|
AudioManager.reset();
|
2026-06-25 19:35:38 +08:00
|
|
|
__shortcodesInitLock = false;
|
2026-06-25 15:47:04 +08:00
|
|
|
}
|
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
|
2026-06-25 15:47:04 +08:00
|
|
|
document.addEventListener('pjax:beforeSend', function() {
|
|
|
|
|
window.__isPjaxNavigation = true;
|
|
|
|
|
cleanupAllShortcodes();
|
|
|
|
|
});
|
2026-06-25 19:35:38 +08:00
|
|
|
document.addEventListener('pjax:complete', function() {
|
|
|
|
|
initAllShortcodes();
|
|
|
|
|
});
|
|
|
|
|
document.addEventListener('pjax:success', function() {
|
|
|
|
|
// pjax:success 在 pjax:complete 之前触发,已经被锁过滤
|
|
|
|
|
initAllShortcodes();
|
|
|
|
|
});
|
2026-06-24 17:25:54 +08:00
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
});
|