From 15715dd76d3e6c0a4c058a4876a15f59cfff0a68 Mon Sep 17 00:00:00 2001 From: Ryan Feigenbaum <48868107+royalfig@users.noreply.github.com> Date: Fri, 4 Aug 2023 16:19:08 -0400 Subject: [PATCH] Playlist (#356) * playlist * progress * parse seconds * muuuusic --- assets/css/abstracts/grid.css | 7 +- assets/css/components/playlist.css | 228 +++++++++++++++++++ assets/css/index.css | 1 + assets/css/pages/home.css | 65 ------ assets/images/noise.svg | 7 + assets/js/app/app.js | 27 ++- assets/js/app/musicPages.js | 183 +++++++++++++-- home.hbs | 51 +---- partials/playlist.hbs | 352 +++++++++++++++++++++++++++++ partials/svg.hbs | 49 ++++ rollup.config.js | 6 +- 11 files changed, 824 insertions(+), 152 deletions(-) create mode 100644 assets/css/components/playlist.css create mode 100644 assets/images/noise.svg create mode 100644 partials/playlist.hbs diff --git a/assets/css/abstracts/grid.css b/assets/css/abstracts/grid.css index b7066ba1..05ba33f6 100644 --- a/assets/css/abstracts/grid.css +++ b/assets/css/abstracts/grid.css @@ -42,17 +42,18 @@ margin-block: var(--spacing-04); } -.sm-music-grid { +.sm-playlist-grid { grid-template-columns: 1fr; grid-column: content; gap: var(--spacing-06); margin-block: var(--spacing-04); overflow: hidden; - background-color: hsl(var(--surface-dark)); + color: #fff; + background-color: #000314; border-radius: var(--radius); @media (--tablet) { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: max-content 1fr; } } diff --git a/assets/css/components/playlist.css b/assets/css/components/playlist.css new file mode 100644 index 00000000..8c1e51ae --- /dev/null +++ b/assets/css/components/playlist.css @@ -0,0 +1,228 @@ +.sm-playlist-grid { + position: relative; + padding: var(--spacing-08); + overflow: hidden; + + &.sm-playing { + background: none; + } + + &::before { + position: absolute; + inset: 0; + content: ''; + background-image: var(--bg); + filter: blur(15px); + background-position-x: center; + opacity: 0; + transition: opacity 500ms cubic-bezier(0.075, 0.82, 0.165, 1); + scale: 1.5; + } + + &.sm-playing::before { + opacity: 1; + } + + h3 { + margin: 0; + font-family: var(--body-typeface); + font-size: var(--h4); + font-style: italic; + font-weight: 400; + } + + p { + margin-block-end: var(--spacing-04); + font-family: var(--ui-typeface); + font-size: var(--xx-small); + font-weight: 300; + text-transform: uppercase; + letter-spacing: var(--letter-spacing-expand); + } + + ol { + min-height: 16rem; + padding-inline-start: 0; + margin-block-end: var(--spacing-04); + font-size: var(--small); + list-style-type: none; + counter-reset: li; + } + + li::before { + margin-inline-end: var(--spacing-01); + font-variant-numeric: tabular-nums; + content: counter(li, decimal-leading-zero) '.'; + content: attr(data-track) '.'; + counter-increment: li; + } + + .sm-active::before { + color: hsl(var(--color-1)); + content: '▶️'; + } + + .sm-playlist-iframe { + display: none; + } +} + +.sm-playlist-item { + text-align: left; +} + +.sm-playlist-item-artist { + &::before { + content: '— '; + opacity: 0.5; + } +} + +.sm-playlist-text { + position: relative; +} + +.sm-playlist-controls { + position: relative; + display: flex; + flex-direction: column; + gap: var(--spacing-02); + align-items: center; + justify-content: center; +} + +.sm-playlist-buttons { + display: flex; + gap: var(--spacing-01); + align-items: center; +} + +.sm-playlist-button { + aspect-ratio: 1; + padding: var(--spacing-01); + line-height: 1; + color: #fff; + border-radius: 50%; + transition: background-color var(--transition); + + &:hover { + background-color: rgb(255 255 255 / 20%); + } + + svg { + display: block; + width: 1.5rem; + height: 1.5rem; + fill: currentcolor; + } + + &[data-control='pause'] { + display: none; + + svg { + width: 2.5rem; + height: 2.5rem; + } + } + + &[data-control='play'] { + svg { + width: 2.5rem; + height: 2.5rem; + } + } +} + +.sm-playing { + [data-control='pause'] { + display: initial; + } + + [data-control='play'] { + display: none; + } + + .sm-playlist-text { + color: white; + } +} + +.sm-playlist-cover-art { + width: 100%; + max-width: 15rem; + aspect-ratio: 1 / 1; + background-color: hsl(var(--surface-darker)); + background-position: center; + background-size: cover; + border-radius: var(--radius); +} + +.sm-playlist-progress { + span { + position: absolute; + top: -3px; + font-size: var(--xx-small); + opacity: 0.7; + + &:last-of-type { + right: 0; + } + } +} + +.sm-playlist-volume { + display: flex; + gap: var(--spacing-01); + align-items: center; + line-height: 1; + + svg { + display: inline-block; + width: 1rem; + height: 1rem; + fill: currentcolor; + } +} + +[data-volume='high'] { + :where(.sm-muted, .sm-low-volume) { + display: none; + } +} + +[data-volume='low'] { + :where(.sm-muted, .sm-high-volume) { + display: none; + } +} + +[data-volume='muted'] { + :where(.sm-low-volume, .sm-high-volume) { + display: none; + } +} + +:where(.sm-playlist-progress, .sm-playlist-volume) { + position: relative; + width: 100%; + max-width: 15rem; + + input { + width: 100%; + height: 5px; + accent-color: hsl(var(--color-1)); + } +} + +.sm-playlist-now-playing { + p { + margin-block-end: 0; + font-size: var(--small); + text-align: center; + text-transform: none; + + &:first-child { + font-size: 1rem; + } + } +} diff --git a/assets/css/index.css b/assets/css/index.css index 288dbb64..dcda2fe3 100644 --- a/assets/css/index.css +++ b/assets/css/index.css @@ -35,6 +35,7 @@ @import 'components/form.css'; @import 'components/gradient.css'; @import 'components/toc.css'; +@import 'components/playlist.css'; /* Vendor */ @import 'vendor/kg.css'; diff --git a/assets/css/pages/home.css b/assets/css/pages/home.css index 073c5cec..4cfabe7a 100644 --- a/assets/css/pages/home.css +++ b/assets/css/pages/home.css @@ -15,68 +15,3 @@ } } } - -.sm-music-grid { - align-items: center; - - h3 { - margin: 0; - font-family: var(--body-typeface); - font-size: var(--h4); - font-style: italic; - font-weight: 400; - } - - p { - margin-block-end: var(--spacing-04); - font-family: var(--ui-typeface); - font-size: var(--xx-small); - font-weight: 300; - color: hsl(var(--element-light) / 80%); - text-transform: uppercase; - letter-spacing: var(--letter-spacing-expand); - } - - ol { - min-height: 16rem; - margin-block-end: var(--spacing-04); - font-size: var(--small); - } - - .sm-music-iframe { - height: 100%; - } - - iframe { - display: block; - width: 100%; - min-height: 16rem; - aspect-ratio: 16 / 9; - - @media (--tablet) { - height: 100%; - aspect-ratio: unset; - } - } -} - -.sm-music-text { - padding: var(--spacing-04); -} - -.sm-music-pager-controls { - gap: var(--spacing-02); - justify-content: flex-start; - - .sm-circle-icon-button { - border: 1px solid hsl(var(--element-light) / 80%); - } -} - -.sm-music-page { - display: none; - - &.sm-music-active { - display: block; - } -} diff --git a/assets/images/noise.svg b/assets/images/noise.svg new file mode 100644 index 00000000..6066b167 --- /dev/null +++ b/assets/images/noise.svg @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/assets/js/app/app.js b/assets/js/app/app.js index bde3f1ac..11508f1f 100644 --- a/assets/js/app/app.js +++ b/assets/js/app/app.js @@ -1,16 +1,16 @@ -import "../../css/index.css"; +import '../../css/index.css'; -import initClickHandler from "./clickHandler"; -import { determineColorModeSupport } from "./colorModeToggle"; -import keyClickHandler from "./keyHandler"; -import generatePagination from "./pagination"; -import initMediumZoom from "./medium_zoom"; -import responsiveTableInit from "./responsiveTables"; -import writeAuthorWebsite from "./authorWebsite"; -import toc from "./toc"; -import animateOnScroll from "./homePageAnimation"; -import toggleShareMenu from "./toggleShareMenu"; -import { createMusicToggle, playlistFacadeGenerator } from "./musicPages"; +import initClickHandler from './clickHandler'; +import { determineColorModeSupport } from './colorModeToggle'; +import keyClickHandler from './keyHandler'; +import generatePagination from './pagination'; +import initMediumZoom from './medium_zoom'; +import responsiveTableInit from './responsiveTables'; +import writeAuthorWebsite from './authorWebsite'; +import toc from './toc'; +import animateOnScroll from './homePageAnimation'; +import toggleShareMenu from './toggleShareMenu'; +import { createPlaylist } from './musicPages'; initClickHandler(); determineColorModeSupport(); @@ -22,5 +22,4 @@ writeAuthorWebsite(); toc(); animateOnScroll(); toggleShareMenu(); -createMusicToggle(); -playlistFacadeGenerator(); +createPlaylist(); diff --git a/assets/js/app/musicPages.js b/assets/js/app/musicPages.js index b8916d0a..c537e798 100644 --- a/assets/js/app/musicPages.js +++ b/assets/js/app/musicPages.js @@ -1,46 +1,185 @@ export function createMusicToggle() { - const musicPages = document.querySelectorAll(".sm-music-page"); + const musicPages = document.querySelectorAll('.sm-playlist-page'); const musicPageToggles = document.querySelectorAll( - ".sm-music-pager-controls > .sm-circle-icon-button" + '.sm-playlist-pager-controls > .sm-circle-icon-button', ); if (musicPages.length === 0) return; musicPageToggles.forEach((toggle) => { // on each click, remove the active class from one page and add it to the other - toggle.addEventListener("click", () => { + toggle.addEventListener('click', () => { musicPages.forEach((page) => { - page.classList.toggle("sm-music-active"); + page.classList.toggle('sm-playlist-active'); }); }); }); } -export function playlistFacadeGenerator() { - const playlistEl = document.querySelector(".sm-music-iframe"); +export function createPlaylist() { + let nIntervId; + let prevSong; - if (!playlistEl) return; + const playlistContainer = document.querySelector('.sm-playlist-grid'); + const { playlistId } = playlistContainer.dataset; + if (!playlistContainer) return; + const progressBar = document.querySelector('#progress'); + const volumeSlider = document.querySelector('#volume'); + const startTime = document.querySelector('.sm-start-time'); + const nowPlaying = document.querySelector('.sm-playlist-now-playing'); + const endTime = document.querySelector('.sm-end-time'); + const coverArtcontainer = document.querySelector('.sm-playlist-cover-art'); + const tag = document.createElement('script'); + tag.src = 'https://www.youtube.com/iframe_api'; - const { id } = playlistEl.dataset; + const buttons = document.querySelectorAll('.sm-playlist-item'); - const observer = new IntersectionObserver(cb); + document.head.append(tag); - function cb(entries, observer) { - entries.forEach((entry) => { - if (entry.isIntersecting) { - const iframe = document.createElement("iframe"); + function parseVolume(value) { + let volume; + console.log( + '🚀 ~ file: musicPages.js:39 ~ parseVolume ~ volume:', + value === 0, + ); - iframe.src = `https://www.youtube.com/embed/videoseries?list=${id}`; - iframe.allow = - "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"; - iframe.allowFullscreen = true; - iframe.frameBorder = 0; + const valueAsNumber = Number(value); + if (valueAsNumber === 0) { + volume = 'muted'; + } - playlistEl.append(iframe); + if (valueAsNumber > 0 && valueAsNumber < 50) { + volume = 'low'; + } - observer.unobserve(playlistEl); - } + if (valueAsNumber >= 50) { + volume = 'high'; + } + + volumeSlider.parentElement.setAttribute('data-volume', volume); + } + + function playlistController() { + const player = new window.YT.Player('player', { + height: '360', + width: '640', + playerVars: { + modestbranding: 1, + enablejsapi: 1, + list: playlistId, + listType: 'playlist', + }, + + events: { + onReady: onPlayerReady, + onStateChange: onPlayerStateChange, + }, + }); + + function onPlayerReady() { + const initialVolume = player.getVolume(); + parseVolume(initialVolume); + volumeSlider.value = initialVolume; + buttons.forEach((button) => { + button.addEventListener('click', () => { + player.playVideoAt(button.dataset.idx); + }); + }); + + const playlistControlButtons = document.querySelectorAll( + '.sm-playlist-button', + ); + + playlistControlButtons.forEach((button) => { + button.addEventListener('click', () => { + const { control } = button.dataset; + if (control === 'play') { + player.playVideo(); + } else if (control === 'pause') { + player.pauseVideo(); + } else if (control === 'forward') { + player.nextVideo(); + } else if (control === 'previous') { + player.previousVideo(); + } + }); + }); + } + + progressBar.addEventListener('input', (e) => { + const ratio = e.target.value / 100; + const newTime = ratio * player.getDuration(); + player.seekTo(newTime); }); + + volumeSlider.addEventListener('input', (e) => { + const newVolume = e.target.value; + parseVolume(newVolume); + player.setVolume(newVolume); + }); + + function parseSeconds(elapsed) { + const minutes = Math.round(Math.floor(elapsed / 60)).toString(); + + const seconds = Math.round(elapsed % 60) + .toString() + .padStart(2, '0'); + return `${minutes}:${seconds}`; + } + + function showSongProgress() { + const currentTime = player.getCurrentTime(); + const ratio = currentTime / player.getDuration(); + progressBar.value = ratio * 100; + startTime.textContent = parseSeconds(currentTime); + } + + function onPlayerStateChange(event) { + console.log( + '🚀 ~ file: musicPages.js:136 ~ onPlayerStateChange ~ event:', + event, + ); + if (event.data === 2) { + clearInterval(nIntervId); + nIntervId = null; + + playlistContainer.style.setProperty('--bg', ''); + playlistContainer.classList.remove('sm-playing'); + prevSong.parentElement.classList.remove('sm-active'); + } else if (event.data === 1) { + if (nIntervId) { + clearInterval(nIntervId); + nIntervId = null; + } + + const currentSong = document.querySelector( + `button[data-idx="${player.getPlaylistIndex()}"]`, + ); + + const listItem = currentSong.parentElement; + listItem.classList.add('sm-active'); + const { title, artist } = currentSong.dataset; + nowPlaying.innerHTML = `
${title}
${artist}
`; + const { imageSrc, timestamp } = currentSong.dataset; + + playlistContainer.style.setProperty( + '--bg', + `url("/assets/images/noise.svg"), linear-gradient(to right, rgb(0 0 0 / 45%), transparent), url(${imageSrc})`, + ); + playlistContainer.classList.add('sm-playing'); + coverArtcontainer.style.backgroundImage = `url(${imageSrc})`; + endTime.textContent = timestamp; + prevSong = currentSong; + nIntervId = setInterval(showSongProgress, 500); + } else if (event.data === 0 || event.data === -1) { + if (prevSong) { + prevSong.parentElement.classList.remove('sm-active'); + } + } else { + clearInterval(nIntervId); + nIntervId = null; + } + } } - observer.observe(playlistEl); + window.onYouTubeIframeAPIReady = playlistController; } diff --git a/home.hbs b/home.hbs index 681fce60..da1a246f 100644 --- a/home.hbs +++ b/home.hbs @@ -56,52 +56,9 @@18 tracks / 1 hour, 17 minutes
-18 Tracks / 1 hour 10 minutes
+