generated from hantim/static-site-template
migrate media to garage, update html references
Build and Push Docker Image / build (push) Successful in 10s
Build and Push Docker Image / build (push) Successful in 10s
This commit is contained in:
@@ -1 +1,2 @@
|
||||
*.swp
|
||||
static/media/
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html><head><meta http-equiv="refresh" content="0; url=https://docs.google.com/document/d/1CNe5J4pQS8NGp3jVk13U8fjM1G8PWJ97USJ6Xnu88W8/edit?usp=sharing" /></head></html>
|
||||
+82
-3
@@ -1,7 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>hcsuzuki.net</title></head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HC Suzuki Violin Class</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #2c1e30; /* Warmer purple-toned background */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.poster-container {
|
||||
margin-top: 10px; /* Default top margin for desktop */
|
||||
max-width: 800px; /* Updated from 500px to 800px */
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.signup-link {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.signup-link a {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.signup-link a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) { /* Updated breakpoint to match new width */
|
||||
body {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.poster-container {
|
||||
max-width: 100%;
|
||||
margin-top: 0; /* Explicitly remove top margin for mobile */
|
||||
}
|
||||
|
||||
.poster-image {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.signup-link {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>hcsuzuki.net</h1>
|
||||
<div class="poster-container">
|
||||
<img src="/media/poster.jpg" alt="Poster Image" class="poster-image">
|
||||
<div class="signup-link">
|
||||
<a href="https://forms.gle/GD8udtMcHGsY9mfe8">Click here to sign up!</a>
|
||||
</div>
|
||||
<div class="signup-link">
|
||||
<a href="mailto:mschoi@hcsuzuki.net">mschoi@hcsuzuki.net</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<!doctype html>
|
||||
<html><head><meta http-equiv="refresh" content="0; url=https://docs.google.com/document/d/1qp2iyhT10miO9S8iIVaogmoDaneBkgv9QAQtfMmOgws/edit?usp=sharing" /></head></html>
|
||||
@@ -0,0 +1,2 @@
|
||||
<html><head><meta http-equiv="refresh" content="0; url=https://forms.gle/HqgGBxk7vF7f65yNA" /></head></html>
|
||||
|
||||
@@ -0,0 +1,509 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Suzuki Violin Class - Practice Videos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* For desktop, limit max-width to maintain mobile aspect ratio */
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#videoContainer {
|
||||
height: calc(100% - 80px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-slide {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-slide.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.video-slide.prev {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain; /* Changed from cover to contain */
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Play button styles */
|
||||
.play-button-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none; /* Hidden by default */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 15;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: white;
|
||||
margin-left: 0px; /* Slight adjustment for visual centering */
|
||||
}
|
||||
|
||||
.mute-button {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mute-icon, .unmute-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.unmute-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#thumbnailBar {
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
border-color: #0095f6;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
background-color: #000; /* Ensure black background extends fully */
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 375px; /* Common mobile width */
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="videoContainer"></div>
|
||||
<div id="thumbnailBar"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Sample video data
|
||||
const videoData = [
|
||||
{
|
||||
src: '/media/violin-class/01/singing-a.mp4',
|
||||
thumbnail: '/media/violin-class/01/singing-a-thumb.jpg',
|
||||
title: 'Sing A looking at the notecard'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/icecream-cone.mp4',
|
||||
thumbnail: '/media/violin-class/01/icecream-cone-thumb.jpg',
|
||||
title: 'New song-Sing and clap ice cream shh cone'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/resting-bow.mp4',
|
||||
thumbnail: '/media/violin-class/01/resting-bow-thumb.jpg',
|
||||
title: 'Resting position and take a bow'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/red-green-feet.mp4',
|
||||
thumbnail: '/media/violin-class/01/red-green-feet-thumb.jpg',
|
||||
title: 'Practice resting position feet (red) and playing position feet (green)'
|
||||
},
|
||||
];
|
||||
|
||||
const videoContainer = document.getElementById('videoContainer');
|
||||
const thumbnailBar = document.getElementById('thumbnailBar');
|
||||
|
||||
let currentVideoIndex = 0;
|
||||
let isMuted = false;
|
||||
|
||||
// Create video slides
|
||||
videoData.forEach((data, index) => {
|
||||
// Create video slide
|
||||
const videoSlide = document.createElement('div');
|
||||
videoSlide.className = `video-slide ${index === 0 ? 'active' : ''}`;
|
||||
videoSlide.dataset.index = index;
|
||||
|
||||
// Create video element
|
||||
const video = document.createElement('video');
|
||||
video.loop = true;
|
||||
video.muted = isMuted;
|
||||
video.playsInline = true;
|
||||
video.dataset.index = index;
|
||||
|
||||
// Create source element
|
||||
const source = document.createElement('source');
|
||||
source.src = data.src;
|
||||
source.type = 'video/mp4';
|
||||
|
||||
// Create title overlay
|
||||
const titleOverlay = document.createElement('div');
|
||||
titleOverlay.className = 'video-title';
|
||||
titleOverlay.textContent = data.title;
|
||||
|
||||
// Create loading overlay
|
||||
const loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.className = 'loading-overlay';
|
||||
loadingOverlay.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
// Create play button overlay
|
||||
const playButtonOverlay = document.createElement('div');
|
||||
playButtonOverlay.className = 'play-button-overlay';
|
||||
playButtonOverlay.innerHTML = `
|
||||
<div class="play-button">
|
||||
<svg class="play-icon" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mute button
|
||||
const muteButton = document.createElement('div');
|
||||
muteButton.className = 'mute-button';
|
||||
muteButton.innerHTML = `
|
||||
<svg class="mute-icon" viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
<svg class="unmute-icon" viewBox="0 0 24 24">
|
||||
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Append elements
|
||||
video.appendChild(source);
|
||||
videoSlide.appendChild(video);
|
||||
videoSlide.appendChild(titleOverlay);
|
||||
videoSlide.appendChild(loadingOverlay);
|
||||
videoSlide.appendChild(playButtonOverlay);
|
||||
videoSlide.appendChild(muteButton);
|
||||
videoContainer.appendChild(videoSlide);
|
||||
|
||||
// Add event listeners for play/pause state
|
||||
video.addEventListener('pause', () => {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
});
|
||||
|
||||
// Play button click handler
|
||||
playButtonOverlay.addEventListener('click', () => {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
});
|
||||
|
||||
video.addEventListener('waiting', () => {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('canplay', () => {
|
||||
loadingOverlay.style.display = 'none';
|
||||
if (video.paused) {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
} else {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Create thumbnail
|
||||
const thumbnail = document.createElement('div');
|
||||
thumbnail.className = `thumbnail ${index === 0 ? 'active' : ''}`;
|
||||
thumbnail.dataset.index = index;
|
||||
|
||||
const thumbnailImg = document.createElement('img');
|
||||
thumbnailImg.src = data.thumbnail;
|
||||
thumbnailImg.alt = `Thumbnail for video ${index + 1}`;
|
||||
|
||||
thumbnail.appendChild(thumbnailImg);
|
||||
thumbnailBar.appendChild(thumbnail);
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Handle thumbnail clicks
|
||||
thumbnailBar.addEventListener('click', (e) => {
|
||||
const thumbnail = e.target.closest('.thumbnail');
|
||||
if (thumbnail) {
|
||||
const newIndex = parseInt(thumbnail.dataset.index);
|
||||
if (newIndex !== currentVideoIndex) {
|
||||
changeVideo(currentVideoIndex, newIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle video tap to pause/play
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName === 'VIDEO') {
|
||||
const video = e.target;
|
||||
if (video.paused) {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mute button clicks
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
const muteButton = e.target.closest('.mute-button');
|
||||
if (muteButton) {
|
||||
e.stopPropagation();
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle swipe
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
|
||||
videoContainer.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
|
||||
videoContainer.addEventListener('touchend', (e) => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
// Add keyboard navigation for desktop
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
// Previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
// Next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (e.key === ' ' || e.key === 'Spacebar') {
|
||||
// Toggle play/pause
|
||||
const currentVideo = document.querySelector(`.video-slide[data-index="${currentVideoIndex}"] video`);
|
||||
if (currentVideo.paused) {
|
||||
currentVideo.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
currentVideo.pause();
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'm') {
|
||||
// Toggle mute
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeThreshold = 50;
|
||||
if (touchEndX + swipeThreshold < touchStartX) {
|
||||
// Swipe left - next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (touchEndX > touchStartX + swipeThreshold) {
|
||||
// Swipe right - previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeVideo(oldIndex, newIndex) {
|
||||
// Update video slides
|
||||
const oldSlide = document.querySelector(`.video-slide[data-index="${oldIndex}"]`);
|
||||
const newSlide = document.querySelector(`.video-slide[data-index="${newIndex}"]`);
|
||||
const oldVideo = oldSlide.querySelector('video');
|
||||
const newVideo = newSlide.querySelector('video');
|
||||
|
||||
// Pause current video
|
||||
oldVideo.pause();
|
||||
|
||||
// Position slides for animation
|
||||
if (newIndex > oldIndex) {
|
||||
oldSlide.classList.remove('active');
|
||||
oldSlide.classList.add('prev');
|
||||
newSlide.classList.add('active');
|
||||
} else {
|
||||
oldSlide.classList.remove('active');
|
||||
newSlide.classList.remove('prev');
|
||||
newSlide.classList.add('active');
|
||||
}
|
||||
|
||||
// Play new video
|
||||
newVideo.muted = isMuted;
|
||||
newVideo.play().catch(e => {
|
||||
console.error("Video play failed:", e);
|
||||
// Show play button on play failure
|
||||
const playButtonOverlay = newSlide.querySelector('.play-button-overlay');
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
// Update thumbnails
|
||||
document.querySelector(`.thumbnail[data-index="${oldIndex}"]`).classList.remove('active');
|
||||
document.querySelector(`.thumbnail[data-index="${newIndex}"]`).classList.add('active');
|
||||
|
||||
// Update current index
|
||||
currentVideoIndex = newIndex;
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
|
||||
// Update all videos
|
||||
document.querySelectorAll('video').forEach(video => {
|
||||
video.muted = isMuted;
|
||||
});
|
||||
|
||||
// Update mute button icons
|
||||
document.querySelectorAll('.mute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'none' : 'block';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.unmute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -0,0 +1,499 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Suzuki Violin Class - Practice Videos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* For desktop, limit max-width to maintain mobile aspect ratio */
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#videoContainer {
|
||||
height: calc(100% - 80px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-slide {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-slide.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.video-slide.prev {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain; /* Changed from cover to contain */
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Play button styles */
|
||||
.play-button-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none; /* Hidden by default */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 15;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: white;
|
||||
margin-left: 0px; /* Slight adjustment for visual centering */
|
||||
}
|
||||
|
||||
.mute-button {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mute-icon, .unmute-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.unmute-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#thumbnailBar {
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
border-color: #0095f6;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
background-color: #000; /* Ensure black background extends fully */
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 375px; /* Common mobile width */
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="videoContainer"></div>
|
||||
<div id="thumbnailBar"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Sample video data
|
||||
const videoData = [
|
||||
{
|
||||
src: '/media/violin-class/02/playing-position.mp4',
|
||||
thumbnail: '/media/violin-class/02/playing-position.jpg',
|
||||
title: 'Playing position<br />Stay still for 5 counts<br />Resting position feet and resting position violin<br />Take a bow'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/02/bowhold-bunny.mp4',
|
||||
thumbnail: '/media/violin-class/02/bowhold-bunny.jpg',
|
||||
title: 'Bowhold Bunny'
|
||||
},
|
||||
];
|
||||
|
||||
const videoContainer = document.getElementById('videoContainer');
|
||||
const thumbnailBar = document.getElementById('thumbnailBar');
|
||||
|
||||
let currentVideoIndex = 0;
|
||||
let isMuted = false;
|
||||
|
||||
// Create video slides
|
||||
videoData.forEach((data, index) => {
|
||||
// Create video slide
|
||||
const videoSlide = document.createElement('div');
|
||||
videoSlide.className = `video-slide ${index === 0 ? 'active' : ''}`;
|
||||
videoSlide.dataset.index = index;
|
||||
|
||||
// Create video element
|
||||
const video = document.createElement('video');
|
||||
video.loop = true;
|
||||
video.muted = isMuted;
|
||||
video.playsInline = true;
|
||||
video.dataset.index = index;
|
||||
|
||||
// Create source element
|
||||
const source = document.createElement('source');
|
||||
source.src = data.src;
|
||||
source.type = 'video/mp4';
|
||||
|
||||
// Create title overlay
|
||||
const titleOverlay = document.createElement('div');
|
||||
titleOverlay.className = 'video-title';
|
||||
titleOverlay.innerHTML = data.title;
|
||||
|
||||
// Create loading overlay
|
||||
const loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.className = 'loading-overlay';
|
||||
loadingOverlay.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
// Create play button overlay
|
||||
const playButtonOverlay = document.createElement('div');
|
||||
playButtonOverlay.className = 'play-button-overlay';
|
||||
playButtonOverlay.innerHTML = `
|
||||
<div class="play-button">
|
||||
<svg class="play-icon" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mute button
|
||||
const muteButton = document.createElement('div');
|
||||
muteButton.className = 'mute-button';
|
||||
muteButton.innerHTML = `
|
||||
<svg class="mute-icon" viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
<svg class="unmute-icon" viewBox="0 0 24 24">
|
||||
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Append elements
|
||||
video.appendChild(source);
|
||||
videoSlide.appendChild(video);
|
||||
videoSlide.appendChild(titleOverlay);
|
||||
videoSlide.appendChild(loadingOverlay);
|
||||
videoSlide.appendChild(playButtonOverlay);
|
||||
videoSlide.appendChild(muteButton);
|
||||
videoContainer.appendChild(videoSlide);
|
||||
|
||||
// Add event listeners for play/pause state
|
||||
video.addEventListener('pause', () => {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
});
|
||||
|
||||
// Play button click handler
|
||||
playButtonOverlay.addEventListener('click', () => {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
});
|
||||
|
||||
video.addEventListener('waiting', () => {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('canplay', () => {
|
||||
loadingOverlay.style.display = 'none';
|
||||
if (video.paused) {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
} else {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Create thumbnail
|
||||
const thumbnail = document.createElement('div');
|
||||
thumbnail.className = `thumbnail ${index === 0 ? 'active' : ''}`;
|
||||
thumbnail.dataset.index = index;
|
||||
|
||||
const thumbnailImg = document.createElement('img');
|
||||
thumbnailImg.src = data.thumbnail;
|
||||
thumbnailImg.alt = `Thumbnail for video ${index + 1}`;
|
||||
|
||||
thumbnail.appendChild(thumbnailImg);
|
||||
thumbnailBar.appendChild(thumbnail);
|
||||
|
||||
|
||||
});
|
||||
|
||||
// Handle thumbnail clicks
|
||||
thumbnailBar.addEventListener('click', (e) => {
|
||||
const thumbnail = e.target.closest('.thumbnail');
|
||||
if (thumbnail) {
|
||||
const newIndex = parseInt(thumbnail.dataset.index);
|
||||
if (newIndex !== currentVideoIndex) {
|
||||
changeVideo(currentVideoIndex, newIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle video tap to pause/play
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName === 'VIDEO') {
|
||||
const video = e.target;
|
||||
if (video.paused) {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mute button clicks
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
const muteButton = e.target.closest('.mute-button');
|
||||
if (muteButton) {
|
||||
e.stopPropagation();
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle swipe
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
|
||||
videoContainer.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
|
||||
videoContainer.addEventListener('touchend', (e) => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
// Add keyboard navigation for desktop
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
// Previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
// Next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (e.key === ' ' || e.key === 'Spacebar') {
|
||||
// Toggle play/pause
|
||||
const currentVideo = document.querySelector(`.video-slide[data-index="${currentVideoIndex}"] video`);
|
||||
if (currentVideo.paused) {
|
||||
currentVideo.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
currentVideo.pause();
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'm') {
|
||||
// Toggle mute
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeThreshold = 50;
|
||||
if (touchEndX + swipeThreshold < touchStartX) {
|
||||
// Swipe left - next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (touchEndX > touchStartX + swipeThreshold) {
|
||||
// Swipe right - previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeVideo(oldIndex, newIndex) {
|
||||
// Update video slides
|
||||
const oldSlide = document.querySelector(`.video-slide[data-index="${oldIndex}"]`);
|
||||
const newSlide = document.querySelector(`.video-slide[data-index="${newIndex}"]`);
|
||||
const oldVideo = oldSlide.querySelector('video');
|
||||
const newVideo = newSlide.querySelector('video');
|
||||
|
||||
// Pause current video
|
||||
oldVideo.pause();
|
||||
|
||||
// Position slides for animation
|
||||
if (newIndex > oldIndex) {
|
||||
oldSlide.classList.remove('active');
|
||||
oldSlide.classList.add('prev');
|
||||
newSlide.classList.add('active');
|
||||
} else {
|
||||
oldSlide.classList.remove('active');
|
||||
newSlide.classList.remove('prev');
|
||||
newSlide.classList.add('active');
|
||||
}
|
||||
|
||||
// Play new video
|
||||
newVideo.muted = isMuted;
|
||||
newVideo.play().catch(e => {
|
||||
console.error("Video play failed:", e);
|
||||
// Show play button on play failure
|
||||
const playButtonOverlay = newSlide.querySelector('.play-button-overlay');
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
// Update thumbnails
|
||||
document.querySelector(`.thumbnail[data-index="${oldIndex}"]`).classList.remove('active');
|
||||
document.querySelector(`.thumbnail[data-index="${newIndex}"]`).classList.add('active');
|
||||
|
||||
// Update current index
|
||||
currentVideoIndex = newIndex;
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
|
||||
// Update all videos
|
||||
document.querySelectorAll('video').forEach(video => {
|
||||
video.muted = isMuted;
|
||||
});
|
||||
|
||||
// Update mute button icons
|
||||
document.querySelectorAll('.mute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'none' : 'block';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.unmute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>Suzuki Violin Class - Practice Videos</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
height: 100dvh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
/* For desktop, limit max-width to maintain mobile aspect ratio */
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#videoContainer {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-slide {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-slide.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.video-slide.prev {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
video {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: contain; /* Changed from cover to contain */
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-title {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 8px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Play button styles */
|
||||
.play-button-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none; /* Hidden by default */
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 15;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.play-button {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
backdrop-filter: blur(2px);
|
||||
transition: transform 0.2s, background-color 0.2s;
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
transform: scale(1.1);
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
fill: white;
|
||||
margin-left: 0px; /* Slight adjustment for visual centering */
|
||||
}
|
||||
|
||||
.mute-button {
|
||||
position: absolute;
|
||||
bottom: 100px;
|
||||
right: 20px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mute-icon, .unmute-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.unmute-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#thumbnailBar {
|
||||
height: 100%px;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
overflow: scroll;
|
||||
left: -400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
/*justify-content: space-around;*/
|
||||
/*align-items: center;*/
|
||||
background-color: rgba(11, 11, 11, 0.8);
|
||||
transition: left 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
#thumbnailBar.show {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
#thumbnailBar #closeButton {
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
border: 2px solid transparent;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.thumbnail.active {
|
||||
border-color: #0095f6;
|
||||
}
|
||||
|
||||
.thumbnail img {
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbnail span{
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Desktop styles */
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
background-color: #000; /* Ensure black background extends fully */
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 375px; /* Common mobile width */
|
||||
max-height: 90vh;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#menu {
|
||||
position: absolute;
|
||||
color: white;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 15px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
background-color: rgba(11,11,11,0.8);
|
||||
transition: left 0.3s ease-in-out;
|
||||
}
|
||||
#menu.hide {
|
||||
left: -400px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="videoContainer"></div>
|
||||
<div id="thumbnailBar"><div id="closeButton">❌ Close</div></div>
|
||||
<div id="menu"><span>☰ All Videos</span></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Sample video data
|
||||
const videoData = [
|
||||
{
|
||||
src: '/media/violin-class/01/singing-a.mp4',
|
||||
thumbnail: '/media/violin-class/01/singing-a-thumb.jpg',
|
||||
title: 'Sing A looking at the notecard'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/icecream-cone.mp4',
|
||||
thumbnail: '/media/violin-class/01/icecream-cone-thumb.jpg',
|
||||
title: 'Sing and clap ice cream shh cone'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/resting-bow.mp4',
|
||||
thumbnail: '/media/violin-class/01/resting-bow-thumb.jpg',
|
||||
title: 'Resting position and take a bow'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/01/red-green-feet.mp4',
|
||||
thumbnail: '/media/violin-class/01/red-green-feet-thumb.jpg',
|
||||
title: 'Practice resting position feet (red) and playing position feet (green)'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/02/playing-position.mp4',
|
||||
thumbnail: '/media/violin-class/02/playing-position.jpg',
|
||||
title: 'Playing position - Stay still for 5 counts<br />Resting position feet and violin<br />Take a bow'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/02/bowhold-bunny.mp4',
|
||||
thumbnail: '/media/violin-class/02/bowhold-bunny.jpg',
|
||||
title: 'Bowhold Bunny'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/sing-clap.mp4',
|
||||
thumbnail: '/media/violin-class/sing-clap.jpg',
|
||||
title: 'Sing and clap Stop Big bow'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/flat-violin.mp4',
|
||||
thumbnail: '/media/violin-class/flat-violin.jpg',
|
||||
title: 'Chicken and the Flat violin. Don\'t drop the chicken!'
|
||||
},
|
||||
{
|
||||
src: '/media/violin-class/bow-with-ty.mp4',
|
||||
thumbnail: '/media/violin-class/bow-with-ty.jpg',
|
||||
title: 'Resting position feet and violin Take a bow and say "Thank you Ms. Choi"'
|
||||
},
|
||||
];
|
||||
|
||||
const videoContainer = document.getElementById('videoContainer');
|
||||
const thumbnailBar = document.getElementById('thumbnailBar');
|
||||
|
||||
let currentVideoIndex = 0;
|
||||
let isMuted = false;
|
||||
|
||||
// Create video slides
|
||||
videoData.forEach((data, index) => {
|
||||
// Create video slide
|
||||
const videoSlide = document.createElement('div');
|
||||
videoSlide.className = `video-slide ${index === 0 ? 'active' : ''}`;
|
||||
videoSlide.dataset.index = index;
|
||||
|
||||
// Create video element
|
||||
const video = document.createElement('video');
|
||||
video.loop = true;
|
||||
video.muted = isMuted;
|
||||
video.playsInline = true;
|
||||
video.dataset.index = index;
|
||||
|
||||
// Create source element
|
||||
const source = document.createElement('source');
|
||||
source.src = data.src;
|
||||
source.type = 'video/mp4';
|
||||
|
||||
// Create title overlay
|
||||
const titleOverlay = document.createElement('div');
|
||||
titleOverlay.className = 'video-title';
|
||||
titleOverlay.innerHTML = data.title;
|
||||
|
||||
// Create loading overlay
|
||||
const loadingOverlay = document.createElement('div');
|
||||
loadingOverlay.className = 'loading-overlay';
|
||||
loadingOverlay.innerHTML = '<div class="spinner"></div>';
|
||||
|
||||
// Create play button overlay
|
||||
const playButtonOverlay = document.createElement('div');
|
||||
playButtonOverlay.className = 'play-button-overlay';
|
||||
playButtonOverlay.innerHTML = `
|
||||
<div class="play-button">
|
||||
<svg class="play-icon" viewBox="0 0 24 24">
|
||||
<path d="M8 5v14l11-7z"/>
|
||||
</svg>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Create mute button
|
||||
const muteButton = document.createElement('div');
|
||||
muteButton.className = 'mute-button';
|
||||
muteButton.innerHTML = `
|
||||
<svg class="mute-icon" viewBox="0 0 24 24">
|
||||
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
|
||||
</svg>
|
||||
<svg class="unmute-icon" viewBox="0 0 24 24">
|
||||
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
// Append elements
|
||||
video.appendChild(source);
|
||||
videoSlide.appendChild(video);
|
||||
videoSlide.appendChild(titleOverlay);
|
||||
videoSlide.appendChild(loadingOverlay);
|
||||
videoSlide.appendChild(playButtonOverlay);
|
||||
videoSlide.appendChild(muteButton);
|
||||
videoContainer.appendChild(videoSlide);
|
||||
|
||||
// Add event listeners for play/pause state
|
||||
video.addEventListener('pause', () => {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
});
|
||||
|
||||
// Play button click handler
|
||||
playButtonOverlay.addEventListener('click', () => {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
});
|
||||
|
||||
video.addEventListener('waiting', () => {
|
||||
loadingOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
video.addEventListener('canplay', () => {
|
||||
loadingOverlay.style.display = 'none';
|
||||
if (video.paused) {
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
} else {
|
||||
playButtonOverlay.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// Create thumbnail
|
||||
const thumbnail = document.createElement('div');
|
||||
thumbnail.className = `thumbnail ${index === 0 ? 'active' : ''}`;
|
||||
thumbnail.dataset.index = index;
|
||||
|
||||
const thumbnailImg = document.createElement('img');
|
||||
thumbnailImg.src = data.thumbnail;
|
||||
thumbnailImg.alt = `Thumbnail for video ${index + 1}`;
|
||||
|
||||
const thumbnailCaption = document.createElement('span');
|
||||
thumbnailCaption.innerHTML = data.title;
|
||||
|
||||
thumbnail.appendChild(thumbnailImg);
|
||||
thumbnail.appendChild(thumbnailCaption);
|
||||
thumbnailBar.appendChild(thumbnail);
|
||||
});
|
||||
|
||||
const close = document.getElementById('closeButton');
|
||||
const menu = document.getElementById('menu');
|
||||
|
||||
close.addEventListener('click', function () {
|
||||
thumbnailBar.classList.toggle('show');
|
||||
menu.classList.toggle('hide');
|
||||
});
|
||||
|
||||
menu.addEventListener('click', function () {
|
||||
thumbnailBar.classList.toggle('show');
|
||||
menu.classList.toggle('hide');
|
||||
});
|
||||
|
||||
// Handle thumbnail clicks
|
||||
thumbnailBar.addEventListener('click', (e) => {
|
||||
const thumbnail = e.target.closest('.thumbnail');
|
||||
if (thumbnail) {
|
||||
const newIndex = parseInt(thumbnail.dataset.index);
|
||||
if (newIndex !== currentVideoIndex) {
|
||||
changeVideo(currentVideoIndex, newIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle video tap to pause/play
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
if (e.target.tagName === 'VIDEO') {
|
||||
const video = e.target;
|
||||
if (video.paused) {
|
||||
video.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
video.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle mute button clicks
|
||||
videoContainer.addEventListener('click', (e) => {
|
||||
const muteButton = e.target.closest('.mute-button');
|
||||
if (muteButton) {
|
||||
e.stopPropagation();
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle swipe
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
|
||||
videoContainer.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
}, { passive: true });
|
||||
|
||||
videoContainer.addEventListener('touchend', (e) => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
handleSwipe();
|
||||
}, { passive: true });
|
||||
|
||||
// Add keyboard navigation for desktop
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowLeft') {
|
||||
// Previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
// Next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (e.key === ' ' || e.key === 'Spacebar') {
|
||||
// Toggle play/pause
|
||||
const currentVideo = document.querySelector(`.video-slide[data-index="${currentVideoIndex}"] video`);
|
||||
if (currentVideo.paused) {
|
||||
currentVideo.play().catch(e => console.error("Video play failed:", e));
|
||||
} else {
|
||||
currentVideo.pause();
|
||||
}
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'm') {
|
||||
// Toggle mute
|
||||
toggleMute();
|
||||
}
|
||||
});
|
||||
|
||||
function handleSwipe() {
|
||||
const swipeThreshold = 50;
|
||||
if (touchEndX + swipeThreshold < touchStartX) {
|
||||
// Swipe left - next video
|
||||
if (currentVideoIndex < videoData.length - 1) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex + 1);
|
||||
}
|
||||
} else if (touchEndX > touchStartX + swipeThreshold) {
|
||||
// Swipe right - previous video
|
||||
if (currentVideoIndex > 0) {
|
||||
changeVideo(currentVideoIndex, currentVideoIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function changeVideo(oldIndex, newIndex) {
|
||||
// Update video slides
|
||||
const oldSlide = document.querySelector(`.video-slide[data-index="${oldIndex}"]`);
|
||||
const newSlide = document.querySelector(`.video-slide[data-index="${newIndex}"]`);
|
||||
const oldVideo = oldSlide.querySelector('video');
|
||||
const newVideo = newSlide.querySelector('video');
|
||||
|
||||
// Pause current video
|
||||
oldVideo.pause();
|
||||
|
||||
// Position slides for animation
|
||||
if (newIndex > oldIndex) {
|
||||
oldSlide.classList.remove('active');
|
||||
oldSlide.classList.add('prev');
|
||||
newSlide.classList.add('active');
|
||||
} else {
|
||||
oldSlide.classList.remove('active');
|
||||
newSlide.classList.remove('prev');
|
||||
newSlide.classList.add('active');
|
||||
}
|
||||
|
||||
// Play new video
|
||||
newVideo.muted = isMuted;
|
||||
newVideo.play().catch(e => {
|
||||
console.error("Video play failed:", e);
|
||||
// Show play button on play failure
|
||||
const playButtonOverlay = newSlide.querySelector('.play-button-overlay');
|
||||
playButtonOverlay.style.display = 'flex';
|
||||
});
|
||||
|
||||
// Update thumbnails
|
||||
document.querySelector(`.thumbnail[data-index="${oldIndex}"]`).classList.remove('active');
|
||||
document.querySelector(`.thumbnail[data-index="${newIndex}"]`).classList.add('active');
|
||||
|
||||
// Update current index
|
||||
currentVideoIndex = newIndex;
|
||||
|
||||
thumbnailBar.classList.remove('show');
|
||||
menu.classList.remove('hide');
|
||||
}
|
||||
|
||||
function toggleMute() {
|
||||
isMuted = !isMuted;
|
||||
|
||||
// Update all videos
|
||||
document.querySelectorAll('video').forEach(video => {
|
||||
video.muted = isMuted;
|
||||
});
|
||||
|
||||
// Update mute button icons
|
||||
document.querySelectorAll('.mute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'none' : 'block';
|
||||
});
|
||||
|
||||
document.querySelectorAll('.unmute-icon').forEach(icon => {
|
||||
icon.style.display = isMuted ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user