Blurred layer
This commit is contained in:
@@ -9,31 +9,53 @@
|
||||
<body>
|
||||
<!-- Navbar -->
|
||||
<nav class="navbar">
|
||||
<button class="nav-btn" id="translateBtn" title="Toggle Translation">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2">
|
||||
<path d="M12 20h9"/>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span class="nav-title"><%= currentEpisode.title %></span>
|
||||
<button class="nav-btn" id="menuBtn" title="Episodes">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2">
|
||||
<path d="M3 12h18"/>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M3 18h18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="nav-left">
|
||||
<button class="nav-btn" id="translateBtn" title="Toggle Translation">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2">
|
||||
<path d="M12 20h9"/>
|
||||
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" id="goToBtn" title="Go to Current Line">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2.5">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<span class="nav-title" id="timeDisplay">00:00</span>
|
||||
<div class="nav-right">
|
||||
<button class="nav-btn" id="playPauseBtn" title="Play/Pause">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" id="playIcon">
|
||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2" id="pauseIcon" style="display: none;">
|
||||
<rect x="6" y="4" width="4" height="16"/>
|
||||
<rect x="14" y="4" width="4" height="16"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-btn" id="menuBtn" title="Episodes">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke-width="2">
|
||||
<path d="M3 12h18"/>
|
||||
<path d="M3 6h18"/>
|
||||
<path d="M3 18h18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content">
|
||||
<div class="lines-container">
|
||||
<% if (lines && lines.length > 0) { %>
|
||||
<% lines.forEach(line => { %>
|
||||
<% lines.forEach((line, index) => { %>
|
||||
<div class="line"
|
||||
data-index="<%= index %>"
|
||||
data-chinese="<%= escapeHtml(line.chinese) %>"
|
||||
data-timestamp="<%= escapeHtml(line.timestamp) %>"
|
||||
data-speaker="<%= escapeHtml(line.speaker) %>">
|
||||
data-speaker="<%= escapeHtml(line.speaker) %>"
|
||||
data-time-seconds="<%= line.timeSeconds %>">
|
||||
<div class="blur-overlay"></div>
|
||||
<div class="english" style="color: <%= colors[line.speaker] || '#777777' %>">
|
||||
<%= line.english %>
|
||||
</div>
|
||||
@@ -71,7 +93,7 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Minimal JavaScript for interactions -->
|
||||
<!-- JavaScript for interactions -->
|
||||
<script>
|
||||
(function() {
|
||||
// DOM Elements
|
||||
@@ -81,29 +103,290 @@
|
||||
const sideMenu = document.getElementById('sideMenu');
|
||||
const sideMenuOverlay = document.getElementById('sideMenuOverlay');
|
||||
const lines = document.querySelectorAll('.line');
|
||||
const timeDisplay = document.getElementById('timeDisplay');
|
||||
const playPauseBtn = document.getElementById('playPauseBtn');
|
||||
const playIcon = document.getElementById('playIcon');
|
||||
const pauseIcon = document.getElementById('pauseIcon');
|
||||
const goToBtn = document.getElementById('goToBtn');
|
||||
|
||||
let selectedLine = null;
|
||||
let expandedLine = null;
|
||||
let isPlaying = false;
|
||||
let currentTime = 0;
|
||||
let playbackInterval = null;
|
||||
let currentLineIndex = 0;
|
||||
let lastTimestamp = Date.now();
|
||||
|
||||
// Line selection
|
||||
lines.forEach(line => {
|
||||
line.addEventListener('click', function(e) {
|
||||
// Remove selected class from all lines
|
||||
lines.forEach(l => l.classList.remove('selected'));
|
||||
|
||||
// Add selected class to clicked line
|
||||
this.classList.add('selected');
|
||||
selectedLine = this;
|
||||
// Parse time from seconds to mm:ss
|
||||
function formatTime(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Get time in seconds from line element
|
||||
function getLineTimeSeconds(line) {
|
||||
return parseFloat(line.dataset.timeSeconds) || 0;
|
||||
}
|
||||
|
||||
// Find current line index based on time
|
||||
function findCurrentLineIndex(time) {
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const lineTime = getLineTimeSeconds(lines[i]);
|
||||
if (lineTime <= time) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Update blur states based on current line
|
||||
function updateBlurStates() {
|
||||
// Cancel any active drag when updating blur states
|
||||
activeDrag = null;
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
if (index < currentLineIndex) {
|
||||
// Lines before current are clear
|
||||
line.classList.remove('blurred');
|
||||
line.classList.add('clear');
|
||||
} else {
|
||||
// Current line and after are blurred
|
||||
line.classList.add('blurred');
|
||||
line.classList.remove('clear');
|
||||
// Reset blur overlay position when blur is reapplied
|
||||
const blurOverlay = line.querySelector('.blur-overlay');
|
||||
if (blurOverlay) {
|
||||
blurOverlay.style.transform = '';
|
||||
blurOverlay.style.opacity = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update visual selection
|
||||
function updateSelection(index) {
|
||||
lines.forEach(l => l.classList.remove('selected'));
|
||||
if (index >= 0 && index < lines.length) {
|
||||
lines[index].classList.add('selected');
|
||||
selectedLine = lines[index];
|
||||
currentLineIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll line into view
|
||||
function scrollToLine(line) {
|
||||
line.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
|
||||
// Update time display
|
||||
function updateTimeDisplay() {
|
||||
timeDisplay.textContent = formatTime(currentTime);
|
||||
}
|
||||
|
||||
// Pause playback
|
||||
function pausePlayback() {
|
||||
if (!isPlaying) return;
|
||||
isPlaying = false;
|
||||
clearInterval(playbackInterval);
|
||||
playIcon.style.display = 'block';
|
||||
pauseIcon.style.display = 'none';
|
||||
}
|
||||
|
||||
// Resume playback
|
||||
function resumePlayback() {
|
||||
if (isPlaying) return;
|
||||
isPlaying = true;
|
||||
lastTimestamp = Date.now();
|
||||
playIcon.style.display = 'none';
|
||||
pauseIcon.style.display = 'block';
|
||||
|
||||
playbackInterval = setInterval(() => {
|
||||
const now = Date.now();
|
||||
const delta = (now - lastTimestamp) / 1000;
|
||||
lastTimestamp = now;
|
||||
|
||||
currentTime += delta;
|
||||
updateTimeDisplay();
|
||||
|
||||
// Check if we need to advance to next line
|
||||
const nextIndex = currentLineIndex + 1;
|
||||
if (nextIndex < lines.length) {
|
||||
const nextLineTime = getLineTimeSeconds(lines[nextIndex]);
|
||||
if (currentTime >= nextLineTime) {
|
||||
// Time reached next line - update without pausing
|
||||
currentLineIndex = nextIndex;
|
||||
updateSelection(currentLineIndex);
|
||||
updateBlurStates();
|
||||
scrollToLine(lines[currentLineIndex]);
|
||||
}
|
||||
} else {
|
||||
// End of episode
|
||||
pausePlayback();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Toggle play/pause
|
||||
function togglePlayPause() {
|
||||
if (isPlaying) {
|
||||
pausePlayback();
|
||||
} else {
|
||||
resumePlayback();
|
||||
}
|
||||
}
|
||||
|
||||
// Go to current line's timestamp
|
||||
function goToCurrentLine() {
|
||||
if (selectedLine) {
|
||||
// Pause playback when going to a specific line
|
||||
pausePlayback();
|
||||
currentTime = getLineTimeSeconds(selectedLine);
|
||||
updateTimeDisplay();
|
||||
// Update blur states per design 2.5
|
||||
const selectedIndex = parseInt(selectedLine.dataset.index);
|
||||
currentLineIndex = selectedIndex;
|
||||
updateBlurStates();
|
||||
scrollToLine(selectedLine);
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive drag-to-reveal for blur overlay
|
||||
const SWIPE_THRESHOLD = 160; // pixels to trigger reveal
|
||||
let activeDrag = null; // Track which line is being dragged
|
||||
|
||||
// Initialize - all lines blurred by default
|
||||
updateBlurStates();
|
||||
|
||||
lines.forEach((line, index) => {
|
||||
let startX = 0;
|
||||
let currentX = 0;
|
||||
let blurOverlay = line.querySelector('.blur-overlay');
|
||||
|
||||
line.addEventListener('click', function(e) {
|
||||
// Don't trigger selection if we just finished a drag
|
||||
if (line._didDrag) {
|
||||
line._didDrag = false;
|
||||
return;
|
||||
}
|
||||
updateSelection(index);
|
||||
});
|
||||
|
||||
// Touch events
|
||||
line.addEventListener('touchstart', function(e) {
|
||||
if (!line.classList.contains('blurred')) return;
|
||||
activeDrag = line;
|
||||
line._didDrag = false;
|
||||
startX = e.touches[0].clientX;
|
||||
currentX = startX;
|
||||
line.style.transition = 'none';
|
||||
blurOverlay.style.transition = 'none';
|
||||
}, { passive: true });
|
||||
|
||||
line.addEventListener('touchmove', function(e) {
|
||||
if (activeDrag !== line) return;
|
||||
currentX = e.touches[0].clientX;
|
||||
const deltaX = currentX - startX;
|
||||
|
||||
// Mark as drag if moved more than 5px
|
||||
if (Math.abs(deltaX) > 5) {
|
||||
line._didDrag = true;
|
||||
}
|
||||
|
||||
// Move blur overlay with finger - no limit, follows finger exactly
|
||||
blurOverlay.style.transform = `translateX(${deltaX}px)`;
|
||||
}, { passive: true });
|
||||
|
||||
line.addEventListener('touchend', function(e) {
|
||||
if (activeDrag !== line) return;
|
||||
const deltaX = currentX - startX;
|
||||
endDrag(line, blurOverlay, deltaX);
|
||||
}, { passive: true });
|
||||
|
||||
line.addEventListener('touchcancel', function(e) {
|
||||
if (activeDrag !== line) return;
|
||||
const deltaX = currentX - startX;
|
||||
endDrag(line, blurOverlay, deltaX);
|
||||
}, { passive: true });
|
||||
|
||||
// Mouse events for desktop
|
||||
line.addEventListener('mousedown', function(e) {
|
||||
if (!line.classList.contains('blurred')) return;
|
||||
activeDrag = line;
|
||||
line._didDrag = false;
|
||||
line._startX = e.clientX;
|
||||
line._currentX = line._startX;
|
||||
line.style.transition = 'none';
|
||||
blurOverlay.style.transition = 'none';
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Store line reference on element for global handlers
|
||||
line._blurOverlay = blurOverlay;
|
||||
line._startX = 0;
|
||||
line._currentX = 0;
|
||||
line._didDrag = false;
|
||||
});
|
||||
|
||||
// Global mouse handlers (single instance)
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (!activeDrag) return;
|
||||
const line = activeDrag;
|
||||
const blurOverlay = line._blurOverlay;
|
||||
line._currentX = e.clientX;
|
||||
const deltaX = line._currentX - line._startX;
|
||||
|
||||
// Mark as drag if moved more than 5px
|
||||
if (Math.abs(deltaX) > 5) {
|
||||
line._didDrag = true;
|
||||
}
|
||||
|
||||
// Move blur overlay with mouse - no limit, follows cursor exactly
|
||||
blurOverlay.style.transform = `translateX(${deltaX}px)`;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', function(e) {
|
||||
if (!activeDrag) return;
|
||||
const line = activeDrag;
|
||||
const blurOverlay = line._blurOverlay;
|
||||
const deltaX = line._currentX - line._startX;
|
||||
endDrag(line, blurOverlay, deltaX);
|
||||
});
|
||||
|
||||
function endDrag(line, blurOverlay, deltaX) {
|
||||
activeDrag = null;
|
||||
|
||||
// Restore transitions
|
||||
line.style.transition = '';
|
||||
blurOverlay.style.transition = 'transform 0.3s ease';
|
||||
|
||||
if (Math.abs(deltaX) >= SWIPE_THRESHOLD) {
|
||||
// Threshold reached - animate blur layer fully away then reveal
|
||||
const direction = deltaX > 0 ? 1 : -1;
|
||||
blurOverlay.style.transform = `translateX(${direction * window.innerWidth}px)`;
|
||||
|
||||
setTimeout(() => {
|
||||
line.classList.remove('blurred');
|
||||
line.classList.add('clear');
|
||||
blurOverlay.style.transform = '';
|
||||
}, 300);
|
||||
} else {
|
||||
// Not past threshold - snap back to initial state
|
||||
blurOverlay.style.transform = 'translateX(0)';
|
||||
// Reset transform after animation completes
|
||||
setTimeout(() => {
|
||||
if (line.classList.contains('blurred')) {
|
||||
blurOverlay.style.transform = '';
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle translation on selected line
|
||||
translateBtn.addEventListener('click', function() {
|
||||
if (!selectedLine) {
|
||||
// If no line selected, select the first one
|
||||
if (lines.length > 0) {
|
||||
lines[0].classList.add('selected');
|
||||
selectedLine = lines[0];
|
||||
updateSelection(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,17 +394,14 @@
|
||||
const translationCard = selectedLine.querySelector('.translation-card');
|
||||
|
||||
if (translationCard) {
|
||||
// If already expanded, collapse it
|
||||
translationCard.remove();
|
||||
expandedLine = null;
|
||||
} else {
|
||||
// Collapse any previously expanded line
|
||||
if (expandedLine) {
|
||||
const prevCard = expandedLine.querySelector('.translation-card');
|
||||
if (prevCard) prevCard.remove();
|
||||
}
|
||||
|
||||
// Expand the selected line
|
||||
const chineseText = selectedLine.dataset.chinese;
|
||||
const timestamp = selectedLine.dataset.timestamp;
|
||||
const speaker = selectedLine.dataset.speaker;
|
||||
@@ -139,7 +419,6 @@
|
||||
selectedLine.appendChild(card);
|
||||
expandedLine = selectedLine;
|
||||
|
||||
// Scroll the expanded card into view smoothly
|
||||
setTimeout(() => {
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}, 100);
|
||||
@@ -148,6 +427,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Play/Pause button
|
||||
playPauseBtn.addEventListener('click', togglePlayPause);
|
||||
|
||||
// Go to button
|
||||
goToBtn.addEventListener('click', goToCurrentLine);
|
||||
|
||||
// Open side menu
|
||||
function openMenu() {
|
||||
sideMenu.classList.add('active');
|
||||
@@ -179,6 +464,11 @@
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Select first line by default
|
||||
if (lines.length > 0) {
|
||||
updateSelection(0);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user