├── safari.sh ├── src.safariextension ├── Icon-64.png ├── Info.plist ├── Settings.plist ├── background.html ├── chrome.js ├── common.js └── data │ ├── error.svg │ ├── inject │ ├── chrome.js │ ├── index.css │ └── index.js │ └── loader.gif ├── src ├── common.js ├── data │ ├── error.svg │ ├── icons │ │ ├── 128.png │ │ ├── 16.png │ │ ├── 256.png │ │ ├── 32.png │ │ ├── 48.png │ │ └── 64.png │ ├── inject │ │ ├── index.css │ │ └── index.js │ ├── loader.gif │ └── options │ │ ├── index.html │ │ └── index.js └── manifest.json └── test └── index.html /safari.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cp src/common.js src.safariextension/common.js 4 | cp src/data/inject/index.js src.safariextension/data/inject/index.js 5 | -------------------------------------------------------------------------------- /src.safariextension/Icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src.safariextension/Icon-64.png -------------------------------------------------------------------------------- /src.safariextension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | Jeremy Schomery 7 | Builder Version 8 | 12602.2.14.0.7 9 | CFBundleDisplayName 10 | Hover Preview for YouTube™ 11 | CFBundleIdentifier 12 | com.add0n.hover-youtube 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 0.1.4 17 | CFBundleVersion 18 | 1 19 | Chrome 20 | 21 | Database Quota 22 | 1048576 23 | Global Page 24 | background.html 25 | 26 | Content 27 | 28 | Blacklist 29 | 30 | *://*.youtube.com/* 31 | *://youtu.be/* 32 | 33 | Scripts 34 | 35 | Start 36 | 37 | data/inject/chrome.js 38 | data/inject/index.js 39 | 40 | 41 | Stylesheets 42 | 43 | data/inject/index.css 44 | 45 | 46 | Description 47 | View YouTube while hovering mouse over a YouTube link on any website 48 | DeveloperIdentifier 49 | RED8XKG2R4 50 | ExtensionInfoDictionaryVersion 51 | 1.0 52 | Permissions 53 | 54 | Website Access 55 | 56 | Include Secure Pages 57 | 58 | Level 59 | All 60 | 61 | 62 | Website 63 | http://add0n.com/youtube-hover.html 64 | 65 | 66 | -------------------------------------------------------------------------------- /src.safariextension/Settings.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DefaultValue 7 | 500 8 | Key 9 | width-to-number 10 | Title 11 | Player width (in pixels) 12 | Type 13 | TextField 14 | 15 | 16 | DefaultValue 17 | true 18 | Key 19 | strike 20 | Title 21 | Draw a strike-through over YouTube links after preview 22 | Type 23 | CheckBox 24 | 25 | 26 | DefaultValue 27 | 0 28 | Key 29 | mode-to-number 30 | Secure 31 | 32 | Title 33 | Player positioning mode: 34 | Titles 35 | 36 | Relative to the link 37 | Center of the screen 38 | 39 | Type 40 | ListBox 41 | Values 42 | 43 | 0 44 | 1 45 | 46 | 47 | 48 | DefaultValue 49 | 0 50 | Key 51 | relative-x-to-number 52 | Title 53 | Player horizontal offset in "relative" mode 54 | Type 55 | TextField 56 | 57 | 58 | DefaultValue 59 | 0 60 | Key 61 | relative-y-to-number 62 | Title 63 | Player vertical offset in "relative" mode 64 | Type 65 | TextField 66 | 67 | 68 | DefaultValue 69 | 0 70 | Key 71 | center-x-to-number 72 | Title 73 | Player horizontal offset in "center" mode 74 | Type 75 | TextField 76 | 77 | 78 | DefaultValue 79 | 0 80 | Key 81 | center-y-to-number 82 | Title 83 | Player vertical offset in "center" mode 84 | Type 85 | TextField 86 | 87 | 88 | DefaultValue 89 | false 90 | Key 91 | dark 92 | Title 93 | Turn off the light while playing 94 | Type 95 | CheckBox 96 | 97 | 98 | DefaultValue 99 | true 100 | Key 101 | scroll 102 | Title 103 | Scroll to player in the "relative" mode 104 | Type 105 | CheckBox 106 | 107 | 108 | DefaultValue 109 | true 110 | Key 111 | smooth 112 | Title 113 | Use smooth scrolling 114 | Type 115 | CheckBox 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /src.safariextension/background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Document 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src.safariextension/chrome.js: -------------------------------------------------------------------------------- 1 | /* globals safari */ 2 | 'use strict'; 3 | 4 | var chrome = {}; 5 | 6 | chrome.runtime = { 7 | getManifest: function () { 8 | return { 9 | version: safari.extension.displayVersion 10 | }; 11 | }, 12 | onMessage: { 13 | addListener: function (callback) { 14 | safari.application.addEventListener('message', function (e) { 15 | if (e.name === 'internal-channel') { 16 | callback(e.message.data, { 17 | tab: e.target 18 | }, function (data) { 19 | e.target.page.dispatchMessage('replied', { 20 | data: data, 21 | id: e.message.id 22 | }); 23 | }); 24 | } 25 | else if (e.name === 'storage-channel') { 26 | chrome.storage.local.get(e.message, function (obj) { 27 | e.target.page.dispatchMessage('storage-channel', obj); 28 | }); 29 | } 30 | },false); 31 | } 32 | } 33 | }; 34 | 35 | chrome.storage = { 36 | local: { 37 | get: function (obj, callback) { 38 | if (typeof obj === 'string') { 39 | var tmp1 = {}; 40 | tmp1[obj] = null; 41 | obj = tmp1; 42 | } 43 | var tmp = {}; 44 | for (var name in obj) { 45 | var val = safari.extension.settings[name]; 46 | tmp[name] = typeof val === 'undefined' ? obj[name] : val; 47 | } 48 | callback(tmp); 49 | }, 50 | set: function (obj, callback) { 51 | for (var name in obj) { 52 | safari.extension.settings[name] = obj[name]; 53 | } 54 | if (callback) { 55 | callback(); 56 | } 57 | } 58 | } 59 | }; 60 | safari.extension.settings.addEventListener('change', function (e) { 61 | if (e.key.indexOf('-to-number') !== -1) { 62 | safari.extension.settings[e.key.replace('-to-number', '')] = +e.newValue; 63 | } 64 | }, false); 65 | 66 | chrome.tabs = { 67 | create: function (obj) { 68 | var tab = safari.application.activeBrowserWindow.openTab(); 69 | tab.url = obj.url; 70 | } 71 | }; 72 | 73 | chrome.history = { 74 | addUrl: function () {} 75 | }; 76 | -------------------------------------------------------------------------------- /src.safariextension/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | chrome.runtime.onMessage.addListener((request, sender, response) => { 4 | if (request.cmd === 'history') { 5 | chrome.history.addUrl({ 6 | url: request.url 7 | }); 8 | } 9 | else if (request.cmd === 'find-id') { 10 | let req = new XMLHttpRequest(); 11 | req.open('GET', request.url); 12 | req.responseType = 'document'; 13 | req.onload = () => { 14 | try { 15 | response(req.response.querySelector('[itemprop="videoId"]').content); 16 | } 17 | catch (e) { 18 | response(); 19 | } 20 | }; 21 | req.onerror = () => response(); 22 | req.send(); 23 | return true; 24 | } 25 | }); 26 | 27 | chrome.storage.local.get('version', (obj) => { 28 | let version = chrome.runtime.getManifest().version; 29 | if (obj.version !== version) { 30 | window.setTimeout(() => { 31 | chrome.storage.local.set({version}, () => { 32 | chrome.tabs.create({ 33 | url: 'http://add0n.com/youtube-hover.html?version=' + 34 | version + '&type=' + 35 | (obj.version ? ('upgrade&p=' + obj.version) : 'install') 36 | }); 37 | }); 38 | }, 3000); 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src.safariextension/data/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src.safariextension/data/inject/chrome.js: -------------------------------------------------------------------------------- 1 | /* globals safari */ 2 | 'use strict'; 3 | 4 | var chrome = {}; 5 | var replies = {}; 6 | 7 | chrome.runtime = { 8 | sendMessage: function (data, callback) { 9 | if (callback) { 10 | var id = Math.random(); 11 | replies[id] = callback; 12 | safari.self.tab.dispatchMessage('internal-channel', { 13 | data: data, 14 | id: id 15 | }); 16 | } 17 | else { 18 | safari.self.tab.dispatchMessage('internal-channel', { 19 | data: data 20 | }); 21 | } 22 | } 23 | }; 24 | safari.self.addEventListener('message', function (e) { 25 | if (e.name === 'replied') { 26 | if (e.message.id in replies) { 27 | replies[e.message.id](e.message.data); 28 | } 29 | delete replies[e.message.id]; 30 | } 31 | else if (e.name === 'storage-channel') { 32 | chrome.storage.getCallbacks.forEach(function (callback) { 33 | callback(e.message); 34 | }); 35 | chrome.storage.getCallbacks = []; 36 | } 37 | }, false); 38 | 39 | chrome.storage = { 40 | getCallbacks: [], 41 | local: { 42 | get: function (obj, callback) { 43 | chrome.storage.getCallbacks.push(callback); 44 | safari.self.tab.dispatchMessage('storage-channel', obj); 45 | } 46 | }, 47 | onChanged: { 48 | addListener: function () {} 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src.safariextension/data/inject/index.css: -------------------------------------------------------------------------------- 1 | .ihvyoutube { 2 | border: solid 1px #dadada; 3 | box-shadow: 1px 1px 5px #dadada; 4 | background: #fff url('../loader-black.gif') center center no-repeat; 5 | z-index: 2147483647; 6 | } 7 | .ihvyoutube[data-error=true] { 8 | background-image: url('../error.svg'); 9 | background-size: 128px; 10 | } 11 | .ihvyoutube[data-dark=true] { 12 | box-shadow: 0 0 0 90000px rgba(0, 0, 0, 0.8); 13 | border: none; 14 | background: #000 url('../loader-white.gif') center center no-repeat; 15 | } 16 | -------------------------------------------------------------------------------- /src.safariextension/data/inject/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var iframe; 4 | var config = { 5 | 'relative-x': 0, 6 | 'relative-y': 0, 7 | 'center-x': 0, 8 | 'center-y': 0, 9 | 'delay': 1000, 10 | 'width': 500, 11 | 'mode': 0, 12 | 'strike': true, 13 | 'history': true, 14 | 'scroll': true, 15 | 'smooth': true, 16 | 'dark': false 17 | }; 18 | chrome.storage.local.get(config, prefs => config = prefs); 19 | chrome.storage.onChanged.addListener(prefs => { 20 | Object.keys(prefs).forEach(name => { 21 | config[name] = prefs[name].newValue; 22 | }); 23 | }); 24 | 25 | var smoothScroll = (function () { 26 | let timeLapsed = 0; 27 | let id, sx, sy, dx, dy, callback; 28 | 29 | let easingPattern = time => time < 0.5 ? 8 * time * time * time * time : 1 - 8 * (--time) * time * time * time; 30 | 31 | function step () { 32 | timeLapsed += 16; 33 | let percentage = timeLapsed / 400; 34 | if (percentage > 1) { 35 | window.scrollTo(sx + dx, sy + dy); 36 | return callback(); 37 | } 38 | window.scrollTo( 39 | Math.floor(sx + (dx * easingPattern(percentage))), 40 | Math.floor(sy + (dy * easingPattern(percentage))) 41 | ); 42 | id = window.setTimeout(step, 16); 43 | } 44 | 45 | return function (x, y, c) { 46 | window.clearTimeout(id); 47 | callback = c; 48 | timeLapsed = 0; 49 | sx = document.body.scrollLeft + document.documentElement.scrollLeft; 50 | sy = document.body.scrollTop + document.documentElement.scrollTop; 51 | dx = Math.max(0, x - sx); 52 | dy = Math.max(0, y - sy); 53 | if (dx === 0 && dy === 0) { 54 | return c(); 55 | } 56 | step(); 57 | }; 58 | })(); 59 | 60 | var youtube = { 61 | play: (id, rect, shared) => { 62 | iframe = document.createElement('iframe'); 63 | iframe.setAttribute('width', config.width); 64 | iframe.setAttribute('height', config.width * 180 / 320); 65 | 66 | function play () { 67 | if (shared) { 68 | chrome.runtime.sendMessage({ 69 | cmd: 'find-id', 70 | url: 'https://www.youtube.com/shared?ci=' + id 71 | }, id => { 72 | if (id) { 73 | iframe.setAttribute('src', `https://www.youtube.com/embed/${id}?autoplay=1`); 74 | } 75 | else { 76 | iframe.dataset.error = true; 77 | } 78 | }); 79 | } 80 | else { 81 | iframe.setAttribute('src', `https://www.youtube.com/embed/${id}?autoplay=1`); 82 | } 83 | } 84 | 85 | if (config.mode === 1) { // center of screen 86 | iframe.setAttribute('style', ` 87 | position: fixed; 88 | left: calc(50% - ${config.width / 2 - config['center-x']}px); 89 | top: calc(50% - ${config.width * 180 / 320 / 2 - config['center-y']}px); 90 | `); 91 | play(); 92 | } 93 | else { 94 | let x1 = Math.max(0, rect.left + document.body.scrollLeft + 95 | document.documentElement.scrollLeft + config['relative-x']); 96 | let y1 = Math.max(0, rect.top + rect.height + document.body.scrollTop + 97 | document.documentElement.scrollTop + config['relative-y']); 98 | let x2 = x1 + config.width; 99 | let y2 = y1 + config.width * 180 / 320; 100 | let vw = document.documentElement.scrollWidth; 101 | let vh = document.documentElement.scrollHeight; 102 | 103 | let left = x1; 104 | let top = y1; 105 | if (x2 > vw - 10) { 106 | left = vw - config.width - 10; 107 | } 108 | if (y2 > vh - 10) { 109 | top = vh - config.width * 180 / 320 - 10; 110 | } 111 | if (config.scroll) { 112 | let x = Math.max( 113 | document.body.scrollLeft, 114 | left + config.width - document.documentElement.clientWidth + 10 115 | ); 116 | let y = Math.max( 117 | document.body.scrollTop, 118 | top + config.width * 180 / 320 - document.documentElement.clientHeight + 10 119 | ); 120 | if (config.smooth) { 121 | smoothScroll(x, y, play); 122 | } 123 | else { 124 | window.scrollTo(x, y); 125 | play(); 126 | } 127 | } 128 | else { 129 | play(); 130 | } 131 | 132 | iframe.setAttribute('style', ` 133 | position: absolute; 134 | left: ${left}px; 135 | top: ${top}px; 136 | `); 137 | 138 | } 139 | iframe.setAttribute('class', 'ihvyoutube'); 140 | iframe.dataset.dark = config.dark; 141 | document.body.appendChild(iframe); 142 | } 143 | }; 144 | 145 | var timer; 146 | document.addEventListener('mouseover', e => { 147 | let target = e.target; 148 | if (target) { 149 | let link = target.closest('a'); 150 | if (link) { 151 | let href = link.href; 152 | if (!href || iframe) { 153 | return; 154 | } 155 | let shared = false; 156 | if ( 157 | href.indexOf('youtube.com/shared') !== -1 || 158 | href.indexOf('youtube.com/attribution_link') !== -1 || 159 | href.indexOf('youtube.com/watch') !== -1 || 160 | href.indexOf('//youtu.be/') !== -1 161 | ) { 162 | let id; 163 | if (href.indexOf('youtube.com/watch') !== -1) { 164 | id = href.match(/v\=([^\&]+)/); 165 | } 166 | else if (href.indexOf('//youtu.be/') !== -1) { 167 | id = href.match(/\.be\/([^\&]+)/); 168 | } 169 | else if (href.indexOf('youtube.com/attribution_link') !== -1) { 170 | id = decodeURIComponent(href).match(/v\=([^\&]+)/); 171 | } 172 | else if (href.indexOf('youtube.com/shared') !== -1) { 173 | shared = true; 174 | id = href.match(/ci\=([^\&]+)/); 175 | } 176 | 177 | if (id && id.length) { 178 | window.clearTimeout(timer); 179 | timer = window.setTimeout((link) => { 180 | let activeLink = [...document.querySelectorAll(':hover')].pop(); 181 | if (link === activeLink) { 182 | let rect = link.getBoundingClientRect(); 183 | youtube.play(id[1], rect, shared); 184 | if (config.strike) { 185 | [...document.querySelectorAll(`a[href="${href}"]`), link]. 186 | forEach(l => l.style['text-decoration'] = 'line-through'); 187 | } 188 | if (config.history) { 189 | chrome.runtime.sendMessage({ 190 | url: href, 191 | cmd: 'history' 192 | }); 193 | } 194 | } 195 | }, config.delay, link); 196 | } 197 | } 198 | } 199 | } 200 | }); 201 | document.addEventListener('click', () => { 202 | if (iframe) { 203 | [...document.querySelectorAll('.ihvyoutube')].forEach(f => f.parentNode.removeChild(f)); 204 | iframe = null; 205 | } 206 | }); 207 | -------------------------------------------------------------------------------- /src.safariextension/data/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src.safariextension/data/loader.gif -------------------------------------------------------------------------------- /src/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | chrome.runtime.onMessage.addListener((request, sender, response) => { 4 | if (request.cmd === 'history') { 5 | chrome.history.addUrl({ 6 | url: request.url 7 | }); 8 | } 9 | else if (request.cmd === 'find-id') { 10 | const req = new XMLHttpRequest(); 11 | req.open('GET', request.url); 12 | req.responseType = 'document'; 13 | req.onload = () => { 14 | try { 15 | response(req.response.querySelector('[itemprop="videoId"]').content); 16 | } 17 | catch (e) { 18 | response(); 19 | } 20 | }; 21 | req.onerror = () => response(); 22 | req.send(); 23 | return true; 24 | } 25 | }); 26 | 27 | // FAQs & Feedback 28 | chrome.storage.local.get('version', prefs => { 29 | const version = chrome.runtime.getManifest().version; 30 | const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; 31 | if (isFirefox ? !prefs.version : prefs.version !== version) { 32 | chrome.storage.local.set({version}, () => { 33 | chrome.tabs.create({ 34 | url: 'http://add0n.com/youtube-hover.html?version=' + version + 35 | '&type=' + (prefs.version ? ('upgrade&p=' + prefs.version) : 'install') 36 | }); 37 | }); 38 | } 39 | }); 40 | (function () { 41 | const {name, version} = chrome.runtime.getManifest(); 42 | chrome.runtime.setUninstallURL('http://add0n.com/feedback.html?name=' + name + '&version=' + version); 43 | })(); 44 | -------------------------------------------------------------------------------- /src/data/error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/128.png -------------------------------------------------------------------------------- /src/data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/16.png -------------------------------------------------------------------------------- /src/data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/256.png -------------------------------------------------------------------------------- /src/data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/32.png -------------------------------------------------------------------------------- /src/data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/48.png -------------------------------------------------------------------------------- /src/data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/icons/64.png -------------------------------------------------------------------------------- /src/data/inject/index.css: -------------------------------------------------------------------------------- 1 | .ihvyoutube { 2 | border: solid 1px #dadada; 3 | box-shadow: 1px 1px 5px #dadada; 4 | z-index: 2147483647; 5 | background: #000 center center no-repeat; 6 | resize: both; 7 | overflow: hidden; 8 | } 9 | ::-webkit-resizer { 10 | border: 2px solid yellow; 11 | background: blue; 12 | box-shadow: 0 0 2px 5px red; 13 | outline: 2px dashed green; 14 | 15 | /*size does not work*/ 16 | display:block; 17 | width: 150px !important; 18 | height: 150px !important; 19 | } 20 | 21 | .ihvyoutube:not([data-loaded=true]) { 22 | background-image: url('chrome-extension://__MSG_@@extension_id__/data/loader.gif'); 23 | } 24 | .ihvyoutube[data-dark=true] { 25 | box-shadow: 0 0 0 90000px rgba(0, 0, 0, 0.8); 26 | border: none; 27 | } 28 | .ihvyoutube[data-dark=true]:not([data-loaded=true]) { 29 | } 30 | .ihvyoutube[data-error=true] { 31 | background-image: url('chrome-extension://__MSG_@@extension_id__/data/error.svg'); 32 | background-size: 128px; 33 | } 34 | 35 | @-moz-document url-prefix() { 36 | .ihvyoutube:not([data-loaded=true]) { 37 | background-image: url('moz-extension://__MSG_@@extension_id__/data/loader.gif'); 38 | } 39 | .ihvyoutube[data-error=true] { 40 | background-image: url('moz-extension://__MSG_@@extension_id__/data/error.svg'); 41 | background-size: 128px; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/data/inject/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var iframe; 4 | var config = { 5 | 'relative-x': 0, 6 | 'relative-y': 0, 7 | 'center-x': 0, 8 | 'center-y': 0, 9 | 'delay': 1000, 10 | 'width': 500, 11 | 'mode': 0, 12 | 'strike': true, 13 | 'history': true, 14 | 'scroll': true, 15 | 'smooth': true, 16 | 'dark': false, 17 | youtube: false 18 | }; 19 | chrome.storage.onChanged.addListener(prefs => { 20 | Object.keys(prefs).forEach(name => { 21 | config[name] = prefs[name].newValue; 22 | }); 23 | }); 24 | 25 | var smoothScroll = (function() { 26 | let timeLapsed = 0; 27 | let id, sx, sy, dx, dy, callback; 28 | 29 | const easingPattern = time => (time < 0.5) ? 30 | (8 * time * time * time * time) : 31 | (1 - 8 * (--time) * time * time * time); 32 | 33 | function step() { 34 | timeLapsed += 16; 35 | const percentage = timeLapsed / 400; 36 | if (percentage > 1) { 37 | window.scrollTo(sx + dx, sy + dy); 38 | return callback(); 39 | } 40 | window.scrollTo( 41 | Math.floor(sx + (dx * easingPattern(percentage))), 42 | Math.floor(sy + (dy * easingPattern(percentage))) 43 | ); 44 | id = window.setTimeout(step, 16); 45 | } 46 | 47 | return function(x, y, c) { 48 | window.clearTimeout(id); 49 | callback = c; 50 | timeLapsed = 0; 51 | sx = document.body.scrollLeft + document.documentElement.scrollLeft; 52 | sy = document.body.scrollTop + document.documentElement.scrollTop; 53 | dx = Math.max(0, x - sx); 54 | dy = Math.max(0, y - sy); 55 | if (dx === 0 && dy === 0) { 56 | return c(); 57 | } 58 | step(); 59 | }; 60 | })(); 61 | 62 | var youtube = { 63 | play: (id, rect, shared) => { 64 | // https://github.com/schomery/youtube-hover/issues/15 65 | let time = (id.split(/[?&]t=/)[1] || '0').split('&')[0]; 66 | const tmp = /(?:(\d+)h)?(?:(\d+)m)?(\d+)s/.exec(time); 67 | if (tmp && tmp.length && tmp[3]) { 68 | time = Number(tmp[3]) + Number(tmp[2] || 0) * 60 + Number(tmp[1] || 0) * 60 * 60; 69 | } 70 | // cleaning id; https://github.com/schomery/youtube-hover/issues/12 71 | id = id.split('&')[0].split('?')[0]; 72 | // 73 | iframe = Object.assign(document.createElement('iframe'), { 74 | width: config.width, 75 | height: config.width * 180 / 320, 76 | sandbox: 'allow-scripts allow-same-origin allow-presentation allow-popups', 77 | // unload the gif loader when player is loaded 78 | onload: () => { 79 | window.setTimeout(() => { 80 | if (iframe) { 81 | iframe.dataset.loaded = true; 82 | } 83 | }, 10000); 84 | } 85 | }); 86 | iframe.setAttribute('allowFullScreen', ''); 87 | 88 | function play() { 89 | if (shared) { 90 | chrome.runtime.sendMessage({ 91 | cmd: 'find-id', 92 | url: 'https://www.youtube.com/shared?ci=' + id 93 | }, id => { 94 | if (id) { 95 | iframe.setAttribute('src', `https://www.youtube.com/embed/${id}?fs=1&autoplay=1&enablejsapi=1&start=${time}`); 96 | } 97 | else { 98 | iframe.dataset.error = true; 99 | } 100 | }); 101 | } 102 | else { 103 | iframe.setAttribute('src', `https://www.youtube.com/embed/${id}?fs=1&autoplay=1&enablejsapi=1&start=${time}`); 104 | } 105 | } 106 | 107 | if (config.mode === 1) { // center of screen 108 | iframe.setAttribute('style', ` 109 | position: fixed; 110 | left: calc(50% - ${config.width / 2 - config['center-x']}px); 111 | top: calc(50% - ${config.width * 180 / 320 / 2 - config['center-y']}px); 112 | `); 113 | play(); 114 | } 115 | else { 116 | const x1 = Math.max(0, rect.left + document.body.scrollLeft + 117 | document.documentElement.scrollLeft + config['relative-x']); 118 | const y1 = Math.max(0, rect.top + rect.height + document.body.scrollTop + 119 | document.documentElement.scrollTop + config['relative-y']); 120 | const x2 = x1 + config.width; 121 | const y2 = y1 + config.width * 180 / 320; 122 | const vw = Math.max( 123 | document.documentElement.scrollWidth, 124 | document.body.scrollWidth 125 | ); 126 | const vh = Math.max( 127 | document.documentElement.scrollHeight, 128 | document.body.scrollHeight 129 | ); 130 | 131 | let left = x1; 132 | let top = y1; 133 | if (x2 > vw - 10) { 134 | left = vw - config.width - 10; 135 | } 136 | if (y2 > vh - 10) { 137 | top = vh - config.width * 180 / 320 - 10; 138 | } 139 | if (config.scroll) { 140 | const x = Math.max( 141 | document.body.scrollLeft, 142 | left + config.width - document.documentElement.clientWidth + 10 143 | ); 144 | const y = Math.max( 145 | document.body.scrollTop, 146 | top + config.width * 180 / 320 - document.documentElement.clientHeight + 10 147 | ); 148 | if (config.smooth) { 149 | smoothScroll(x, y, play); 150 | } 151 | else { 152 | window.scrollTo(x, y); 153 | play(); 154 | } 155 | } 156 | else { 157 | play(); 158 | } 159 | 160 | iframe.setAttribute('style', ` 161 | position: absolute; 162 | left: ${left}px; 163 | top: ${top}px; 164 | `); 165 | } 166 | iframe.setAttribute('class', 'ihvyoutube'); 167 | iframe.dataset.dark = config.dark; 168 | document.body.appendChild(iframe); 169 | } 170 | }; 171 | 172 | var timer; 173 | 174 | function mouseover(e) { 175 | if (timer) { 176 | timer = window.clearTimeout(timer); 177 | } 178 | const target = e.target; 179 | if (target && target.nodeType === 1) { 180 | const link = target.closest('a'); 181 | if (link) { 182 | const href = link.href; 183 | if (!href || iframe) { 184 | return; 185 | } 186 | let shared = false; 187 | if ( 188 | href.indexOf('youtube.com/shared') !== -1 || 189 | href.indexOf('youtube.com/attribution_link') !== -1 || 190 | href.indexOf('youtube.com/watch') !== -1 || 191 | href.indexOf('//youtu.be/') !== -1 192 | ) { 193 | let id; 194 | if (href.indexOf('youtube.com/watch') !== -1) { 195 | id = href.match(/v=(.+)/); 196 | } 197 | else if (href.indexOf('//youtu.be/') !== -1) { 198 | id = href.match(/\.be\/(.+)/); 199 | } 200 | else if (href.indexOf('youtube.com/attribution_link') !== -1) { 201 | id = decodeURIComponent(href).match(/v=(.+)/); 202 | } 203 | else if (href.indexOf('youtube.com/shared') !== -1) { 204 | shared = true; 205 | id = href.match(/ci=(.+)/); 206 | } 207 | 208 | if (id && id.length) { 209 | timer = window.setTimeout(link => { 210 | const rect = link.getBoundingClientRect(); 211 | youtube.play(id[1], rect, shared); 212 | if (config.strike) { 213 | [...document.querySelectorAll(`a[href="${href}"]`), link] 214 | .forEach(l => l.style['text-decoration'] = 'line-through'); 215 | } 216 | if (config.history) { 217 | chrome.runtime.sendMessage({ 218 | url: href, 219 | cmd: 'history' 220 | }); 221 | } 222 | }, config.delay, link); 223 | } 224 | } 225 | } 226 | } 227 | } 228 | function click(e) { 229 | window.clearTimeout(timer); 230 | if (iframe && e.target.closest('.ihvyoutube') === null) { 231 | [...document.querySelectorAll('.ihvyoutube')].forEach(f => f.parentNode.removeChild(f)); 232 | iframe = null; 233 | e.preventDefault(); 234 | } 235 | } 236 | function keydown(e) { 237 | if (iframe && e.code === 'Escape') { 238 | document.body.dispatchEvent(new Event('click', {bubbles: true})); 239 | e.preventDefault(); 240 | } 241 | /* 242 | else if (iframe && e.code === 'Space') { 243 | iframe.contentWindow.postMessage('{"event":"command","func":"stopVideo","args":""}', '*'); 244 | } 245 | */ 246 | } 247 | 248 | chrome.storage.local.get(config, prefs => { 249 | config = prefs; 250 | if (document.location.hostname === 'www.youtube.com' && !config.youtube) { 251 | return; 252 | } 253 | if (document.location.hostname === 'www.youtube.com' && window.top !== window) { 254 | return; 255 | } 256 | document.addEventListener('mouseover', mouseover); 257 | document.addEventListener('click', click); 258 | document.addEventListener('keydown', keydown); 259 | }); 260 | -------------------------------------------------------------------------------- /src/data/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/schomery/youtube-hover/88212b8ce263f5c7f91e71f06a16784cbee457b5/src/data/loader.gif -------------------------------------------------------------------------------- /src/data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | YouTube on Hover Options 5 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
YouTube player width (in pixels)
Draw a strike-through over YouTube links after preview
Add previewed links to the browser history
Turn off the light while playing
Player positioning mode 45 | 49 |
Scroll to player in the "relative" mode
Use smooth scrolling
Player offset in the "relative" mode 61 |  x  62 |
Player offset in the "center" mode 67 |  x  68 |
Display the player if mouse is over a link for (in milliseconds)
Display the preview panel on youtube.com as well
79 | 80 |

81 | 82 |

83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function restore() { 4 | // Use default value color = 'red' and likesColor = true. 5 | chrome.storage.local.get({ 6 | 'relative-x': 0, 7 | 'relative-y': 0, 8 | 'center-x': 0, 9 | 'center-y': 0, 10 | 'delay': 1000, 11 | 'width': 500, 12 | 'mode': 0, 13 | 'strike': true, 14 | 'history': true, 15 | 'scroll': true, 16 | 'smooth': true, 17 | 'dark': false, 18 | 'youtube': false 19 | }, prefs => { 20 | document.getElementById('relative-x').value = prefs['relative-x']; 21 | document.getElementById('relative-y').value = prefs['relative-y']; 22 | document.getElementById('center-x').value = prefs['center-x']; 23 | document.getElementById('center-y').value = prefs['center-y']; 24 | document.getElementById('delay').value = prefs.delay; 25 | document.getElementById('width').value = prefs.width; 26 | document.getElementById('mode').selectedIndex = prefs.mode; 27 | document.getElementById('strike').checked = prefs.strike; 28 | document.getElementById('history').checked = prefs.history; 29 | document.getElementById('scroll').checked = prefs.scroll; 30 | document.getElementById('smooth').checked = prefs.smooth; 31 | document.getElementById('dark').checked = prefs.dark; 32 | document.getElementById('youtube').checked = prefs.youtube; 33 | }); 34 | } 35 | 36 | function save() { 37 | const rx = document.getElementById('relative-x').value; 38 | const ry = document.getElementById('relative-y').value; 39 | const cx = document.getElementById('center-x').value; 40 | const cy = document.getElementById('center-y').value; 41 | const delay = document.getElementById('delay').value; 42 | const width = document.getElementById('width').value; 43 | const mode = document.getElementById('mode').selectedIndex; 44 | const strike = document.getElementById('strike').checked; 45 | const history = document.getElementById('history').checked; 46 | const scroll = document.getElementById('scroll').checked; 47 | const smooth = document.getElementById('smooth').checked; 48 | const dark = document.getElementById('dark').checked; 49 | const youtube = document.getElementById('youtube').checked; 50 | 51 | chrome.storage.local.set({ 52 | 'relative-x': Number(rx), 53 | 'relative-y': Number(ry), 54 | 'center-x': Number(cx), 55 | 'center-y': Number(cy), 56 | 'width': Number(width), 57 | 'delay': Number(delay), 58 | 'mode': mode, 59 | 'strike': strike, 60 | 'history': history, 61 | 'scroll': scroll, 62 | 'smooth': smooth, 63 | 'dark': dark, 64 | 'youtube': youtube 65 | }, () => { 66 | const status = document.getElementById('status'); 67 | status.textContent = 'Options saved.'; 68 | restore(); 69 | setTimeout(() => status.textContent = '', 750); 70 | }); 71 | } 72 | 73 | document.addEventListener('DOMContentLoaded', restore); 74 | document.getElementById('save').addEventListener('click', save); 75 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "YouTube™ on Hover Preview", 3 | "short_name": "ihvyoutube", 4 | "description": "View YouTube while hovering mouse over a YouTube link on any website", 5 | "author": "Jeremy Schomery", 6 | "version": "0.1.9", 7 | "manifest_version": 2, 8 | "permissions": [ 9 | "storage", 10 | "tabs", 11 | "history", 12 | "*://www.youtube.com/*" 13 | ], 14 | "background": { 15 | "persistent": false, 16 | "scripts": [ 17 | "common.js" 18 | ] 19 | }, 20 | "content_scripts": [{ 21 | "matches": [""], 22 | "js": ["data/inject/index.js"], 23 | "css": ["data/inject/index.css"], 24 | "run_at": "document_start", 25 | "all_frames": true 26 | }], 27 | "homepage_url": "http://add0n.com/youtube-hover.html", 28 | "icons": { 29 | "16": "data/icons/16.png", 30 | "32": "data/icons/32.png", 31 | "48": "data/icons/48.png", 32 | "128": "data/icons/128.png" 33 | }, 34 | "web_accessible_resources": [ 35 | "data/*.gif", 36 | "data/*.svg" 37 | ], 38 | "options_ui": { 39 | "page": "data/options/index.html", 40 | "chrome_style": true 41 | }, 42 | "applications": { 43 | "gecko": { 44 | "id": "{732db00c-07ee-4763-a8c7-fd666803287e}" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Untitled Document 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 |
YouTube Link 1YouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
YouTube LinkYouTube LinkYouTube Link
373 | 374 | 375 | --------------------------------------------------------------------------------