diff --git a/assets/bookmark.png b/assets/bookmark.png new file mode 100644 index 0000000..85102c9 Binary files /dev/null and b/assets/bookmark.png differ diff --git a/assets/delete.png b/assets/delete.png new file mode 100644 index 0000000..2bb9bd1 Binary files /dev/null and b/assets/delete.png differ diff --git a/assets/ext-icon.png b/assets/ext-icon.png new file mode 100644 index 0000000..78b0c4c Binary files /dev/null and b/assets/ext-icon.png differ diff --git a/assets/play.png b/assets/play.png new file mode 100644 index 0000000..527bb67 Binary files /dev/null and b/assets/play.png differ diff --git a/assets/save.png b/assets/save.png new file mode 100644 index 0000000..beed6fd Binary files /dev/null and b/assets/save.png differ diff --git a/background.js b/background.js new file mode 100644 index 0000000..a57a2cf --- /dev/null +++ b/background.js @@ -0,0 +1,11 @@ +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"), + }); + } +}); diff --git a/contentScript.js b/contentScript.js new file mode 100644 index 0000000..2ea049d --- /dev/null +++ b/contentScript.js @@ -0,0 +1,72 @@ +(() => { + 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); +}; diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..d9b899c --- /dev/null +++ b/manifest.json @@ -0,0 +1,37 @@ +{ + "name": "youtube Bookmarks", + "version": "1.0.0", + "description": "Saving timestamps in YouTube 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": "My YT Bookmarks", + "default_popup": "popup.html" + }, + "manifest_version": 3 +} diff --git a/popup.css b/popup.css new file mode 100644 index 0000000..2dc0ae4 --- /dev/null +++ b/popup.css @@ -0,0 +1,53 @@ +.container { + width: 280px; + color: #314d3e; +} + +.title { + font-size: 14px; + font-weight: bold; + padding: 8px; +} + +.textbox { + width: 100%; + font-size: 12px; + margin: 0; + padding: 0px 2px; +} + +.textbox:focus { + outline: 0; + border-color: #66afe9; +} + +.bookmarks { + margin: 5px 5px; + padding: 3px; +} + +.bookmark { + display: flex; + border-bottom-color: #00254d; + border-bottom-width: 1px; + border-bottom-style: solid; + border-radius: 0.8rem; + padding: 3px; + padding-bottom: 7px; + margin-bottom: 7px; +} + +.bookmark-title { + padding-left: 2px; +} + +.bookmark-controls img { + margin: 0 4px; + width: 18px; + height: 18px; + cursor: pointer; +} +.bookmark-controls { + flex: auto; + text-align: right; +} diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..7591a8c --- /dev/null +++ b/popup.html @@ -0,0 +1,10 @@ + + + +
+
+
Your bookmarks for this video
+ +
+
+
diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..6e1390c --- /dev/null +++ b/popup.js @@ -0,0 +1,93 @@ +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 = 'No bookmarks to show'; + } + + 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 = '
This is not a youtube video page.
'; + } +}); + diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..3db4a89 --- /dev/null +++ b/utils.js @@ -0,0 +1,9 @@ +export async function getActiveTabURL() { + const tabs = await chrome.tabs.query({ + currentWindow: true, + active: true + }); + + return tabs[0]; +} +