migrate media to garage, update html references
Build and Push Docker Image / build (push) Successful in 10s

This commit is contained in:
2026-03-19 22:22:54 -04:00
parent 1ba1fcd463
commit a77da6f755
8 changed files with 1689 additions and 3 deletions
+592
View File
@@ -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>