- // ==UserScript==
- // @name Youtube Peek Preview
- // @namespace http://tampermonkey.net/
- // @version 0.2.3
- // @description See video thumbnails, ratings and other details when you mouse over a Youtube link from almost any website
- // @author scriptpost
- // @match *://*/*
- // @exclude https://twitter.com/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_registerMenuCommand
- // ==/UserScript==
- (function () {
- // Remove deprecated storage structure from earlier versions.
- // Configure settings through your browser extension icon, under "Youtube Peek Settings"
- const settings = JSON.parse(GM_getValue('userSettings', '{}'));
- if (settings.hasOwnProperty('REGIONS')) {
- GM_deleteValue('userSettings');
- }
- })();
- /*!
- * Clamp.js 0.5.1
- *
- * Copyright 2011-2013, Joseph Schmitt http://joe.sh
- * Released under the WTFPL license
- * http://sam.zoy.org/wtfpl/
- */
- (function () {
- /**
- * Clamps a text node.
- * @param {HTMLElement} element. Element containing the text node to clamp.
- * @param {Object} options. Options to pass to the clamper.
- */
- function clamp(element, options) {
- options = options || {};
- var self = this, win = window, opt = {
- clamp: options.clamp || 2,
- useNativeClamp: typeof (options.useNativeClamp) != 'undefined' ? options.useNativeClamp : true,
- splitOnChars: options.splitOnChars || ['.', '-', '–', '—', ' '],
- animate: options.animate || false,
- truncationChar: options.truncationChar || '…',
- truncationHTML: options.truncationHTML
- }, sty = element.style, originalText = element.innerHTML, supportsNativeClamp = typeof (element.style.webkitLineClamp) != 'undefined', clampValue = opt.clamp, isCSSValue = clampValue.indexOf && (clampValue.indexOf('px') > -1 || clampValue.indexOf('em') > -1), truncationHTMLContainer;
- if (opt.truncationHTML) {
- truncationHTMLContainer = document.createElement('span');
- truncationHTMLContainer.innerHTML = opt.truncationHTML;
- }
- // UTILITY FUNCTIONS
- /**
- * Return the current style for an element.
- * @param {HTMLElement} elem The element to compute.
- * @param {string} prop The style property.
- * @returns {number}
- */
- function computeStyle(elem, prop) {
- if (!win.getComputedStyle) {
- win.getComputedStyle = function (el, pseudo) {
- this.el = el;
- this.getPropertyValue = function (prop) {
- var re = /(\-([a-z]){1})/g;
- if (prop == 'float')
- prop = 'styleFloat';
- if (re.test(prop)) {
- prop = prop.replace(re, function () {
- return arguments[2].toUpperCase();
- });
- }
- return el.currentStyle && el.currentStyle[prop] ? el.currentStyle[prop] : null;
- };
- return this;
- };
- }
- return win.getComputedStyle(elem, null).getPropertyValue(prop);
- }
- /**
- * Returns the maximum number of lines of text that should be rendered based
- * on the current height of the element and the line-height of the text.
- */
- function getMaxLines(height) {
- var availHeight = height || element.clientHeight, lineHeight = getLineHeight(element);
- return Math.max(Math.floor(availHeight / lineHeight), 0);
- }
- /**
- * Returns the maximum height a given element should have based on the line-
- * height of the text and the given clamp value.
- */
- function getMaxHeight(clmp) {
- var lineHeight = getLineHeight(element);
- return lineHeight * clmp;
- }
- /**
- * Returns the line-height of an element as an integer.
- */
- function getLineHeight(elem) {
- var lh = computeStyle(elem, 'line-height');
- if (lh == 'normal') {
- // Normal line heights vary from browser to browser. The spec recommends
- // a value between 1.0 and 1.2 of the font size. Using 1.1 to split the diff.
- lh = parseInt(computeStyle(elem, 'font-size')) * 1.2;
- }
- return parseInt(lh);
- }
- // MEAT AND POTATOES (MMMM, POTATOES...)
- var splitOnChars = opt.splitOnChars.slice(0), splitChar = splitOnChars[0], chunks, lastChunk;
- /**
- * Gets an element's last child. That may be another node or a node's contents.
- */
- function getLastChild(elem) {
- //Current element has children, need to go deeper and get last child as a text node
- if (elem.lastChild.children && elem.lastChild.children.length > 0) {
- return getLastChild(Array.prototype.slice.call(elem.children).pop());
- }
- //This is the absolute last child, a text node, but something's wrong with it. Remove it and keep trying
- else if (!elem.lastChild || !elem.lastChild.nodeValue || elem.lastChild.nodeValue == '' || elem.lastChild.nodeValue == opt.truncationChar) {
- elem.lastChild.parentNode.removeChild(elem.lastChild);
- return getLastChild(element);
- }
- //This is the last child we want, return it
- else {
- return elem.lastChild;
- }
- }
- /**
- * Removes one character at a time from the text until its width or
- * height is beneath the passed-in max param.
- */
- function truncate(target, maxHeight) {
- if (!maxHeight) {
- return;
- }
- /**
- * Resets global variables.
- */
- function reset() {
- splitOnChars = opt.splitOnChars.slice(0);
- splitChar = splitOnChars[0];
- chunks = null;
- lastChunk = null;
- }
- var nodeValue = target.nodeValue.replace(opt.truncationChar, '');
- //Grab the next chunks
- if (!chunks) {
- //If there are more characters to try, grab the next one
- if (splitOnChars.length > 0) {
- splitChar = splitOnChars.shift();
- }
- //No characters to chunk by. Go character-by-character
- else {
- splitChar = '';
- }
- chunks = nodeValue.split(splitChar);
- }
- //If there are chunks left to remove, remove the last one and see if
- // the nodeValue fits.
- if (chunks.length > 1) {
- // console.log('chunks', chunks);
- lastChunk = chunks.pop();
- // console.log('lastChunk', lastChunk);
- applyEllipsis(target, chunks.join(splitChar));
- }
- //No more chunks can be removed using this character
- else {
- chunks = null;
- }
- //Insert the custom HTML before the truncation character
- if (truncationHTMLContainer) {
- target.nodeValue = target.nodeValue.replace(opt.truncationChar, '');
- element.innerHTML = target.nodeValue + ' ' + truncationHTMLContainer.innerHTML + opt.truncationChar;
- }
- //Search produced valid chunks
- if (chunks) {
- //It fits
- if (element.clientHeight <= maxHeight) {
- //There's still more characters to try splitting on, not quite done yet
- if (splitOnChars.length >= 0 && splitChar != '') {
- applyEllipsis(target, chunks.join(splitChar) + splitChar + lastChunk);
- chunks = null;
- }
- //Finished!
- else {
- return element.innerHTML;
- }
- }
- }
- //No valid chunks produced
- else {
- //No valid chunks even when splitting by letter, time to move
- //on to the next node
- if (splitChar == '') {
- applyEllipsis(target, '');
- target = getLastChild(element);
- reset();
- }
- }
- //If you get here it means still too big, let's keep truncating
- if (opt.animate) {
- setTimeout(function () {
- truncate(target, maxHeight);
- }, opt.animate === true ? 10 : opt.animate);
- }
- else {
- return truncate(target, maxHeight);
- }
- }
- function applyEllipsis(elem, str) {
- elem.nodeValue = str + opt.truncationChar;
- }
- // CONSTRUCTOR
- if (clampValue == 'auto') {
- clampValue = getMaxLines();
- }
- else if (isCSSValue) {
- clampValue = getMaxLines(parseInt(clampValue));
- }
- var clampedText;
- if (supportsNativeClamp && opt.useNativeClamp) {
- sty.overflow = 'hidden';
- sty.textOverflow = 'ellipsis';
- sty.webkitBoxOrient = 'vertical';
- sty.display = '-webkit-box';
- sty.webkitLineClamp = clampValue;
- if (isCSSValue) {
- sty.height = opt.clamp + 'px';
- }
- }
- else {
- var height = getMaxHeight(clampValue);
- if (height <= element.clientHeight) {
- clampedText = truncate(getLastChild(element), height);
- }
- }
- return {
- 'original': originalText,
- 'clamped': clampedText
- };
- }
- window.$clamp = clamp;
- })();
- (function () {
- // Begin script: Youtube Peek
- 'use strict';
- const DEFAULT_OPTIONS = {
- regions: [],
- noTooltip: true,
- allowOnYoutube: false
- };
- const OPTIONS = JSON.parse(GM_getValue('userSettings', JSON.stringify(DEFAULT_OPTIONS)));
- const apiKey = 'AIzaSyBnibVlVDGC7t_wd3ZErVK6XF3hp3G7xtA';
- const re = {
- isVideoLink: /(?:youtube\.com\/(?:watch\?.*v=|attribution_link)|youtu\.be\/|y2u\.be\/)/i,
- getVideoId: /(?:youtube\.com\/watch\?.*v=|youtu\.be\/|y2u\.be\/)([-_A-Za-z0-9]{11})/i,
- getTimeLength: /\d+[A-Z]/g,
- };
- const cache = {};
- const delay_open = 100;
- const delay_close = 0;
- let tmo_open;
- let tmo_close;
- const _stylesheet = String.raw `<style type="text/css" id="yt-peek">.yt-peek,.yt-peek-loading{position:absolute;z-index:123456789}.yt-peek,.yt-peek-cfg{box-shadow:var(--shadow-big);--shadow-big:0 4px 8px hsla(0,0%,0%,.2),0 8px 16px hsla(0,0%,0%,.2),0 4px 4px hsla(0,0%,100%,.1)}.yt-peek-loading{width:16px;height:16px;border-radius:50%;background:#fff;border-width:6px 0;border-style:solid;border-color:#8aa4b1;box-sizing:border-box;animation-duration:1s;animation-name:spin;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(.67,.88,.53,.37)}.yt-peek .yt-peek-loading{top:0;bottom:0;left:0;right:0;margin:auto;background:0 0;border-color:hsla(200,20%,62%,.5);width:32px;height:32px}.yt-peek .yt-peek-chan,.yt-peek-blocked{border-top:1px solid hsla(0,0%,100%,.1);box-sizing:border-box}@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}.yt-peek{box-sizing:border-box;background:#232628;margin:0;padding:0;color:#999!important;font:400 12px/1.2 "segoe ui",arial,sans-serif!important;border-radius:3px!important;overflow:hidden}.yt-peek-cols{display:flex;flex-direction:row;position:relative}.yt-peek-cols>div{display:flex;flex:1 1 auto}.yt-peek-info{box-sizing:border-box;max-width:230px;display:flex;flex:1 0 auto;flex-direction:column}.yt-peek-row{display:flex;justify-content:space-between}.yt-peek-info>div{padding:6px 12px}.yt-peek .yt-peek-title{font-size:14px;color:#fff}.yt-peek .yt-peek-desc{padding-top:0;font-size:14px}.yt-peek .yt-peek-date{display:inline-block;order:-1}.yt-peek .yt-peek-views{display:inline-block}.yt-peek .yt-peek-chan{color:#fff;position:absolute;bottom:0;width:100%}.yt-peek-preview{position:relative;flex-direction:column;order:-1;justify-content:space-between}.yt-peek-thumb{position:relative;min-height:169px;width:300px}.yt-peek-thumb img{object-fit:none;display:block;width:100%}.yt-peek-length{font:700 12px/1 arial,sans-serif;position:absolute;bottom:8px;left:4px;padding:2px 5px;color:#fff;background:hsla(0,0%,0%,.9);border-radius:3px}.yt-peek-score{margin:1px 0;width:100%;height:3px;background:#ccc}.yt-peek-score div{height:inherit;background:#0098e5}.yt-peek-blocked{padding:5px 12px;color:#b2b2b2;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;max-width:530px}.yt-peek-blocked em{font-weight:700;font-style:normal;color:#fff;padding:0 2px;background:#dc143c;border-radius:2px}.yt-peek-cfg{font:400 12px/1.35 sans-serif;position:fixed;top:0;right:15px;left:0;margin:auto;padding:0 15px;width:300px;box-sizing:border-box;color:#000;background:#fff;border-radius:0 0 3px 3px;border-width:0 1px 1px;border-style:solid;border-color:#999;max-height:100vh;overflow:auto;z-index:12345679}.yt-peek-cfg-footer,.yt-peek-cfg-item{padding:10px 0}.yt-peek-cfg-heading{padding:10px 0;font:400 14px/1 sans-serif}.yt-peek-cfg-label{font-weight:700}.yt-peek-cfg-item label{display:block}.yt-peek-cfg-desc{color:#8c8c8c;margin:.25em 0 0}.yt-peek-cfg-item textarea{box-sizing:border-box;min-width:100px;width:100%;min-height:2em}.yt-peek-cfg button{display:inline-block;font:400 12px/1 sans-serif;border:none;border-radius:3px;margin:0 .5em 0 0;padding:10px 18px;transition:background .2s;cursor:default}.yt-peek-cfg-save{color:#fff;background:#d82626}.yt-peek-cfg-cancel{color:#000;background:0 0}.yt-peek-cfg-save:hover{background:#b71414}.yt-peek-cfg-cancel:hover{background:#e5e5e5}.yt-peek-missing .yt-peek-chan,.yt-peek-missing .yt-peek-row,.yt-peek-missing .yt-peek-thumb{display:none}.yt-peek,.yt-peek-loading,.yt-peek-thumb img{opacity:0;transition:opacity .25s}.yt-peek-ready{opacity:1!important}</style>`;
- document.body.insertAdjacentHTML('beforeend', _stylesheet);
- function containsEncodedComponents(x) {
- return (decodeURI(x) !== decodeURIComponent(x));
- }
- /**
- * Check if we're on a particular domain name.
- * @param host Name of the website.
- */
- function site(host) {
- return window.location.host.includes(host);
- }
- function handleMouseOver(ev) {
- let target = ev.target;
- target = target.closest('a');
- if (!target)
- return;
- let href = target.href;
- if (!href)
- return;
- // Some sites put the URL in a dataset. (note: twitter blocks goog API)
- if (site('twitter.com')) {
- const dataUrl = target.dataset.expandedUrl;
- if (dataUrl)
- href = dataUrl;
- }
- // Check if the URL goes to a youtube video.
- if (!re.isVideoLink.test(href))
- return;
- // Need to know if it's an attribution link so we can read the encoded params.
- if (/attribution_link\?/i.test(href)) {
- const URIComponent = href.substr(href.indexOf('%2Fwatch%3Fv%3D'));
- if (containsEncodedComponents(URIComponent)) {
- href = 'https://www.youtube.com' + decodeURIComponent(URIComponent);
- }
- }
- // Finally get the video ID;
- const id = re.getVideoId.exec(href)[1];
- if (!id)
- return console.error('Invalid video ID');
- window.clearTimeout(tmo_open);
- window.clearTimeout(tmo_close);
- const noTooltip = JSON.parse(GM_getValue('userSettings', JSON.stringify(DEFAULT_OPTIONS))).noTooltip;
- if (noTooltip) {
- target.removeAttribute('title');
- }
- tmo_open = window.setTimeout(() => {
- if (!cache.hasOwnProperty(id)) {
- const parts = 'snippet,contentDetails,statistics';
- requestVideoData(ev, id, parts);
- }
- else {
- handleSuccess(ev, id, cache[id]);
- }
- }, delay_open);
- function handleMouseLeave(ev) {
- target.removeEventListener('mouseleave', handleMouseLeave);
- window.clearTimeout(tmo_open);
- tmo_open = null;
- tmo_close = window.setTimeout(() => {
- removePeekBoxes();
- }, delay_close);
- }
- target.addEventListener('mouseleave', handleMouseLeave);
- }
- function loadImage(path) {
- return new Promise(resolve => {
- const img = new Image();
- img.onload = ev => resolve(img);
- img.onerror = ev => resolve(undefined);
- img.src = path || '';
- });
- }
- function getScorePercent(lovers, haters) {
- if (isNaN(lovers) || isNaN(haters))
- return undefined;
- return Math.round(100 * lovers / (lovers + haters));
- }
- function toDigitalTime(str) {
- if (!str)
- return undefined;
- function pad(s) {
- return s.length < 2 ? `0${s}` : s;
- }
- const hours = /(\d+)H/.exec(str);
- const mins = /(\d+)M/.exec(str);
- const secs = /(\d+)S/.exec(str);
- const output = [];
- if (hours)
- output.push(pad(hours[1]));
- output.push(mins ? pad(mins[1]) : '00');
- output.push(secs ? pad(secs[1]) : '00');
- return output.join(':');
- }
- function insertPeekBox(ev, d) {
- const a = ev.target;
- const settings = JSON.parse(GM_getValue('userSettings', JSON.stringify(DEFAULT_OPTIONS)));
- // Tokens:
- const title = d.snippet.localized.title;
- const desc = d.snippet.localized.description;
- const date = dateAsAge(d.snippet.publishedAt);
- const chan = d.snippet.channelTitle;
- const thumbs = d.snippet.thumbnails;
- const imagePath = thumbs.hasOwnProperty('medium') ? thumbs.medium.url : undefined;
- let blockMatched = [];
- let blockOther = [];
- if (settings.regions.length && d.contentDetails.hasOwnProperty('regionRestriction')) {
- const blocked = d.contentDetails.regionRestriction.blocked;
- if (blocked) {
- blockMatched = blocked.filter(v => settings.regions.includes(v)).map(v => `<em>${v}</em>`);
- blockOther = blocked.filter(v => !settings.regions.includes(v));
- }
- }
- const viewCount = +d.statistics.viewCount;
- const views = viewCount ? viewCount.toLocaleString() : undefined;
- const score = getScorePercent(+d.statistics.likeCount, +d.statistics.dislikeCount);
- const length = toDigitalTime(d.contentDetails.duration);
- loadImage(imagePath).then(img => {
- finishedLoading();
- if (!img)
- return;
- img.setAttribute('alt', title);
- container.querySelector('.yt-peek-thumb').appendChild(img);
- window.setTimeout(() => {
- img.classList.add('yt-peek-ready');
- }, 70);
- });
- // Create HTML:
- const container = document.createElement('div');
- container.innerHTML = `
- <div class="yt-peek-cols">
- <div class="yt-peek-info">
- <div class="yt-peek-row">
- <div class="yt-peek-views">${views ? views + ' views' : ''}</div>
- <div class="yt-peek-date">${date ? date : ''}</div>
- </div>
- <div class="yt-peek-title">${title ? title : `Not found`}</div>
- <div class="yt-peek-desc">${desc ? desc : ''}</div>
- <div class="yt-peek-chan">${chan ? chan : ''}</div>
- </div>
- <div class="yt-peek-preview">
- <div class="yt-peek-thumb"></div>
- <div class="yt-peek-loading yt-peek-ready"></div>
- ${length ? `<div class="yt-peek-length">${length}</div>` : ``}
- ${score ? `<div class="yt-peek-score"><div style="width: ${score}%;"></div></div>` : ``}
- </div>
- </div>
- ${blockMatched.length ? `<div class="yt-peek-blocked"><span>Blocked in:</span> ${blockMatched.join(' ')} ${blockOther.join(' ')}</div>` : ``}
- `;
- container.classList.add('yt-peek');
- if (!title) {
- container.classList.add('yt-peek-missing');
- }
- document.body.insertAdjacentElement('beforeend', container);
- // Clamp long lines of text:
- const $title = container.querySelector('.yt-peek-title');
- const $description = container.querySelector('.yt-peek-desc');
- $clamp($title, { clamp: 4, useNativeClamp: false });
- $clamp($description, { clamp: 4, useNativeClamp: false });
- // Find optimal position within viewport:
- setPosition(a, container);
- // Allow for smooth CSS transition:
- window.setTimeout(() => {
- container.classList.add('yt-peek-ready');
- }, 0);
- // Event listener to remove container because it shouldn't be interacted with:
- container.addEventListener('mouseenter', ev => {
- removePeekBoxes();
- });
- }
- function removePeekBoxes() {
- const elements = document.getElementsByClassName('yt-peek');
- for (const element of elements) {
- element.classList.remove('yt-peek-ready');
- // Allow for smooth CSS transition:
- window.setTimeout(() => {
- element.remove();
- }, 250);
- }
- }
- // Utility to check if a peek box is currently open in the document.
- function activePeekBox() {
- const elements = document.getElementsByClassName('yt-peek');
- if (elements.length)
- return elements[0];
- }
- function startedLoading(ev) {
- const indicator = document.createElement('div');
- indicator.classList.add('yt-peek-loading', 'yt-peek-ready');
- document.body.insertAdjacentElement('beforeend', indicator);
- setPosition(ev.target, indicator);
- }
- function finishedLoading() {
- const elements = document.getElementsByClassName('yt-peek-loading');
- for (const element of elements) {
- element.classList.remove('yt-peek-ready');
- window.setTimeout(() => {
- element.remove();
- }, 250);
- }
- }
- function handleSuccess(ev, id, d) {
- removePeekBoxes();
- if (!d) {
- d = {};
- d.id = id;
- d.contentDetails = {
- duration: undefined
- };
- d.snippet = {
- channelTitle: '',
- thumbnails: { medium: { url: undefined } },
- localized: {
- title: undefined,
- description: `The video might be removed.`
- },
- publishedAt: undefined
- };
- d.statistics = {};
- }
- insertPeekBox(ev, d);
- if (!cache.hasOwnProperty(id))
- cache[id] = d;
- }
- function requestVideoData(ev, id, parts) {
- startedLoading(ev);
- const xhr = new XMLHttpRequest();
- xhr.open('GET', `https://www.googleapis.com/youtube/v3/videos?id=${id}&part=${parts}&key=${apiKey}`);
- xhr.onreadystatechange = function () {
- if (xhr.readyState === 4) {
- finishedLoading();
- if (!tmo_open)
- return;
- if (!xhr.responseText.length)
- return;
- const response = JSON.parse(xhr.responseText);
- if (xhr.status === 200) {
- handleSuccess(ev, id, response.items[0]);
- }
- else {
- // handleError()
- }
- }
- else {
- finishedLoading();
- }
- };
- xhr.send();
- }
- function dateAsAge(inputValue) {
- if (!inputValue)
- return undefined;
- let date = new Date(inputValue);
- const difference = new Date(new Date().valueOf() - date.valueOf());
- let y = parseInt(difference.toISOString().slice(0, 4), 10) - 1970;
- let m = +difference.getMonth();
- let d = difference.getDate() - 1;
- let result;
- if (y > 0)
- result = (y === 1) ? y + ' year ago' : y + ' years ago';
- else if (m > 0)
- result = (m === 1) ? m + ' month ago' : m + ' months ago';
- else if (d > 0) {
- result = (d === 1) ? d + ' day ago' : d + ' days ago';
- }
- else {
- result = 'Today';
- }
- return result;
- }
- /**
- *
- * @param source Element to use for the relative position.
- * @param element The element to position.
- */
- function setPosition(source, element) {
- const srcRect = source.getBoundingClientRect();
- const clearanceHeight = element.clientHeight < 60 ? 60 : element.clientHeight;
- // Viewport dimensions:
- const vw = document.documentElement.clientWidth;
- const vh = document.documentElement.clientHeight;
- // Calculate:
- const leftOfTarget = vw < (srcRect.left + element.clientWidth);
- // Add extra space for browser status tooltip.
- const topOfTarget = vh < (srcRect.top + srcRect.height + clearanceHeight + 24);
- // Apply position:
- if (leftOfTarget) {
- element.style.right = vw - srcRect.right + 'px';
- }
- else {
- element.style.left = srcRect.left + 'px';
- }
- if (topOfTarget && (vh / 2 < srcRect.top)) {
- element.style.bottom = (vh - srcRect.top) - window.scrollY + 'px';
- }
- else {
- element.style.top = srcRect.bottom + window.scrollY + 'px';
- }
- }
- function insertSettingsDialog() {
- if (document.querySelector('.yt-peek-cfg'))
- return closeSettingsDialog();
- const data = JSON.parse(GM_getValue('userSettings', JSON.stringify(DEFAULT_OPTIONS)));
- const container = document.createElement('div');
- container.addEventListener('click', handleSettingsClick);
- container.classList.add('yt-peek-cfg');
- container.innerHTML = `
- <div class="yt-peek-cfg-heading">Youtube Peek</div>
- <div class="yt-peek-cfg-item">
- <label class="yt-peek-cfg-label" for="yt-peek-cfg-regions">Warn me if the video is blocked in:</label>
- <textarea id="yt-peek-cfg-regions">${data.regions.join(' ')}</textarea>
- <div class="yt-peek-cfg-desc">Space-separated list of region codes. E.g. US GB CA. Leave blank to ignore.</div>
- </div>
- <div class="yt-peek-cfg-item">
- <label>
- <input type="checkbox" id="yt-peek-cfg-noTooltip"${data.noTooltip ? ` checked` : ``}>
- Remove tooltips from video links
- </label>
- <div class="yt-peek-cfg-desc">Because tooltips can get in the way of the video preview.</div>
- </div>
- <div class="yt-peek-cfg-item">
- <label>
- <input type="checkbox" id="yt-peek-cfg-youtube"${data.allowOnYoutube ? ` checked` : ``}>
- Enable on youtube.com
- </label>
- <div class="yt-peek-cfg-desc">Peek isn't intended for use on youtube.com, but you can still use it there. (this change takes effect after reloading)</div>
- </div>
- <div class="yt-peek-cfg-footer">
- <button class="yt-peek-cfg-save" id="yt-peek-cfg-save">SAVE</button>
- <button class="yt-peek-cfg-cancel" id="yt-peek-cfg-cancel">CANCEL</button>
- </div>
- `;
- document.body.appendChild(container);
- }
- function handleSaveSettings() {
- const dialog = document.querySelector('.yt-peek-cfg');
- if (!dialog)
- return;
- // Retrieve values:
- const regionsInput = document.getElementById('yt-peek-cfg-regions');
- const noTooltipInput = document.getElementById('yt-peek-cfg-noTooltip');
- const allowOnYoutube = document.getElementById('yt-peek-cfg-youtube');
- // Format values:
- let regions = regionsInput.value.trim().replace(/\s\s+/g, ' ').toUpperCase();
- // Prepare data object for storage:
- const db_entry = {
- regions: regions.split(/\s/),
- noTooltip: noTooltipInput.checked,
- allowOnYoutube: allowOnYoutube.checked
- };
- GM_setValue('userSettings', JSON.stringify(db_entry));
- closeSettingsDialog();
- }
- function handleSettingsClick(ev) {
- if (ev.target.id === 'yt-peek-cfg-cancel') {
- closeSettingsDialog();
- }
- if (ev.target.id === 'yt-peek-cfg-save') {
- handleSaveSettings();
- }
- }
- function closeSettingsDialog() {
- const dialog = document.querySelector('.yt-peek-cfg');
- if (dialog)
- dialog.remove();
- }
- function handleMenuCommand() {
- insertSettingsDialog();
- }
- GM_registerMenuCommand('Youtube Peek Settings', handleMenuCommand);
- if (site('youtube.com') && !OPTIONS.allowOnYoutube)
- return;
- document.addEventListener('mouseover', handleMouseOver);
- })();