├── background ├── background.html ├── onInstall.js └── webNavigation.js ├── content_scripts ├── autoplay.js └── setup.js ├── globals.js ├── logos ├── logo-128.png ├── logo-16.png ├── logo-32.png ├── logo-48.png └── logo-64.png ├── manifest.json ├── popup ├── popup.css ├── popup.html └── popup.js ├── script.js └── utils └── chromeStorage.js /background/background.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /background/onInstall.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_STORAGE_VALUES } from "../globals.js" 2 | import { getStorageItem, setAllStorageItems } from "../utils/chromeStorage.js" 3 | 4 | chrome.runtime.onInstalled.addListener(async () => { 5 | const storage = await getStorageItem() 6 | setAllStorageItems({ ...DEFAULT_STORAGE_VALUES, ...storage }) 7 | }) 8 | -------------------------------------------------------------------------------- /background/webNavigation.js: -------------------------------------------------------------------------------- 1 | let tabId 2 | let currentUrl = "" 3 | 4 | chrome.webRequest.onCompleted.addListener( 5 | details => { 6 | const parsedUrl = new URL(details.url) 7 | 8 | if (currentUrl && currentUrl.indexOf(parsedUrl.pathname) > -1 && tabId) { 9 | chrome.tabs.sendMessage(tabId, { type: "page-changed" }) 10 | } 11 | }, 12 | { urls: ["https://courses.webdevsimplified.com/courses/*"] } 13 | ) 14 | 15 | chrome.webNavigation.onHistoryStateUpdated.addListener( 16 | details => { 17 | tabId = details.tabId 18 | currentUrl = details.url 19 | }, 20 | { 21 | url: [ 22 | { 23 | hostEquals: "courses.webdevsimplified.com", 24 | pathPrefix: "/courses/" 25 | } 26 | ] 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /content_scripts/autoplay.js: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEYS } from "../globals.js" 2 | import { getStorageItem } from "../utils/chromeStorage.js" 3 | 4 | let startVideo = false 5 | 6 | export default async function setupAutoplay() { 7 | const autoplay = await getStorageItem(STORAGE_KEYS.AUTOPLAY) 8 | if (!autoplay) return 9 | 10 | const nextButton = getNextButton() 11 | if (nextButton == null) return 12 | 13 | if (startVideo) { 14 | const playButton = await getPlayButton() 15 | playButton.click() 16 | } 17 | 18 | startVideo = false 19 | const video = await getVideo() 20 | video.addEventListener("ended", () => { 21 | startVideo = true 22 | nextButton.click() 23 | }) 24 | } 25 | 26 | function getNextButton() { 27 | const courseButtons = [ 28 | ...document.querySelectorAll(".user-course-pager a[data-block-id]") 29 | ] 30 | 31 | return courseButtons.find(button => { 32 | return ( 33 | button.textContent.includes("Next") || button.textContent.includes("→") 34 | ) 35 | }) 36 | } 37 | 38 | function getPlayButton() { 39 | const hasResumeButton = getWistiaData().resume_time > 0 40 | if (hasResumeButton) { 41 | return getResumeButton() 42 | } else { 43 | return getStartButton() 44 | } 45 | } 46 | 47 | function getStartButton() { 48 | return new Promise(resolve => { 49 | const observer = new MutationObserver((mutationList, o) => { 50 | mutationList.forEach(mutation => { 51 | if ( 52 | mutation.type === "childList" && 53 | (mutation.target.matches(".w-big-play-button") || 54 | mutation.target.querySelector(".w-big-play-button")) 55 | ) { 56 | o.disconnect() 57 | if (mutation.target.matches(".w-big-play-button")) { 58 | resolve(mutation.target) 59 | } else { 60 | resolve(mutation.target.querySelector(".w-big-play-button")) 61 | } 62 | } 63 | }) 64 | }) 65 | observer.observe(document.querySelector(".wistia_embed"), { 66 | subtree: true, 67 | childList: true 68 | }) 69 | 70 | const button = document.querySelector(".w-big-play-button") 71 | if (button) { 72 | observer.disconnect() 73 | resolve(button) 74 | } 75 | }) 76 | } 77 | 78 | function getResumeButton() { 79 | return new Promise(resolve => { 80 | const observer = new MutationObserver((mutationList, o) => { 81 | mutationList.forEach(mutation => { 82 | if ( 83 | mutation.type === "childList" && 84 | mutation.target.textContent.includes("Skip to where you left off") 85 | ) { 86 | o.disconnect() 87 | if (mutation.target.matches("a")) return resolve(mutation.target) 88 | const buttons = [...mutation.target.querySelectorAll("a")] 89 | const resumeButton = buttons.find(button => { 90 | return button.textContent.includes("Skip to where you left off") 91 | }) 92 | if (resumeButton) resolve(resumeButton) 93 | } 94 | }) 95 | }) 96 | 97 | observer.observe(document.querySelector(".wistia_embed"), { 98 | subtree: true, 99 | childList: true 100 | }) 101 | 102 | const buttons = [...document.querySelectorAll(".w-chrome a")] 103 | const resumeButton = buttons.find(button => { 104 | return button.textContent.includes("Skip to where you left off") 105 | }) 106 | if (resumeButton) { 107 | observer.disconnect() 108 | resolve(resumeButton) 109 | } 110 | }) 111 | } 112 | 113 | function getVideo() { 114 | return new Promise(resolve => { 115 | const observer = new MutationObserver((mutationList, o) => { 116 | mutationList.forEach(mutation => { 117 | if (mutation.type === "childList" && mutation.target.matches("video")) { 118 | o.disconnect() 119 | resolve(mutation.target) 120 | } 121 | }) 122 | }) 123 | observer.observe(document.querySelector(".wistia_embed"), { 124 | subtree: true, 125 | childList: true 126 | }) 127 | 128 | const video = document.querySelector(".w-video-wrapper > video") 129 | if (video) { 130 | observer.disconnect() 131 | resolve(video) 132 | } 133 | }) 134 | } 135 | 136 | function getWistiaId() { 137 | const wistiaEmbed = document.querySelector(".wistia_embed") 138 | const wistiaIdClass = Array.from(wistiaEmbed.classList).find(c => { 139 | return c.startsWith("wistia_async_") 140 | }) 141 | 142 | if (wistiaIdClass != null) return wistiaIdClass.replace(/^wistia_async_/, "") 143 | return wistiaEmbed.id.replace(/-1$/, "") 144 | } 145 | 146 | function getWistiaData() { 147 | const localWistiaData = JSON.parse(localStorage.getItem("wistia")) || {} 148 | const currentWistiaVideo = localWistiaData[window.location] || {} 149 | return currentWistiaVideo[getWistiaId()] || { resume_time: 0 } 150 | } 151 | -------------------------------------------------------------------------------- /content_scripts/setup.js: -------------------------------------------------------------------------------- 1 | import setupAutoplay from "./autoplay.js" 2 | 3 | function setup() { 4 | setupAutoplay() 5 | } 6 | 7 | setup() 8 | 9 | chrome.runtime.onMessage.addListener(function(request) { 10 | if (request && request.type === "page-changed") setup() 11 | }) 12 | -------------------------------------------------------------------------------- /globals.js: -------------------------------------------------------------------------------- 1 | export const STORAGE_KEYS = { 2 | AUTOPLAY: "autoplay" 3 | } 4 | 5 | export const DEFAULT_STORAGE_VALUES = { 6 | [STORAGE_KEYS.AUTOPLAY]: true 7 | } 8 | -------------------------------------------------------------------------------- /logos/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/course-site-chrome-extension/e7c6606b47919af2a7dd34aa37d0d15ba1cd668e/logos/logo-128.png -------------------------------------------------------------------------------- /logos/logo-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/course-site-chrome-extension/e7c6606b47919af2a7dd34aa37d0d15ba1cd668e/logos/logo-16.png -------------------------------------------------------------------------------- /logos/logo-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/course-site-chrome-extension/e7c6606b47919af2a7dd34aa37d0d15ba1cd668e/logos/logo-32.png -------------------------------------------------------------------------------- /logos/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/course-site-chrome-extension/e7c6606b47919af2a7dd34aa37d0d15ba1cd668e/logos/logo-48.png -------------------------------------------------------------------------------- /logos/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebDevSimplified/course-site-chrome-extension/e7c6606b47919af2a7dd34aa37d0d15ba1cd668e/logos/logo-64.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WDS Course Site Extension", 3 | "description" : "Adds additional functionality to Web Dev Simplified's course platform.", 4 | "version": "0.1.0", 5 | "manifest_version": 2, 6 | "permissions": ["https://courses.webdevsimplified.com/courses/*", "webRequest", "webNavigation", "storage"], 7 | "background": { 8 | "page": "background/background.html", 9 | "persistent": true 10 | }, 11 | "browser_action": { 12 | "default_popup": "popup/popup.html" 13 | }, 14 | "content_scripts": [ 15 | { 16 | "matches": ["https://courses.webdevsimplified.com/courses/*"], 17 | "js": ["script.js"] 18 | } 19 | ], 20 | "icons": { 21 | "16": "logos/logo-16.png", 22 | "32": "logos/logo-16.png", 23 | "48": "logos/logo-48.png", 24 | "64": "logos/logo-64.png", 25 | "128": "logos/logo-128.png" 26 | }, 27 | "web_accessible_resources": [ 28 | "content_scripts/*", 29 | "utils/*", 30 | "globals.js" 31 | ] 32 | } -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | width: max-content; 8 | font-size: 1rem; 9 | } 10 | 11 | .container { 12 | padding: 1rem; 13 | /* This makes it look more centered at least for now */ 14 | padding-bottom: 1.25rem; 15 | } 16 | 17 | .header { 18 | padding: 1rem; 19 | background-color: #00AAFF; 20 | color: white; 21 | } 22 | 23 | .header-title { 24 | text-align: center; 25 | font-size: 1.5rem; 26 | margin: 0; 27 | } 28 | 29 | .form-group { 30 | display: flex; 31 | flex-direction: column; 32 | margin-bottom: 1rem; 33 | align-items: start; 34 | } 35 | 36 | .form-group:last-child { 37 | margin-bottom: 0; 38 | } 39 | 40 | .form-label { 41 | color: #555; 42 | margin-bottom: .5rem; 43 | font-size: .9rem; 44 | font-weight: bold; 45 | } 46 | 47 | .footer { 48 | padding: 1rem; 49 | background-color: #F5F5F5; 50 | } 51 | 52 | .footer-link { 53 | color: #333; 54 | } 55 | 56 | /* CHECKBOX */ 57 | 58 | .switch { 59 | position: relative; 60 | display: inline-block; 61 | } 62 | 63 | .switch-input { 64 | display: none; 65 | } 66 | 67 | .switch-label { 68 | display: block; 69 | width: 48px; 70 | height: 24px; 71 | text-indent: -150%; 72 | clip: rect(0 0 0 0); 73 | color: transparent; 74 | user-select: none; 75 | } 76 | 77 | .switch-label::before, 78 | .switch-label::after { 79 | content: ""; 80 | display: block; 81 | position: absolute; 82 | cursor: pointer; 83 | } 84 | 85 | .switch-label::before { 86 | width: 100%; 87 | height: 100%; 88 | background-color: #dedede; 89 | border-radius: 9999em; 90 | -webkit-transition: background-color 0.25s ease; 91 | transition: background-color 0.25s ease; 92 | } 93 | 94 | .switch-label::after { 95 | top: 0; 96 | left: 0; 97 | width: 24px; 98 | height: 24px; 99 | border-radius: 50%; 100 | background-color: #fff; 101 | box-shadow: 0 0 2px rgba(0, 0, 0, 0.45); 102 | -webkit-transition: left 0.25s ease; 103 | transition: left 0.25s ease; 104 | } 105 | 106 | .switch-input:checked + .switch-label::before { 107 | background-color: #00AAFF; 108 | } 109 | 110 | .switch-input:checked + .switch-label::after { 111 | left: 24px; 112 | } -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

WDS Course

10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | 23 | 24 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | import { STORAGE_KEYS } from "../globals.js" 2 | import { setStorageItem, getStorageItem } from "../utils/chromeStorage.js" 3 | 4 | const INPUTS = [ 5 | { 6 | selector: "#autoplay-checkbox", 7 | valueProperty: "checked", 8 | key: STORAGE_KEYS.AUTOPLAY 9 | } 10 | ] 11 | 12 | function setupInputs() { 13 | INPUTS.map(input => { 14 | const element = document.querySelector(input.selector) 15 | getStorageItem(input.key).then(value => { 16 | element[input.valueProperty] = value 17 | }) 18 | 19 | element.addEventListener("change", () => { 20 | setStorageItem(input.key, element[input.valueProperty]) 21 | }) 22 | }) 23 | } 24 | 25 | setupInputs() 26 | 27 | document.querySelectorAll("a").forEach(link => { 28 | link.addEventListener("click", () => { 29 | chrome.tabs.create({ active: true, url: link.href }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | async function load() { 2 | await import(chrome.extension.getURL("content_scripts/setup.js")) 3 | } 4 | 5 | load() 6 | -------------------------------------------------------------------------------- /utils/chromeStorage.js: -------------------------------------------------------------------------------- 1 | export function getStorageItem(key) { 2 | return new Promise(resolve => { 3 | if (key) { 4 | chrome.storage.sync.get(key, obj => { 5 | resolve(obj[key]) 6 | }) 7 | } else { 8 | chrome.storage.sync.get(resolve) 9 | } 10 | }) 11 | } 12 | 13 | export function setStorageItem(key, value) { 14 | return new Promise(resolve => { 15 | chrome.storage.sync.set({ [key]: value }, resolve) 16 | }) 17 | } 18 | 19 | export function setAllStorageItems(obj) { 20 | return new Promise(resolve => { 21 | chrome.storage.sync.set(obj, resolve) 22 | }) 23 | } 24 | --------------------------------------------------------------------------------