What is Chrome extension?
A Chrome extension is a small software program that extends the functionality of Google's Chrome web browser. It allows users to customize and enhance their browsing experience by adding new features and functionalities to the browser. Chrome extensions are written in web technologies like HTML, CSS, and JavaScript, making them easy to develop and deploy.
When developing a Chrome extension, the first step is to create the manifest.json file. The manifest.json file serves as the heart of the extension, providing crucial information about the extension, including its name, version, description, permissions, background scripts, content scripts, icons, and more.
Manifest.json
{
"name": "Friendly YouTube",
"version": "0.1.0",
"description": "Save important timestamps in YT videos",
"permissions": ["storage", "tabs"],
"host_permissions": ["https://*.youtube.com/*"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["https://*.youtube.com/*"],
"js": ["contentScript.js"]
}
],
"web_accessible_resources": [
{
"resources": [
"assets/bookmark.png",
"assets/play.png",
"assets/delete.png",
"assets/save.png"
],
"matches": ["https://*.youtube.com/*"]
}
],
"action": {
"default_icon": {
"16": "assets/ext-icon.png",
"24": "assets/ext-icon.png",
"32": "assets/ext-icon.png"
},
"default_title": "Friendly YouTube",
"default_popup": "popup.html"
},
"manifest_version": 3
}
"name": "Friendly YouTube"
: Specifies the name of the extension."version": "0.1.0"
: Indicates the version of the extension."description": "Save important timestamps in YT videos"
: Describes the purpose of the extension."permissions": ["storage", "tabs"]
: Specifies the permissions required by the extension. In this case, it needs access to storage (to save data) and tabs (to interact with browser tabs)."host_permissions": ["https://*.
youtube.com/*
"]
: Grants the extension access to specific websites (in this case, all subdomains of youtube.com)."background": { "service_worker": "background.js" }
: Defines the background script for the extension using a service worker named "background.js.""content_scripts": [...]
: Specifies the content scripts to be injected into web pages. The extension will inject "contentScript.js" into all YouTube pages."web_accessible_resources": [...]
: Lists resources (images in this case) that can be accessed by web pages. Here, the extension makes the images in "assets" folder available to be used on YouTube pages."action": { ... }
: Configures the browser action (extension icon) behavior. It sets the default icon and popup content to be shown when the icon is clicked.
"manifest_version": 3
: Indicates the version of the manifest format being used (Manifest V3).
ContentScript.js
(() => {
let youtubeLeftControls, youtubePlayer;
let currentVideo = "";
let currentVideoBookmarks = [];
const fetchBookmarks = () => {
return new Promise((resolve) => {
chrome.storage.sync.get([currentVideo], (obj) => {
resolve(obj[currentVideo] ? JSON.parse(obj[currentVideo]) : []);
});
});
};
const addNewBookmarkEventHandler = async () => {
const currentTime = youtubePlayer.currentTime;
const newBookmark = {
time: currentTime,
desc: "Bookmark at " + getTime(currentTime),
};
currentVideoBookmarks = await fetchBookmarks();
chrome.storage.sync.set({
[currentVideo]: JSON.stringify([...currentVideoBookmarks, newBookmark].sort((a, b) => a.time - b.time))
});
};
const newVideoLoaded = async () => {
const bookmarkBtnExists = document.getElementsByClassName("bookmark-btn")[0];
currentVideoBookmarks = await fetchBookmarks();
if (!bookmarkBtnExists) {
const bookmarkBtn = document.createElement("img");
bookmarkBtn.src = chrome.runtime.getURL("assets/bookmark.png");
bookmarkBtn.className = "ytp-button " + "bookmark-btn";
bookmarkBtn.title = "Click to bookmark current timestamp";
youtubeLeftControls = document.getElementsByClassName("ytp-left-controls")[0];
youtubePlayer = document.getElementsByClassName('video-stream')[0];
youtubeLeftControls.appendChild(bookmarkBtn);
bookmarkBtn.addEventListener("click", addNewBookmarkEventHandler);
}
};
chrome.runtime.onMessage.addListener((obj, sender, response) => {
const { type, value, videoId } = obj;
if (type === "NEW") {
currentVideo = videoId;
newVideoLoaded();
} else if (type === "PLAY") {
youtubePlayer.currentTime = value;
} else if ( type === "DELETE") {
currentVideoBookmarks = currentVideoBookmarks.filter((b) => b.time != value);
chrome.storage.sync.set({ [currentVideo]: JSON.stringify(currentVideoBookmarks) });
response(currentVideoBookmarks);
}
});
newVideoLoaded();
})();
const getTime = t => {
var date = new Date(0);
date.setSeconds(t);
return date.toISOString().substr(11, 8);
};
let youtubeLeftControls, youtubePlayer;
: These variables will store references to the elements in the YouTube video player interface where the bookmark button will be added.let currentVideo = "";
: This variable will store the video ID of the currently loaded YouTube video.let currentVideoBookmarks = [];
: This array will hold the bookmarks for the current video. Each bookmark is an object with properties "time" (timestamp in seconds) and "desc" (description of the bookmark).fetchBookmarks()
: This function retrieves the bookmarks for the current video from the Chrome storage using thechrome.storage
.sync.get()
method. It returns a Promise that resolves with the bookmarks array or an empty array if there are no bookmarks stored.addNewBookmarkEventHandler()
: This function is called when the user clicks the bookmark button. It calculates the current time of the YouTube video and creates a new bookmark object. Then it fetches the current video's bookmarks usingfetchBookmarks()
, adds the new bookmark to the array, sorts the bookmarks by time, and saves the updated bookmarks to Chrome storage usingchrome.storage
.sync.set()
.newVideoLoaded()
: This function is called when a new YouTube video is loaded. It checks if the bookmark button already exists on the page. If not, it creates a new bookmark button, adds it to the YouTube video player's left controls, and sets up a click event listener to calladdNewBookmarkEventHandler()
when the button is clicked.chrome.runtime.onMessage.addListener()
: This is an event listener that listens for messages sent from other parts of the extension (background script or popup) usingchrome.runtime.sendMessage()
. Depending on the message type, the script handles "NEW" messages (indicating a new video is loaded), "PLAY" messages (to play the video from a specific bookmarked timestamp), and "DELETE" messages (to delete a bookmark).
getTime(t)
: This is a utility function that converts a given time in seconds to a formatted time string in the format "HH:mm:ss" (hours:minutes:seconds). It takes the time in seconds as input and returns the formatted time string.
background.js
chrome.tabs.onUpdated.addListener((tabId, tab) => {
if (tab.url && tab.url.includes("youtube.com/watch")) {
const queryParameters = tab.url.split("?")[1];
const urlParameters = new URLSearchParams(queryParameters);
chrome.tabs.sendMessage(tabId, {
type: "NEW",
videoId: urlParameters.get("v"),
});
}
});
chrome.tabs.onUpdated.addListener((tabId, tab) => { ... })
: This function registers an event listener for thechrome.tabs.onUpdated
event. TheonUpdated
event is triggered whenever the status of a tab changes, including when a page is fully loaded or when the URL of a tab is modified.tabId
: This parameter represents the ID of the updated tab.tab
: This parameter is an object that contains information about the updated tab, including the URL and other details.if (tab.url && tab.url.includes("
youtube.com/watch
")) { ... }
: This condition checks if the updated tab has a valid URL and if that URL includes "youtube.com/watch". This ensures that the event listener only acts on YouTube video pages.const queryParameters = tab.url.split("?")[1];
: This line extracts the query parameters part of the YouTube video URL. The query parameters are the parts of the URL that come after the "?" symbol.const urlParameters = new URLSearchParams(queryParameters);
: This line creates a newURLSearchParams
object using the query parameters extracted from the URL. It allows easy access to individual parameters.
chrome.tabs.sendMessage(tabId, { type: "NEW", videoId: urlParameters.get("v") });
: If the tab is a YouTube video page, this code sends a message to the content script of that tab using chrome.tabs.sendMessage()
. The message contains an object with two properties: type
and videoId
. The type
is set to "NEW", indicating that a new video is loaded or updated. The videoId
is extracted from the query parameters, representing the unique ID of the YouTube video being played.
Utils.js
export async function getActiveTabURL() {
const tabs = await chrome.tabs.query({
currentWindow: true,
active: true
});
return tabs[0];
}
export async function getActiveTabURL() { ... }
: This line defines an exported asynchronous function namedgetActiveTabURL()
. Theasync
keyword indicates that the function will use asynchronous operations, and theexport
keyword makes the function accessible to other modules.const tabs = await chrome.tabs.query({ currentWindow: true, active: true });
: This line uses thechrome.tabs.query()
method to retrieve information about the tabs in the current window. The method takes an object as an argument with properties to specify the query criteria. In this case, it searches for the active tab in the current window (identified bycurrentWindow: true
andactive: true
).await
: Theawait
keyword is used before thechrome.tabs.query()
method call to wait for the asynchronous operation to complete. As the function is marked asasync
, it allows the use ofawait
to handle promises in a synchronous-looking manner.return tabs[0];
: Once thechrome.tabs.query()
method completes, the result is an array of tab objects that match the query criteria. Since we specifiedactive: true
, the array will contain only one element, representing the currently active tab. This line returns the first element of thetabs
array, which is the tab object representing the active tab.
Now we add a bit of css to the popups and finally create popup.js
import { getActiveTabURL } from "./utils.js";
const addNewBookmark = (bookmarks, bookmark) => {
const bookmarkTitleElement = document.createElement("div");
const controlsElement = document.createElement("div");
const newBookmarkElement = document.createElement("div");
bookmarkTitleElement.textContent = bookmark.desc;
bookmarkTitleElement.className = "bookmark-title";
controlsElement.className = "bookmark-controls";
setBookmarkAttributes("play", onPlay, controlsElement);
setBookmarkAttributes("delete", onDelete, controlsElement);
newBookmarkElement.id = "bookmark-" + bookmark.time;
newBookmarkElement.className = "bookmark";
newBookmarkElement.setAttribute("timestamp", bookmark.time);
newBookmarkElement.appendChild(bookmarkTitleElement);
newBookmarkElement.appendChild(controlsElement);
bookmarks.appendChild(newBookmarkElement);
};
const viewBookmarks = (currentBookmarks=[]) => {
const bookmarksElement = document.getElementById("bookmarks");
bookmarksElement.innerHTML = "";
if (currentBookmarks.length > 0) {
for (let i = 0; i < currentBookmarks.length; i++) {
const bookmark = currentBookmarks[i];
addNewBookmark(bookmarksElement, bookmark);
}
} else {
bookmarksElement.innerHTML = '<i class="row">No bookmarks to show</i>';
}
return;
};
const onPlay = async e => {
const bookmarkTime = e.target.parentNode.parentNode.getAttribute("timestamp");
const activeTab = await getActiveTabURL();
chrome.tabs.sendMessage(activeTab.id, {
type: "PLAY",
value: bookmarkTime,
});
};
const onDelete = async e => {
const activeTab = await getActiveTabURL();
const bookmarkTime = e.target.parentNode.parentNode.getAttribute("timestamp");
const bookmarkElementToDelete = document.getElementById(
"bookmark-" + bookmarkTime
);
bookmarkElementToDelete.parentNode.removeChild(bookmarkElementToDelete);
chrome.tabs.sendMessage(activeTab.id, {
type: "DELETE",
value: bookmarkTime,
}, viewBookmarks);
};
const setBookmarkAttributes = (src, eventListener, controlParentElement) => {
const controlElement = document.createElement("img");
controlElement.src = "assets/" + src + ".png";
controlElement.title = src;
controlElement.addEventListener("click", eventListener);
controlParentElement.appendChild(controlElement);
};
document.addEventListener("DOMContentLoaded", async () => {
const activeTab = await getActiveTabURL();
const queryParameters = activeTab.url.split("?")[1];
const urlParameters = new URLSearchParams(queryParameters);
const currentVideo = urlParameters.get("v");
if (activeTab.url.includes("youtube.com/watch") && currentVideo) {
chrome.storage.sync.get([currentVideo], (data) => {
const currentVideoBookmarks = data[currentVideo] ? JSON.parse(data[currentVideo]) : [];
viewBookmarks(currentVideoBookmarks);
});
} else {
const container = document.getElementsByClassName("container")[0];
container.innerHTML = '<div class="title">This is not a youtube video page.</div>';
}
});
import { getActiveTabURL } from "./utils.js";
: This line imports thegetActiveTabURL
function from the "utils.js" file. The function is used to retrieve the URL of the currently active tab in the Chrome browser.addNewBookmark(bookmarks, bookmark) { ... }
: This function is responsible for adding a new bookmark to the list of bookmarks displayed on the popup page. It takes two parameters:bookmarks
, which is the container element for displaying bookmarks, andbookmark
, which is the bookmark object to be added.viewBookmarks(currentBookmarks=[]) { ... }
: This function updates the popup page to display the list of bookmarks. It takes an optional parametercurrentBookmarks
, which is an array of bookmark objects. If there are bookmarks, it iterates through the array and callsaddNewBookmark
to add each bookmark to the display. If there are no bookmarks, it displays a message indicating that there are no bookmarks.onPlay(e) { ... }
: This function is called when the user clicks the play button associated with a bookmark. It retrieves the timestamp of the bookmark from the HTML element, gets the active tab's URL usinggetActiveTabURL
, and sends a message to the content script of the active tab with the bookmark's timestamp. This message indicates that the video should play from that bookmarked timestamp.onDelete(e) { ... }
: This function is called when the user clicks the delete button associated with a bookmark. It gets the active tab's URL usinggetActiveTabURL
, retrieves the timestamp of the bookmark from the HTML element, removes the bookmark's HTML element from the display, and sends a message to the content script of the active tab with the bookmark's timestamp. This message indicates that the bookmark should be deleted.setBookmarkAttributes(src, eventListener, controlParentElement) { ... }
: This function is a utility function to create and set attributes for the play and delete control elements (images) associated with each bookmark. It takes the source of the image (play or delete), an event listener, and the container element for the controls.document.addEventListener("DOMContentLoaded", async () => { ... }
: This event listener triggers when the popup page's content is loaded. It performs several actions, including:Getting the URL of the active tab using
getActiveTabURL
.Checking if the active tab is a YouTube video page and extracting the video ID from the URL.
If it is a YouTube video page, it fetches the bookmarks for the current video from Chrome storage using
chrome.storage
.sync.get()
and callsviewBookmarks
to display them.If it is not a YouTube video page, it displays a message indicating that it is not a YouTube video page.
This Chrome extension allows users to save bookmarks (timestamps) on YouTube videos by clicking the bookmark button on the video player. Bookmarks are stored in Chrome storage, and the extension dynamically displays the list of bookmarks on the popup page. Users can click the play button associated with a bookmark to play the video from that timestamp or click the delete button to remove a bookmark. The extension communicates between the background script, content script, and popup script to ensure seamless functionality across different parts of the extension.