Greasy Fork

油管视频旋转

油管的视频旋转插件.

目前为 2022-03-25 提交的版本。查看 最新版本

// ==UserScript==
// @author          zhzLuke96
// @name            油管视频旋转
// @name:en         youtube player rotate plug
// @version         1.3
// @description     油管的视频旋转插件.
// @description:en  rotate youtebe player.
// @namespace       https://github.com/zhzLuke96/
// @match           https://www.youtube.com/*
// @grant           none
// ==/UserScript==

(async function () {
  "use strict";
  const rule_name = "ytp_player_user_js";

  // ref:https://stackoverflow.com/questions/27078285/simple-throttle-in-js
  function throttle(callback, limit) {
    var waiting = false;
    return function () {
      if (!waiting) {
        callback.apply(this, arguments);
        waiting = true;
        setTimeout(function () {
          waiting = false;
        }, limit);
      }
    };
  }

  const dommap = {};
  const applydom = (key, fn) => dommap[key] && fn(dommap[key]);

  // init ytp_horizintal ytp_vertical class
  ((rule) => {
    var style = document.createElement("style");
    document.getElementsByTagName("head")[0].appendChild(style);
    style.innerHTML = rule;
  })(`
    video.ytp_horizintal{transform:rotateX(180deg)}
    video.ytp_vertical{transform:rotateY(180deg)}
    `);

  const $ = (q) => document.querySelector(q);
  const currentLang =
    (
      navigator.language ||
      navigator.browserLanguage ||
      navigator.systemLanguage
    ).toLowerCase() || "";
  const on_zh_lang = currentLang.indexOf("zh") > -1;

  let $styleElem = null; // cache var

  function setup($ytp_player, $ytp_player_vid) {
    if (!$ytp_player || !$ytp_player_vid) {
      return;
    }
    if (!$styleElem) {
      $styleElem = document.createElement("style");
      document.getElementsByTagName("head")[0].appendChild($styleElem);
    }

    // mount <video> element
    $ytp_player_vid.classList.add(rule_name);
    let currentCSS = {};
    const state = {
      rotate: 0, // 0 1 2 3 => 0 90 180 270
      horizintal: false,
      vertical: false,

      plugin_working: false,
    };
    // utils
    function setStyle(rule) {
      $styleElem.innerHTML = `.${rule_name}{${rule}}`;
    }

    function css_dump(obj) {
      let ret = "";
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          ret += `${key}:${obj[key]} !important;`;
        }
      }
      return ret;
    }

    function toggleRuleTransform(rule) {
      if (currentCSS["transform"]) {
        if (currentCSS["transform"].indexOf(rule) > -1)
          currentCSS["transform"] = currentCSS["transform"].replace(rule, "");
        else currentCSS["transform"] += rule;
      } else {
        currentCSS["transform"] = rule;
      }
      setStyle(css_dump(currentCSS));
    }

    // bind play event
    setTimeout(() => {
      $ytp_player_vid.addEventListener(
        "play",
        () => state.plugin_working && update()
      );
      $ytp_player.addEventListener(
        "resize",
        throttle(() => state.plugin_working && setTimeout(update, 100), 500)
      );
    }, 500);

    const update = () => {
      // ---------- rotate ---------- 👇
      const y = (() => {
        const [pw, ph] = [$ytp_player.clientWidth, $ytp_player.clientHeight];
        let [w, h] = [
          $ytp_player_vid.clientWidth,
          $ytp_player_vid.clientHeight,
        ];
        if (state.rotate % 2 == 1) {
          [w, h] = [h, w];
        }
        // pw === w
        if (~~((pw * h) / w) <= h) {
          // 💥💥💥
          return pw / w;
        }
        // ph === h
        return ph / h;
      })();

      const css = {};
      css["transform"] = `rotate(${state.rotate * 90}deg)`;
      css["transform"] += ` scale(${y})`;
      currentCSS = css;
      setStyle(css_dump(css));
      // ---------- rotate ---------- 👆
      if (state.horizintal) {
        if (state.rotate % 2 == 1) toggleRuleTransform("rotateX(180deg)");
        else toggleRuleTransform("rotateY(180deg)");
      }
      if (state.vertical) {
        if (state % 2 == 1) toggleRuleTransform("rotateY(180deg)");
        else toggleRuleTransform("rotateX(180deg)");
      }

      // ---------- update UI ---------- 👇
      applydom("menu_rotate", (elem) => {
        // FIXME: 同步更新度数
        elem.querySelector(".ytp-menuitem-content").innerHTML =
          state.rotate * 90 + "°";
      });
      // TODO: 下面这几个为啥没有打开呢?忘记了😂...需要看下
      // applydom('menu_horizintal', (elem) => {
      //     elem.setAttribute('aria-checked', state.horizintal.toString());
      // })
      // applydom('menu_vertical', (elem) => {
      //     elem.setAttribute('aria-checked', state.vertical.toString());
      // })
      // applydom('menu_pip', (elem) => {
      //     //
      // })
      // ---------- update UI ---------- 👆
    };

    return {
      rotate() {
        state.plugin_working = true;
        state.rotate = (state.rotate + 1) % 4;
        update();
        return state.rotate;
      },
      toogleHorizintal() {
        state.plugin_working = true;
        state.horizintal = !state.horizintal;
        update();
        return state.horizintal;
      },
      toogleVertical() {
        state.plugin_working = true;
        state.vertical = !state.vertical;
        update();
        return state.vertical;
      },
      reset() {
        state.rotate = 0;
        state.horizintal = false;
        state.vertical = false;
        update();
        return state;
      },
    };
  }

  let playerApi = null; //

  playerApi = await new Promise((resolve) => {
    // 网络情况较差,或者其他情况。轮询查出 playerApi
    function c() {
      const $vid = $(".html5-main-video");
      const $player = $(".html5-video-player");
      if ($vid && $player) {
        resolve(setup($player, $vid));
      } else {
        setTimeout(() => {
          c();
        }, 2000);
      }
    }
    c();
  });

  // button and menu
  async function addbutton(html, options, onRight = true) {
    let p, push;
    if (onRight) {
      p = await new Promise((resolve, reject) => {
        function c() {
          if ($(".ytp-right-controls")) {
            resolve($(".ytp-right-controls"));
          } else {
            setTimeout(() => {
              c();
            }, 2000);
          }
        }
        c();
      });
      push = (n) => p.insertBefore(n, p.firstElementChild);
    } else {
      // left
      p = await new Promise((resolve, reject) => {
        function c() {
          if ($(".ytp-left-controls")) {
            resolve($(".ytp-left-controls"));
          } else {
            setTimeout(() => {
              c();
            }, 2000);
          }
        }
        c();
      });
      push = (n) => p.appendChild(n);
    }

    let b = await new Promise((resolve, reject) => {
      function c() {
        if ($(".ytp-settings-button")) {
          resolve($(".ytp-settings-button").cloneNode(true));
        } else {
          setTimeout(() => {
            c();
          }, 1000);
        }
      }
      c();
    });
    b.innerHTML = html;
    b.className = "ytp-button";
    push(b);
    if (options.key) dommap[options.key] = b;
    if (options.click) b.addEventListener("click", options.click);
    if (options.css) b.style.cssText = options.css;
    if (options.id) b.id = options.id;
    if (options.title) b.title = options.title;
    return b;
  }

  // rotate 90

  await addbutton(
    `
<svg viewBox="0 0 1536 1536" aria-labelledby="rwsi-awesome-repeat-title" id="si-awesome-repeat" width="100%" height="100%"><title id="rwsi-awesome-repeat-title">icon repeat</title><path d="M1536 128v448q0 26-19 45t-45 19h-448q-42 0-59-40-17-39 14-69l138-138Q969 256 768 256q-104 0-198.5 40.5T406 406 296.5 569.5 256 768t40.5 198.5T406 1130t163.5 109.5T768 1280q119 0 225-52t179-147q7-10 23-12 14 0 25 9l137 138q9 8 9.5 20.5t-7.5 22.5q-109 132-264 204.5T768 1536q-156 0-298-61t-245-164-164-245T0 768t61-298 164-245T470 61 768 0q147 0 284.5 55.5T1297 212l130-129q29-31 70-14 39 17 39 59z"></path></svg>
            `,
    {
      click: () => playerApi.rotate && playerApi.rotate(),
      css: "fill:white;width:20px;margin-right:1rem;",
      id: "rotate-btn",
      title: on_zh_lang ? "旋转视频" : "rotate",
      key: "btn_rotate",
    }
  );

  function append_context_menu({
    label = "",
    content = '<div class="ytp-menuitem-toggle-checkbox"></div>',
    href,
    icon,
    callback,
    key,
  }) {
    const $menu = document.querySelector(
      ".ytp-contextmenu>.ytp-panel>.ytp-panel-menu"
    );
    let $ele = null;
    if (href) {
      $ele = document
        .querySelector(
          ".ytp-contextmenu>.ytp-panel>.ytp-panel-menu>a.ytp-menuitem"
        )
        .cloneNode(true);
      $ele.href = href;
    } else {
      $ele = document
        .querySelector(
          ".ytp-contextmenu>.ytp-panel>.ytp-panel-menu>div.ytp-menuitem"
        )
        .cloneNode(true);
    }
    key && (dommap[key] = $ele);
    label && ($ele.querySelector(".ytp-menuitem-label").innerHTML = label);
    content &&
      ($ele.querySelector(".ytp-menuitem-content").innerHTML = content);
    icon && ($ele.querySelector(".ytp-menuitem-icon").innerHTML = icon);
    if (typeof callback == "function") $ele.addEventListener("click", callback);

    $menu.appendChild($ele);
  }

  (function mountMenu() {
    const $menu = document.querySelector(
      ".ytp-contextmenu>.ytp-panel>.ytp-panel-menu"
    );
    if (!$menu || $menu.innerHTML === "") {
      // waiting framework do something
      return setTimeout(mountMenu, 300);
    }

    // github link
    append_context_menu({
      key: "menu_github",
      label: "-- github --",
      content: "️️⭐",
      href: "https://github.com/zhzLuke96/ytp-rotate",
    });

    // rotate menuitem
    append_context_menu({
      key: "menu_rotate",
      callback: (ev) => {
        if (!playerApi) return;
        const elem = ev.currentTarget;
        // FIXME: 这里的角度其实没同步
        elem.querySelector(".ytp-menuitem-content").innerHTML =
          playerApi.rotate() * 90 + "°";
      },
      label: on_zh_lang ? "旋转90°" : "rotate 90°",
      content: "️️0°",
    });

    // flip horizintal
    append_context_menu({
      key: "menu_horizintal",
      callback(ev) {
        if (!playerApi) return;
        const elem = ev.currentTarget;
        elem.setAttribute(
          "aria-checked",
          playerApi.toogleHorizintal().toString()
        );
      },
      label: on_zh_lang ? "水平翻转" : "flip horizintal",
      icon: '<div style="text-align: center;">↔️</div>',
    });

    // flip vertical
    append_context_menu({
      key: "menu_vertical",
      callback(ev) {
        if (!playerApi) return;
        const elem = ev.currentTarget;
        elem.setAttribute(
          "aria-checked",
          playerApi.toogleVertical().toString()
        );
      },
      label: on_zh_lang ? "垂直翻转" : "flip vertical",
      icon: '<div style="text-align: center;">↕️</div>',
    });

    // picture in picture
    append_context_menu({
      key: "menu_pip",
      callback(ev) {
        const elem = ev.currentTarget;
        try {
          if (document.pictureInPictureElement) {
            document.exitPictureInPicture();
            elem.setAttribute("aria-checked", false.toString());
          } else {
            $("video").requestPictureInPicture();
            elem.setAttribute("aria-checked", true.toString());
          }
        } catch (error) {
          let msg = on_zh_lang
            ? "浏览器不支持[PictureInPicture]或脚本加载错误"
            : "Browser does not support [PictureInPicture] or script loading error";
          console.log(msg);
          alert(msg);
        }
      },
      label: on_zh_lang ? "画中画" : "PIP",
      icon: '<div style="text-align: center;">🖼️</div>',
    });
  })();
})();