├── images ├── screenshots │ ├── 1.png │ ├── 2.png │ ├── 3.png │ └── 4.png ├── ytGREPArchitecture.png └── promotional │ ├── 1400x560.png │ ├── 440x280.png │ └── 920x680.png ├── src ├── assets │ └── icons │ │ ├── ytGrep16.png │ │ ├── ytGrep24.png │ │ ├── ytGrep32.png │ │ ├── ytGrep48.png │ │ ├── ytGrep128.png │ │ ├── ytGrep_inactive16.png │ │ └── ytGrep_inactive24.png ├── inject │ ├── ytPlayer.js │ └── getTranscript.js ├── ui │ ├── index.html │ ├── styles.css │ └── base.js ├── background.js ├── manifest.json └── contentScript.js ├── LICENSE └── README.md /images/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/screenshots/1.png -------------------------------------------------------------------------------- /images/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/screenshots/2.png -------------------------------------------------------------------------------- /images/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/screenshots/3.png -------------------------------------------------------------------------------- /images/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/screenshots/4.png -------------------------------------------------------------------------------- /images/ytGREPArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/ytGREPArchitecture.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep16.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep24.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep32.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep48.png -------------------------------------------------------------------------------- /images/promotional/1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/promotional/1400x560.png -------------------------------------------------------------------------------- /images/promotional/440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/promotional/440x280.png -------------------------------------------------------------------------------- /images/promotional/920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/images/promotional/920x680.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep128.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep_inactive16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep_inactive16.png -------------------------------------------------------------------------------- /src/assets/icons/ytGrep_inactive24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sr1jan/ytGREP/HEAD/src/assets/icons/ytGrep_inactive24.png -------------------------------------------------------------------------------- /src/inject/ytPlayer.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | // let player = document.getElementById('movie_player'); 3 | 4 | window.addEventListener("message", function(event){ 5 | if(event.source != window) return; 6 | 7 | if (event.data.type && (event.data.type === "PLAYER") && (event.data.action === "PAUSE")) { 8 | // player.pauseVideo(); 9 | document.getElementById('movie_player').pauseVideo(); 10 | } 11 | 12 | if (event.data.type && (event.data.type === "PLAYER") && (event.data.action === "PLAY")) { 13 | // player.playVideo(); 14 | document.getElementById('movie_player').playVideo(); 15 | } 16 | 17 | if (event.data.type && (event.data.type === "PLAYER") && (event.data.action === "SEEK")) { 18 | // player.seekTo(event.data.time); 19 | // player.playVideo(); 20 | document.getElementById('movie_player').seekTo(event.data.time); 21 | document.getElementById('movie_player').playVideo(); 22 | } 23 | }) 24 | })(); 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Srijan Singh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ytGREP 5 | 6 | 7 | 8 | 9 | 10 | 11 |

ytGREP

12 |
13 |

github

14 |

15 |

raise issue

16 |

17 |

contact

18 |
19 |

load transcript

20 |
21 |
22 | 23 | 24 |
25 |
26 |

No transcript loaded!

27 |
28 |
29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | console.log("[YTGREP] BackgroundJS init"); 4 | 5 | // extension only active on youtube watch page 6 | const default_rule = { 7 | id: "enable_action", 8 | conditions: [ 9 | new chrome.declarativeContent.PageStateMatcher({ 10 | pageUrl: { 11 | hostEquals: "www.youtube.com", 12 | schemes: ["https"], 13 | pathContains: "watch", 14 | }, 15 | }), 16 | ], 17 | actions: [ 18 | new chrome.declarativeContent.ShowAction(), 19 | new chrome.declarativeContent.SetIcon({ 20 | path: { 21 | 16: "./assets/icons/ytGrep16.png", 22 | 24: "./assets/icons/ytGrep24.png", 23 | }, 24 | }), 25 | ], 26 | }; 27 | 28 | chrome.runtime.onInstalled.addListener(function () { 29 | console.log("ytGrep installed successfully!"); 30 | chrome.action.disable(); 31 | 32 | chrome.declarativeContent.onPageChanged.removeRules( 33 | undefined, 34 | function callback() { 35 | chrome.declarativeContent.onPageChanged.addRules([default_rule]); 36 | } 37 | ); 38 | }); 39 | 40 | chrome.tabs.onRemoved.addListener(function (tabId) { 41 | try { 42 | chrome.storage.local.remove(tabId.toString(), function () { 43 | if (chrome.runtime.lastError === undefined) { 44 | console.log("Removed:", tabId); 45 | } else { 46 | console.log(chrome.runtime.lastError); 47 | } 48 | }); 49 | } catch (e) { 50 | console.log(e); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ytGREP", 4 | "version": "1.4.2", 5 | "description": "Search for words or sentences in youtube videos", 6 | "author": "Srijan Singh", 7 | "homepage_url": "https://github.com/sr1jan/ytGREP", 8 | "icons": { 9 | "16": "assets/icons/ytGrep16.png", 10 | "24": "assets/icons/ytGrep24.png", 11 | "32": "assets/icons/ytGrep32.png", 12 | "48": "assets/icons/ytGrep48.png", 13 | "128": "assets/icons/ytGrep128.png" 14 | }, 15 | "permissions": ["activeTab", "declarativeContent", "storage", "scripting", "tabs"], 16 | "background": { 17 | "service_worker": "background.js" 18 | }, 19 | "web_accessible_resources": [{ 20 | "resources": ["inject/getTranscript.js", "inject/ytPlayer.js"], 21 | "matches": ["*://www.youtube.com/*"] 22 | }], 23 | "content_scripts": [ 24 | { 25 | "matches": ["https://www.youtube.com/*"], 26 | "js": ["contentScript.js"], 27 | "run_at": "document_idle" 28 | } 29 | ], 30 | "action": { 31 | "default_popup": "ui/index.html", 32 | "default_icon": { 33 | "16": "assets/icons/ytGrep_inactive16.png", 34 | "24": "assets/icons/ytGrep_inactive24.png", 35 | "32": "assets/icons/ytGrep32.png", 36 | "48": "assets/icons/ytGrep48.png", 37 | "128": "assets/icons/ytGrep128.png" 38 | } 39 | }, 40 | "commands": { 41 | "_execute_page_action": { 42 | "suggested_key": { 43 | "default": "Ctrl+Shift+S", 44 | "windows": "Alt+Shift+S", 45 | "mac": "Alt+Shift+S" 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/contentScript.js: -------------------------------------------------------------------------------- 1 | // init 2 | console.log("ytGREP extension loaded!"); 3 | 4 | // receive from webpage via window 5 | // transmit to extension via runtime 6 | window.addEventListener( 7 | "message", 8 | function (event) { 9 | // We only accept messages from ourselves 10 | if (event.source != window) return; 11 | 12 | if (event.data.type && event.data.type === "CAPS") { 13 | // console.log( 14 | // "Message from webpage: " + event.data.text, 15 | // event.data.capsArr 16 | // ); 17 | if (chrome.runtime?.id) { 18 | chrome.runtime.sendMessage( 19 | { 20 | type: "CAPS", 21 | status: event.data.text, 22 | capsArr: event.data.capsArr, 23 | }, 24 | function (response) { 25 | // console.log('Message from extension:', response.reply); 26 | } 27 | ); 28 | } 29 | } 30 | }, 31 | false 32 | ); 33 | 34 | // listening to extension 35 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 36 | if (request.type === "PLAYER" && request.action === "SEEK") { 37 | window.postMessage( 38 | { type: "PLAYER", action: "SEEK", time: request.time }, 39 | "*" 40 | ); 41 | } 42 | 43 | if (request.type === "PLAYER" && request.action === "PLAY") { 44 | window.postMessage({ type: "PLAYER", action: "PLAY" }, "*"); 45 | } 46 | 47 | if (request.type === "PLAYER" && request.action === "PAUSE") { 48 | window.postMessage({ type: "PLAYER", action: "PAUSE" }, "*"); 49 | } 50 | }); 51 | 52 | // inject ytPlayer into the webpage 53 | let script = document.createElement("script"); 54 | script.id = "ytGrep"; 55 | script.src = chrome.runtime.getURL("inject/ytPlayer.js"); 56 | script.onload = function () { 57 | this.remove(); 58 | }; 59 | (document.head || document.documentElement).appendChild(script); 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | ytGREP:promotional 3 |

4 | 5 |

A simple chrome extension to search for words or sentences used in a youtube video.

6 | 7 |

8 | Version 9 | Issues 10 | Ratings 11 |

12 | 13 |
14 | 15 |

16 | ytGREP - grep youtube videos like a word doc | Product Hunt 17 |

18 | 19 |

20 | Chrome Web Store 21 |

22 | 23 | ## TODO 24 | 25 | - Add support for Firefox 26 | - Semantic search 27 | - Work on feature requests from users 28 | 29 | ## How to contribute 30 | 31 | - Raise issue for bugs or feature requests [here](https://github.com/sr1jan/ytGREP/issues) 32 | - Submit a [PR](https://github.com/sr1jan/ytGREP/pulls) for collaboration 33 | 34 | ## Architecture 35 | 36 |

37 | ytGREP:Architecture 38 |

39 | 40 | ## Screenshots 41 | 42 |

43 | ytGREP:Screenshot:notranscript 44 |

45 |
46 |

47 | ytGREP:Screenshot:transcriptloaded 48 |

49 |
50 |

51 | ytGREP:Screenshot:matchfound 52 |

53 |
54 |

55 | ytGREP:Screenshot:nomatch 56 |

57 |
58 | -------------------------------------------------------------------------------- /src/ui/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #181818; 3 | color: #FF0000; 4 | border: 2px solid #FF0000; 5 | align-items: center; 6 | justfiy-content: flex-start; 7 | box-sizing: content-box; 8 | display: flex; 9 | flex-direction: column; 10 | margin: 0 auto; 11 | position: relative; 12 | height: 190px; 13 | width: 280px; 14 | padding: 10px; 15 | } 16 | 17 | h1 { 18 | font-size: 25px; 19 | margin: 2px 0; 20 | } 21 | 22 | a { 23 | color: grey; 24 | text-decoration: none; 25 | } 26 | 27 | #loader { 28 | display:none; 29 | position: absolute; 30 | left: 47%; 31 | top: 80%; 32 | z-index: 1; 33 | border: 3px solid #282828; 34 | border-radius: 50%; 35 | border-top: 3px solid #ff0000; 36 | width: 15px; 37 | height: 15px; 38 | -webkit-animation: spin 2s linear infinite; /* Safari */ 39 | animation: spin 2s linear infinite; 40 | } 41 | 42 | /* Safari */ 43 | @-webkit-keyframes spin { 44 | 0% { -webkit-transform: rotate(0deg); } 45 | 100% { -webkit-transform: rotate(360deg); } 46 | } 47 | 48 | @keyframes spin { 49 | 0% { transform: rotate(0deg); } 50 | 100% { transform: rotate(360deg); } 51 | } 52 | 53 | #info { 54 | display: flex; 55 | flex-direction: row; 56 | align-items: center; 57 | margin: 0 0 5px; 58 | } 59 | 60 | .bullet { 61 | color: grey; 62 | margin: 0 5px; 63 | font-size: 7px; 64 | } 65 | 66 | #github, #issue, #contact { 67 | font-size: 8px; 68 | letter-spacing: 1px; 69 | } 70 | 71 | #github:hover, #issue:hover, #contact:hover { 72 | opacity: 0.8; 73 | } 74 | 75 | #controls { 76 | display: flex; 77 | flex-direction: column; 78 | align-items: center; 79 | justify-content: center; 80 | } 81 | 82 | #ctltxt { 83 | color: grey; 84 | font-family: monospace; 85 | font-size: 10px; 86 | font-size: 3.2vw; 87 | letter-spacing: 0.8px; 88 | text-align: center; 89 | } 90 | 91 | #ctlnum { 92 | margin-top: 12px; 93 | margin-bottom: 5px; 94 | letter-spacing: 2px; 95 | font-size: 8px; 96 | color: grey; 97 | } 98 | 99 | #ctlbtns { 100 | display:flex; 101 | flex-direction: row; 102 | align-items: center; 103 | } 104 | 105 | #load, #prev, #next { 106 | font-size: 8px; 107 | letter-spacing: 1px; 108 | padding: 3px 4px; 109 | color: #181818; 110 | background-color: #FF0000; 111 | cursor: pointer; 112 | } 113 | 114 | #load:hover, #prev:hover, #next:hover, #media:hover { 115 | opacity: 0.8; 116 | } 117 | 118 | #prev, #next, #media { 119 | margin: 0 6px; 120 | font-size: 10px; 121 | } 122 | 123 | #media { 124 | font-size: 9px; 125 | color: grey; 126 | cursor: pointer; 127 | } 128 | 129 | #status { 130 | letter-spacing: 1px; 131 | font-family: monospace; 132 | font-size: 12px; 133 | margin-top: 20px; 134 | } 135 | 136 | form { 137 | display: flex; 138 | flex-flow: row wrap; 139 | align-items: center; 140 | } 141 | 142 | fieldset { 143 | display: flex; 144 | align-items: center; 145 | border: 0; 146 | } 147 | 148 | input[type=text] { 149 | padding:5px; 150 | border: 1px solid #181818; 151 | cursor: no-drop; 152 | } 153 | 154 | input[type=text]:focus { 155 | border-color: #181818; 156 | } 157 | 158 | input[type=submit] { 159 | letter-spacing: 1px; 160 | padding:5px 7px; 161 | background: #FF0000; 162 | border:0 none; 163 | cursor: no-drop; 164 | } 165 | 166 | input[type=submit]:hover { 167 | opacity: 1; 168 | } 169 | -------------------------------------------------------------------------------- /src/inject/getTranscript.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // console.log("injected getTranscript!"); 3 | 4 | async function getTranscript() { 5 | // console.log("getTranscript ran!"); 6 | 7 | // check if transcript available using cc button 8 | let ccbtn = document.getElementsByClassName( 9 | "ytp-subtitles-button ytp-button" 10 | )[0]; 11 | if (ccbtn.title.includes("unavailable")) { 12 | // console.log("[getTranscript] CC is not available!"); 13 | window.postMessage( 14 | { type: "CAPS", text: "No transcript available!", capsArr: [] }, 15 | "*" 16 | ); 17 | return; 18 | } 19 | 20 | // console.log("[getTranscript] CC is available!"); 21 | 22 | let more = document.querySelector( 23 | "div.style-scope.ytd-video-primary-info-renderer [id='menu'] .dropdown-trigger.style-scope.ytd-menu-renderer .style-scope.yt-icon-button" 24 | ); 25 | 26 | // open dropdown 27 | await more.click(); 28 | 29 | // check if transcript present 30 | let popArr = await document.getElementsByClassName( 31 | "style-scope ytd-menu-service-item-renderer" 32 | ); 33 | 34 | // console.log(`[getTranscript] - Checking if transcript available`); 35 | let pop; 36 | for (j = 0; j < popArr.length; ++j) { 37 | if ( 38 | popArr[j] !== undefined && 39 | popArr[j].innerText.includes("Open transcript") 40 | ) { 41 | pop = popArr[j]; 42 | // console.log(`[getTranscript] - Transcript found!`); 43 | break; 44 | } 45 | } 46 | 47 | // close dropdown 48 | await more.click(); 49 | 50 | // no transcript 51 | if (pop === undefined) { 52 | window.postMessage( 53 | { type: "CAPS", text: "No transcript available!", capsArr: [] }, 54 | "*" 55 | ); 56 | return; 57 | } 58 | 59 | await pop.click(); 60 | 61 | // transcript close button 62 | let close = document.querySelector( 63 | '#button [aria-label="Close transcript"]' 64 | ); 65 | 66 | let capsNode = []; 67 | let c = 0; 68 | while (1) { 69 | capsNode = await document.getElementsByClassName( 70 | "cue-group style-scope ytd-transcript-body-renderer" 71 | ); 72 | 73 | // transcript loaded 74 | if (capsNode.length > 0) { 75 | capsNode = [...capsNode]; 76 | break; 77 | } 78 | 79 | ++c; 80 | if (c > 10) { 81 | await close.click(); 82 | window.postMessage( 83 | { type: "CAPS", text: "No transcript available!", capsArr: [] }, 84 | "*" 85 | ); 86 | return; // no transcript or very slow fetch 87 | } 88 | 89 | // sleep for 1 sec before trying again 90 | await new Promise((r) => setTimeout(r, 1000)); 91 | } 92 | 93 | // close transcript 94 | await close.click(); 95 | 96 | let capsArr = []; 97 | for (i = 0; i < capsNode.length; ++i) { 98 | let vals = capsNode[i].innerText 99 | .trim() 100 | .replace(/\s{2,}/g, "\n") 101 | .split("\n"); // trim whiteplace, divide by newline then split 102 | let t = vals[0].split(":")[0] * 60 + parseInt(vals[0].split(":")[1]); // 10:10 -> 10mins * 60 + 10secs -> 610secs 103 | vals[0] = t; 104 | capsArr.push(vals); 105 | } 106 | 107 | // console.log("capsArr", capsArr.length); 108 | 109 | // console.log('Retrieved capsArray!'); 110 | window.postMessage( 111 | { type: "CAPS", text: "Transcript available!", capsArr: capsArr }, 112 | "*" 113 | ); 114 | } 115 | 116 | getTranscript(); 117 | 118 | // document.addEventListener('readystatechange', event => { 119 | // if (event.target.readyState === 'complete') { 120 | // getTranscript(); 121 | // } 122 | // }); 123 | })(); 124 | -------------------------------------------------------------------------------- /src/ui/base.js: -------------------------------------------------------------------------------- 1 | console.log("[YTGREP] ytGrep base script init"); 2 | 3 | let form = document.getElementById("searchForm"); 4 | let fieldSet = document.getElementById("formFieldSet"); 5 | let status = document.getElementById("status"); 6 | let loader = document.getElementById("loader"); 7 | let loadTranscript = document.getElementById("load"); 8 | let capsArr = []; 9 | 10 | // retreive transcript from local storage if available 11 | new Promise(function (resolve, reject) { 12 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 13 | chrome.tabs.get(tabs[0].id, function (tab) { 14 | chrome.storage.local.get(null, function (items) { 15 | console.log(items); 16 | let key = `${tab.title}`; 17 | try { 18 | let result = items[tab.id][key]; 19 | if (result !== undefined) resolve(result); 20 | else reject(new Error("Transcript not available locally!")); 21 | } catch (e) { 22 | reject(new Error(e)); 23 | } 24 | }); 25 | }); 26 | }); 27 | }).then( 28 | function (result) { 29 | capsArr = result; 30 | disableLoadTranscript(); 31 | activateForm(); 32 | }, 33 | function (error) { 34 | console.log(error); 35 | } 36 | ); 37 | 38 | // triggered on search 39 | form.onsubmit = function () { 40 | let query = form["search"].value; 41 | if (query === "") { 42 | return false; 43 | } else { 44 | // search, setup controls 45 | ytGrep(query); 46 | return false; 47 | } 48 | }; 49 | 50 | // triggered on load transcript 51 | loadTranscript.onclick = async function () { 52 | status.style.display = "none"; 53 | loader.style.display = "block"; 54 | let script = chrome.runtime.getURL("inject/getTranscript.js"); 55 | // function injectScript(src) { 56 | // let script = document.createElement("script"); 57 | // script.src = src; 58 | // script.id = "getTranscript"; 59 | // document.body.appendChild(script); 60 | // } 61 | const tab = await getCurrentTab(); 62 | chrome.scripting.executeScript( 63 | { 64 | target: { tabId: tab.id }, 65 | files: ["inject/getTranscript.js"], 66 | }, 67 | () => {} 68 | ); 69 | }; 70 | 71 | // listening to content script 72 | chrome.runtime.onMessage.addListener(async function ( 73 | request, 74 | sender, 75 | sendResponse 76 | ) { 77 | if (request.status !== "") { 78 | loader.style.display = "none"; 79 | status.style.display = "block"; 80 | status.innerText = request.status; 81 | } 82 | 83 | if (request.type === "CAPS") { 84 | if (request.capsArr.length > 0) { 85 | disableLoadTranscript(); 86 | activateForm(); 87 | capsArr = request.capsArr; 88 | const tab = await getCurrentTab(); 89 | chrome.tabs.get(tab.id, function (tab) { 90 | storeTranscriptLocal(capsArr, tab.id, tab.title); 91 | }); 92 | } else { 93 | console.log("No video transcript found!"); 94 | } 95 | } 96 | 97 | // reponse to content script 98 | sendResponse({ reply: "Thanks for the status update!" }); 99 | }); 100 | 101 | // ********************* 102 | // HELPERS 103 | // ********************* 104 | 105 | async function getCurrentTab() { 106 | let queryOptions = { active: true, currentWindow: true }; 107 | let [tab] = await chrome.tabs.query(queryOptions); 108 | return tab; 109 | } 110 | 111 | function disableLoadTranscript() { 112 | loadTranscript.onclick = ""; 113 | loadTranscript.style.opacity = 0.5; 114 | loadTranscript.style.cursor = "no-drop"; 115 | loadTranscript.classList.remove("hover"); 116 | } 117 | 118 | function activateForm() { 119 | let submit = document.getElementById("submit"); 120 | let search = document.getElementById("search"); 121 | fieldSet.disabled = ""; 122 | search.style.cursor = "auto"; 123 | submit.style.cursor = "pointer"; 124 | submit.onmouseover = function () { 125 | submit.style.opacity = 0.8; 126 | }; 127 | submit.onmouseout = function () { 128 | submit.style.opacity = 1; 129 | }; 130 | status.innerText = "Transcript loaded!"; 131 | } 132 | 133 | function storeTranscriptLocal(capsArr, tabID, tabTitle) { 134 | let key = `${tabTitle}`; 135 | chrome.storage.local.set({ [tabID]: { [key]: capsArr } }, function () { 136 | console.log("SAVED:", tabID, key); 137 | }); 138 | } 139 | 140 | function sendTimeToPlayer(time) { 141 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 142 | chrome.tabs.sendMessage(tabs[0].id, { 143 | type: "PLAYER", 144 | action: "SEEK", 145 | time: time, 146 | }); 147 | }); 148 | } 149 | 150 | function sendActionToPlayer(action) { 151 | chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { 152 | chrome.tabs.sendMessage(tabs[0].id, { type: "PLAYER", action: action }); 153 | }); 154 | } 155 | 156 | function createControlsNode(data) { 157 | let controls = document.getElementById("controls"); 158 | controls.innerHTML = ""; // clear prev search 159 | 160 | let ctlbtns = document.createElement("div"); 161 | let ctltxt = document.createElement("p"); 162 | let prev = document.createElement("p"); 163 | let next = document.createElement("p"); 164 | let media = document.createElement("p"); 165 | let ctlnum = document.createElement("p"); 166 | 167 | ctlnum.id = "ctlnum"; 168 | 169 | prev.id = "prev"; 170 | prev.innerText = "prev"; 171 | next.id = "next"; 172 | next.innerText = "next"; 173 | media.id = "media"; 174 | media.innerText = "pause"; 175 | ctlbtns.id = "ctlbtns"; 176 | ctlbtns.append(prev, media, next); 177 | 178 | ctltxt.id = "ctltxt"; 179 | 180 | document.body.style.height = "265px"; 181 | controls.append(ctltxt, ctlbtns, ctlnum); 182 | } 183 | 184 | function makeCtlFunctional(data) { 185 | let prev = document.getElementById("prev"); 186 | let next = document.getElementById("next"); 187 | let media = document.getElementById("media"); 188 | let ctltxt = document.getElementById("ctltxt"); 189 | let ctlnum = document.getElementById("ctlnum"); 190 | 191 | let idx = 0; 192 | prev.addEventListener("click", function () { 193 | if (idx <= 0) return; 194 | --idx; 195 | ctltxt.innerHTML = data[idx][1]; 196 | ctlnum.innerText = `${idx + 1}/${data.length}`; 197 | if (media.innerHTML === "play") { 198 | media.innerText = "pause"; 199 | } 200 | sendTimeToPlayer(data[idx][0]); 201 | }); 202 | next.addEventListener("click", function () { 203 | if (idx >= data.length - 1) return; 204 | ++idx; 205 | ctltxt.innerHTML = data[idx][1]; 206 | ctlnum.innerText = `${idx + 1}/${data.length}`; 207 | if (media.innerText === "play") { 208 | media.innerText = "pause"; 209 | } 210 | sendTimeToPlayer(data[idx][0]); 211 | }); 212 | media.addEventListener("click", function () { 213 | if (media.innerText === "pause") { 214 | media.innerText = "play"; // set symbol to play 215 | sendActionToPlayer("PAUSE"); 216 | } else { 217 | media.innerText = "pause"; // set symbol to pause 218 | sendActionToPlayer("PLAY"); 219 | } 220 | }); 221 | 222 | ctltxt.innerHTML = data[0][1]; 223 | ctlnum.innerText = `${idx + 1}/${data.length}`; 224 | sendTimeToPlayer(data[idx][0]); 225 | } 226 | 227 | async function ytGrep(query) { 228 | let results = []; 229 | query = query.toLowerCase(); 230 | let highlight = function (q) { 231 | return `${q}`; 232 | }; 233 | for (i = 0; i < capsArr.length; ++i) { 234 | if (capsArr[i][1] === undefined) continue; 235 | if (new RegExp(`\\b${query}\\b`).test(capsArr[i][1].toLowerCase())) { 236 | let regEx = new RegExp(query, "ig"); //case insensitive 237 | capsArr[i][1] = capsArr[i][1].replace(regEx, highlight(query)); 238 | results.push(capsArr[i]); 239 | } 240 | } 241 | 242 | if (results.length > 0) { 243 | status.innerText = "Match found!"; 244 | createControlsNode(results); 245 | makeCtlFunctional(results); 246 | } else { 247 | status.innerText = ""; 248 | await new Promise((r) => setTimeout(r, 200)); 249 | status.innerText = "No match found!"; 250 | document.getElementById("controls").innerHTML = ""; 251 | document.body.style.height = "190px"; 252 | } 253 | } 254 | --------------------------------------------------------------------------------