Skip to content

Commit

Permalink
Merge pull request #75 from xwp/update/reuse-downloader-instances
Browse files Browse the repository at this point in the history
Video downloader registry. Refactor params passing in the router.
  • Loading branch information
derekherman authored Mar 10, 2021
2 parents cdb1ebd + e24c78c commit da8ec7d
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 65 deletions.
10 changes: 9 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import Router from './js/modules/Router.module';
import updateOnlineStatus from './js/utils/updateOnlineStatus';
import initializeGlobalToggle from './js/utils/initializeGlobalToggle';
import VideoDownloaderRegistry from './js/modules/VideoDownloaderRegistry.module';

/**
* Web Components implementation.
Expand Down Expand Up @@ -34,10 +35,17 @@ customElements.define('video-grid', VideoGrid);
customElements.define('toggle-button', ToggleButton);
customElements.define('progress-ring', ProgressRing);

/**
* Initialize a registry holding instances of the `VideoDownload` web components.
*
* This is to allow us to share these instances between pages.
*/
const videoDownloaderRegistry = new VideoDownloaderRegistry();

/**
* Router setup.
*/
const router = new Router();
const router = new Router({ videoDownloaderRegistry });
router.route('/', HomePage);
router.route('/settings', SettingsPage);
router.route('/downloads', DownloadsPage);
Expand Down
39 changes: 21 additions & 18 deletions src/js/modules/Router.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ const globalClickHandler = (navigate) => (e) => {
};

export default class Router {
constructor() {
/**
*
* @param {object} context Any initial context to be passed to pages.
*/
constructor(context = {}) {
this.routes = [];
this.context = context;

window.addEventListener('popstate', () => this.run());

Expand All @@ -29,39 +34,37 @@ export default class Router {
}

async init() {
this.videoDataArray = await fetch('/api.json')
.then((response) => response.json());
const response = await fetch('/api.json');
const apiData = await response.json();

this.context.apiData = apiData;
this.context.mainContent = document.querySelector('main');
this.context.navigate = this.navigate.bind(this);

this.run();
}

run() {
const mainContent = document.querySelector('main');
const { videoDataArray } = this;
const path = window.location.pathname;
this.context.path = window.location.pathname;

const navigate = this.navigate.bind(this);
const callbackArgs = {
mainContent, videoDataArray, path, navigate,
};
const foundRoute = this.routes.find((route) => {
if (route.path instanceof RegExp) {
return route.path.test(path);
return route.path.test(this.context.path);
}
return route.path === path;
return route.path === this.context.path;
});

this.context.mainContent.innerHTML = '';
if (foundRoute) {
mainContent.innerHTML = '';
foundRoute.callback(callbackArgs);
foundRoute.callback(this.context);
} else {
const catchAllRoute = this.routes.find((route) => route.path === '*');
if (catchAllRoute) {
mainContent.innerHTML = '';
catchAllRoute.callback(callbackArgs);
catchAllRoute.callback(this.context);
} else {
mainContent.innerHTML = '<h1>404</h1>';
this.context.mainContent.innerHTML = '<h1>404</h1>';
}
}

window.scrollTo(0, 0);
}

Expand Down
38 changes: 38 additions & 0 deletions src/js/modules/VideoDownloaderRegistry.module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import VideoDownloader from '../components/VideoDownloader';

/**
* The `VideoDownloader` web component contains a download method that is potentially
* long running. This registry allows different pages to share a single `VideoDownloader`
* instance per videoId.
*
* This helps maintain the component's state even across page loads.
*/
export default class VideoDownloaderRegistry {
constructor() {
this.instances = {};
}

/**
* Creates a new `VideoDownloader` instance for the given `videoId`.
*
* @param {string} videoId Video ID.
*
* @returns {VideoDownloader} Instantiated VideoDownloader.
*/
create(videoId) {
this.instances[videoId] = new VideoDownloader();

return this.instances[videoId];
}

/**
* Returns a previously instantiated VideoDownloader.
*
* @param {string} videoId Get the `VideoDownloader` instance for this video id.
*
* @returns {VideoDownloader|null} `VideoDownloader` instance.
*/
get(videoId) {
return this.instances[videoId] || null;
}
}
38 changes: 26 additions & 12 deletions src/js/pages/Category.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import appendVideoToGallery from '../utils/appendVideoToGallery';
* @todo Later on if we introduce Categories endpoint that will be changed.
*
* @param {string} slug Slug.
* @param {object[]} videoDataArray Array of video metadata objects.
* @param {object[]} apiData Array of video metadata objects.
*
* @returns {string} Category name or empty string.
*/
function findCategoryNameBySlug(slug, videoDataArray) {
for (let i = 0; i < videoDataArray.length; i += 1) {
for (let j = 0; j < videoDataArray[i].categories.length; j += 1) {
const cat = videoDataArray[i].categories[j];
function findCategoryNameBySlug(slug, apiData) {
for (let i = 0; i < apiData.length; i += 1) {
for (let j = 0; j < apiData[i].categories.length; j += 1) {
const cat = apiData[i].categories[j];
if (slugify(cat) === slug) {
return cat;
}
Expand All @@ -23,21 +23,35 @@ function findCategoryNameBySlug(slug, videoDataArray) {
return '';
}

export default ({
mainContent, videoDataArray, path, navigate,
}) => {
/**
* @param {RouterContext} routerContext Context object passed by the Router.
*/
export default (routerContext) => {
const {
mainContent,
apiData,
path,
} = routerContext;

const categorySlug = path.replace('/category/', '');
const categoryName = findCategoryNameBySlug(categorySlug, videoDataArray);
const categoryName = findCategoryNameBySlug(categorySlug, apiData);
mainContent.innerHTML = `
<div class="page-title">
<h2>${categoryName}</h2>
<img src="/images/arrow-down.svg" alt="" />
</div>
<div class="category"></div>
`;
const content = mainContent.querySelector('.category');
const filteredVideoDataArray = videoDataArray.filter(

const filteredApiData = apiData.filter(
(videoData) => videoData.categories.includes(categoryName),
);
appendVideoToGallery(filteredVideoDataArray, navigate, '', content);
const localContext = {
content: mainContent.querySelector('.category'),
};

appendVideoToGallery({
...routerContext,
apiData: filteredApiData,
}, localContext);
};
21 changes: 17 additions & 4 deletions src/js/pages/Downloads.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import getIDBConnection from '../modules/IDBConnection.module';
import { SW_CACHE_NAME } from '../constants';

export default async ({ mainContent, videoDataArray, navigate }) => {
/**
* @param {RouterContext} routerContext Context object passed by the Router.
*/
export default async (routerContext) => {
const {
mainContent,
apiData,
navigate,
videoDownloaderRegistry,
} = routerContext;
mainContent.innerHTML = `
<style>
.grid {
Expand Down Expand Up @@ -42,10 +51,14 @@ export default async ({ mainContent, videoDataArray, navigate }) => {
}

allMeta.forEach((meta) => {
const videoData = videoDataArray.find((vd) => vd.id === meta.videoId);
const videoData = apiData.find((vd) => vd.id === meta.videoId);
const card = document.createElement('video-card');
const downloader = document.createElement('video-downloader');
downloader.init(videoData, SW_CACHE_NAME);
let downloader = videoDownloaderRegistry.get(videoData.id);
if (!downloader) {
downloader = videoDownloaderRegistry.create(videoData.id);
downloader.init(videoData, SW_CACHE_NAME);
}
downloader.setAttribute('expanded', 'false');
card.render(videoData, navigate);
card.attachDownloader(downloader);
grid.appendChild(card);
Expand Down
22 changes: 17 additions & 5 deletions src/js/pages/Home.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
import appendVideoToGallery from '../utils/appendVideoToGallery';
import getPoster from './partials/Poster.partial';

export default ({ mainContent, videoDataArray, navigate }) => {
if (videoDataArray[0]) {
const mainVideoData = videoDataArray[0] || {};
/**
* @param {RouterContext} routerContext Context object passed by the Router.
*/
export default (routerContext) => {
const { mainContent, apiData } = routerContext;
if (apiData[0]) {
const mainVideoData = apiData[0] || {};
mainContent.appendChild(getPoster(mainVideoData));
}

const videosByCategories = videoDataArray.reduce((acc, videoData) => {
const videosByCategories = apiData.reduce((acc, videoData) => {
videoData.categories.forEach((category) => {
if (!(category in acc)) acc[category] = [];
acc[category].push(videoData);
});
return acc;
}, {});

Object.keys(videosByCategories).forEach((category, index) => {
appendVideoToGallery(videosByCategories[category], navigate, category, mainContent, index);
const localContext = {
category,
index,
};
appendVideoToGallery({
...routerContext,
apiData: videosByCategories[category],
}, localContext);
});
};
6 changes: 5 additions & 1 deletion src/js/pages/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ const onChange = (key) => ({ detail }) => {
saveSetting(key, detail.value);
};

export default ({ mainContent }) => {
/**
* @param {RouterContext} routerContext Context object passed by the Router.
*/
export default (routerContext) => {
const { mainContent } = routerContext;
mainContent.innerHTML = `
<div class="page-title">
<h2>Settings</h2>
Expand Down
60 changes: 46 additions & 14 deletions src/js/pages/Video.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,68 @@ import appendVideoToGallery from '../utils/appendVideoToGallery';
import getPoster from './partials/Poster.partial';
import { SW_CACHE_NAME } from '../constants';

export default ({
mainContent,
videoDataArray,
path,
navigate,
}) => {
const videoData = videoDataArray.find((vd) => `/${slugify(vd.title)}` === path);
const posterWrapper = getPoster(videoData, true);
/**
* @param {RouterContext} routerContext Context object passed by the Router.
*/
export default (routerContext) => {
const {
mainContent,
apiData,
path,
videoDownloaderRegistry,
} = routerContext;

/**
* Pick the current video data out of the `apiData` array
* and also return the rest of that data.
*/
const [currentVideoData, restVideoData] = apiData.reduce(
(returnValue, videoMeta) => {
if (`/${slugify(videoMeta.title)}` === path) {
returnValue[0] = videoMeta;
} else {
returnValue[1].push(videoMeta);
}
return returnValue;
},
[null, []],
);

const posterWrapper = getPoster(currentVideoData, true);

mainContent.innerHTML = `
<article>
<div class="container">
<h2>${videoData.title}</h2>
<h2>${currentVideoData.title}</h2>
<div class="info">
<span class="date">4th March, 2017</span>
<span class="length">7mins 43secs</span>
</div>
<p>${videoData.description}</p>
<p>${currentVideoData.description}</p>
<span class="downloader"></span>
</div>
</article>
<div class="category"></div>
`;
mainContent.prepend(posterWrapper);

const downloader = document.createElement('video-downloader');
let downloader = videoDownloaderRegistry.get(currentVideoData.id);
if (!downloader) {
downloader = videoDownloaderRegistry.create(currentVideoData.id);
downloader.init(currentVideoData, SW_CACHE_NAME);
}
downloader.setAttribute('expanded', 'true');
downloader.init(videoData, SW_CACHE_NAME);

mainContent.querySelector('.downloader').appendChild(downloader);
const localContext = {
content: mainContent.querySelector('.category'),
};

const content = mainContent.querySelector('.category');
appendVideoToGallery(videoDataArray, navigate, '', content);
/**
* Passing `restVideoData` to avoid duplication of content on the single page.
*/
appendVideoToGallery({
...routerContext,
apiData: restVideoData,
}, localContext);
};
17 changes: 17 additions & 0 deletions src/js/typedefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,20 @@
* @property {string} canPlayTypeNatively Can the client play the source natively or using MSE?
* @property {string} canPlayTypeMSE Can the client natively play the returned source?
*/

/**
* @typedef {object} VideoDownloaderRegistry
* @property {Function} create Creates a new `VideoDownload` instance.
* @property {Function} get Returns a previously created `VideoDownload` instance or null.
* @property {object} instance Holds `VideoDownload` instances keyed by video IDs.
*/

/**
* @typedef {object} RouterContext
* @property {Array} apiData Array of sets of video metadata information from the API.
* @property {HTMLElement} mainContent Elements representing the main content area in the page.
* @property {Function} navigate Method to navigate between pages.
* @property {string} path Current URL path.
* @property {VideoDownloaderRegistry} VideoDownloaderRegistry Storage for `videoDownload`
* instances reuse.
*/
Loading

0 comments on commit da8ec7d

Please sign in to comment.