mirror of
				https://github.com/SoPat712/videospeed.git
				synced 2025-10-30 18:34:02 -04:00 
			
		
		
		
	Compare commits
	
		
			14 Commits
		
	
	
		
			v1.0.0
			...
			1277750716
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1277750716 | ||
|   | 247a46d430 | ||
|   | 703658335c | ||
|   | b2ed0fcb41 | ||
|   | 3dfee251ec | ||
|   | 8e0183d8af | ||
|   | 73827b5ee0 | ||
|   | b07e7cb394 | ||
|   | c3166cf347 | ||
|   | 77a25c4f1e | ||
|   | 3fee61d2b6 | ||
|   | b7684aad09 | ||
|   | 43dc8b773b | ||
|   | 2d8a4fc25f | 
| @@ -1,3 +1,7 @@ | |||||||
|  | [](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| # The science of accelerated playback | # The science of accelerated playback | ||||||
|  |  | ||||||
| **TL;DR: faster playback translates to better engagement and retention.** | **TL;DR: faster playback translates to better engagement and retention.** | ||||||
| @@ -74,10 +78,10 @@ You can try manually disabling Flash from the browser. | |||||||
| [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) repository | [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) repository | ||||||
| is a port of [`igrigorik`](https://github.com/igrigorik)'s videospeed Chrome  | is a port of [`igrigorik`](https://github.com/igrigorik)'s videospeed Chrome  | ||||||
| add-on for Firefox. This fork modifies the Chrome add-on code so that it works  | add-on for Firefox. This fork modifies the Chrome add-on code so that it works  | ||||||
| in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-us/firefox/addon/videospeed/) | in Firefox. This repo is the code behind the [Firefox Extension](https://addons.mozilla.org/en-US/firefox/addon/video-speed-controller-v1/) | ||||||
| whereas the [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) | whereas the [`igrigorik/videospeed`](https://github.com/igrigorik/videospeed) | ||||||
| repository contains the code behind the [Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk). | repository contains the code behind the [Chrome Extension](https://chrome.google.com/webstore/detail/video-speed-controller/nffaoalbilbmmfgbnbgppjihopabppdk). | ||||||
|  |  | ||||||
| ### License | ### License | ||||||
|  |  | ||||||
| (MIT License) - Copyright (c) 2014 Ilya Grigorik | (MIT License) - Copyright (c) 2025 Josh Patra | ||||||
|   | |||||||
							
								
								
									
										118
									
								
								build.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								build.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | |||||||
|  | import glob | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | import shutil | ||||||
|  | import tempfile | ||||||
|  | import zipfile | ||||||
|  |  | ||||||
|  | SCRIPT_NAME = os.path.basename(__file__) | ||||||
|  | TARGET_FILE = "manifest.json" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zip_folder(output_name, folder, exclude_files, exclude_dirs): | ||||||
|  |     with zipfile.ZipFile(output_name, "w", zipfile.ZIP_DEFLATED) as zipf: | ||||||
|  |         for root, dirs, files in os.walk(folder): | ||||||
|  |             dirs[:] = [ | ||||||
|  |                 d | ||||||
|  |                 for d in dirs | ||||||
|  |                 if os.path.relpath(os.path.join(root, d), folder) not in exclude_dirs | ||||||
|  |             ] | ||||||
|  |             for file in files: | ||||||
|  |                 rel_path = os.path.relpath(os.path.join(root, file), folder) | ||||||
|  |                 if ( | ||||||
|  |                     file in exclude_files | ||||||
|  |                     or rel_path in exclude_files | ||||||
|  |                     or any(rel_path.startswith(ed + os.sep) for ed in exclude_dirs) | ||||||
|  |                 ): | ||||||
|  |                     continue | ||||||
|  |                 zipf.write(os.path.join(root, file), arcname=rel_path) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_version_line(file_path, new_version): | ||||||
|  |     with open(file_path, "r", encoding="utf-8") as f: | ||||||
|  |         lines = f.readlines() | ||||||
|  |  | ||||||
|  |     updated = False | ||||||
|  |     for i, line in enumerate(lines): | ||||||
|  |         match = re.match(r'\s*"version":\s*"([^"]+)"', line) | ||||||
|  |         if match: | ||||||
|  |             old_version = match.group(1) | ||||||
|  |             lines[i] = re.sub( | ||||||
|  |                 r'"version":\s*".+?"', f'"version": "{new_version}"', line | ||||||
|  |             ) | ||||||
|  |             updated = True | ||||||
|  |             print( | ||||||
|  |                 f"🛠️ Changed version in {file_path} from {old_version} ➜ {new_version}" | ||||||
|  |             ) | ||||||
|  |             break | ||||||
|  |  | ||||||
|  |     if updated: | ||||||
|  |         with open(file_path, "w", encoding="utf-8") as f: | ||||||
|  |             f.writelines(lines) | ||||||
|  |     else: | ||||||
|  |         print(f"⚠️ No version line found in {file_path}.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |     # Step 0: Remove all existing .xpi files upfront | ||||||
|  |     xpi_files = glob.glob("*.xpi") | ||||||
|  |     for f in xpi_files: | ||||||
|  |         try: | ||||||
|  |             os.remove(f) | ||||||
|  |             print(f"🗑️ Removed existing archive: {f}") | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"⚠️ Failed to remove {f}: {e}") | ||||||
|  |  | ||||||
|  |     base_version = input("Enter the new base version (e.g., 2.0.1): ").strip() | ||||||
|  |     if not base_version: | ||||||
|  |         print("❌ No version entered. Exiting.") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     firefox_version = f"{base_version}.0" | ||||||
|  |     current_dir = os.getcwd() | ||||||
|  |     manifest_path = os.path.join(current_dir, TARGET_FILE) | ||||||
|  |  | ||||||
|  |     # Step 1: Update manifest.json on disk to base_version | ||||||
|  |     if os.path.exists(manifest_path): | ||||||
|  |         update_version_line(manifest_path, base_version) | ||||||
|  |     else: | ||||||
|  |         print(f"❌ {TARGET_FILE} not found. Aborting.") | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     # Step 2: Create videospeed-github.xpi (exclude script, .git, AND videospeed-github.xpi itself) | ||||||
|  |     exclude_files = [SCRIPT_NAME, "videospeed-github.xpi"] | ||||||
|  |     exclude_dirs = [".git"] | ||||||
|  |     zip_folder("videospeed-github.xpi", current_dir, exclude_files, exclude_dirs) | ||||||
|  |     print("✅ Created videospeed-github.xpi") | ||||||
|  |  | ||||||
|  |     # Step 3: Re-scan for .xpi files after GitHub archive creation, exclude them for Firefox zip | ||||||
|  |     current_xpi_files = set(glob.glob("*.xpi")) | ||||||
|  |     exclude_temp_files = current_xpi_files.union({SCRIPT_NAME}) | ||||||
|  |     exclude_temp_dirs = set(exclude_dirs) | ||||||
|  |  | ||||||
|  |     # Step 4: Create videospeed-firefox.xpi from temp folder with version bumped to .0 | ||||||
|  |     with tempfile.TemporaryDirectory() as temp_dir: | ||||||
|  |         for item in os.listdir(current_dir): | ||||||
|  |             if item in exclude_temp_files or item in exclude_temp_dirs: | ||||||
|  |                 continue | ||||||
|  |             src = os.path.join(current_dir, item) | ||||||
|  |             dst = os.path.join(temp_dir, item) | ||||||
|  |             if os.path.isdir(src): | ||||||
|  |                 shutil.copytree(src, dst) | ||||||
|  |             else: | ||||||
|  |                 shutil.copy2(src, dst) | ||||||
|  |  | ||||||
|  |         temp_manifest = os.path.join(temp_dir, TARGET_FILE) | ||||||
|  |         if os.path.exists(temp_manifest): | ||||||
|  |             update_version_line(temp_manifest, firefox_version) | ||||||
|  |         else: | ||||||
|  |             print(f"⚠️ {TARGET_FILE} not found in temp folder.") | ||||||
|  |  | ||||||
|  |         zip_folder( | ||||||
|  |             "videospeed-firefox.xpi", temp_dir, exclude_files=[], exclude_dirs=[] | ||||||
|  |         ) | ||||||
|  |         print("✅ Created videospeed-firefox.xpi") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     main() | ||||||
| @@ -1,13 +1,13 @@ | |||||||
| { | { | ||||||
|   "name": "Video Speed Controller", |   "name": "Video Speed Controller", | ||||||
|   "short_name": "videospeed", |   "short_name": "videospeed", | ||||||
|   "version": "0.6.3.3", |   "version": "1.4.1", | ||||||
|   "manifest_version": 2, |   "manifest_version": 2, | ||||||
|   "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts", |   "description": "Speed up, slow down, advance and rewind HTML5 audio/video with shortcuts", | ||||||
|   "homepage_url": "https://github.com/codebicycle/videospeed", |   "homepage_url": "https://github.com/SoPat712/videospeed", | ||||||
|   "browser_specific_settings": { |   "browser_specific_settings": { | ||||||
|     "gecko": { |     "gecko": { | ||||||
|       "id": "{7be2ba16-0f1e-4d93-9ebc-5164397477a9}" |       "id": "{ed860648-f54f-4dc9-9a0d-501aec4313f5}" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "icons": { |   "icons": { | ||||||
|   | |||||||
							
								
								
									
										64
									
								
								options.html
									
									
									
									
									
								
							
							
						
						
									
										64
									
								
								options.html
									
									
									
									
									
								
							| @@ -1,4 +1,4 @@ | |||||||
| <!DOCTYPE html> | <!doctype html> | ||||||
| <html> | <html> | ||||||
|   <head> |   <head> | ||||||
|     <title>Video Speed Controller: Options</title> |     <title>Video Speed Controller: Options</title> | ||||||
| @@ -148,8 +148,13 @@ | |||||||
|         <input id="rememberSpeed" type="checkbox" /> |         <input id="rememberSpeed" type="checkbox" /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
|         <label for="forceLastSavedSpeed">Force last saved speed<br /> |         <label for="forceLastSavedSpeed" | ||||||
|         <em>Useful for video players that override the speeds set by VideoSpeed</em></label> |           >Force last saved speed<br /> | ||||||
|  |           <em | ||||||
|  |             >Useful for video players that override the speeds set by | ||||||
|  |             VideoSpeed</em | ||||||
|  |           ></label | ||||||
|  |         > | ||||||
|         <input id="forceLastSavedSpeed" type="checkbox" /> |         <input id="forceLastSavedSpeed" type="checkbox" /> | ||||||
|       </div> |       </div> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
| @@ -175,6 +180,47 @@ | |||||||
|       </div> |       </div> | ||||||
|     </section> |     </section> | ||||||
|  |  | ||||||
|  |     <section id="nudgeSettings"> | ||||||
|  |       <h3>Subtitle Nudge Settings (Experimental - YouTube Only)</h3> | ||||||
|  |       <div class="row"> | ||||||
|  |         <label for="enableSubtitleNudge" | ||||||
|  |           >Enable Subtitle Nudge <br /><em | ||||||
|  |             >Periodically 'nudges' video speed by a tiny amount to help keep | ||||||
|  |             subtitles in sync on some sites (e.g. YouTube).</em | ||||||
|  |           > | ||||||
|  |         </label> | ||||||
|  |         <input id="enableSubtitleNudge" type="checkbox" /> | ||||||
|  |       </div> | ||||||
|  |       <div class="row"> | ||||||
|  |         <label for="subtitleNudgeInterval" | ||||||
|  |           >Nudge Interval (milliseconds) <br /><em | ||||||
|  |             >How often to nudge (e.g., 25-1000). Smaller values are more | ||||||
|  |             frequent. Default: 25.</em | ||||||
|  |           > | ||||||
|  |         </label> | ||||||
|  |         <input | ||||||
|  |           id="subtitleNudgeInterval" | ||||||
|  |           type="text" | ||||||
|  |           value="" | ||||||
|  |           placeholder="25" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |       <div class="row"> | ||||||
|  |         <label for="subtitleNudgeAmount" | ||||||
|  |           >Nudge Amount (decimal) <br /><em | ||||||
|  |             >How much to change speed by (e.g., 0.001). Very small values | ||||||
|  |             recommended. Default: 0.001.</em | ||||||
|  |           > | ||||||
|  |         </label> | ||||||
|  |         <input | ||||||
|  |           id="subtitleNudgeAmount" | ||||||
|  |           type="text" | ||||||
|  |           value="" | ||||||
|  |           placeholder="0.001" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </section> | ||||||
|  |  | ||||||
|     <button id="save">Save</button> |     <button id="save">Save</button> | ||||||
|     <button id="restore">Restore Defaults</button> |     <button id="restore">Restore Defaults</button> | ||||||
|     <button id="experimental">Show Experimental Features</button> |     <button id="experimental">Show Experimental Features</button> | ||||||
| @@ -186,12 +232,12 @@ | |||||||
|  |  | ||||||
|       <h4>Extension controls not appearing?</h4> |       <h4>Extension controls not appearing?</h4> | ||||||
|       <p> |       <p> | ||||||
|         This extension is only compatible with HTML5 audio and video. If you don't |         This extension is only compatible with HTML5 audio and video. If you | ||||||
|         see the controls showing up, chances are you are viewing a Flash content. |         don't see the controls showing up, chances are you are viewing a Flash | ||||||
|         If you want to confirm, try right-clicking on the content and inspect the |         content. If you want to confirm, try right-clicking on the content and | ||||||
|         menu: if it mentions flash, then that's the issue. That said, <b>most sites |         inspect the menu: if it mentions flash, then that's the issue. That | ||||||
|         will fallback to HTML5</b> if they detect that Flash is not available. You |         said, <b>most sites will fallback to HTML5</b> if they detect that Flash | ||||||
|         can try manually disabling Flash from the browser. |         is not available. You can try manually disabling Flash from the browser. | ||||||
|       </p> |       </p> | ||||||
|     </div> |     </div> | ||||||
|   </body> |   </body> | ||||||
|   | |||||||
							
								
								
									
										264
									
								
								options.js
									
									
									
									
									
								
							
							
						
						
									
										264
									
								
								options.js
									
									
									
									
									
								
							| @@ -22,13 +22,17 @@ var tcDefaults = { | |||||||
|     twitter.com |     twitter.com | ||||||
|     imgur.com |     imgur.com | ||||||
|     teams.microsoft.com |     teams.microsoft.com | ||||||
|   `.replace(regStrip, "") |   `.replace(regStrip, ""), | ||||||
|  |   // ADDED: Nudge defaults | ||||||
|  |   enableSubtitleNudge: true, | ||||||
|  |   subtitleNudgeInterval: 25, | ||||||
|  |   subtitleNudgeAmount: 0.001 | ||||||
| }; | }; | ||||||
|  |  | ||||||
| var keyBindings = []; | var keyBindings = []; // This is populated during save/restore | ||||||
|  |  | ||||||
| var keyCodeAliases = { | var keyCodeAliases = { | ||||||
|   0: "null", |   /* ... same as your original ... */ 0: "null", | ||||||
|   null: "null", |   null: "null", | ||||||
|   undefined: "null", |   undefined: "null", | ||||||
|   32: "Space", |   32: "Space", | ||||||
| @@ -76,83 +80,53 @@ var keyCodeAliases = { | |||||||
|   222: "'", |   222: "'", | ||||||
|   59: ";", |   59: ";", | ||||||
|   61: "+", |   61: "+", | ||||||
|   173: "-", |   173: "-" | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function recordKeyPress(e) { | function recordKeyPress(e) { | ||||||
|  |   /* ... same as your original ... */ | ||||||
|   if ( |   if ( | ||||||
|     (e.keyCode >= 48 && e.keyCode <= 57) || // Numbers 0-9 |     (e.keyCode >= 48 && e.keyCode <= 57) || | ||||||
|     (e.keyCode >= 65 && e.keyCode <= 90) || // Letters A-Z |     (e.keyCode >= 65 && e.keyCode <= 90) || | ||||||
|     keyCodeAliases[e.keyCode] // Other character keys |     keyCodeAliases[e.keyCode] | ||||||
|   ) { |   ) { | ||||||
|     e.target.value = |     e.target.value = | ||||||
|       keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode); |       keyCodeAliases[e.keyCode] || String.fromCharCode(e.keyCode); | ||||||
|     e.target.keyCode = e.keyCode; |     e.target.keyCode = e.keyCode; | ||||||
|  |  | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     e.stopPropagation(); |     e.stopPropagation(); | ||||||
|   } else if (e.keyCode === 8) { |   } else if (e.keyCode === 8) { | ||||||
|     // Clear input when backspace pressed |  | ||||||
|     e.target.value = ""; |     e.target.value = ""; | ||||||
|   } else if (e.keyCode === 27) { |   } else if (e.keyCode === 27) { | ||||||
|     // When esc clicked, clear input |  | ||||||
|     e.target.value = "null"; |     e.target.value = "null"; | ||||||
|     e.target.keyCode = null; |     e.target.keyCode = null; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function inputFilterNumbersOnly(e) { | function inputFilterNumbersOnly(e) { | ||||||
|  |   /* ... same as your original ... */ | ||||||
|   var char = String.fromCharCode(e.keyCode); |   var char = String.fromCharCode(e.keyCode); | ||||||
|   if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) { |   if (!/[\d\.]$/.test(char) || !/^\d+(\.\d*)?$/.test(e.target.value + char)) { | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
|     e.stopPropagation(); |     e.stopPropagation(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| function inputFocus(e) { | function inputFocus(e) { | ||||||
|   e.target.value = ""; |   /* ... same as your original ... */ e.target.value = ""; | ||||||
| } | } | ||||||
|  |  | ||||||
| function inputBlur(e) { | function inputBlur(e) { | ||||||
|   e.target.value = |   /* ... same as your original ... */ e.target.value = | ||||||
|     keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode); |     keyCodeAliases[e.target.keyCode] || String.fromCharCode(e.target.keyCode); | ||||||
| } | } | ||||||
|  | // function updateShortcutInputText(inputId, keyCode) { /* ... same as your original ... */ } // Not directly used in provided options.js logic flow | ||||||
| function updateShortcutInputText(inputId, keyCode) { |  | ||||||
|   document.getElementById(inputId).value = |  | ||||||
|     keyCodeAliases[keyCode] || String.fromCharCode(keyCode); |  | ||||||
|   document.getElementById(inputId).keyCode = keyCode; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function updateCustomShortcutInputText(inputItem, keyCode) { | function updateCustomShortcutInputText(inputItem, keyCode) { | ||||||
|   inputItem.value = keyCodeAliases[keyCode] || String.fromCharCode(keyCode); |   /* ... same as your original ... */ inputItem.value = | ||||||
|  |     keyCodeAliases[keyCode] || String.fromCharCode(keyCode); | ||||||
|   inputItem.keyCode = keyCode; |   inputItem.keyCode = keyCode; | ||||||
| } | } | ||||||
|  | var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"]; // Original | ||||||
| // List of custom actions for which customValue should be disabled |  | ||||||
| var customActionsNoValues = ["pause", "muted", "mark", "jump", "display"]; |  | ||||||
|  |  | ||||||
| function add_shortcut() { | function add_shortcut() { | ||||||
|   var html = `<select class="customDo"> |   /* ... same as your original ... */ | ||||||
|     <option value="slower">Decrease speed</option> |   var html = `<select class="customDo"><option value="slower">Decrease speed</option><option value="faster">Increase speed</option><option value="rewind">Rewind</option><option value="advance">Advance</option><option value="reset">Reset speed</option><option value="fast">Preferred speed</option><option value="muted">Mute</option><option value="pause">Pause</option><option value="mark">Set marker</option><option value="jump">Jump to marker</option><option value="display">Show/hide controller</option></select><input class="customKey" type="text" placeholder="press a key"/><input class="customValue" type="text" placeholder="value (0.10)"/><select class="customForce"><option value="false">Do not disable website key bindings</option><option value="true">Disable website key bindings</option></select><button class="removeParent">X</button>`; | ||||||
|     <option value="faster">Increase speed</option> |  | ||||||
|     <option value="rewind">Rewind</option> |  | ||||||
|     <option value="advance">Advance</option> |  | ||||||
|     <option value="reset">Reset speed</option> |  | ||||||
|     <option value="fast">Preferred speed</option> |  | ||||||
|     <option value="muted">Mute</option> |  | ||||||
|     <option value="pause">Pause</option> |  | ||||||
|     <option value="mark">Set marker</option> |  | ||||||
|     <option value="jump">Jump to marker</option> |  | ||||||
|     <option value="display">Show/hide controller</option> |  | ||||||
|     </select> |  | ||||||
|     <input class="customKey" type="text" placeholder="press a key"/> |  | ||||||
|     <input class="customValue" type="text" placeholder="value (0.10)"/> |  | ||||||
|     <select class="customForce"> |  | ||||||
|     <option value="false">Do not disable website key bindings</option> |  | ||||||
|     <option value="true">Disable website key bindings</option> |  | ||||||
|     </select> |  | ||||||
|     <button class="removeParent">X</button>`; |  | ||||||
|   var div = document.createElement("div"); |   var div = document.createElement("div"); | ||||||
|   div.setAttribute("class", "row customs"); |   div.setAttribute("class", "row customs"); | ||||||
|   div.innerHTML = html; |   div.innerHTML = html; | ||||||
| @@ -162,14 +136,13 @@ function add_shortcut() { | |||||||
|     customs_element.children[customs_element.childElementCount - 1] |     customs_element.children[customs_element.childElementCount - 1] | ||||||
|   ); |   ); | ||||||
| } | } | ||||||
|  |  | ||||||
| function createKeyBindings(item) { | function createKeyBindings(item) { | ||||||
|  |   /* ... same as your original ... */ | ||||||
|   const action = item.querySelector(".customDo").value; |   const action = item.querySelector(".customDo").value; | ||||||
|   const key = item.querySelector(".customKey").keyCode; |   const key = item.querySelector(".customKey").keyCode; | ||||||
|   const value = Number(item.querySelector(".customValue").value); |   const value = Number(item.querySelector(".customValue").value); | ||||||
|   const force = item.querySelector(".customForce").value; |   const force = item.querySelector(".customForce").value; | ||||||
|   const predefined = !!item.id; //item.id ? true : false; |   const predefined = !!item.id; | ||||||
|  |  | ||||||
|   keyBindings.push({ |   keyBindings.push({ | ||||||
|     action: action, |     action: action, | ||||||
|     key: key, |     key: key, | ||||||
| @@ -178,9 +151,8 @@ function createKeyBindings(item) { | |||||||
|     predefined: predefined |     predefined: predefined | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| // Validates settings before saving |  | ||||||
| function validate() { | function validate() { | ||||||
|  |   /* ... same as your original ... */ | ||||||
|   var valid = true; |   var valid = true; | ||||||
|   var status = document.getElementById("status"); |   var status = document.getElementById("status"); | ||||||
|   document |   document | ||||||
| @@ -190,7 +162,7 @@ function validate() { | |||||||
|       match = match.replace(regStrip, ""); |       match = match.replace(regStrip, ""); | ||||||
|       if (match.startsWith("/")) { |       if (match.startsWith("/")) { | ||||||
|         try { |         try { | ||||||
|           var regexp = new RegExp(match); |           new RegExp(match); | ||||||
|         } catch (err) { |         } catch (err) { | ||||||
|           status.textContent = |           status.textContent = | ||||||
|             "Error: Invalid blacklist regex: " + match + ". Unable to save"; |             "Error: Invalid blacklist regex: " + match + ". Unable to save"; | ||||||
| @@ -202,24 +174,45 @@ function validate() { | |||||||
|   return valid; |   return valid; | ||||||
| } | } | ||||||
|  |  | ||||||
| // Saves options to chrome.storage | // MODIFIED: save_options to include nudge settings | ||||||
| function save_options() { | function save_options() { | ||||||
|   if (validate() === false) { |   if (validate() === false) return; | ||||||
|     return; |  | ||||||
|   } |   keyBindings = []; // Reset global keyBindings before populating from DOM | ||||||
|   keyBindings = []; |  | ||||||
|   Array.from(document.querySelectorAll(".customs")).forEach((item) => |   Array.from(document.querySelectorAll(".customs")).forEach((item) => | ||||||
|     createKeyBindings(item) |     createKeyBindings(item) | ||||||
|   ); // Remove added shortcuts |   ); | ||||||
|  |  | ||||||
|   var rememberSpeed = document.getElementById("rememberSpeed").checked; |   var s = {}; // Object to hold all settings to be saved | ||||||
|   var forceLastSavedSpeed = document.getElementById("forceLastSavedSpeed").checked; |   s.rememberSpeed = document.getElementById("rememberSpeed").checked; | ||||||
|   var audioBoolean = document.getElementById("audioBoolean").checked; |   s.forceLastSavedSpeed = document.getElementById( | ||||||
|   var enabled = document.getElementById("enabled").checked; |     "forceLastSavedSpeed" | ||||||
|   var startHidden = document.getElementById("startHidden").checked; |   ).checked; | ||||||
|   var controllerOpacity = document.getElementById("controllerOpacity").value; |   s.audioBoolean = document.getElementById("audioBoolean").checked; | ||||||
|   var blacklist = document.getElementById("blacklist").value; |   s.enabled = document.getElementById("enabled").checked; | ||||||
|  |   s.startHidden = document.getElementById("startHidden").checked; | ||||||
|  |   s.controllerOpacity = document.getElementById("controllerOpacity").value; | ||||||
|  |   s.blacklist = document | ||||||
|  |     .getElementById("blacklist") | ||||||
|  |     .value.replace(regStrip, ""); | ||||||
|  |   s.keyBindings = keyBindings; // Use the populated global keyBindings | ||||||
|  |  | ||||||
|  |   // ADDED: Save nudge settings | ||||||
|  |   s.enableSubtitleNudge = document.getElementById( | ||||||
|  |     "enableSubtitleNudge" | ||||||
|  |   ).checked; | ||||||
|  |   s.subtitleNudgeInterval = | ||||||
|  |     parseInt(document.getElementById("subtitleNudgeInterval").value, 10) || | ||||||
|  |     tcDefaults.subtitleNudgeInterval; | ||||||
|  |   s.subtitleNudgeAmount = | ||||||
|  |     parseFloat(document.getElementById("subtitleNudgeAmount").value) || | ||||||
|  |     tcDefaults.subtitleNudgeAmount; | ||||||
|  |   // Basic validation for nudge interval and amount | ||||||
|  |   if (s.subtitleNudgeInterval < 10) s.subtitleNudgeInterval = 10; // Min 10ms | ||||||
|  |   if (s.subtitleNudgeAmount <= 0 || s.subtitleNudgeAmount > 0.1) | ||||||
|  |     s.subtitleNudgeAmount = tcDefaults.subtitleNudgeAmount; | ||||||
|  |  | ||||||
|  |   // Remove old flat settings (original logic) | ||||||
|   chrome.storage.sync.remove([ |   chrome.storage.sync.remove([ | ||||||
|     "resetSpeed", |     "resetSpeed", | ||||||
|     "speedStep", |     "speedStep", | ||||||
| @@ -233,33 +226,22 @@ function save_options() { | |||||||
|     "advanceKeyCode", |     "advanceKeyCode", | ||||||
|     "fastKeyCode" |     "fastKeyCode" | ||||||
|   ]); |   ]); | ||||||
|   chrome.storage.sync.set( |  | ||||||
|     { |   chrome.storage.sync.set(s, function () { | ||||||
|       rememberSpeed: rememberSpeed, |  | ||||||
|       forceLastSavedSpeed: forceLastSavedSpeed, |  | ||||||
|       audioBoolean: audioBoolean, |  | ||||||
|       enabled: enabled, |  | ||||||
|       startHidden: startHidden, |  | ||||||
|       controllerOpacity: controllerOpacity, |  | ||||||
|       keyBindings: keyBindings, |  | ||||||
|       blacklist: blacklist.replace(regStrip, "") |  | ||||||
|     }, |  | ||||||
|     function () { |  | ||||||
|       // Update status to let user know options were saved. |  | ||||||
|     var status = document.getElementById("status"); |     var status = document.getElementById("status"); | ||||||
|     status.textContent = "Options saved"; |     status.textContent = "Options saved"; | ||||||
|     setTimeout(function () { |     setTimeout(function () { | ||||||
|       status.textContent = ""; |       status.textContent = ""; | ||||||
|     }, 1000); |     }, 1000); | ||||||
|     } |   }); | ||||||
|   ); |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // Restores options from chrome.storage | // MODIFIED: restore_options to include nudge settings | ||||||
| function restore_options() { | function restore_options() { | ||||||
|   chrome.storage.sync.get(tcDefaults, function (storage) { |   chrome.storage.sync.get(tcDefaults, function (storage) { | ||||||
|     document.getElementById("rememberSpeed").checked = storage.rememberSpeed; |     document.getElementById("rememberSpeed").checked = storage.rememberSpeed; | ||||||
|     document.getElementById("forceLastSavedSpeed").checked = storage.forceLastSavedSpeed; |     document.getElementById("forceLastSavedSpeed").checked = | ||||||
|  |       storage.forceLastSavedSpeed; | ||||||
|     document.getElementById("audioBoolean").checked = storage.audioBoolean; |     document.getElementById("audioBoolean").checked = storage.audioBoolean; | ||||||
|     document.getElementById("enabled").checked = storage.enabled; |     document.getElementById("enabled").checked = storage.enabled; | ||||||
|     document.getElementById("startHidden").checked = storage.startHidden; |     document.getElementById("startHidden").checked = storage.startHidden; | ||||||
| @@ -267,65 +249,86 @@ function restore_options() { | |||||||
|       storage.controllerOpacity; |       storage.controllerOpacity; | ||||||
|     document.getElementById("blacklist").value = storage.blacklist; |     document.getElementById("blacklist").value = storage.blacklist; | ||||||
|  |  | ||||||
|     // ensure that there is a "display" binding for upgrades from versions that had it as a separate binding |     // ADDED: Restore nudge settings | ||||||
|  |     document.getElementById("enableSubtitleNudge").checked = | ||||||
|  |       storage.enableSubtitleNudge; | ||||||
|  |     document.getElementById("subtitleNudgeInterval").value = | ||||||
|  |       storage.subtitleNudgeInterval; | ||||||
|  |     document.getElementById("subtitleNudgeAmount").value = | ||||||
|  |       storage.subtitleNudgeAmount; | ||||||
|  |  | ||||||
|  |     // Original key binding restoration logic | ||||||
|  |     if ( | ||||||
|  |       !Array.isArray(storage.keyBindings) || | ||||||
|  |       storage.keyBindings.length === 0 | ||||||
|  |     ) { | ||||||
|  |       // If keyBindings missing or not an array, use defaults from tcDefaults | ||||||
|  |       storage.keyBindings = tcDefaults.keyBindings; | ||||||
|  |     } | ||||||
|     if (storage.keyBindings.filter((x) => x.action == "display").length == 0) { |     if (storage.keyBindings.filter((x) => x.action == "display").length == 0) { | ||||||
|       storage.keyBindings.push({ |       storage.keyBindings.push({ | ||||||
|         action: "display", |         action: "display", | ||||||
|         value: 0, |         value: 0, | ||||||
|         force: false, |         force: false, | ||||||
|         predefined: true |         predefined: true, | ||||||
|  |         key: storage.displayKeyCode || tcDefaults.displayKeyCode | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     // Clear existing dynamic shortcuts before restoring (if any were added by mistake) | ||||||
|  |     const dynamicShortcuts = document.querySelectorAll(".customs:not([id])"); | ||||||
|  |     dynamicShortcuts.forEach((sc) => sc.remove()); | ||||||
|  |  | ||||||
|     for (let i in storage.keyBindings) { |     for (let i in storage.keyBindings) { | ||||||
|       var item = storage.keyBindings[i]; |       var item = storage.keyBindings[i]; | ||||||
|       if (item.predefined) { |       if (item.predefined) { | ||||||
|         //do predefined ones because their value needed for overlay |  | ||||||
|         // document.querySelector("#" + item["action"] + " .customDo").value = item["action"]; |  | ||||||
|         if (item["action"] == "display" && typeof item["key"] === "undefined") { |         if (item["action"] == "display" && typeof item["key"] === "undefined") { | ||||||
|           item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; // V |           item["key"] = storage.displayKeyCode || tcDefaults.displayKeyCode; | ||||||
|         } |         } | ||||||
|  |         if (customActionsNoValues.includes(item["action"])) { | ||||||
|         if (customActionsNoValues.includes(item["action"])) |           const el = document.querySelector( | ||||||
|           document.querySelector( |  | ||||||
|             "#" + item["action"] + " .customValue" |             "#" + item["action"] + " .customValue" | ||||||
|           ).disabled = true; |  | ||||||
|  |  | ||||||
|         updateCustomShortcutInputText( |  | ||||||
|           document.querySelector("#" + item["action"] + " .customKey"), |  | ||||||
|           item["key"] |  | ||||||
|           ); |           ); | ||||||
|         document.querySelector("#" + item["action"] + " .customValue").value = |           if (el) el.disabled = true; | ||||||
|           item["value"]; |         } | ||||||
|         document.querySelector("#" + item["action"] + " .customForce").value = |         const keyEl = document.querySelector( | ||||||
|           item["force"]; |           "#" + item["action"] + " .customKey" | ||||||
|  |         ); | ||||||
|  |         const valEl = document.querySelector( | ||||||
|  |           "#" + item["action"] + " .customValue" | ||||||
|  |         ); | ||||||
|  |         const forceEl = document.querySelector( | ||||||
|  |           "#" + item["action"] + " .customForce" | ||||||
|  |         ); | ||||||
|  |         if (keyEl) updateCustomShortcutInputText(keyEl, item["key"]); | ||||||
|  |         if (valEl) valEl.value = item["value"]; | ||||||
|  |         if (forceEl) forceEl.value = String(item["force"]); // Ensure string for select value | ||||||
|       } else { |       } else { | ||||||
|         // new ones |         // Non-predefined, dynamically added shortcuts | ||||||
|         add_shortcut(); |         add_shortcut(); | ||||||
|         const dom = document.querySelector(".customs:last-of-type"); |         const dom = document.querySelector(".customs:last-of-type"); // Gets the newly added one | ||||||
|         dom.querySelector(".customDo").value = item["action"]; |         dom.querySelector(".customDo").value = item["action"]; | ||||||
|  |         if (customActionsNoValues.includes(item["action"])) { | ||||||
|         if (customActionsNoValues.includes(item["action"])) |  | ||||||
|           dom.querySelector(".customValue").disabled = true; |           dom.querySelector(".customValue").disabled = true; | ||||||
|  |         } | ||||||
|         updateCustomShortcutInputText( |         updateCustomShortcutInputText( | ||||||
|           dom.querySelector(".customKey"), |           dom.querySelector(".customKey"), | ||||||
|           item["key"] |           item["key"] | ||||||
|         ); |         ); | ||||||
|         dom.querySelector(".customValue").value = item["value"]; |         dom.querySelector(".customValue").value = item["value"]; | ||||||
|         dom.querySelector(".customForce").value = item["force"]; |         dom.querySelector(".customForce").value = String(item["force"]); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function restore_defaults() { | function restore_defaults() { | ||||||
|  |   /* ... same as your original, tcDefaults now includes nudge defaults ... */ | ||||||
|  |   // Remove all dynamically added shortcuts first | ||||||
|  |   document.querySelectorAll(".customs:not([id])").forEach((el) => el.remove()); | ||||||
|  |   // Then set defaults and restore options, which will re-add predefined ones correctly | ||||||
|   chrome.storage.sync.set(tcDefaults, function () { |   chrome.storage.sync.set(tcDefaults, function () { | ||||||
|     restore_options(); |     restore_options(); // This will populate based on tcDefaults | ||||||
|     document |  | ||||||
|       .querySelectorAll(".removeParent") |  | ||||||
|       .forEach((button) => button.click()); // Remove added shortcuts |  | ||||||
|     // Update status to let user know options were saved. |  | ||||||
|     var status = document.getElementById("status"); |     var status = document.getElementById("status"); | ||||||
|     status.textContent = "Default options restored"; |     status.textContent = "Default options restored"; | ||||||
|     setTimeout(function () { |     setTimeout(function () { | ||||||
| @@ -335,14 +338,15 @@ function restore_defaults() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function show_experimental() { | function show_experimental() { | ||||||
|  |   /* ... same as your original ... */ | ||||||
|   document |   document | ||||||
|     .querySelectorAll(".customForce") |     .querySelectorAll(".customForce") | ||||||
|     .forEach((item) => (item.style.display = "inline-block")); |     .forEach((item) => (item.style.display = "inline-block")); | ||||||
| } | } | ||||||
|  |  | ||||||
| document.addEventListener("DOMContentLoaded", function () { | document.addEventListener("DOMContentLoaded", function () { | ||||||
|  |   /* ... same as your original event listeners setup ... */ | ||||||
|   restore_options(); |   restore_options(); | ||||||
|  |  | ||||||
|   document.getElementById("save").addEventListener("click", save_options); |   document.getElementById("save").addEventListener("click", save_options); | ||||||
|   document.getElementById("add").addEventListener("click", add_shortcut); |   document.getElementById("add").addEventListener("click", add_shortcut); | ||||||
|   document |   document | ||||||
| @@ -353,34 +357,32 @@ document.addEventListener("DOMContentLoaded", function () { | |||||||
|     .addEventListener("click", show_experimental); |     .addEventListener("click", show_experimental); | ||||||
|  |  | ||||||
|   function eventCaller(event, className, funcName) { |   function eventCaller(event, className, funcName) { | ||||||
|     if (!event.target.classList || !event.target.classList.contains(className)) { |     if (!event.target.classList || !event.target.classList.contains(className)) | ||||||
|       return; |       return; | ||||||
|     } |  | ||||||
|     funcName(event); |     funcName(event); | ||||||
|   } |   } | ||||||
|  |   document.addEventListener("keypress", (event) => | ||||||
|   document.addEventListener("keypress", (event) => { |     eventCaller(event, "customValue", inputFilterNumbersOnly) | ||||||
|     eventCaller(event, "customValue", inputFilterNumbersOnly); |   ); | ||||||
|   }); |   document.addEventListener("focus", (event) => | ||||||
|   document.addEventListener("focus", (event) => { |     eventCaller(event, "customKey", inputFocus) | ||||||
|     eventCaller(event, "customKey", inputFocus); |   ); | ||||||
|   }); |   document.addEventListener("blur", (event) => | ||||||
|   document.addEventListener("blur", (event) => { |     eventCaller(event, "customKey", inputBlur) | ||||||
|     eventCaller(event, "customKey", inputBlur); |   ); | ||||||
|   }); |   document.addEventListener("keydown", (event) => | ||||||
|   document.addEventListener("keydown", (event) => { |     eventCaller(event, "customKey", recordKeyPress) | ||||||
|     eventCaller(event, "customKey", recordKeyPress); |   ); | ||||||
|   }); |   document.addEventListener("click", (event) => | ||||||
|   document.addEventListener("click", (event) => { |  | ||||||
|     eventCaller(event, "removeParent", function () { |     eventCaller(event, "removeParent", function () { | ||||||
|       event.target.parentNode.remove(); |       event.target.parentNode.remove(); | ||||||
|     }); |     }) | ||||||
|   }); |   ); | ||||||
|   document.addEventListener("change", (event) => { |   document.addEventListener("change", (event) => { | ||||||
|     eventCaller(event, "customDo", function () { |     eventCaller(event, "customDo", function () { | ||||||
|       if (customActionsNoValues.includes(event.target.value)) { |       if (customActionsNoValues.includes(event.target.value)) { | ||||||
|         event.target.nextElementSibling.nextElementSibling.disabled = true; |         event.target.nextElementSibling.nextElementSibling.disabled = true; | ||||||
|         event.target.nextElementSibling.nextElementSibling.value = 0; |         event.target.nextElementSibling.nextElementSibling.value = 0; // Or "" if placeholder is preferred | ||||||
|       } else { |       } else { | ||||||
|         event.target.nextElementSibling.nextElementSibling.disabled = false; |         event.target.nextElementSibling.nextElementSibling.disabled = false; | ||||||
|       } |       } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user