diff options
| -rw-r--r-- | README.md | 11 | ||||
| -rw-r--r-- | content-script.js | 240 | ||||
| -rw-r--r-- | icons/icon.png | bin | 0 -> 1086005 bytes | |||
| -rw-r--r-- | manifest.json | 26 | ||||
| -rw-r--r-- | popup.html | 70 | ||||
| -rw-r--r-- | popup.js | 22 | ||||
| -rw-r--r-- | styles.css | 118 |
7 files changed, 487 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..6fa196e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# YTQuickStamper +A tool for creating timestamps on YouTube videos quickly and easily. + +Based on [Krazete's ytlivestamper bookmarklet](https://github.com/Krazete/bookmarklets/blob/master/ytlivestamper.js). Adapted for use as an extension with additional features! + +- Create timestamps with descriptions in a single click on both ongoing live streams and regular videos +- Individually delete and adjust timing of each timestamp +- Export timestamps to text, YouTube links, or Markdown +- Adjustable offset to add context to timestamps + +[]
\ No newline at end of file diff --git a/content-script.js b/content-script.js new file mode 100644 index 0000000..be64b4b --- /dev/null +++ b/content-script.js @@ -0,0 +1,240 @@ +(function() { + 'use strict'; + if (!document.querySelector("#ytls-pane")) { + var pane = document.createElement("div"); + var exit = document.createElement("span"); + var list = document.createElement("ul"); + var nowli = document.createElement("li"); + var nowa = document.createElement("a"); + var nowid; + var nowtext = document.createElement("input"); + var box = document.createElement("textarea"); + var buttons = document.createElement("div"); + var paster = document.createElement("button"); + var adder = document.createElement("button"); + var copier = document.createElement("button"); + var offset = 5; + var copyMode = 0; + + const storedOffset = localStorage.getItem('offset'); + offset = storedOffset !== null ? parseInt(storedOffset) : 5; + + browser.runtime.onMessage.addListener((message) => { + if (message.type === 'SET_OFFSET') { + offset = message.offset; + localStorage.setItem('offset', offset); + } + }); + + function closePane() { + if (confirm("Close timestamp tool?")) { + pane.remove(); + cancelAnimationFrame(nowid); + window.removeEventListener("beforeunload", warn) + } + } + + function updateStamp(stamp, time) { + stamp.innerHTML = formatTime(time); + stamp.dataset.time = time; + stamp.href = "https://youtu.be/" + location.search.split(/.+v=|&/)[1] + "?t=" + time + } + + function clickStamp(e) { + if (e.target.dataset.time) { + e.preventDefault(); + document.querySelector("video").currentTime = e.target.dataset.time + } else if (e.target.classList.contains("remove-timestamp")) { + e.preventDefault(); + var li = e.target.parentElement; + li.remove(); + } else if (e.target.dataset.increment) { + e.preventDefault(); + var li = e.target.parentElement; + var a = li.children[3]; + var time = parseInt(a.dataset.time) + parseInt(e.target.dataset.increment); + updateStamp(a, time) + } + } + + function watchTime() { + try { + var time = Math.floor(document.querySelector("video").duration); + updateStamp(nowa, time) + } catch (e) {} + nowid = requestAnimationFrame(watchTime) + } + + function unformatTime(stamp) { + var hms = stamp.split(":").map(e => parseInt(e)); + if (hms.length < 3) { + return 60 * hms[0] + hms[1] + } + return 3600 * hms[0] + 60 * hms[1] + hms[2] + } + + function newLi(time) { + var li = document.createElement("li"); + var minus = document.createElement("span"); + var plus = document.createElement("span"); + var a = document.createElement("a"); + var text = document.createElement("input"); + var removeButton = document.createElement("span"); + removeButton.innerHTML = "❌"; + removeButton.classList.add("remove-timestamp"); + minus.classList.add("ts-decrement"); + plus.classList.add("ts-increment"); + text.classList.add("ts-note"); + + minus.innerHTML = "➖"; + minus.dataset.increment = -1; + plus.innerHTML = "➕"; + plus.dataset.increment = 1; + updateStamp(a, time); + li.appendChild(removeButton); + li.appendChild(minus); + li.appendChild(plus); + li.appendChild(a); + li.appendChild(text); + list.appendChild(li); + return text + } + + function pasteList() { + if (!confirm("This will replace the current list. Are you sure?")) { + return; + } + var lines = box.value.split("\n"); + list.innerHTML = ""; + for (var i = 0; i < lines.length; i++) { + var line = lines[i].trim(); + var stamp = line.split(/\s+/, 1)[0]; + var time = unformatTime(stamp); + var note = line.slice(stamp.length + 1); + var text = newLi(time, note); + text.value = note + } + list.appendChild(nowli) + } + + function formatTime(time) { + var h = Math.floor(time / 3600); + var m = Math.floor(time / 60) % 60; + var s = Math.floor(time) % 60; + return (h ? (h + ":" + String(m).padStart(2, 0)) : m) + ":" + String(s).padStart(2, 0) + } + + function addStamp() { + var time = Math.max(0, Math.floor(document.querySelector("video").currentTime - offset)); + var text = newLi(time); + list.appendChild(nowli); + text.focus() + } + + function resetCopier() { + copyMode = 0; + copier.innerHTML = "Copy List"; + } + + function showToast(message) { + var toast = document.getElementById("ytls-toast"); + toast.innerText = message; + toast.className = "show"; + setTimeout(function() { + toast.className = toast.className.replace("show", ""); + }, 1500); + } + + + function copyList() { + var string = ""; + if (copyMode === 0) { + copyMode = 1; + copier.innerHTML = "Copy YT Links"; + for (var i = 0; i < list.children.length - 1; i++) { + var stamp = list.children[i].querySelector("a").innerHTML; + var note = list.children[i].querySelector("input").value; + string += (i > 0 ? "\n" : "") + (stamp + " " + note).trim(); + } + showToast("Copied YouTube Linked Timestamps"); + } else if (copyMode === 1) { + copyMode = 2; + copier.innerHTML = "Copy Markdown"; + for (var i = 0; i < list.children.length - 1; i++) { + var stamp = list.children[i].querySelector("a").href; + var note = list.children[i].querySelector("input").value; + string += (i > 0 ? "\n" : "") + (note + " " + stamp).trim(); + } + showToast("Copied Markdown Timestamps"); + } else { // if (firstCopy === 2) + resetCopier(); + for (var i = 0; i < list.children.length - 1; i++) { + var stamp = list.children[i].querySelector("a").href; + var note = list.children[i].querySelector("input").value; + string += (i > 0 ? "\n" : "") + `- [${note}](${stamp})`.trim(); + } + showToast("Copied Text Timestamps"); + } + box.value = string; + box.select(); + navigator.clipboard.writeText(string) + .then(() => { + console.log('Text copied to clipboard'); + }) + .catch(err => { + console.error('Error in copying text: ', err); + }); + } + + var toast = document.createElement("div"); + toast.id = "ytls-toast"; + document.body.appendChild(toast); + + + function warn(e) { + e.preventDefault(); + e.returnValue = "Close timestamp tool?"; + return e.returnValue + } + + list.style.maxHeight = "10em" + list.style.overflowY = "auto"; + + pane.id = "ytls-pane"; + exit.innerHTML = "×"; + exit.classList.add("ytls-exit"); + watchTime(); + nowtext.disabled = true; + nowtext.value = "End of Video"; + box.id = "ytls-box"; + buttons.id = "ytls-buttons"; + paster.innerHTML = "Import List"; + adder.innerHTML = "Add Timestamp"; + copier.innerHTML = "Copy List"; + + var link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = chrome.runtime.getURL("styles.css"); + document.head.appendChild(link); + + exit.addEventListener("click", closePane); + list.addEventListener("click", clickStamp); + list.addEventListener("touchstart", clickStamp); + paster.addEventListener("click", pasteList); + adder.addEventListener("click", addStamp); + copier.addEventListener("click", copyList); + window.addEventListener("beforeunload", warn); + pane.appendChild(exit); + nowli.appendChild(nowa); + nowli.appendChild(nowtext); + list.appendChild(nowli); + pane.appendChild(list); + pane.appendChild(box); + buttons.appendChild(paster); + buttons.appendChild(adder); + buttons.appendChild(copier); + pane.appendChild(buttons); + document.body.appendChild(pane); + box.focus() + } +})();
\ No newline at end of file diff --git a/icons/icon.png b/icons/icon.png Binary files differnew file mode 100644 index 0000000..3738246 --- /dev/null +++ b/icons/icon.png diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..418a778 --- /dev/null +++ b/manifest.json @@ -0,0 +1,26 @@ +{ + "manifest_version": 3, + "name": "QuickStamper", + "version": "1.0", + "description": "A tool for generating timestamps on YouTube videos or livestreams", + "permissions": [ + "activeTab", + "scripting", + "clipboardWrite" + ], + "icons": { + "48": "icons/icon.png" + }, + "action": { + "default_popup": "popup.html", + "default_icon": { + "48": "icons/icon.png" + } + }, + "web_accessible_resources": [ + { + "resources": ["styles.css"], + "matches": ["<all_urls>"] + } + ] +}
\ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 0000000..97887f6 --- /dev/null +++ b/popup.html @@ -0,0 +1,70 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>YTQuickStamper</title> + <style> + body { + font-family: Arial, sans-serif; + padding: 20px; + max-width: 350px; + background-color: #181818; + color: #f1f1f1; + } + h3 { + text-align: center; + color: #FF0000; + } + button { + padding: 10px; + background-color: #FF0000; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.3s; + } + button:hover { + background-color: #CC0000; + } + #offset { + padding: 8px; + border: 2px solid #FF0000; + border-radius: 4px; + width: 60px; + font-size: 1rem; + background-color: #333; + color: #f1f1f1; + } + label { + margin-top: 2rem; + font-size: 1rem; + color: #f1f1f1; + } + .offset-container { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 20px; + padding: 10px; + background-color: #282828; + border: 1px solid #444; + border-radius: 6px; + } + </style> +</head> +<body> + <h3>QuickStamper</h3> + <button id="activate">Launch Timestamp Tool</button> + <div> + <label for="offset">Offset (sec):</label> + <input type="number" + id="offset" + value="5" + title="Offset each timestamp by this many seconds, ex. 3 = 3s before each timestamp, -3 = 3s after each timestamp" + > + </div> + <script src="popup.js"></script> +</body> +</html>
\ No newline at end of file diff --git a/popup.js b/popup.js new file mode 100644 index 0000000..21cfed4 --- /dev/null +++ b/popup.js @@ -0,0 +1,22 @@ +document.getElementById('activate').addEventListener('click', () => { + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + if (tabs[0].url.includes('youtube.com/watch')) { + browser.scripting.executeScript({ + target: { tabId: tabs[0].id }, + files: ['content-script.js'] + }); + } else { + alert("Please open a YouTube video to use the Timestamp Tool."); + } + }); +}); + +document.getElementById('offset').addEventListener('input', () => { + const offset = parseInt(document.getElementById('offset').value, 10); + browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { + browser.tabs.sendMessage(tabs[0].id, { type: 'SET_OFFSET', offset: offset }); + localStorage.setItem('offset', offset); + }); +}); + +localStorage.getItem('offset') !== null ? document.getElementById('offset').value = localStorage.getItem('offset') : document.getElementById('offset').value = 5;
\ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..454edac --- /dev/null +++ b/styles.css @@ -0,0 +1,118 @@ +#ytls-pane { + text-align: right; + position: fixed; + bottom: 0; + padding: 0 5px; + opacity: .5; + z-index: 5000; +} + +#ytls-pane:hover { + opacity: 1; + background-color: black; +} + +#ytls-pane a, +#ytls-pane input, +#ytls-pane span { + background: 0 0; + color: #fff; + font-family: inherit; + font-size: initial; + text-decoration: none; + border: none; + outline: 0; +} + +#ytls-box { + font-family: monospace; + width: 100%; + display: block; + padding: 0; + border: none; + outline: 0; + resize: none; + height: 3em; + overflow-y: scroll; +} + +#ytls-buttons { + display: flex; +} + +#ytls-buttons button { + background: 0 0; + color: #fff; + font-size: 12px; + flex: auto; + padding: 2px; + border: 1px solid #fff +; +} + +.ytls-exit{ + cursor: pointer; +} + +.remove-timestamp { + cursor: pointer; + margin-right: 5px; +} + +.ts-increment { + cursor: pointer; + margin-right: 5px; + user-select: none; +} + +.ts-decrement { + cursor: pointer; + margin-right: 5px; + user-select: none; +} + +.ts-note { + margin-left: 5px; +} + +#ytls-toast { + visibility: hidden; + min-width: 250px; + margin-left: -125px; + background-color: #333; + color: #fff; + text-align: center; + border-radius: 2px; + padding: 16px; + position: fixed; + z-index: 1; + left: 50%; + bottom: 30px; + font-size: 17px; +} + +#ytls-toast.show { + visibility: visible; + -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; + animation: fadein 0.5s, fadeout 0.5s 2.5s; +} + +@-webkit-keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: 30px; opacity: 1;} +} + +@-webkit-keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +} + +@keyframes fadeout { + from {bottom: 30px; opacity: 1;} + to {bottom: 0; opacity: 0;} +}
\ No newline at end of file |
