diff --git a/themes/Ying/assets/css/shortcodes.css b/themes/Ying/assets/css/shortcodes.css new file mode 100644 index 00000000..312a7f18 --- /dev/null +++ b/themes/Ying/assets/css/shortcodes.css @@ -0,0 +1,345 @@ +/* ============================================================ + Shortcodes 样式 - 实况照片、语音、音乐卡片 + 移植自 Hugo-Theme-Amigo + ============================================================ */ + +/* ---- Live Photo ---- */ +.live-photo { + --border-radius: 4px; + --live-photo-aspect: 4 / 5; + position: relative; + overflow: hidden; + border-radius: var(--border-radius); + width: 100%; + max-width: min(360px, 100%); + margin: 12px auto; + aspect-ratio: var(--live-photo-aspect); + background: #f5f5f5; + container-type: inline-size; +} + +.live-photo .live-photo-video { + position: absolute; + display: block; + width: 100%; + height: 100%; + object-fit: cover; + inset: 0; + opacity: 0; + transition: opacity 0.2s ease; + z-index: 1; +} + +.live-photo .live-photo-poster { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + opacity: 1; + transition: opacity 0.25s ease; + z-index: 0; +} + +.live-photo.is-playing .live-photo-video { + opacity: 1; +} + +.live-photo.is-playing .live-photo-poster { + opacity: 0; +} + +.live-photo .live-photo-controls-group { + position: absolute; + left: 10px; + top: 10px; + display: flex; + align-items: center; + gap: 6px; + z-index: 2; +} + +.live-photo .live-photo-control-btn { + border: none; + padding: 4px 8px; + border-radius: var(--border-radius); + height: 24px; + display: inline-flex; + align-items: center; + gap: 6px; + background-color: transparent; + color: #606060; + cursor: pointer; + user-select: none; + border: 1px solid rgba(255, 255, 255, 0.7); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + transition: transform 0.18s ease, box-shadow 0.18s ease, background-color 0.18s ease; +} + +.live-photo .live-photo-control-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); + background-color: transparent; +} + +.live-photo .live-photo-toggle-btn { + background-color: rgba(44, 44, 46, 0.62); + border-color: rgba(255, 255, 255, 0.22); + color: rgb(205 205 205 / 92%); +} + +.live-photo .live-photo-toggle-btn:hover { + background-color: rgba(44, 44, 46, 0.72); +} + +.live-photo .live-photo-mute-btn { + background-color: rgba(44, 44, 46, 0.35); + border-color: rgba(255, 255, 255, 0.18); + color: rgb(205 205 205 / 92%); +} + +.live-photo .live-photo-mute-btn:hover { + background-color: rgba(44, 44, 46, 0.42); +} + +.live-photo-mute-btn .live-photo-mute-icon { + display: block; + width: 18px; + height: 18px; + font-size: 15px; + line-height: 19px; + color: rgb(205 205 205 / 92%); +} + +.live-photo-mute-btn[data-muted="true"] .icon-unmuted { + display: none; +} + +.live-photo-mute-btn[data-muted="false"] .icon-muted { + display: none; +} + +/* ---- Voice Bubble ---- */ +.amigo-voice-bubble { + display: inline-flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: linear-gradient(135deg, #95ec69 0%, #7dd652 100%); + border-radius: 16px; + cursor: pointer; + position: relative; + max-width: 200px; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.amigo-voice-bubble:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.amigo-voice-bubble:active { + transform: translateY(0); +} + +.amigo-voice-icon { + display: flex; + align-items: center; + gap: 2px; + height: 20px; +} + +.amigo-voice-icon span { + display: block; + width: 3px; + background: #2d2d2d; + border-radius: 2px; + animation: voiceWave 1.2s ease-in-out infinite; +} + +.amigo-voice-icon span:nth-child(1) { + height: 8px; + animation-delay: 0s; +} + +.amigo-voice-icon span:nth-child(2) { + height: 14px; + animation-delay: 0.2s; +} + +.amigo-voice-icon span:nth-child(3) { + height: 10px; + animation-delay: 0.4s; +} + +.amigo-voice-bubble.playing .amigo-voice-icon span { + animation: voiceWaveActive 0.6s ease-in-out infinite; +} + +.amigo-voice-duration { + font-size: 14px; + color: #2d2d2d; + font-weight: 500; + min-width: 24px; +} + +.amigo-voice-unread { + position: absolute; + top: -4px; + right: -4px; + width: 8px; + height: 8px; + background: #ff4d4f; + border-radius: 50%; + border: 2px solid #fff; +} + +@keyframes voiceWave { + 0%, 100% { transform: scaleY(0.5); } + 50% { transform: scaleY(1); } +} + +@keyframes voiceWaveActive { + 0%, 100% { transform: scaleY(0.3); } + 50% { transform: scaleY(1); } +} + +/* ---- Music Card ---- */ +.amigo-music-card { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px; + max-width: 400px; + margin: 16px 0; + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.amigo-music-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.6); +} + +.amigo-music-card__art { + position: relative; + width: 80px; + height: 80px; + flex-shrink: 0; +} + +.amigo-music-card__cover { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 8px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); +} + +.amigo-music-card__play { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); + border-radius: 8px; + border: none; + cursor: pointer; + opacity: 0; + transition: opacity 0.2s ease; +} + +.amigo-music-card__art:hover .amigo-music-card__play { + opacity: 1; +} + +.amigo-music-card__play-icon { + width: 36px; + height: 36px; + background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E") no-repeat center; + background-size: contain; +} + +.amigo-music-card__meta { + flex: 1; + min-width: 0; +} + +.amigo-music-card__title { + display: block; + font-size: 16px; + font-weight: 600; + color: #fff; + margin-bottom: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.amigo-music-card__artist { + display: block; + font-size: 14px; + color: rgba(255, 255, 255, 0.8); + margin-bottom: 12px; +} + +.amigo-music-card__timeline { + display: flex; + align-items: center; + gap: 8px; +} + +.amigo-music-card__time { + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + min-width: 36px; +} + +.amigo-music-card__seek-wrap { + flex: 1; + position: relative; +} + +.amigo-music-card__seek { + width: 100%; + height: 4px; + -webkit-appearance: none; + appearance: none; + background: rgba(255, 255, 255, 0.3); + border-radius: 2px; + outline: none; + cursor: pointer; +} + +.amigo-music-card__seek::-webkit-slider-thumb { + -webkit-appearance: none; + width: 12px; + height: 12px; + background: #fff; + border-radius: 50%; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +/* Dark theme support */ +[data-theme="dark"] .live-photo { + background: #1a1a1a; +} + +[data-theme="dark"] .amigo-voice-bubble { + background: linear-gradient(135deg, #3d3d3d 0%, #2d2d2d 100%); +} + +[data-theme="dark"] .amigo-voice-icon span, +[data-theme="dark"] .amigo-voice-duration { + color: #fff; +} + +[data-theme="dark"] .amigo-music-card { + background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%); +} diff --git a/themes/Ying/assets/js/shortcodes.js b/themes/Ying/assets/js/shortcodes.js new file mode 100644 index 00000000..4a9b3ab1 --- /dev/null +++ b/themes/Ying/assets/js/shortcodes.js @@ -0,0 +1,282 @@ +/** + * Shortcodes JavaScript - 实况照片、语音、音乐卡片 + * 移植自 Hugo-Theme-Amigo + */ + +// ============================================================ +// Live Photo +// ============================================================ + +function initLivePhotoShortcodes() { + document.querySelectorAll('.live-photo').forEach(function(livePhoto) { + if (livePhoto.__liveBound) return; + livePhoto.__liveBound = true; + + var video = livePhoto.querySelector('video.live-photo-video') || livePhoto.querySelector('video'); + var posterImg = livePhoto.querySelector('img.live-photo-poster') || livePhoto.querySelector('img'); + var toggleBtn = livePhoto.querySelector('.live-photo-toggle-btn'); + var muteBtn = livePhoto.querySelector('.live-photo-mute-btn'); + + if (!video || !toggleBtn || !muteBtn) return; + + var HOVER_DELAY = 500; + var hoverTimer = null; + var isLoaded = false; + + function syncAspectFromPoster() { + if (!posterImg) return; + var w = posterImg.naturalWidth || 0; + var h = posterImg.naturalHeight || 0; + if (!w || !h) return; + livePhoto.style.setProperty('--live-photo-aspect', w + ' / ' + h); + } + + if (posterImg && posterImg.complete) { + syncAspectFromPoster(); + } else if (posterImg) { + posterImg.addEventListener('load', syncAspectFromPoster, { once: true }); + } + + 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', ''); + else video.removeAttribute('muted'); + muteBtn.setAttribute('data-muted', isMuted ? 'true' : 'false'); + } + + setMuted(true); + + function playVideo() { + ensureLoaded(); + livePhoto.classList.add('is-playing'); + toggleBtn.setAttribute('data-state', 'live'); + try { video.play(); } catch (e) {} + } + + function stopVideo() { + livePhoto.classList.remove('is-playing'); + toggleBtn.setAttribute('data-state', 'static'); + try { video.pause(); video.currentTime = 0; } catch (e) {} + } + + // Toggle button click + toggleBtn.addEventListener('click', function(e) { + e.stopPropagation(); + if (livePhoto.classList.contains('is-playing')) { + stopVideo(); + } else { + playVideo(); + } + }); + + // Mute button click + muteBtn.addEventListener('click', function(e) { + e.stopPropagation(); + setMuted(!video.muted); + }); + + // Hover to play + livePhoto.addEventListener('mouseenter', function() { + hoverTimer = setTimeout(playVideo, HOVER_DELAY); + }); + + livePhoto.addEventListener('mouseleave', function() { + clearTimeout(hoverTimer); + if (toggleBtn.getAttribute('data-state') !== 'live') { + stopVideo(); + } + }); + + // Mobile touch + livePhoto.addEventListener('touchstart', function() { + if (livePhoto.classList.contains('is-playing')) { + stopVideo(); + } else { + playVideo(); + } + }); + + // Video ended + video.addEventListener('ended', function() { + if (toggleBtn.getAttribute('data-state') !== 'live') { + stopVideo(); + } + }); + }); +} + +// ============================================================ +// 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); + 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('playing'); + isPlaying = false; + } else { + // Stop other playing voices + document.querySelectorAll('.amigo-voice-bubble.playing').forEach(function(other) { + if (other !== bubble) { + other.classList.remove('playing'); + } + }); + + audio.play(); + bubble.classList.add('playing'); + isPlaying = true; + } + }); + + audio.addEventListener('ended', function() { + bubble.classList.remove('playing'); + isPlaying = false; + }); + }); +} + +// ============================================================ +// 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); + + // 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) { + seekInput.value = (audio.currentTime / audio.duration * 1000).toString(); + } + }); + + // Play/Pause + function togglePlay() { + if (isPlaying) { + audio.pause(); + card.classList.remove('playing'); + isPlaying = false; + } else { + // Stop other playing music + document.querySelectorAll('.amigo-music-card.playing').forEach(function(other) { + if (other !== card) { + other.classList.remove('playing'); + } + }); + + audio.play(); + card.classList.add('playing'); + 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) { + audio.currentTime = (parseFloat(seekInput.value) / 1000) * audio.duration; + } + }); + } + + // Ended + audio.addEventListener('ended', function() { + card.classList.remove('playing'); + isPlaying = false; + }); + }); +} + +function formatTime(seconds) { + var m = Math.floor(seconds / 60); + var s = Math.floor(seconds % 60); + return m + ':' + (s < 10 ? '0' : '') + s; +} + +// ============================================================ +// Initialize all shortcodes +// ============================================================ + +function initAllShortcodes() { + initLivePhotoShortcodes(); + initVoiceBubbles(); + initMusicCards(); +} + +// DOMContentLoaded +document.addEventListener('DOMContentLoaded', initAllShortcodes); + +// Page load +window.addEventListener('load', initAllShortcodes); + +// PJAX support - re-initialize after page transition +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); diff --git a/themes/Ying/layouts/partials/footer.html b/themes/Ying/layouts/partials/footer.html index 009094f8..8d0f3fa4 100644 --- a/themes/Ying/layouts/partials/footer.html +++ b/themes/Ying/layouts/partials/footer.html @@ -170,8 +170,9 @@ {{/* ====== 5. 延迟加载的非关键JS ====== */}} {{ $toast := resources.Get "js/modules/toast.js" }} {{ $easylazyload := resources.Get "js/modules/lazyload.js" }} +{{ $shortcodesJs := resources.Get "js/shortcodes.js" }} -{{ $deferredScripts := slice $toast $easylazyload | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }} +{{ $deferredScripts := slice $toast $easylazyload $shortcodesJs | resources.Concat "js/deferred.js" | resources.Minify | resources.Fingerprint }}