// ==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);
})();