Greasy Fork镜像 还支持 简体中文。

Play video on hover

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址