diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cc62ec7 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{js,ts,json}] +indent_size = 2 +indent_style = space diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..7067280 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + "extends": "eslint:recommended", + "env": { + "browser": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 2017 + } +}; diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b91f40f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +ko_fi: lauwerens diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08b8c1f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +web-ext-artifacts \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..cada016 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "editor.formatOnSave": true, + "editor.tabSize": 2, + "eslint.validate": [ + "javascript", + "typescript", + ], + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "eslint.format.enable": true, +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d22b36d --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/L3L0BR8QG) + +# Video Seeking Everywhere +_A [Firefox](https://addons.mozilla.org/en-US/firefox/addon/video-seeking-everywhere/) extension that allows you to seek through any playing video on most websites using the < or > keys._ + +- Use < or , to rewind with 5 seconds +- Use > or . to seek forward with 5 seconds + +🛠️ It's possible to change the amount of seconds using the Options page of this extension. + +## Development +Please feel free to contribute by sending a Pull Request. + +### Dependencies +You can either use `npm` or `pnpm` with the commands below or run `web-ext` directly: + +### Running the extension +`pnpx web-ext run` + +### Building +`pnpx web-ext build` + +## Reference +See https://extensionworkshop.com/documentation/develop/getting-started-with-web-ext/ for information about the `web-ext` command. diff --git a/browser-polyfill.min.js b/browser-polyfill.min.js new file mode 100644 index 0000000..37f6ee9 --- /dev/null +++ b/browser-polyfill.min.js @@ -0,0 +1,8 @@ +(function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if(!globalThis.chrome?.runtime?.id)throw new Error("This script should only be loaded in a browser extension.");if("undefined"==typeof globalThis.browser||Object.getPrototypeOf(globalThis.browser)!==Object.prototype){a.exports=(a=>{const b={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0},elements:{createSidebarPane:{minArgs:1,maxArgs:1}}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},goBack:{minArgs:0,maxArgs:1},goForward:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(new Error(a.runtime.lastError.message)):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e=Object.create(a);return new Proxy(e,{has(b,c){return c in a||c in d},get(e,f){if(f in d)return d[f];if(!(f in a))return;let k=a[f];if("function"==typeof k){if("function"==typeof b[f])k=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);k=h(a,a[f],b)}else k=k.bind(a);}else if("object"==typeof k&&null!==k&&(i(b,f)||i(c,f)))k=j(k,b[f],c[f]);else if(i(c,"*"))k=j(k,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),k;return d[f]=k,k},set(b,c,e){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}})},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}}),l=new c(a=>"function"==typeof a?function(b){const c=j(b,{},{getContent:{minArgs:0,maxArgs:0}});a(c)}:a),m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(new Error(a.runtime.lastError.message)):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={devtools:{network:{onRequestFinished:k(l)}},runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=globalThis.browser}); +//# sourceMappingURL=browser-polyfill.min.js.map + +// webextension-polyfill v.0.10.0 (https://github.com/mozilla/webextension-polyfill) + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ diff --git a/contentscript.js b/contentscript.js new file mode 100644 index 0000000..1e8164f --- /dev/null +++ b/contentscript.js @@ -0,0 +1,35 @@ +; +(function () { + const settings = { + rewindSec: 5, + seekForwardSec: 5 + } + + function onError(error) { + console.warn(`Could not load settings for Video Seeking Everywhere, falling back to defaults...`, error); + inject(); + } + + function onGot(item) { + if (item.rewindSec > 0) { + settings.rewindSec = item.rewindSec; + } + if (item.seekForwardSec > 0) { + settings.seekForwardSec = item.seekForwardSec; + } + inject() + } + + let getting = browser.storage.sync.get(); + getting.then(onGot, onError); + + // Inject the script to the page: + function inject() { + var s = document.createElement('script'); + s.src = chrome.runtime.getURL('video-seeking-everywhere.js?') + new URLSearchParams(settings); + s.onload = function () { + this.remove(); + }; + (document.head || document.documentElement).appendChild(s); + } +})() diff --git a/icons/128.png b/icons/128.png new file mode 100644 index 0000000..d3d1c88 Binary files /dev/null and b/icons/128.png differ diff --git a/icons/video-seeking-everywhere.svg b/icons/video-seeking-everywhere.svg new file mode 100644 index 0000000..fadff44 --- /dev/null +++ b/icons/video-seeking-everywhere.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..8b48be5 --- /dev/null +++ b/manifest.json @@ -0,0 +1,45 @@ +{ + "manifest_version": 3, + "name": "Video Seeking Everywhere", + "version": "1.0.0", + "description": "Allows you to seek through any playing video on most websites using the < or > keys.", + "icons": { + "48": "icons/128.png" + }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "browser-polyfill.min.js", + "contentscript.js" + ] + } + ], + "web_accessible_resources": [ + { + "resources": [ + "video-seeking-everywhere.js" + ], + "matches": [ + "http://*/*", + "https://*/*" + ] + } + ], + "permissions": [ + "storage" + ], + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, + "browser_specific_settings": { + "gecko": { + "id": "@video-seeking-everywhere", + "strict_min_version": "109.0" + } + } +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..6334ece --- /dev/null +++ b/options.html @@ -0,0 +1,62 @@ + + + + + + + Options for Video Seeking Everywhere + + + + + + + + +
+
+ +

+ + Options for Video Seeking Everywhere +

+
+ +

+ Allows you to seek through any playing video on most websites using the <> keys. +

+ +

+ Use < (or ,) to rewind with 5 seconds
+ Use > (or .) to seek forward with 5 seconds +

+ +

+ You can change the amount of seconds to rewind or seek forward. +

+ +
+ + + + + + +
+
+ + +
+ + + \ No newline at end of file diff --git a/options/options.css b/options/options.css new file mode 100644 index 0000000..0b993a0 --- /dev/null +++ b/options/options.css @@ -0,0 +1,99 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: sans-serif; + line-height: 1.5; +} + +.wrapper { + max-width: 46rem; + margin: 2.5em auto; +} + +p { + margin: .5em 0 2em; +} + +.footer { + margin-top: 1em; + text-align: center; + display: flex; + justify-content: space-around; +} + +kbd { + background-color: #eee; + border: 1px solid #ccc; + border-radius: 0.5em; + padding: 0.2em 0.5em; + font-size: 0.9em; + font-family: monospace; + margin: 0 0.2em; + box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.2); + white-space: nowrap; +} + +a { + text-decoration: none; + padding: .5em; + display: inline-block; + color: rgb(108, 128, 144); + border-radius: 0.5em; +} + +a span { + white-space: nowrap; +} + +a:hover { + background-color: black; + color: white; +} + +fieldset { + border: 1px solid #ccc; + padding: 1em; + border-radius: 0.5em; +} + +form { + display: grid; + flex-wrap: wrap; + gap: 2.5em; + justify-content: space-between; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); +} + +label { + font-weight: bold; + grid-column: span 1; +} + +input { + display: block; + padding: 0.5em; + width: 100%; + border: 1px solid #ccc; + border-radius: 0.5em; + font-size: 1.2em; +} + +button { + grid-column: span 1; + padding: 0.5em; + border: 1px solid #ccc; + border-radius: 0.5em; +} + +button#save { + color: white; + background-color: rgb(51, 51, 51); +} + +button#save:hover { + background-color: black; +} diff --git a/options/options.js b/options/options.js new file mode 100644 index 0000000..3bd77e6 --- /dev/null +++ b/options/options.js @@ -0,0 +1,43 @@ +let infoMessageTimeout; + +function saveOptions(e, reset = false) { + if (!reset) e.preventDefault(); + + const defaults = { + rewindSec: 5, + seekForwardSec: 5 + }; + + browser.storage.sync.set({ + rewindSec: !reset ? document.querySelector("#rewind-seconds").value : defaults.rewindSec, + seekForwardSec: !reset ? document.querySelector("#forward-seconds").value : defaults.seekForwardSec + }); + + document.querySelector('[data-rewind-sec]').innerText = !reset ? document.querySelector("#rewind-seconds").value : defaults.rewindSec; + document.querySelector('[data-forward-sec]').innerText = !reset ? document.querySelector("#forward-seconds").value : defaults.seekForwardSec; + + let btn = document.querySelector(reset ? "#reset" : "#save"); + btn.textContent = `✓ ${reset ? 'Defaults saved' : 'Saved'}!`; + clearTimeout(infoMessageTimeout); + infoMessageTimeout = setTimeout(() => { + btn.textContent = reset ? "Reset to defaults" : "Save"; + }, 750); +} + +function restoreOptions() { + function setCurrentChoice(result) { + document.querySelector("#rewind-seconds").value = result.rewindSec || defaults.rewindSec; + document.querySelector("#forward-seconds").value = result.seekForwardSec || defaults.seekForwardSec; + } + + function onError(error) { + console.log(`Error: ${error}`); + } + + let getting = browser.storage.sync.get(); + getting.then(setCurrentChoice, onError); +} + +document.addEventListener("DOMContentLoaded", restoreOptions); +document.querySelector("form").addEventListener("submit", saveOptions); +document.querySelector("form").addEventListener("reset", (e) => saveOptions(e, true)); diff --git a/video-seeking-everywhere.js b/video-seeking-everywhere.js new file mode 100644 index 0000000..4fdd82f --- /dev/null +++ b/video-seeking-everywhere.js @@ -0,0 +1,30 @@ +console.debug("💾 'Video Seeking Everywhere' plugin loaded."); +const params = new URLSearchParams(document.currentScript.src.split('?')[1]); + +const config = { + rewindSec: params.get('rewindSec'), + seekForwardSec: params.get('seekForwardSec') +}; + +function onSeek(e) { + const players = [...document.querySelectorAll('video')].filter(vid => !vid.paused); + players.forEach((player) => { + if (player) { + switch (e.key) { + case '<': + case ',': + console.debug(`⏪ Seeking backwards by ${config.rewindSec} second${config.rewindSec > 1 ? 's' : ''}`); + player.currentTime -= parseInt(config.rewindSec); + break; + case '>': + case '.': + console.debug(`⏩ Seeking forwards by ${config.seekForwardSec} second${config.seekForwardSec > 1 ? 's' : ''}`); + player.currentTime += parseInt(config.seekForwardSec); + break; + } + } + }); +} + +// Listen to keydown events: +document.addEventListener('keydown', onSeek);