aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md11
-rw-r--r--content-script.js240
-rw-r--r--icons/icon.pngbin0 -> 1086005 bytes
-rw-r--r--manifest.json26
-rw-r--r--popup.html70
-rw-r--r--popup.js22
-rw-r--r--styles.css118
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
+
+[![Screenshot-20241008-005545.png](https://i.postimg.cc/DZGp23TQ/Screenshot-20241008-005545.png)] \ 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 = "&times;";
+ 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
new file mode 100644
index 0000000..3738246
--- /dev/null
+++ b/icons/icon.png
Binary files differ
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
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage