From 84355645bf0ff71c412e09fedf419c37b7692a0e Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 15:31:01 -0500 Subject: [PATCH 01/36] Make eleventy js templates asynchronus --- .eleventy.js | 2 +- components-eleventy/video/card.js | 4 ++-- components-eleventy/video/row.js | 12 +++++++----- pages-eleventy/tv.11ty.js | 8 ++++++-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.eleventy.js b/.eleventy.js index 8999d32..f9baf7f 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -25,7 +25,7 @@ module.exports = function ( eleventyConfig ) { const cssManager = new InlineCodeManager() const jsManager = new InlineCodeManager() - eleventyConfig.addJavaScriptFunction('usingComponent', function ( componentName ) { + eleventyConfig.addJavaScriptFunction('usingComponent', async function ( componentName ) { // console.log('Getting component', componentName) if ( componentName.includes('.js') ) { diff --git a/components-eleventy/video/card.js b/components-eleventy/video/card.js index 0f77819..2035852 100644 --- a/components-eleventy/video/card.js +++ b/components-eleventy/video/card.js @@ -9,14 +9,14 @@ function pill ( text ) { ` } -export default function ( video, options = {} ) { +export default async function ( video, options = {} ) { const { width = '325px', classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden' } = options // Setup inline lazysizes - this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) + await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) // console.log('video', video) diff --git a/components-eleventy/video/row.js b/components-eleventy/video/row.js index 308e9c5..43675c4 100644 --- a/components-eleventy/video/row.js +++ b/components-eleventy/video/row.js @@ -10,7 +10,7 @@ function getCardType ( video ) { return VideoCard } -export default function ( videos, options = {} ) { +export default async function ( videos, options = {} ) { const { cardWidth = '325', @@ -24,17 +24,19 @@ export default function ( videos, options = {} ) { const rowId = `row-${ uid }` // Setup inline lazysizes - this.usingComponent( 'helpers/scroll.js' ) + await this.usingComponent( 'helpers/scroll.js' ) // console.log('video', video) - const cardsHtml = videos.map( video => { + const renderedCards = await Promise.all(videos.map( async video => { const Card = getCardType( video ) // console.log('Card', this.boundComponent(Card)( video ) ) - return this.boundComponent(Card)( video ) - } ).join('') + return await this.boundComponent(Card)( video ) + } )) + + const cardsHtml = renderedCards.join('') // console.log( 'cardsHtml', cardsHtml ) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index a5d29f3..2d5c7f0 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -57,10 +57,14 @@ class TV { } } - render({ payload: { video } }) { + async render({ payload: { video } }) { // console.log('video.payload', Object.keys(video.payload)) + const rowHtml = await this.boundComponent(VideoRow)( video.payload.relatedVideos ) + + // const rowHtml = renderedRow.join('') + return /* html */`
@@ -87,7 +91,7 @@ class TV { From 5edfa574bb10f11e1ded286cbe55f05cbefa4d3d Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 15:31:11 -0500 Subject: [PATCH 02/36] Install terser --- package-lock.json | 25 +++++++------------------ package.json | 1 + 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79b08ae..86448fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "pretty-bytes": "^5.5.0", "scroll-into-view-if-needed": "^2.2.26", "slugify": "^1.4.6", + "terser": "^4.8.0", "vue-full-screen-file-drop": "^2.0.0" }, "devDependencies": { @@ -5999,8 +6000,7 @@ "node_modules/buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "node_modules/buffer-json": { "version": "2.0.0", @@ -6795,8 +6795,7 @@ "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/commondir": { "version": "1.0.1", @@ -22997,7 +22996,6 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -23007,7 +23005,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -23882,7 +23879,6 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, "dependencies": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -23994,7 +23990,6 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -31632,8 +31627,7 @@ "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "buffer-json": { "version": "2.0.0", @@ -32280,8 +32274,7 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "commondir": { "version": "1.0.1", @@ -45481,7 +45474,6 @@ "version": "0.5.19", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -45490,8 +45482,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, @@ -46198,7 +46189,6 @@ "version": "4.8.0", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", - "dev": true, "requires": { "commander": "^2.20.0", "source-map": "~0.6.1", @@ -46208,8 +46198,7 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, diff --git a/package.json b/package.json index 7d1bc97..3569af3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "pretty-bytes": "^5.5.0", "scroll-into-view-if-needed": "^2.2.26", "slugify": "^1.4.6", + "terser": "^4.8.0", "vue-full-screen-file-drop": "^2.0.0" }, "devDependencies": { From 6b17690f2131076fc8a08184f5ff7c3a1718baf9 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 15:32:04 -0500 Subject: [PATCH 03/36] Minify inline javascript --- .eleventy.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.eleventy.js b/.eleventy.js index f9baf7f..22ec279 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -2,6 +2,7 @@ import fs from 'fs' import replace_css_url from 'replace-css-url' import dotenv from 'dotenv' import { InlineCodeManager } from '@11ty/eleventy-assets' +import { minify } from 'terser' import nuxtConfig from './nuxt.config' @@ -28,14 +29,26 @@ module.exports = function ( eleventyConfig ) { eleventyConfig.addJavaScriptFunction('usingComponent', async function ( componentName ) { // console.log('Getting component', componentName) + const assetFileName = getAssetFilePath( componentName ) + if ( componentName.includes('.js') ) { + // If a never before seen component, add the JS code if(!jsManager.hasComponentCode(componentName)) { - const fileContents = fs.readFileSync(getAssetFilePath(componentName), { encoding: "UTF-8" }) + + let contents = '' + + console.log('Reading component file', componentName) + const javascriptCode = fs.readFileSync( assetFileName, { encoding: "UTF-8" }) + + console.log('Minifying code', componentName) + const minified = await minify( javascriptCode ) + + contents = minified.code // console.log('Got component', componentName, componentCss) - jsManager.addComponentCode(componentName, fileContents) + jsManager.addComponentCode(componentName, contents) } // Log usage for this url @@ -44,7 +57,7 @@ module.exports = function ( eleventyConfig ) { } else if ( componentName.includes('.css') ) { // If a never before seen component, add the CSS code if(!cssManager.hasComponentCode(componentName)) { - const fileContents = fs.readFileSync(getAssetFilePath(componentName), { encoding: "UTF-8" }) + const fileContents = fs.readFileSync( assetFileName, { encoding: "UTF-8" }) // Replace urls in css with relative urls for dist folder const parsedCss = replace_css_url( From 0cbe9b33d58c04dc9926ef636ae42784e197b61c Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 15:48:54 -0500 Subject: [PATCH 04/36] Fix filter failing on undefined endpoints --- pages-eleventy/app.11ty.js | 3 +++ pages-eleventy/formula.11ty.js | 3 +++ pages-eleventy/tv.11ty.js | 3 +++ 3 files changed, 9 insertions(+) diff --git a/pages-eleventy/app.11ty.js b/pages-eleventy/app.11ty.js index f77b64e..5373a5e 100644 --- a/pages-eleventy/app.11ty.js +++ b/pages-eleventy/app.11ty.js @@ -56,6 +56,9 @@ export class AppTemplate { before: function( data ) { return data.filter( entry => { + // Skip endpoints with no payload + if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + const appType = getAppType( entry.payload.app ) return appType === 'app' diff --git a/pages-eleventy/formula.11ty.js b/pages-eleventy/formula.11ty.js index 6d0bc2d..d644935 100644 --- a/pages-eleventy/formula.11ty.js +++ b/pages-eleventy/formula.11ty.js @@ -33,6 +33,9 @@ class FormulaTemplate extends AppTemplate { alias: 'app', before: function( data ) { return data.filter( entry => { + // Skip endpoints with no payload + if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + const appType = getAppType( entry.payload.app ) return appType === 'formula' diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 2d5c7f0..f3f89c2 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -35,6 +35,9 @@ class TV { alias: 'payload', before: function( data ) { return data.filter( entry => { + // Skip endpoints with no payload + if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + return entry.payload.hasOwnProperty('video') && isVideo( entry.payload.video ) }) } From f576c72451b0380e40bc23d622f773ac1bf969e2 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 15:49:13 -0500 Subject: [PATCH 05/36] Fix app related videos not rendering --- pages-eleventy/app.11ty.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pages-eleventy/app.11ty.js b/pages-eleventy/app.11ty.js index 5373a5e..0a6e1ca 100644 --- a/pages-eleventy/app.11ty.js +++ b/pages-eleventy/app.11ty.js @@ -86,13 +86,15 @@ export class AppTemplate { } } - render( data ) { + async render( data ) { const { app: { payload: { app, relatedVideos = [] } }, 'device-list': deviceList } = data + const hasRelatedVideos = relatedVideos.length > 0 + // console.log('deviceList', deviceList) // console.log('video.payload', Object.keys(video.payload)) @@ -110,6 +112,9 @@ export class AppTemplate { const relatedLinksHtml = renderPageLinksHtml( app.relatedLinks ) + + const relatedVideosRowHtml = hasRelatedVideos ? await this.boundComponent(VideoRow)( relatedVideos ) : null + return /* html */`
@@ -151,7 +156,7 @@ export class AppTemplate {
- ${ relatedVideos.length > 0 ? /* html */` + ${ hasRelatedVideos ? /* html */` ` : '' } From 44fa023a06eb495d31c4683b23de8d9b5c71d9bc Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 16:44:11 -0500 Subject: [PATCH 06/36] Generate tv endpoints with eleventy --- build-lists.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/build-lists.js b/build-lists.js index f3daa2e..096fc2d 100644 --- a/build-lists.js +++ b/build-lists.js @@ -236,12 +236,12 @@ class BuildLists { const appType = getAppType( app ) if ( isVideo( app ) ) { - // this.endpointMaps.eleventy.add({ - // route: getVideoEndpoint(app), - // payload: buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) - // }) + this.endpointMaps.eleventy.set( + getVideoEndpoint(app), + buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) + ) - this.endpointMaps.nuxt.set( getVideoEndpoint(app), buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) ) + // this.endpointMaps.nuxt.set( getVideoEndpoint(app), buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) ) return } From 37a296adfe9b86a2197d26e4727548764927199b Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 16:48:08 -0500 Subject: [PATCH 07/36] Filter eleventy endpoints by route, not payload --- helpers/app-derived.js | 6 ++++++ pages-eleventy/app.11ty.js | 10 ++++------ pages-eleventy/formula.11ty.js | 9 +++------ pages-eleventy/tv.11ty.js | 7 +++---- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/helpers/app-derived.js b/helpers/app-derived.js index da98483..ba9f1ca 100644 --- a/helpers/app-derived.js +++ b/helpers/app-derived.js @@ -56,4 +56,10 @@ export function getVideoEndpoint ( video ) { return `/tv/${video.slug}` } +export function getRouteType ( routeString ) { + // Remove first slash and split by remaining + // slashes to get first part of route + const [ routeType ] = routeString.substring(1).split('/') + return routeType +} diff --git a/pages-eleventy/app.11ty.js b/pages-eleventy/app.11ty.js index 0a6e1ca..f0a97a8 100644 --- a/pages-eleventy/app.11ty.js +++ b/pages-eleventy/app.11ty.js @@ -2,7 +2,7 @@ import dotenv from 'dotenv' import config from '../nuxt.config.js' -import { getAppType } from '../helpers/app-derived.js' +import { getAppType, getRouteType } from '../helpers/app-derived.js' import { deviceSupportsApp } from '../helpers/devices.js' import { makeLastUpdatedFriendly } from '../helpers/parse-date' @@ -56,12 +56,10 @@ export class AppTemplate { before: function( data ) { return data.filter( entry => { - // Skip endpoints with no payload - if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + // const [ _, routeType ] = entry.route.split('/') + const routeType = getRouteType( entry.route ) - const appType = getAppType( entry.payload.app ) - - return appType === 'app' + return routeType === 'app' }) } }, diff --git a/pages-eleventy/formula.11ty.js b/pages-eleventy/formula.11ty.js index d644935..3e34c93 100644 --- a/pages-eleventy/formula.11ty.js +++ b/pages-eleventy/formula.11ty.js @@ -2,7 +2,7 @@ import dotenv from 'dotenv' import config from '../nuxt.config.js' -import { getAppType } from '../helpers/app-derived.js' +import { getAppType, getRouteType } from '../helpers/app-derived.js' import { AppTemplate } from './app.11ty.js' @@ -33,12 +33,9 @@ class FormulaTemplate extends AppTemplate { alias: 'app', before: function( data ) { return data.filter( entry => { - // Skip endpoints with no payload - if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + const routeType = getRouteType( entry.route ) - const appType = getAppType( entry.payload.app ) - - return appType === 'formula' + return routeType === 'formula' }) } }, diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index f3f89c2..6519780 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -3,7 +3,7 @@ import dotenv from 'dotenv' import config from '../nuxt.config' import VideoRow from '../components-eleventy/video/row.js' -import { isVideo } from '../helpers/app-derived' +import { isVideo, getRouteType } from '../helpers/app-derived' // Setup dotenv dotenv.config() @@ -35,10 +35,9 @@ class TV { alias: 'payload', before: function( data ) { return data.filter( entry => { - // Skip endpoints with no payload - if ( entry === undefined || !entry.hasOwnProperty('payload') ) return false + const routeType = getRouteType( entry.route ) - return entry.payload.hasOwnProperty('video') && isVideo( entry.payload.video ) + return routeType === 'tv' }) } }, From 4372f462c36ff384a29c87006ebc230ad2b28dcc Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 16:49:26 -0500 Subject: [PATCH 08/36] Fix tv template to handle new data structure --- pages-eleventy/tv.11ty.js | 47 ++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 6519780..b3bf8a0 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -8,16 +8,16 @@ import { isVideo, getRouteType } from '../helpers/app-derived' // Setup dotenv dotenv.config() -export const makeTitle = function ( video ) { - return `${ video.name } - ${ config.head.title }` +export const makeTitle = function ( tvEntry ) { + return `${ tvEntry.name } - ${ config.head.title }` } -export const makeDescription = function ( video ) { - if ( video.payload.featuredApps.length === 0 ) return 'Apple Silicon performance and support videos' +export const makeDescription = function ( tvEntry ) { + if ( tvEntry.payload.featuredApps.length === 0 ) return 'Apple Silicon performance and support videos' - const featuredAppsString = video.payload.featuredApps.slice(0, 5).map(app => app.name).join(', ') + const featuredAppsString = tvEntry.payload.featuredApps.slice(0, 5).map(app => app.name).join(', ') - // console.log('video.payload.featuredApps', video.payload.featuredApps) + // console.log('tvEntry.payload.featuredApps', tvEntry.payload.featuredApps) return `Apple Silicon performance and support videos for ${ featuredAppsString }` } @@ -32,7 +32,7 @@ class TV { pagination: { data: 'eleventy-endpoints', size: 1, - alias: 'payload', + alias: 'tvEntry', before: function( data ) { return data.filter( entry => { const routeType = getRouteType( entry.route ) @@ -43,27 +43,44 @@ class TV { }, eleventyComputed: { - title: ({ payload: { video } }) => { + title: ( { tvEntry } ) => { // console.log('data', data) - return makeTitle( video ) + return makeTitle( tvEntry ) }, - description: ({ payload: { video } }) => { - return makeDescription( video ) + description: ( { tvEntry } ) => { + return makeDescription( tvEntry ) }, }, - permalink: ({ payload: { video } }) => { + permalink: ( { tvEntry: { route } } ) => { // console.log('data', data) - return `tv/${ video.slug }/` + // return `tv/${ video.slug }/` + + return route.substring(1) + '/' } } } - async render({ payload: { video } }) { + async render( data ) { + + const { + tvEntry: { + // route, + payload: { + video, + relatedVideos = [] + } + }, + // tvEntry: { + // video, + // relatedVideos = [] + // }, + // 'device-list': deviceList + } = data // console.log('video.payload', Object.keys(video.payload)) - const rowHtml = await this.boundComponent(VideoRow)( video.payload.relatedVideos ) + const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) // const rowHtml = renderedRow.join('') From 2c3e0ab688883d46c804dc61f1a0ea23d7e846bc Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 16:50:44 -0500 Subject: [PATCH 09/36] Increase space betweet devices and videos --- pages-eleventy/app.11ty.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pages-eleventy/app.11ty.js b/pages-eleventy/app.11ty.js index f0a97a8..de25701 100644 --- a/pages-eleventy/app.11ty.js +++ b/pages-eleventy/app.11ty.js @@ -114,7 +114,8 @@ export class AppTemplate { const relatedVideosRowHtml = hasRelatedVideos ? await this.boundComponent(VideoRow)( relatedVideos ) : null return /* html */` -
+
+

${ data.mainHeading } From d68e5a1f5670e254ba60cf0cb5f9bda7edd187be Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 17:55:31 -0500 Subject: [PATCH 10/36] Load lazy sizes from row to reduce usingComponent --- components-eleventy/video/card.js | 2 +- components-eleventy/video/row.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/components-eleventy/video/card.js b/components-eleventy/video/card.js index 2035852..a21fb49 100644 --- a/components-eleventy/video/card.js +++ b/components-eleventy/video/card.js @@ -16,7 +16,7 @@ export default async function ( video, options = {} ) { } = options // Setup inline lazysizes - await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) + // await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) // console.log('video', video) diff --git a/components-eleventy/video/row.js b/components-eleventy/video/row.js index 43675c4..a796970 100644 --- a/components-eleventy/video/row.js +++ b/components-eleventy/video/row.js @@ -23,9 +23,12 @@ export default async function ( videos, options = {} ) { const uid = Math.random().toString(36).substr(2, 9) const rowId = `row-${ uid }` - // Setup inline lazysizes + // Setup inline scroll script await this.usingComponent( 'helpers/scroll.js' ) + // Setup inline lazysizes + await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) + // console.log('video', video) const renderedCards = await Promise.all(videos.map( async video => { From 7698df87af3a998878eb77c037b80e0f4d0aac7e Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 17:55:47 -0500 Subject: [PATCH 11/36] Add dev-eleventy-debug --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 3569af3..4fbaa8c 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "generate-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet", "generate-postcss": "ENV=production postcss assets/css/tailwind.css --o static/tailwind.css", "dev-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", + "dev-eleventy-debug": "DEBUG=Eleventy* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "lint:fix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint", From 2dc8caa5d21d8adc959d61b86452fea06db94fca Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 17:56:41 -0500 Subject: [PATCH 12/36] Restrict template formats to js only --- .eleventy.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.eleventy.js b/.eleventy.js index 22ec279..bcdf12e 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -107,6 +107,10 @@ module.exports = function ( eleventyConfig ) { return { + // https://www.11ty.dev/docs/config/#template-formats + // Default: html,liquid,ejs,md,hbs,mustache,haml,pug,njk,11ty.js + templateFormats: [ '11ty.js' ], + dir: { input: 'pages-eleventy', output: 'dist', From 4ffc7bf404567090ecc7fd3d5cc528c2514a65db Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 18:02:02 -0500 Subject: [PATCH 13/36] Add eleventy benchmark script https://www.11ty.dev/docs/debug-performance/ --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 4fbaa8c..dc85d45 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "generate-postcss": "ENV=production postcss assets/css/tailwind.css --o static/tailwind.css", "dev-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "dev-eleventy-debug": "DEBUG=Eleventy* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", + "dev-eleventy-benchmark": "DEBUG=Eleventy:Benchmark* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "lint:fix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint", From 4b29c210bac730ea172cd56109da5278ee770306 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 8 May 2021 18:05:35 -0500 Subject: [PATCH 14/36] Try caching inline assets with js Map --- .eleventy.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.eleventy.js b/.eleventy.js index bcdf12e..9af5159 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -14,6 +14,8 @@ function getAssetFilePath(componentName) { return `./${componentName}` } +const inlineAssetCache = new Map() + module.exports = function ( eleventyConfig ) { // console.log('eleventyConfig', eleventyConfig) @@ -31,13 +33,25 @@ module.exports = function ( eleventyConfig ) { const assetFileName = getAssetFilePath( componentName ) + // console.log('Cache size', inlineAssetCache) + if ( componentName.includes('.js') ) { // If a never before seen component, add the JS code - if(!jsManager.hasComponentCode(componentName)) { + if( !inlineAssetCache.has( assetFileName ) ) { let contents = '' + // if ( inlineAssetCache.has( assetFileName ) ) { + // console.log('Reading component from cache', componentName) + // console.log('Cache size', inlineAssetCache.size) + + // contents = inlineAssetCache.get( assetFileName ) + + // } else { + + // } + console.log('Reading component file', componentName) const javascriptCode = fs.readFileSync( assetFileName, { encoding: "UTF-8" }) @@ -46,6 +60,8 @@ module.exports = function ( eleventyConfig ) { contents = minified.code + inlineAssetCache.set( assetFileName, contents ) + // console.log('Got component', componentName, componentCss) jsManager.addComponentCode(componentName, contents) From 12773ab251d4a5d3687f761613874a82a4366c54 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Mon, 10 May 2021 12:05:23 -0500 Subject: [PATCH 15/36] Add computed script --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index dc85d45..c64075d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "dev-eleventy": "node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "dev-eleventy-debug": "DEBUG=Eleventy* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "dev-eleventy-benchmark": "DEBUG=Eleventy:Benchmark* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", + "dev-eleventy-computed": "DEBUG=Eleventy:ComputedData* node --max-old-space-size=60000 -r esm node_modules/.bin/eleventy --quiet --watch --serve", "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", "lint:fix": "eslint --fix --ext .js,.vue --ignore-path .gitignore .", "precommit": "npm run lint", From 6346d3a169df027fdd5a091d6c0e721dbf97fcd3 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Thu, 13 May 2021 20:41:07 -0500 Subject: [PATCH 16/36] Install esbuild --- package-lock.json | 15 +++++++++++++++ package.json | 1 + 2 files changed, 16 insertions(+) diff --git a/package-lock.json b/package-lock.json index 86448fb..93aca74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "can-autoplay": "^3.0.0", "chance": "^1.1.7", "cross-env": "^5.2.0", + "esbuild": "^0.11.20", "jsdom": "^16.4.0", "lazysizes": "^5.3.0-beta1", "markdown-it": "^11.0.1", @@ -9127,6 +9128,15 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.11.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.20.tgz", + "integrity": "sha512-QOZrVpN/Yz74xfat0H6euSgn3RnwLevY1mJTEXneukz1ln9qB+ieaerRMzSeETpz/UJWsBMzRVR/andBht5WKw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + } + }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -34222,6 +34232,11 @@ "is-symbol": "^1.0.2" } }, + "esbuild": { + "version": "0.11.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.20.tgz", + "integrity": "sha512-QOZrVpN/Yz74xfat0H6euSgn3RnwLevY1mJTEXneukz1ln9qB+ieaerRMzSeETpz/UJWsBMzRVR/andBht5WKw==" + }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", diff --git a/package.json b/package.json index c64075d..c392de1 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "can-autoplay": "^3.0.0", "chance": "^1.1.7", "cross-env": "^5.2.0", + "esbuild": "^0.11.20", "jsdom": "^16.4.0", "lazysizes": "^5.3.0-beta1", "markdown-it": "^11.0.1", From 3b36f4f26c08e5090be20dad82c6b6663d820ecb Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Thu, 13 May 2021 22:21:15 -0500 Subject: [PATCH 17/36] Use esbuild for inline code --- .eleventy.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.eleventy.js b/.eleventy.js index d4b2dea..dcdfef8 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -28,7 +28,8 @@ import fs from 'fs' import replace_css_url from 'replace-css-url' import dotenv from 'dotenv' import { InlineCodeManager } from '@11ty/eleventy-assets' -import { minify } from 'terser' +import { transformSync } from 'esbuild' + import nuxtConfig from './nuxt.config' @@ -82,7 +83,14 @@ module.exports = function ( eleventyConfig ) { const javascriptCode = fs.readFileSync( assetFileName, { encoding: "UTF-8" }) console.log('Minifying code', componentName) - const minified = await minify( javascriptCode ) + const minified = await transformSync(javascriptCode, { + // loader: 'ts', + // bundle: true, + minify: true, + // format: 'iife', + })//minify( javascriptCode ) + + // console.log('minified', minified) contents = minified.code From b2c9774ca8b6aa9f51598c7fc926291373600b1a Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Thu, 13 May 2021 22:23:14 -0500 Subject: [PATCH 18/36] Pull title and description from tvEntry --- pages-eleventy/tv.11ty.js | 47 +++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index b3bf8a0..68e3b6d 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -3,13 +3,15 @@ import dotenv from 'dotenv' import config from '../nuxt.config' import VideoRow from '../components-eleventy/video/row.js' -import { isVideo, getRouteType } from '../helpers/app-derived' +import { getRouteType } from '../helpers/app-derived' // Setup dotenv dotenv.config() -export const makeTitle = function ( tvEntry ) { - return `${ tvEntry.name } - ${ config.head.title }` +export const makeTitle = function ( name ) { + // console.log('tvEntry', tvEntry) + + return `${ name } - ${ config.head.title }` } export const makeDescription = function ( tvEntry ) { @@ -31,32 +33,42 @@ class TV { pagination: { data: 'eleventy-endpoints', + // data: 'collections.tv', size: 1, alias: 'tvEntry', - before: function( data ) { - return data.filter( entry => { + before: function( endpoint ) { + // console.log('Before runs') + + return endpoint.filter( entry => { const routeType = getRouteType( entry.route ) return routeType === 'tv' }) - } + }, }, + // tags: [ 'tv' ], + eleventyComputed: { - title: ( { tvEntry } ) => { - // console.log('data', data) - return makeTitle( tvEntry ) + title: data => { + // Declare dependencies for Eleventy + // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies + data.tvEntry + + return makeTitle( data.tvEntry.payload.video.name ) }, - description: ( { tvEntry } ) => { - return makeDescription( tvEntry ) + description: ( data ) => { + // Declare dependencies for Eleventy + // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies + data.tvEntry + + return makeDescription( data.tvEntry ) }, }, - permalink: ( { tvEntry: { route } } ) => { - // console.log('data', data) - // return `tv/${ video.slug }/` + permalink: ( data ) => { - return route.substring(1) + '/' + return data.tvEntry.route.substring(1) + '/' } } } @@ -71,13 +83,10 @@ class TV { relatedVideos = [] } }, - // tvEntry: { - // video, - // relatedVideos = [] - // }, // 'device-list': deviceList } = data + // console.log('video.payload', Object.keys(video.payload)) const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) From b35c8075a00956666a229aeeedb1f56a4ae5c186 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Fri, 14 May 2021 12:44:14 -0500 Subject: [PATCH 19/36] Fix extra space --- pages-eleventy/app.11ty.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages-eleventy/app.11ty.js b/pages-eleventy/app.11ty.js index de25701..26b5073 100644 --- a/pages-eleventy/app.11ty.js +++ b/pages-eleventy/app.11ty.js @@ -65,7 +65,7 @@ export class AppTemplate { }, eleventyComputed: { - title: ({ app: { payload: { app } } }) => { + title: ({ app: { payload: { app } } }) => { // console.log('data', data) return makeTitle( app ) }, From 3bd434068211931fa7e2af2380706962a743a846 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 13:11:07 -0500 Subject: [PATCH 20/36] Add basic player component --- components-eleventy/video/player.js | 51 +++++++++++++ helpers/lite-youtube.js | 107 ++++++++++++++++++++++++++++ pages-eleventy/tv.11ty.js | 16 ++--- 3 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 components-eleventy/video/player.js create mode 100644 helpers/lite-youtube.js diff --git a/components-eleventy/video/player.js b/components-eleventy/video/player.js new file mode 100644 index 0000000..b505a23 --- /dev/null +++ b/components-eleventy/video/player.js @@ -0,0 +1,51 @@ +export default async function ( video, options = {} ) { + const { + width = '325px', + classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden' + } = options + + // Setup inline player script + await this.usingComponent( 'node_modules/can-autoplay/build/can-autoplay.min.js' ) + + await this.usingComponent( 'helpers/lite-youtube.js' ) + + // Setup inline lazysizes + await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) + + // console.log('video', video) + + return /* html */` + +
+
+
+ + + + M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5! + +
+
+
+ + + +
+
+
+

M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5!

+
+
+
+
+
+
+
+ +
+
+ ` +} diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js new file mode 100644 index 0000000..29639cc --- /dev/null +++ b/helpers/lite-youtube.js @@ -0,0 +1,107 @@ +// https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.js + +// import canAutoPlay from 'can-autoplay' + + +/** + * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint. + * + * Thx to these as the inspiration + * https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html + * https://autoplay-youtube-player.glitch.me/ + * + * Once built it, I also found these: + * https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍) + * https://github.com/Daugilas/lazyYT + * https://github.com/vb/lazyframe + */ +class LiteYTEmbed extends HTMLElement { + constructor() { + // Always call super first in constructor + super() + + // write element functionality in here + + // console.log('canAutoplay', canAutoplay) + + } + + connectedCallback() { + this.videoId = this.getAttribute('videoid') + + console.log('canAutoplay from connectedCallback', canAutoplay) + + // On hover (or tap), warm up the TCP connections we're (likely) about to use. + this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {once: true}) + + // Once the user clicks, add the real iframe and drop our play button + // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time + // We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 + this.addEventListener('click', e => this.addIframe()) + } + + // // TODO: Support the the user changing the [videoid] attribute + // attributeChangedCallback() { + // } + + /** + * Add a to the head + */ + static addPrefetch(kind, url, as) { + const linkEl = document.createElement('link') + linkEl.rel = kind + linkEl.href = url + if (as) { + linkEl.as = as + } + document.head.append(linkEl) + } + + /** + * Begin pre-connecting to warm up the iframe load + * Since the embed's network requests load within its iframe, + * preload/prefetch'ing them outside the iframe will only cause double-downloads. + * So, the best we can do is warm up a few connections to origins that are in the critical path. + * + * Maybe `` would work, but it's unsupported: http://crbug.com/593267 + * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity. + */ + static warmConnections() { + if (LiteYTEmbed.preconnected) return + + // The iframe document and most of its subresources come right off youtube.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com') + // The botguard script is fetched off from google.com + LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com') + + // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. + LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net') + LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net') + + LiteYTEmbed.preconnected = true + } + + addIframe() { + const params = new URLSearchParams(this.getAttribute('params') || []) + params.append('autoplay', '1') + + const iframeEl = document.createElement('iframe') + iframeEl.width = 560 + iframeEl.height = 315 + // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include + iframeEl.title = this.playLabel + iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' + iframeEl.allowFullscreen = true + // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL + // https://stackoverflow.com/q/64959723/89484 + iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${params.toString()}` + this.append(iframeEl) + + this.classList.add('lyt-activated') + + // Set focus for a11y + this.querySelector('iframe').focus() + } +} +// Register custom element +window.customElements.define('lite-youtube', LiteYTEmbed) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 68e3b6d..7e007f1 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -2,7 +2,9 @@ import dotenv from 'dotenv' import config from '../nuxt.config' +import VideoPlayer from '../components-eleventy/video/player.js' import VideoRow from '../components-eleventy/video/row.js' + import { getRouteType } from '../helpers/app-derived' // Setup dotenv @@ -33,7 +35,6 @@ class TV { pagination: { data: 'eleventy-endpoints', - // data: 'collections.tv', size: 1, alias: 'tvEntry', before: function( endpoint ) { @@ -89,6 +90,8 @@ class TV { // console.log('video.payload', Object.keys(video.payload)) + const playerHtml = await this.boundComponent(VideoPlayer)() + const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) // const rowHtml = renderedRow.join('') @@ -96,14 +99,9 @@ class TV { return /* html */`
-
-
-
- -
-
- -
+ + ${ playerHtml } +

${ video.name }

From 96815012f316f600da86cd64b4824014c27d3d22 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 15:13:18 -0500 Subject: [PATCH 21/36] Enable Loading youtube iframe --- components-eleventy/video/player.js | 37 +-- helpers/lite-youtube.js | 359 +++++++++++++++++++++++++--- pages-eleventy/tv.11ty.js | 2 +- 3 files changed, 346 insertions(+), 52 deletions(-) diff --git a/components-eleventy/video/player.js b/components-eleventy/video/player.js index b505a23..8ba495b 100644 --- a/components-eleventy/video/player.js +++ b/components-eleventy/video/player.js @@ -19,28 +19,31 @@ export default async function ( video, options = {} ) { class="video-canvas w-screen flex flex-col justify-center items-center bg-black pt-16" style="left:50%;right:50%;margin-left:-50vw;margin-right:-50vw;" > +
-
+
- - - - M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5! - -
-
-
- - - -
-
-
-

M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5!

+ + + + M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5! + +
+
+
+ + + +
+
+
+

M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5!

+
-
diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index 29639cc..52f5cee 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -20,24 +20,35 @@ class LiteYTEmbed extends HTMLElement { // Always call super first in constructor super() - // write element functionality in here - - // console.log('canAutoplay', canAutoplay) - + this._uid = Date.now() } connectedCallback() { this.videoId = this.getAttribute('videoid') + this.videoDataScript = this.querySelector('.video-data') + this.video = JSON.parse( this.videoDataScript.innerHTML ) + + this.playerContainer = this.querySelector('.player-container') + this.playerPoster = this.querySelector('.player-poster') + + // console.log('canAutoplay from connectedCallback', canAutoplay) + + console.log('video', this.video) + console.log('this.playerContainer', this.playerContainer) + + + // Start watchers here - console.log('canAutoplay from connectedCallback', canAutoplay) // On hover (or tap), warm up the TCP connections we're (likely) about to use. - this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {once: true}) + this.playerContainer.addEventListener('pointerover', this.warmConnections, {once: true}) // Once the user clicks, add the real iframe and drop our play button // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time // We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 - this.addEventListener('click', e => this.addIframe()) + this.playerPoster.addEventListener('click', e => this.addIframe()) + + } // // TODO: Support the the user changing the [videoid] attribute @@ -47,15 +58,15 @@ class LiteYTEmbed extends HTMLElement { /** * Add a to the head */ - static addPrefetch(kind, url, as) { - const linkEl = document.createElement('link') - linkEl.rel = kind - linkEl.href = url - if (as) { - linkEl.as = as - } - document.head.append(linkEl) - } + // static c(kind, url, as) { + // const linkEl = document.createElement('link') + // linkEl.rel = kind + // linkEl.href = url + // if (as) { + // linkEl.as = as + // } + // document.head.append(linkEl) + // } /** * Begin pre-connecting to warm up the iframe load @@ -66,9 +77,192 @@ class LiteYTEmbed extends HTMLElement { * Maybe `` would work, but it's unsupported: http://crbug.com/593267 * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity. */ - static warmConnections() { + // static warmConnections() { + // if (LiteYTEmbed.preconnected) return + + // // The iframe document and most of its subresources come right off youtube.com + // LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com') + // // The botguard script is fetched off from google.com + // LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com') + + // // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. + // LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net') + // LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net') + + // LiteYTEmbed.preconnected = true + // } + + addIframe() { + const classNames = 'absolute inset-0 h-full w-full object-cover' + + // https://www.youtube-nocookie.com/embed/${video.id}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1 + + // const params = new URLSearchParams(this.getAttribute('params') || []) + // params.append('autoplay', '1') + + const iframeEl = document.createElement('iframe') + + iframeEl.width = '100%' + iframeEl.height = '100%' + // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include + // iframeEl.title = this.playLabel + iframeEl.id = this.frameId() + iframeEl.classList.add(...classNames.split(' ')) + iframeEl.frameborder = '0' + iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' + iframeEl.allowFullscreen = true + // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL + // https://stackoverflow.com/q/64959723/89484 + iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.video.id)}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1` + + // this.append(iframeEl) + this.playerContainer.innerHTML = '' + this.playerContainer.append( iframeEl ) + + this.classList.add('lyt-activated') + + // Set focus for a11y + this.querySelector('iframe').focus() + } + + + // Computed methods + + posterSources () { + const webpSource = { + ...this.video.thumbnail, + srcset: this.video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp') + } + + return { + webp: webpSource, + jpeg: this.video.thumbnail + } + } + + frameId () { + return `youtube-player-${this.video.id}-${this._uid}` + } + + timestamps () { + return this.video.timestamps.map( timestamp => { + const [ minutes, seconds ] = timestamp.time.split(':') + + return { + ...timestamp, + inSeconds: (minutes * 60) + Number(seconds) + } + }) + } + + hasTimestamps () { + return this.timestamps.length > 0 + } + + hasPlayer () { + return this.player !== null + } + + activeTimestamp () { + const currentTime = this.playerTime// / 100 + + const reversesTimestamps = [ + ...this.timestamps + ] + + // reversesTimestamps.reverse() + + let foundTimestamp = null + + for (const timestamp of reversesTimestamps) { + const hasStarted = currentTime > 1 + const currentTimeisAfterPreviousTimestamp = (foundTimestamp !== null) ? currentTime > foundTimestamp.inSeconds : true + // const isPastCurrentTime = currentTime > timestamp.inSeconds + // const isBeforeCurrentTime = currentTime > timestamp.inSeconds + const currentTimeIsBeforeThisTimestamp = currentTime < timestamp.inSeconds + + if (currentTimeisAfterPreviousTimestamp && currentTimeIsBeforeThisTimestamp) { + return foundTimestamp + } + + foundTimestamp = timestamp + } + + // No active timestamp + return null + } + + + scrollRow ( timestamp ) { + + // If timestamp button doesn't exist + // then stop + if (!this.$refs[`timestamp-${timestamp.time}`]) return + + const timestampsScroller = this.$refs['timestamps-scroll-container'] + const [ timestampButton ] = this.$refs[`timestamp-${timestamp.time}`] + + // https://stackoverflow.com/a/63773123/1397641 + const newScrollPosition = timestampButton.offsetLeft - timestampsScroller.offsetLeft + + timestampsScroller.scroll({ left: newScrollPosition, behavior: 'smooth' }) + } + + async detectAutoplay () { + + if ( !process.client ) return { willAutoplay: false } + + const { default: canAutoPlay } = await import('can-autoplay') + + const willAutoplay = await canAutoPlay.video() + // const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true }) + + return { + willAutoplay: willAutoplay.result + } + } + + async seekTo (timestampInSeconds) { + + if (this.playerLoaded === false) { + await this.startPlayerLoad() + } + + this.player.seekTo(timestampInSeconds) + } + + // async playVideo() { + + // if (this.playerLoaded === false) { + // await this.startPlayerLoad() + // } + + // this.$nextTick(() => { + // // console.log('this.player', JSON.stringify(this.player)) + // this.player.playVideo() + // }) + // }, + + static addPrefetch(kind, url, as) { + console.log('prefetching', url) + + const linkEl = document.createElement('link') + + linkEl.rel = kind + linkEl.href = url + + if (as) { + linkEl.as = as; + } + + document.head.append(linkEl) + } + + warmConnections () { if (LiteYTEmbed.preconnected) return + console.log('Warming connections') + // The iframe document and most of its subresources come right off youtube.com LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com') // The botguard script is fetched off from google.com @@ -81,26 +275,123 @@ class LiteYTEmbed extends HTMLElement { LiteYTEmbed.preconnected = true } - addIframe() { - const params = new URLSearchParams(this.getAttribute('params') || []) - params.append('autoplay', '1') + async startPlayerLoad () { + this.playerLoaded = true - const iframeEl = document.createElement('iframe') - iframeEl.width = 560 - iframeEl.height = 315 - // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include - iframeEl.title = this.playLabel - iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture' - iframeEl.allowFullscreen = true - // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL - // https://stackoverflow.com/q/64959723/89484 - iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${params.toString()}` - this.append(iframeEl) + await this.initializePlayer() - this.classList.add('lyt-activated') + // this.$nextTick(() => { + // this.initializePlayer() + // }) + } - // Set focus for a11y - this.querySelector('iframe').focus() + async initializePlayer () { + // console.log('Youtube Embed API Ready') + + // Clear player + this.player = null + + // Clear progession interval + clearInterval(this.progressInterval) + + // If there are no timestamps + // then stop + if (!this.hasTimestamps) { + this.playerLoaded = true + return + } + + if (typeof YT === 'undefined') { + await this.initializeApi() + } + + const stateHandlers = { + // unstarted + '-1': () => null, + // ended + '0': () => null, + // playing + '1': this.onPlayerPlaying, + // paused + '2': this.onPlayerPaused, + // buffering + '3': () => null, + // video cued + '4': () => null, + } + + // console.log('frame', this.$refs['frame']) + // console.log('frame id', this.$refs['frame'].id) + + const onReady = () => new Promise( resolve => { + + this.player = new YT.Player(this.$refs['frame'].id, { + events: { + 'onReady': readyEvent => { + this.onPlayerReady( readyEvent ) + + resolve( readyEvent ) + }, + 'onStateChange': event => { + // console.log('state changed', event) + + const stateHandler = stateHandlers[String(event.data)] + // console.log('stateHandler', stateHandler) + stateHandler(event) + } + } + }) + + }) + + await onReady() + + // console.log('Youtube Player API ready', JSON.stringify(this.player)) + } + + initializeApi () { + return new Promise( resolve => { + const tag = document.createElement('script') + tag.id = `youtube-api-script-${this._uid}` + tag.src = 'https://www.youtube.com/iframe_api' + + const firstScriptTag = document.getElementsByTagName('script')[0] + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag) + + + window.onYouTubeIframeAPIReady = resolve + }) + } + + onPlayerPlaying () { + console.log('Player playing') + this.playing = true + + this.progressInterval = setInterval(() => { + // console.log('this.player.getCurrentTime()', this.player.getCurrentTime()) + + // If player is empty + // then stop + if (this.player === null) { + clearInterval(this.progressInterval) + return + } + + // console.log('this.player', this.player.hasOwnProperty('getCurrentTime')) + + this.playerTime = this.player.getCurrentTime() + }, 500) + } + + onPlayerPaused () { + console.log('Player paused') + this.playing = false + + clearInterval(this.progressInterval) + } + + onPlayerReady (event) { + console.log('Player is ready', event, this.player ) } } // Register custom element diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 7e007f1..a6f41e2 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -90,7 +90,7 @@ class TV { // console.log('video.payload', Object.keys(video.payload)) - const playerHtml = await this.boundComponent(VideoPlayer)() + const playerHtml = await this.boundComponent(VideoPlayer)( video ) const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) From 2ac5f693b4877f54c70b9316ccc6da0e16991a41 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 15:19:00 -0500 Subject: [PATCH 22/36] Autoplay videos when allowed --- helpers/lite-youtube.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index 52f5cee..8151b8e 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -49,6 +49,18 @@ class LiteYTEmbed extends HTMLElement { this.playerPoster.addEventListener('click', e => this.addIframe()) + // Mounted + + this.detectAutoplay() + .then( ({ willAutoplay }) => { + console.log('willAutoplay', willAutoplay) + + // If we're allowed to autoplay + // then start loading the player + if ( willAutoplay === true ) { + this.startPlayerLoad() + } + }) } // // TODO: Support the the user changing the [videoid] attribute @@ -210,11 +222,11 @@ class LiteYTEmbed extends HTMLElement { async detectAutoplay () { - if ( !process.client ) return { willAutoplay: false } + // if ( !process.client ) return { willAutoplay: false } - const { default: canAutoPlay } = await import('can-autoplay') + // const { default: canAutoPlay } = await import('can-autoplay') - const willAutoplay = await canAutoPlay.video() + const willAutoplay = await canAutoplay.video() // const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true }) return { From 50f679698e78221eafc2cdd7e6d81827fdeebbd2 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 16:23:33 -0500 Subject: [PATCH 23/36] Enable player api --- components-eleventy/video/player.js | 27 ++++++++++++++++-- helpers/lite-youtube.js | 43 ++++++++++++++++++++++------- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/components-eleventy/video/player.js b/components-eleventy/video/player.js index 8ba495b..52954be 100644 --- a/components-eleventy/video/player.js +++ b/components-eleventy/video/player.js @@ -1,3 +1,22 @@ +function renderTimestamps ( video ) { + const timestampsForRender = video.timestamps.map( timestamp => { + const [ minutes, seconds ] = timestamp.time.split(':') + + return { + ...timestamp, + inSeconds: (minutes * 60) + Number(seconds) + } + }) + + return timestampsForRender.map( timestamp => (/* html */` + + `) ).join('') +} + export default async function ( video, options = {} ) { const { width = '325px', @@ -14,6 +33,8 @@ export default async function ( video, options = {} ) { // console.log('video', video) + const timestampsHtml = renderTimestamps( video ) + return /* html */`
-
- +
+
+ ${ timestampsHtml } +
` diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index 8151b8e..a9dfcce 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -21,6 +21,14 @@ class LiteYTEmbed extends HTMLElement { super() this._uid = Date.now() + this.$refs = {} + + this.playerLoaded = false + this.player = null + this.playing = false + this.progressInterval = null + this.playerTime = 0 + this.preconnected = false } connectedCallback() { @@ -46,7 +54,10 @@ class LiteYTEmbed extends HTMLElement { // Once the user clicks, add the real iframe and drop our play button // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time // We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 - this.playerPoster.addEventListener('click', e => this.addIframe()) + this.playerPoster.addEventListener('click', e => { + this.addIframe() + this.startPlayerLoad() + }) // Mounted @@ -114,6 +125,8 @@ class LiteYTEmbed extends HTMLElement { const iframeEl = document.createElement('iframe') + this.$refs['frame'] = iframeEl + iframeEl.width = '100%' iframeEl.height = '100%' // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include @@ -168,7 +181,7 @@ class LiteYTEmbed extends HTMLElement { } hasTimestamps () { - return this.timestamps.length > 0 + return this.timestamps().length > 0 } hasPlayer () { @@ -179,7 +192,7 @@ class LiteYTEmbed extends HTMLElement { const currentTime = this.playerTime// / 100 const reversesTimestamps = [ - ...this.timestamps + ...this.timestamps() ] // reversesTimestamps.reverse() @@ -256,7 +269,7 @@ class LiteYTEmbed extends HTMLElement { // }, static addPrefetch(kind, url, as) { - console.log('prefetching', url) + // console.log('prefetching', url) const linkEl = document.createElement('link') @@ -288,6 +301,8 @@ class LiteYTEmbed extends HTMLElement { } async startPlayerLoad () { + // console.log('Starting player load') + this.playerLoaded = true await this.initializePlayer() @@ -298,7 +313,7 @@ class LiteYTEmbed extends HTMLElement { } async initializePlayer () { - // console.log('Youtube Embed API Ready') + console.log('Initializing player') // Clear player this.player = null @@ -308,7 +323,9 @@ class LiteYTEmbed extends HTMLElement { // If there are no timestamps // then stop - if (!this.hasTimestamps) { + if ( !this.hasTimestamps() ) { + console.log('No timestamps. Skipping Youtube API initialization') + this.playerLoaded = true return } @@ -337,9 +354,13 @@ class LiteYTEmbed extends HTMLElement { const onReady = () => new Promise( resolve => { + // console.log('Started onReady') + this.player = new YT.Player(this.$refs['frame'].id, { events: { 'onReady': readyEvent => { + // console.log('Resolving onReady') + this.onPlayerReady( readyEvent ) resolve( readyEvent ) @@ -356,9 +377,11 @@ class LiteYTEmbed extends HTMLElement { }) - await onReady() + // console.log('Waiting for ready') - // console.log('Youtube Player API ready', JSON.stringify(this.player)) + const readyEvent = await onReady() + + // console.log('Youtube Player API ready', readyEvent, JSON.stringify(this.player)) } initializeApi () { @@ -375,8 +398,8 @@ class LiteYTEmbed extends HTMLElement { }) } - onPlayerPlaying () { - console.log('Player playing') + onPlayerPlaying = () => { + // console.log('Player playing') this.playing = true this.progressInterval = setInterval(() => { From 0a54de23098c300219aa23ec773b13f48ab71d54 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 16:27:48 -0500 Subject: [PATCH 24/36] Use arrow functions to keep methods bound to class --- helpers/lite-youtube.js | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index a9dfcce..c361b60 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -153,7 +153,7 @@ class LiteYTEmbed extends HTMLElement { // Computed methods - posterSources () { + posterSources = () => { const webpSource = { ...this.video.thumbnail, srcset: this.video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp') @@ -165,11 +165,11 @@ class LiteYTEmbed extends HTMLElement { } } - frameId () { + frameId = () => { return `youtube-player-${this.video.id}-${this._uid}` } - timestamps () { + timestamps = () => { return this.video.timestamps.map( timestamp => { const [ minutes, seconds ] = timestamp.time.split(':') @@ -180,15 +180,15 @@ class LiteYTEmbed extends HTMLElement { }) } - hasTimestamps () { + hasTimestamps = () => { return this.timestamps().length > 0 } - hasPlayer () { + hasPlayer = () => { return this.player !== null } - activeTimestamp () { + activeTimestamp = () => { const currentTime = this.playerTime// / 100 const reversesTimestamps = [ @@ -233,7 +233,7 @@ class LiteYTEmbed extends HTMLElement { timestampsScroller.scroll({ left: newScrollPosition, behavior: 'smooth' }) } - async detectAutoplay () { + detectAutoplay = async () => { // if ( !process.client ) return { willAutoplay: false } @@ -247,7 +247,7 @@ class LiteYTEmbed extends HTMLElement { } } - async seekTo (timestampInSeconds) { + seekTo = async (timestampInSeconds) => { if (this.playerLoaded === false) { await this.startPlayerLoad() @@ -268,7 +268,7 @@ class LiteYTEmbed extends HTMLElement { // }) // }, - static addPrefetch(kind, url, as) { + addPrefetch = (kind, url, as) => { // console.log('prefetching', url) const linkEl = document.createElement('link') @@ -283,24 +283,24 @@ class LiteYTEmbed extends HTMLElement { document.head.append(linkEl) } - warmConnections () { - if (LiteYTEmbed.preconnected) return + warmConnections = () => { + if (this.preconnected) return console.log('Warming connections') // The iframe document and most of its subresources come right off youtube.com - LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com') + this.addPrefetch('preconnect', 'https://www.youtube-nocookie.com') // The botguard script is fetched off from google.com - LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com') + this.addPrefetch('preconnect', 'https://www.google.com') // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling. - LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net') - LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net') + this.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net') + this.addPrefetch('preconnect', 'https://static.doubleclick.net') - LiteYTEmbed.preconnected = true + this.preconnected = true } - async startPlayerLoad () { + startPlayerLoad = async () => { // console.log('Starting player load') this.playerLoaded = true @@ -312,7 +312,7 @@ class LiteYTEmbed extends HTMLElement { // }) } - async initializePlayer () { + initializePlayer = async () => { console.log('Initializing player') // Clear player @@ -384,7 +384,7 @@ class LiteYTEmbed extends HTMLElement { // console.log('Youtube Player API ready', readyEvent, JSON.stringify(this.player)) } - initializeApi () { + initializeApi = () => { return new Promise( resolve => { const tag = document.createElement('script') tag.id = `youtube-api-script-${this._uid}` @@ -418,7 +418,7 @@ class LiteYTEmbed extends HTMLElement { }, 500) } - onPlayerPaused () { + onPlayerPaused = () => { console.log('Player paused') this.playing = false From adc336e1c2bb3366c37b4c492d1a829c27d493f1 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 17:10:59 -0500 Subject: [PATCH 25/36] Enable timestamps interaction --- helpers/lite-youtube.js | 74 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index c361b60..af23ce6 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -15,6 +15,10 @@ * https://github.com/Daugilas/lazyYT * https://github.com/vb/lazyframe */ + +function isObject( maybeObject ) { + return maybeObject === Object( maybeObject ) +} class LiteYTEmbed extends HTMLElement { constructor() { // Always call super first in constructor @@ -23,12 +27,16 @@ class LiteYTEmbed extends HTMLElement { this._uid = Date.now() this.$refs = {} + this.$refs['timestamps-scroll-container'] = this.querySelector('.player-timestamps-wrapper') + this.playerLoaded = false this.player = null this.playing = false this.progressInterval = null this.playerTime = 0 this.preconnected = false + + this.highlightedTimestampElement = null } connectedCallback() { @@ -51,11 +59,22 @@ class LiteYTEmbed extends HTMLElement { // On hover (or tap), warm up the TCP connections we're (likely) about to use. this.playerContainer.addEventListener('pointerover', this.warmConnections, {once: true}) + if ( this.hasTimestamps() ) { + // Array.from( this.querySelectorAll(`.player-timestamps [time]`) ) + + this.timestamps().map( timestamp => { + const timestampButton = this.querySelector(`.player-timestamps [time="${timestamp.time}"]`) + + timestampButton.addEventListener('click', e => { + this.seekTo(timestamp.inSeconds) + }) + }) + } + // Once the user clicks, add the real iframe and drop our play button // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time // We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003 this.playerPoster.addEventListener('click', e => { - this.addIframe() this.startPlayerLoad() }) @@ -217,19 +236,59 @@ class LiteYTEmbed extends HTMLElement { return null } + highlightActiveTimestamp = () => { + const activeClassList = 'border-opacity-100 bg-darkest' + const inactiveClassList = 'border-opacity-0 neumorphic-shadow-inner' - scrollRow ( timestamp ) { + const activeTimestamp = this.activeTimestamp() + + // If there's no active timestamp + // then stop + if ( activeTimestamp === null ) return + + // console.log('activeTimestamp', activeTimestamp) + + if ( isObject( this.highlightedTimestampElement ) && this.highlightedTimestampElement.time === activeTimestamp.time) return + + // Change active timestamp + + const newActiveTimestamp = this.querySelector(`[time="${activeTimestamp.time}"]`) + + // If there's already a highlited time stamp + // then unhighlight it + if ( isObject( this.highlightedTimestampElement ) ) { + this.highlightedTimestampElement.classList.remove(...activeClassList.split(' ')) + this.highlightedTimestampElement.classList.add(...inactiveClassList.split(' ')) + } + + newActiveTimestamp.classList.remove(...inactiveClassList.split(' ')) + newActiveTimestamp.classList.add(...activeClassList.split(' ')) + + // Scroll to new timestamp + this.scrollToTimestampButton( newActiveTimestamp ) + + this.highlightedTimestampElement = newActiveTimestamp + + // console.log('newActiveTimestamp', newActiveTimestamp) + // console.log('this.highlightedTimestampElement', this.highlightedTimestampElement) + } + + scrollToTimestampButton ( timestampButton ) { // If timestamp button doesn't exist // then stop - if (!this.$refs[`timestamp-${timestamp.time}`]) return + if ( !isObject( timestampButton ) ) return const timestampsScroller = this.$refs['timestamps-scroll-container'] - const [ timestampButton ] = this.$refs[`timestamp-${timestamp.time}`] + // const [ timestampButton ] = this.$refs[`timestamp-${timestamp.time}`] // https://stackoverflow.com/a/63773123/1397641 const newScrollPosition = timestampButton.offsetLeft - timestampsScroller.offsetLeft + // console.log('timestampsScroller', timestampsScroller) + // console.log('timestampButton', timestampButton) + // console.log('newScrollPosition', newScrollPosition) + timestampsScroller.scroll({ left: newScrollPosition, behavior: 'smooth' }) } @@ -303,6 +362,8 @@ class LiteYTEmbed extends HTMLElement { startPlayerLoad = async () => { // console.log('Starting player load') + this.addIframe() + this.playerLoaded = true await this.initializePlayer() @@ -415,6 +476,9 @@ class LiteYTEmbed extends HTMLElement { // console.log('this.player', this.player.hasOwnProperty('getCurrentTime')) this.playerTime = this.player.getCurrentTime() + + this.highlightActiveTimestamp() + }, 500) } @@ -426,7 +490,7 @@ class LiteYTEmbed extends HTMLElement { } onPlayerReady (event) { - console.log('Player is ready', event, this.player ) + // console.log('Player is ready', event, this.player ) } } // Register custom element From 1e1dd7ada76e4b80260abf8618c9a09976e43aaf Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 17:30:43 -0500 Subject: [PATCH 26/36] Add channel subscription link --- pages-eleventy/tv.11ty.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index a6f41e2..ed71896 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -10,6 +10,8 @@ import { getRouteType } from '../helpers/app-derived' // Setup dotenv dotenv.config() +export const myChannelId = 'UCB3jOb5QVjX7lYecvyCoTqQ' + export const makeTitle = function ( name ) { // console.log('tvEntry', tvEntry) @@ -103,8 +105,20 @@ class TV { ${ playerHtml }
-

${ video.name }

- + + ${ video.channel.id !== myChannelId ? /* html */` + + ` : '' }

From 911b67d5695eca8d1d718fec2bb33ed02ea5766e Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 17:35:08 -0500 Subject: [PATCH 27/36] Add video coverBottomHtml --- components-eleventy/video/player.js | 13 ++++++------- pages-eleventy/tv.11ty.js | 10 +++++++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/components-eleventy/video/player.js b/components-eleventy/video/player.js index 52954be..39658fa 100644 --- a/components-eleventy/video/player.js +++ b/components-eleventy/video/player.js @@ -19,8 +19,8 @@ function renderTimestamps ( video ) { export default async function ( video, options = {} ) { const { - width = '325px', - classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden' + coverBottomHtml = '' + // classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden' } = options // Setup inline player script @@ -43,14 +43,17 @@ export default async function ( video, options = {} ) { +
+ M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5! +
@@ -58,11 +61,7 @@ export default async function ( video, options = {} ) {
-
-
-

M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5!

-
-
+
${ coverBottomHtml }
diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index ed71896..0c40a29 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -92,7 +92,15 @@ class TV { // console.log('video.payload', Object.keys(video.payload)) - const playerHtml = await this.boundComponent(VideoPlayer)( video ) + const coverBottomHtml = /* html */` +
+

${ video.name }

+
+ ` + + const playerHtml = await this.boundComponent(VideoPlayer)( video, { + coverBottomHtml + } ) const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) From f45d48db1d560f64d2aa75f028617bb3aa0d9051 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:01:02 -0500 Subject: [PATCH 28/36] Move buildVideoStructuredData to helper --- helpers/structured-data.js | 45 ++++++++++++++++++++++++++++++++++++++ pages/tv/_slug.vue | 43 +++++------------------------------- 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 helpers/structured-data.js diff --git a/helpers/structured-data.js b/helpers/structured-data.js new file mode 100644 index 0000000..a4800fd --- /dev/null +++ b/helpers/structured-data.js @@ -0,0 +1,45 @@ +function makeFeaturedAppsString ( featuredApps ) { + return featuredApps.slice(0, 5).map(app => app.name).join(', ') +} + +export function buildVideoStructuredData ( video, featuredApps, options ) { + // console.log('video', video) + + const { + siteUrl + } = options + + const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => { + const [ imageUrl ] = srcSetImage.trim().split(' ') + + return imageUrl + }) + + const featuredAppsString = makeFeaturedAppsString( featuredApps ) + + const embedUrl = new URL( `${ siteUrl }/embed/rich-results-player` ) + + embedUrl.searchParams.append( 'youtube-id', video.id ) + embedUrl.searchParams.append( 'name', video.name ) + + return { + "@context": "https://schema.org", + // https://developers.google.com/search/docs/data-types/video + // https://schema.org/VideoObject + "@type": "VideoObject", + "name": video.name, + "description": `Includes the following apps: ${featuredAppsString}`, + "thumbnailUrl": thumbnailUrls, + // https://en.wikipedia.org/wiki/ISO_8601 + "uploadDate": video.lastUpdated.raw, + // "duration": "PT1M54S", // Need to updaet Youtube API Request for this + // "contentUrl": "https://www.example.com/video/123/file.mp4", + "embedUrl": embedUrl.href, + // "interactionStatistic": { + // "@type": "InteractionCounter", + // "interactionType": { "@type": "http://schema.org/WatchAction" }, + // "userInteractionCount": 5647018 + // }, + // "regionsAllowed": "US,NL" + } +} diff --git a/pages/tv/_slug.vue b/pages/tv/_slug.vue index dc7a4dc..eed6ad1 100644 --- a/pages/tv/_slug.vue +++ b/pages/tv/_slug.vue @@ -81,6 +81,8 @@ import { getAppEndpoint } from '~/helpers/app-derived.js' +import { buildVideoStructuredData } from '~/helpers/structure-data.js' + import LinkButton from '~/components/link-button.vue' import EmailSubscribe from '~/components/email-subscribe.vue' import VideoRow from '~/components/video/row.vue' @@ -93,43 +95,6 @@ function makeFeaturedAppsString ( featuredApps ) { } -function buildVideoStructuredData ( video, featuredApps ) { - // console.log('video', video) - - const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => { - const [ imageUrl ] = srcSetImage.trim().split(' ') - - return imageUrl - }) - - const featuredAppsString = makeFeaturedAppsString( featuredApps ) - - const embedUrl = new URL( `${ this.$config.siteUrl }/embed/rich-results-player` ) - - embedUrl.searchParams.append( 'youtube-id', video.id ) - embedUrl.searchParams.append( 'name', video.name ) - - return { - "@context": "https://schema.org", - // https://developers.google.com/search/docs/data-types/video - // https://schema.org/VideoObject - "@type": "VideoObject", - "name": video.name, - "description": `Includes the following apps: ${featuredAppsString}`, - "thumbnailUrl": thumbnailUrls, - // https://en.wikipedia.org/wiki/ISO_8601 - "uploadDate": video.lastUpdated.raw, - // "duration": "PT1M54S", // Need to updaet Youtube API Request for this - // "contentUrl": "https://www.example.com/video/123/file.mp4", - "embedUrl": embedUrl.href, - // "interactionStatistic": { - // "@type": "InteractionCounter", - // "interactionType": { "@type": "http://schema.org/WatchAction" }, - // "userInteractionCount": 5647018 - // }, - // "regionsAllowed": "US,NL" - } -} export default { components: { LinkButton, @@ -184,7 +149,9 @@ export default { getAppEndpoint }, head() { - const structuredData = buildVideoStructuredData.bind(this)( this.video, this.featuredApps ) + const structuredData = buildVideoStructuredData( this.video, this.featuredApps, { + siteUrl: this.$config.siteUrl + } ) return { From 95f9b24174e8534767a68de1987ac245c54883ed Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:02:05 -0500 Subject: [PATCH 29/36] Enable structured data for eleventy layout --- layouts-eleventy/default.11ty.js | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/layouts-eleventy/default.11ty.js b/layouts-eleventy/default.11ty.js index 3a32b14..a053e2a 100644 --- a/layouts-eleventy/default.11ty.js +++ b/layouts-eleventy/default.11ty.js @@ -1,7 +1,7 @@ import fs from 'fs' import { JSDOM } from 'jsdom' -import config from '../nuxt.config' +import config from '../nuxt.config.js' console.log('Running Default Layout file') @@ -94,6 +94,9 @@ const cleanNuxtLayout = ( layout ) => { // Set link tags document.querySelector('title').insertAdjacentHTML('afterend', templateVar('link-tags') ) + // Add meta tags after title node + document.querySelector('title').insertAdjacentHTML('afterend', templateVar('structured-data') ) + // Add meta tags after title node document.querySelector('title').insertAdjacentHTML('afterend', templateVar('meta-tags') ) @@ -172,6 +175,21 @@ class DefaultLayout { return Object.values(meta).join('') } + generateStructuredData = function ( renderData ) { + + const { + structuredData = null + } = renderData + + // console.log('renderData', Object.keys(renderData)) + + if ( structuredData === null ) return '' + + const structuredDataJson = JSON.stringify( structuredData ) + + return `` + } + generateLinkTags = ( pageLinkTags = [] ) => { const linkTags = { @@ -211,6 +229,9 @@ class DefaultLayout { // this.generateMetaTags( data ) workingLayoutString = workingLayoutString.replace( templateVar('meta-tags'), this.generateMetaTags( data ) ) + // Add structured data + workingLayoutString = workingLayoutString.replace( templateVar('structured-data'), this.generateStructuredData( data ) ) + // Set page css // document.querySelector('head').insertAdjacentHTML('beforeend', this.getCss() ) From 9cf7c13861cbef928cfeb0c84a20417acb935c78 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:02:35 -0500 Subject: [PATCH 30/36] Add video structure data to eleventy tv template --- pages-eleventy/tv.11ty.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 0c40a29..669f751 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -1,11 +1,12 @@ import dotenv from 'dotenv' -import config from '../nuxt.config' +import config from '../nuxt.config.js' import VideoPlayer from '../components-eleventy/video/player.js' import VideoRow from '../components-eleventy/video/row.js' -import { getRouteType } from '../helpers/app-derived' +import { getRouteType } from '../helpers/app-derived.js' +import { buildVideoStructuredData } from '../helpers/structured-data.js' // Setup dotenv dotenv.config() @@ -67,6 +68,15 @@ class TV { return makeDescription( data.tvEntry ) }, + structuredData: data => { + // Declare dependencies for Eleventy + // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies + data.tvEntry + + return buildVideoStructuredData( data.tvEntry.payload.video, data.tvEntry.payload.featuredApps, { + siteUrl: process.env.URL + } ) + } }, permalink: ( data ) => { From c631ba1c9ae9ee7494aeafc0cf819c19e6797f36 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:21:04 -0500 Subject: [PATCH 31/36] Fix misspelling --- pages/tv/_slug.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/tv/_slug.vue b/pages/tv/_slug.vue index eed6ad1..c21e8e3 100644 --- a/pages/tv/_slug.vue +++ b/pages/tv/_slug.vue @@ -81,7 +81,7 @@ import { getAppEndpoint } from '~/helpers/app-derived.js' -import { buildVideoStructuredData } from '~/helpers/structure-data.js' +import { buildVideoStructuredData } from '~/helpers/structured-data.js' import LinkButton from '~/components/link-button.vue' import EmailSubscribe from '~/components/email-subscribe.vue' From 5cfcd5a7aa2e0b8b523ab7fab4fd875105df4aa0 Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:28:15 -0500 Subject: [PATCH 32/36] Enable custom links tags on eleventy layout --- layouts-eleventy/default.11ty.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/layouts-eleventy/default.11ty.js b/layouts-eleventy/default.11ty.js index a053e2a..5c45153 100644 --- a/layouts-eleventy/default.11ty.js +++ b/layouts-eleventy/default.11ty.js @@ -190,11 +190,15 @@ class DefaultLayout { return `` } - generateLinkTags = ( pageLinkTags = [] ) => { + generateLinkTags = ( renderData ) => { + + const { + headLinkTags = [] + } = renderData const linkTags = { ...defaultLinkTags, - ...Object.fromEntries(pageLinkTags.map( mapLinkTag )) + ...Object.fromEntries( headLinkTags.map( mapLinkTag ) ) } return Object.values( linkTags ).join('') @@ -223,7 +227,7 @@ class DefaultLayout { // Set link tags // this.generateLinkTags() - workingLayoutString = workingLayoutString.replace( templateVar('link-tags'), this.generateLinkTags() ) + workingLayoutString = workingLayoutString.replace( templateVar('link-tags'), this.generateLinkTags( data ) ) // Add meta tags after title node // this.generateMetaTags( data ) From 7b004ea590c35e75d108fccde8a258cbfbfdd86a Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 18:28:51 -0500 Subject: [PATCH 33/36] Add tv page posters with preload --- components-eleventy/video/player.js | 39 +++++++++++++++++++++++++---- helpers/lite-youtube.js | 12 --------- pages-eleventy/tv.11ty.js | 17 +++++++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/components-eleventy/video/player.js b/components-eleventy/video/player.js index 39658fa..c87eca2 100644 --- a/components-eleventy/video/player.js +++ b/components-eleventy/video/player.js @@ -1,3 +1,34 @@ +function renderPoster ( video ) { + const webpSource = { + ...video.thumbnail, + srcset: video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp') + } + + const mergedSources = { + webp: webpSource, + jpeg: video.thumbnail + } + + return /* html */` + + + ${ Object.entries( mergedSources ).map( ([ key, source ]) => (/* html */` + + `) ).join('') } + + ${ video.name } + + ` +} + function renderTimestamps ( video ) { const timestampsForRender = video.timestamps.map( timestamp => { const [ minutes, seconds ] = timestamp.time.split(':') @@ -33,6 +64,8 @@ export default async function ( video, options = {} ) { // console.log('video', video) + const posterHtml = renderPoster( video ) + const timestampsHtml = renderTimestamps( video ) return /* html */` @@ -48,11 +81,7 @@ export default async function ( video, options = {} ) {
- - - - M1 Macs + Windows 10 GAMING and PERFORMANCE Improvements with Parallels 16.5! - + ${ posterHtml }
diff --git a/helpers/lite-youtube.js b/helpers/lite-youtube.js index af23ce6..7c4d7f8 100644 --- a/helpers/lite-youtube.js +++ b/helpers/lite-youtube.js @@ -172,18 +172,6 @@ class LiteYTEmbed extends HTMLElement { // Computed methods - posterSources = () => { - const webpSource = { - ...this.video.thumbnail, - srcset: this.video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp') - } - - return { - webp: webpSource, - jpeg: this.video.thumbnail - } - } - frameId = () => { return `youtube-player-${this.video.id}-${this._uid}` } diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index 669f751..e254365 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -68,6 +68,23 @@ class TV { return makeDescription( data.tvEntry ) }, + + headLinkTags: data => { + // Declare dependencies for Eleventy + // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies + data.tvEntry + + return [ + // Preload video thumbnail + // + { + 'rel': 'preload', + 'as': 'image', + 'href': `https://i.ytimg.com/vi_webp/${ data.tvEntry.payload.video.id }/sddefault.webp` + }, + ] + }, + structuredData: data => { // Declare dependencies for Eleventy // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies From f7d7c7266fda2222428941b3d1ee37a0b02ec8bf Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 19:01:06 -0500 Subject: [PATCH 34/36] Use poster component for cards --- components-eleventy/video/card.js | 16 ++++----------- components-eleventy/video/player.js | 31 +--------------------------- components-eleventy/video/poster.js | 32 +++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 42 deletions(-) create mode 100644 components-eleventy/video/poster.js diff --git a/components-eleventy/video/card.js b/components-eleventy/video/card.js index a21fb49..ba5501a 100644 --- a/components-eleventy/video/card.js +++ b/components-eleventy/video/card.js @@ -1,3 +1,4 @@ +import renderPoster from './poster.js' function pill ( text ) { return /* html */` @@ -20,6 +21,8 @@ export default async function ( video, options = {} ) { // console.log('video', video) + const posterHtml = renderPoster( video ) + return /* html */`
- - - ${video.name} - + ${ posterHtml }
- - ${ Object.entries( mergedSources ).map( ([ key, source ]) => (/* html */` - - `) ).join('') } - - ${ video.name } - - ` -} +import renderPoster from './poster.js' function renderTimestamps ( video ) { const timestampsForRender = video.timestamps.map( timestamp => { diff --git a/components-eleventy/video/poster.js b/components-eleventy/video/poster.js new file mode 100644 index 0000000..d78d888 --- /dev/null +++ b/components-eleventy/video/poster.js @@ -0,0 +1,32 @@ + + +export default function ( video ) { + const webpSource = { + ...video.thumbnail, + srcset: video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp') + } + + const mergedSources = { + webp: webpSource, + jpeg: video.thumbnail + } + + return /* html */` + + + ${ Object.entries( mergedSources ).map( ([ key, source ]) => (/* html */` + + `) ).join('') } + + ${ video.name } + + ` +} From d27e3af03be81a472813a27cd9c105a742aef8ec Mon Sep 17 00:00:00 2001 From: Sam Carlton Date: Sat, 15 May 2021 19:01:17 -0500 Subject: [PATCH 35/36] Render featured apps --- pages-eleventy/tv.11ty.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pages-eleventy/tv.11ty.js b/pages-eleventy/tv.11ty.js index e254365..c316d85 100644 --- a/pages-eleventy/tv.11ty.js +++ b/pages-eleventy/tv.11ty.js @@ -29,6 +29,28 @@ export const makeDescription = function ( tvEntry ) { return `Apple Silicon performance and support videos for ${ featuredAppsString }` } + +function renderFeaturedApps ( featuredApps ) { + return /* html */` + + ` +} + class TV { // or `async data() {` // or `get data() {` @@ -110,7 +132,8 @@ class TV { // route, payload: { video, - relatedVideos = [] + relatedVideos = [], + featuredApps = [] } }, // 'device-list': deviceList @@ -129,6 +152,9 @@ class TV { coverBottomHtml } ) + const hasFeaturedApps = featuredApps.length > 0 + const featuredAppsHtml = hasFeaturedApps ? renderFeaturedApps( featuredApps ) : '' + const rowHtml = await this.boundComponent(VideoRow)( relatedVideos ) // const rowHtml = renderedRow.join('') @@ -158,10 +184,7 @@ class TV {
- + ${ featuredAppsHtml }
-
-
- ${ timestampsHtml } -
-
+ + ${ timestampsHtml } + ` }