- // ==UserScript==
- // @name Play video on hover
- // @namespace https://lukaszmical.pl/
- // @version 0.6.0
- // @description Facebook, Vimeo, Youtube, Streamable, Tiktok, Instagram, Twitter, X, Dailymotion, Coub, Spotify, SoundCloud, Apple Podcasts, Amazon Music, Deezer, Tidal, Ted, Pbs, Odysee, Playeur, Bitchute, Rss - play on hover
- // @author Łukasz Micał
- // @match *://*/*
- // @icon https://static-00.iconduck.com/assets.00/cursor-hover-icon-512x439-vou7bdac.png
- // ==/UserScript==
-
- // libs/share/src/ui/SvgComponent.ts
- const SvgComponent = class {
- constructor(tag, props = {}) {
- this.element = Dom.createSvg({ tag, ...props });
- }
-
- addClassName(...className) {
- this.element.classList.add(...className);
- }
-
- event(event, callback) {
- this.element.addEventListener(event, callback);
- }
-
- getElement() {
- return this.element;
- }
-
- mount(parent) {
- parent.appendChild(this.element);
- }
- };
-
- // libs/share/src/ui/Dom.ts
- var Dom = class _Dom {
- static appendChildren(element, children, isSvgMode = false) {
- if (children) {
- element.append(
- ..._Dom.array(children).map((item) => {
- if (typeof item === 'string') {
- return document.createTextNode(item);
- }
- if (item instanceof HTMLElement || item instanceof SVGElement) {
- return item;
- }
- if (item instanceof Component || item instanceof SvgComponent) {
- return item.getElement();
- }
- const isSvg =
- 'svg' === item.tag
- ? true
- : 'foreignObject' === item.tag
- ? false
- : isSvgMode;
- if (isSvg) {
- return _Dom.createSvg(item);
- }
- return _Dom.create(item);
- })
- );
- }
- }
-
- static applyAttrs(element, attrs) {
- if (attrs) {
- Object.entries(attrs).forEach(([key, value]) => {
- if (value === void 0 || value === false) {
- element.removeAttribute(key);
- } else {
- element.setAttribute(key, `${value}`);
- }
- });
- }
- }
-
- static applyClass(element, classes) {
- if (classes) {
- element.classList.add(...classes.split(' ').filter(Boolean));
- }
- }
-
- static applyEvents(element, events) {
- if (events) {
- Object.entries(events).forEach(([name, callback]) => {
- element.addEventListener(name, callback);
- });
- }
- }
-
- static applyStyles(element, styles) {
- if (styles) {
- Object.entries(styles).forEach(([key, value]) => {
- const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
- element.style.setProperty(name, value);
- });
- }
- }
-
- static array(element) {
- return Array.isArray(element) ? element : [element];
- }
-
- static create(data) {
- const element = document.createElement(data.tag);
- _Dom.appendChildren(element, data.children);
- _Dom.applyClass(element, data.classes);
- _Dom.applyAttrs(element, data.attrs);
- _Dom.applyEvents(element, data.events);
- _Dom.applyStyles(element, data.styles);
- return element;
- }
-
- static createSvg(data) {
- const element = document.createElementNS(
- 'http://www.w3.org/2000/svg',
- data.tag
- );
- _Dom.appendChildren(element, data.children, true);
- _Dom.applyClass(element, data.classes);
- _Dom.applyAttrs(element, data.attrs);
- _Dom.applyEvents(element, data.events);
- _Dom.applyStyles(element, data.styles);
- return element;
- }
-
- static element(tag, classes, children) {
- return _Dom.create({ tag, children, classes });
- }
-
- static elementSvg(tag, classes, children) {
- return _Dom.createSvg({ tag, children, classes });
- }
- };
-
- // libs/share/src/ui/Component.ts
- var Component = class {
- constructor(tag, props = {}) {
- this.element = Dom.create({ tag, ...props });
- }
-
- addClassName(...className) {
- this.element.classList.add(...className);
- }
-
- event(event, callback) {
- this.element.addEventListener(event, callback);
- }
-
- getElement() {
- return this.element;
- }
-
- mount(parent) {
- parent.appendChild(this.element);
- }
- };
-
- // apps/on-hover-preview/src/components/PreviewPopup.ts
- const PreviewPopup = class _PreviewPopup extends Component {
- constructor() {
- super('div', {
- attrs: {
- id: _PreviewPopup.ID,
- },
- children: {
- tag: 'iframe',
- attrs: {
- allow:
- 'autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share',
- allowFullscreen: true,
- },
- styles: {
- width: '100%',
- border: 'none',
- height: '100%',
- },
- },
- styles: {
- width: '500px',
- background: '#444',
- boxShadow: 'rgb(218, 218, 218) 1px 1px 5px',
- display: 'none',
- height: '300px',
- overflow: 'hidden',
- position: 'absolute',
- zIndex: '9999',
- },
- });
- this.iframeActive = false;
- this.iframe = this.element.children[0];
- if (!document.querySelector(`#${_PreviewPopup.ID}`)) {
- this.mount(document.body);
- document.addEventListener('click', this.hidePopup.bind(this));
- }
- }
-
- static {
- this.ID = 'play-on-hover-popup';
- }
-
- hidePopup() {
- this.iframeActive = false;
- this.iframe.src = '';
- this.element.style.display = 'none';
- }
-
- showPopup(e, url, service) {
- if (!this.iframeActive) {
- this.iframe.src = url;
- this.iframeActive = true;
- Dom.applyStyles(this.element, {
- display: 'block',
- left: `${e.pageX}px`,
- top: `${e.pageY}px`,
- ...service.styles,
- });
- }
- }
- };
-
- // libs/share/src/ui/Events.ts
- const Events = class {
- static intendHover(validate, mouseover, mouseleave, timeout = 500) {
- let hover = false;
- let id = 0;
- const onHover = (event) => {
- if (!event.target || !validate(event.target)) {
- return;
- }
- const element = event.target;
- hover = true;
- element.addEventListener(
- 'mouseleave',
- (ev) => {
- mouseleave?.call(element, ev);
- clearTimeout(id);
- hover = false;
- },
- { once: true }
- );
- clearTimeout(id);
- id = window.setTimeout(() => {
- if (hover) {
- mouseover.call(element, event);
- }
- }, timeout);
- };
- document.body.addEventListener('mouseover', onHover);
- }
- };
-
- // apps/on-hover-preview/src/helpers/LinkHover.ts
- const LinkHover = class {
- constructor(services, onHover) {
- this.services = services;
- this.onHover = onHover;
- Events.intendHover(
- this.isValidLink.bind(this),
- this.onAnchorHover.bind(this)
- );
- }
-
- anchorElement(node) {
- if (!(node instanceof HTMLElement)) {
- return void 0;
- }
- if (node instanceof HTMLAnchorElement) {
- return node;
- }
- const parent = node.closest('a');
- if (parent instanceof HTMLElement) {
- return parent;
- }
- return void 0;
- }
-
- findService(url = '') {
- return this.services.find((service) => service.isValidUrl(url));
- }
-
- isValidLink(node) {
- const anchor = this.anchorElement(node);
- if (!anchor || !anchor.href || anchor.href === '#') {
- return false;
- }
- return true;
- }
-
- async onAnchorHover(ev) {
- const anchor = this.anchorElement(ev.target);
- if (!anchor) {
- return;
- }
- const service = this.findService(anchor.href);
- if (!service) {
- return;
- }
- const previewUrl = await service.embeddedVideoUrl(anchor);
- if (!previewUrl) {
- return;
- }
- this.onHover(ev, previewUrl, service);
- }
- };
-
- // apps/on-hover-preview/src/services/base/BaseService.ts
- const defaultServiceStyle = {
- width: '500px',
- height: '282px',
- };
- const BaseService = class {
- createUrl(url, params) {
- if (params) {
- return `${url}?${this.params(params)}`;
- }
- return url;
- }
-
- extractId(url, match) {
- const result = this.match(url, match);
- return result?.id || '';
- }
-
- isDarkmode() {
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
- }
-
- match(url, match) {
- const result = url.match(match);
- if (result && result.groups) {
- return result.groups;
- }
- return void 0;
- }
-
- params(params) {
- return Object.entries(params)
- .map(([key, value]) => `${key}=${value}`)
- .join('&');
- }
-
- theme(light, dark) {
- return window.matchMedia('(prefers-color-scheme: dark)').matches
- ? dark
- : light;
- }
- };
-
- // apps/on-hover-preview/src/services/base/ServiceFactory.ts
- const ServiceFactory = class extends BaseService {
- constructor(config, styles = defaultServiceStyle) {
- super();
- this.config = config;
- this.styles = styles;
- this.initialStyles = styles;
- }
-
- bindParams(url, params) {
- return Object.entries(params).reduce(
- (acc, [key, value]) =>
- acc.replace(`:${key}`, value !== void 0 ? `${value}` : ''),
- url
- );
- }
-
- async embeddedVideoUrl(element) {
- const isDarkMode = this.isDarkmode();
- const patternParams = this.match(element.href, this.config.pattern) || {};
- const urlParams = {
- ...patternParams,
- ...this.urlParams(element),
- theme: isDarkMode ? 'dark' : 'light',
- };
- this.styles = {
- ...this.initialStyles,
- height: this.getHeight(urlParams),
- };
- const embedUrl = this.bindParams(
- this.createUrl(this.config.embedUrl, this.config.queryParams),
- urlParams
- );
- if (this.config.urlFunction) {
- return this.config.urlFunction({
- ...urlParams,
- url: embedUrl,
- });
- }
- return embedUrl;
- }
-
- isValidUrl(url) {
- return this.config.pattern.test(url);
- }
-
- getHeight(urlParams) {
- if (this.config.heightFunction) {
- return this.config.heightFunction(urlParams);
- }
- if (this.config.typeHeight && urlParams.type in this.config.typeHeight) {
- return this.config.typeHeight[urlParams.type];
- }
- return this.initialStyles.height;
- }
-
- urlParams(element) {
- return {
- href: element.href,
- pathname: element.pathname,
- search: element.search,
- };
- }
- };
-
- // apps/on-hover-preview/src/services/AmazonMusic.ts
- const AmazonMusic = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://music.amazon.com/embed/:id',
- pattern:
- /music\.amazon\.com\/(?<type>albums|tracks|artists|playlists)\/(?<id>[^/?]+)/,
- typeHeight: { tracks: '250px' },
- },
- {
- width: '500px',
- borderRadius: '12px',
- height: '372px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/AppleMusic.ts
- const AppleMusic = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://embed.:service.apple.com:pathname',
- pattern:
- /(?<service>music|podcasts)\.apple\.com\/.{2}\/(?<type>song|music-video|artist|album|podcast)/,
- typeHeight: {
- 'music-video': '281px',
- song: '175px',
- },
- },
- {
- width: '500px',
- borderRadius: '12px',
- height: '450px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Bitchute.ts
- const Bitchute = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://bitchute.com/embed/:id',
- pattern: /bitchute\.com\/video\/(?<id>[^/?]+)\/?/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Coub.ts
- const Coub = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://coub.com/embed/:id',
- pattern: /coub\.com\/view\/(?<id>[^/]+)\/?/,
- queryParams: {
- autostart: 'true',
- muted: 'false',
- originalSize: 'false',
- startWithHD: 'true',
- },
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Dailymotion.ts
- const Dailymotion = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://geo.dailymotion.com/player.html?video=:id',
- pattern: /dailymotion\.com\/video\/(?<id>[^/?]+)/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Deezer.ts
- const Deezer = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://widget.deezer.com/widget/:theme/:type/:id',
- pattern:
- /deezer\.com\/.{2}\/(?<type>album|playlist|track|artist|show|episode)\/(?<id>\d+)/,
- queryParams: {
- autoplay: 'true',
- radius: 'true',
- tracklist: 'false',
- },
- },
- {
- width: '500px',
- borderRadius: '10px',
- height: '300px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Facebook.ts
- const Facebook = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://www.facebook.com/plugins/video.php',
- pattern: /https:\/\/(www\.|m\.)?facebook\.com\/[\w\-_]+\/videos\//,
- queryParams: {
- width: '500',
- autoplay: 'true',
- href: ':href',
- show_text: 'false',
- },
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Instagram.ts
- const Instagram = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://www.instagram.com/p/:id/embed/',
- pattern: /instagram\.com\/(.+\/)?reel\/(?<id>[^/?]+)/,
- },
- {
- width: '300px',
- height: '500px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Odysee.ts
- const Odysee = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://odysee.com/$/embed:pathname',
- pattern: /odysee\.com\/@/,
- queryParams: {
- autoplay: 'true',
- },
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Pbs.ts
- const Pbs = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://player.pbs.org/portalplayer/:id',
- pattern: /pbs\.org\/video\/(?<id>.+)?/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Playeur.ts
- const Playeur = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://playeur.com/embed/:id',
- pattern: /playeur\.com\/(v|embed)\/(?<id>[^/]+)\/?/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Podbean.ts
- const Podbean = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://www.podbean.com/player-v2',
- pattern: /podbean\.com\/.+\/(?<type>dir|pb)-(?<id>[^/?]+)\/?/,
- queryParams: {
- i: ':id-:type',
- },
- },
- {
- width: '500px',
- height: '150px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Rss.ts
- const Rss = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://player.rss.com/:show/:id',
- heightFunction: ({ id }) => (id ? '152px' : '320px'),
- pattern: /rss\.com\/podcasts\/(?<show>[^/]+)\/(?<id>\d*)/,
- queryParams: {
- theme: ':theme',
- },
- },
- {
- width: '500px',
- borderRadius: '8px',
- height: '152px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/SoundCloud.ts
- const SoundCloud = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://w.soundcloud.com/player',
- pattern: /soundcloud\.com\/[^/]+\/[^/?]+/,
- queryParams: {
- hide_related: 'true',
- auto_play: 'true',
- show_artwork: 'true',
- show_comments: 'false',
- show_teaser: 'false',
- url: ':href',
- visual: 'false',
- },
- },
- {
- width: '600px',
- height: '166px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Spotify.ts
- const Spotify = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://open.spotify.com/embed/:type/:id',
- pattern:
- /spotify\.com\/(.+\/)?(?<type>track|album|playlist|episode|artist|show)\/(?<id>[\w-]+)/,
- typeHeight: { track: '152px' },
- urlFunction: ({ type, url }) =>
- ['episode', 'show'].includes(type) ? `${url}/video` : url,
- },
- {
- width: '600px',
- borderRadius: '12px',
- height: '352px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Streamable.ts
- const Streamable = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://streamable.com/o/:id',
- pattern: /streamable\.com\/([s|o]\/)?(?<id>[^?/]+).*$/,
- queryParams: {
- autoplay: '1',
- },
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Ted.ts
- const Ted = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://embed.ted.com/talks/:id',
- pattern: /ted\.com\/talks\/(?<id>[^/]+)\/?/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Tidal.ts
- const Tidal = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://embed.tidal.com/:types/:id',
- pattern:
- /tidal\.com\/(.+\/)?(?<type>track|album|video|playlist)\/(?<id>\d+|[\w-]+)/,
- typeHeight: {
- video: '281px',
- playlist: '400px',
- track: '120px',
- },
- },
- {
- width: '500px',
- borderRadius: '10px',
- height: '300px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Tiktok.ts
- const Tiktok = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://www.tiktok.com/player/v1/:id',
- pattern: /tiktok\.com\/.+\/video\/(?<id>\d+)/,
- queryParams: {
- autoplay: 1,
- rel: 0,
- },
- },
- {
- width: '338px',
- height: '575px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Twitter.ts
- const Twitter = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://platform.:platform.com/embed/Tweet.html',
- pattern: /(?<platform>twitter|x)\.com\/.+\/status\/(?<id>\d+)\/video/,
- queryParams: {
- id: ':id',
- maxWidth: 480,
- width: 480,
- theme: ':theme',
- },
- },
- {
- width: '500px',
- height: '300px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/services/Vimeo.ts
- const Vimeo = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://player.vimeo.com/video/:id',
- pattern: /vimeo\.com(.+)*\/(?<id>\d+)\/?$/,
- });
- }
- };
-
- // apps/on-hover-preview/src/services/Youtube.ts
- const YoutubeHelper = class {
- static getId(search) {
- return new URLSearchParams(search).get('v') || '';
- }
-
- static getStartTime(search) {
- const start = new URLSearchParams(search).get('t') || '0s';
- const result = start.match(/(?:(?<h>\d+)h)?(?:(?<m>\d+)m)?(?<s>\d+)s/);
- if (result && result.groups) {
- return (
- Number(result.groups.h || '0') * 3600 +
- Number(result.groups.m || '0') * 60 +
- Number(result.groups.s || '0')
- );
- }
- return 0;
- }
- };
- const Youtube = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://www.youtube.com/embed/:id',
- pattern: /youtube\.com\/watch/,
- queryParams: {
- autoplay: 1,
- start: ':start',
- },
- urlFunction: ({ search, url }) =>
- this.bindParams(url, {
- id: YoutubeHelper.getId(search),
- start: YoutubeHelper.getStartTime(search),
- }),
- });
- }
- };
- const YoutubeShortcut = class extends ServiceFactory {
- constructor() {
- super({
- embedUrl: 'https://www.youtube.com/embed/:id',
- pattern: /youtu\.be\/(?<id>[^?/]+)/,
- queryParams: {
- autoplay: 1,
- start: ':start',
- },
- urlFunction: ({ search, url }) =>
- this.bindParams(url, {
- start: YoutubeHelper.getStartTime(search),
- }),
- });
- }
- };
- const YoutubeShorts = class extends ServiceFactory {
- constructor() {
- super(
- {
- embedUrl: 'https://www.youtube.com/embed/:id',
- pattern: /youtube\.com\/shorts\/(?<id>[^?/]+).*$/,
- queryParams: {
- autoplay: 1,
- },
- },
- {
- width: '256px',
- height: '454px',
- }
- );
- }
- };
-
- // apps/on-hover-preview/src/main.ts
- function run() {
- const services = [
- Youtube,
- YoutubeShortcut,
- YoutubeShorts,
- Vimeo,
- Streamable,
- Facebook,
- Tiktok,
- Instagram,
- Twitter,
- Dailymotion,
- Dailymotion,
- Coub,
- Spotify,
- SoundCloud,
- AppleMusic,
- Deezer,
- Tidal,
- Ted,
- Pbs,
- Odysee,
- Playeur,
- Bitchute,
- Podbean,
- Rss,
- AmazonMusic,
- // Rumble,
- ].map((Service) => new Service());
- const previewPopup = new PreviewPopup();
- new LinkHover(services, previewPopup.showPopup.bind(previewPopup));
- }
-
- if (window.top == window.self) {
- run();
- }