├── 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 |
19 | load transcript
20 |
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 |
3 |
4 |
5 | A simple chrome extension to search for words or sentences used in a youtube video.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
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 |
38 |
39 |
40 | ## Screenshots
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
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 |
--------------------------------------------------------------------------------