facebook.com 停用重播 解除靜音 可點擊進度條 按 H 顯示/隱藏面板

停用重播 解除靜音 可點擊進度條 按 H 顯示/隱藏面板

// ==UserScript==
// @name        facebook.com 停用重播 解除靜音 可點擊進度條 按 H 顯示/隱藏面板
// @namespace   fb Scripts
// @match       *://www.facebook.com/*
// @grant       none
// @icon        https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @version     1.1
// @author      huang-wei-lun
// @license     MIT
// @description 停用重播 解除靜音 可點擊進度條 按 H 顯示/隱藏面板
// ==/UserScript==
(function () {
  // 狀態
  let disableLoop = JSON.parse(localStorage.getItem("fb_disableLoop") || "true");
  let unmuteVideo = JSON.parse(localStorage.getItem("fb_unmuteVideo") || "true");
  let clickableBar = JSON.parse(localStorage.getItem("fb_clickableBar") || "true");

  // 建立控制面板
  const panel = document.createElement("div");
  Object.assign(panel.style, {
    position: "fixed",
    top: "20px",
    right: "20px",
    zIndex: 999999,
    background: "rgba(32,32,32,0.92)",
    color: "#e4e4e4",
    padding: "10px",
    borderRadius: "10px",
    fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif",
    fontSize: "14px",
    border: "1px solid #3c3c3c",
    boxShadow: "0 6px 16px rgba(0,0,0,0.4)",
    transition: "opacity .25s"
  });
  panel.innerHTML = `
    <label style="display:block;margin-bottom:6px;">
      <input type="checkbox" id="fb_disableLoop"> 停用重播
    </label>
    <label style="display:block;margin-bottom:6px;">
      <input type="checkbox" id="fb_unmuteVideo"> 解除靜音
    </label>
    <label style="display:block;margin-bottom:6px;">
      <input type="checkbox" id="fb_clickableBar"> 可點擊進度條
    </label>
    <hr style="border:0;border-top:1px solid #3c3c3c;margin:8px 0;">
    <small style="color:#a8a8a8;">按 H 顯示/隱藏面板</small>
  `;
  document.body.appendChild(panel);

  panel.querySelector("#fb_disableLoop").checked = disableLoop;
  panel.querySelector("#fb_unmuteVideo").checked = unmuteVideo;
  panel.querySelector("#fb_clickableBar").checked = clickableBar;

  panel.querySelector("#fb_disableLoop").addEventListener("change", (e) => {
    disableLoop = e.target.checked;
    localStorage.setItem("fb_disableLoop", JSON.stringify(disableLoop));
    applyToAllVideos();
  });
  panel.querySelector("#fb_unmuteVideo").addEventListener("change", (e) => {
    unmuteVideo = e.target.checked;
    localStorage.setItem("fb_unmuteVideo", JSON.stringify(unmuteVideo));
    applyToAllVideos();
  });
  panel.querySelector("#fb_clickableBar").addEventListener("change", (e) => {
    clickableBar = e.target.checked;
    localStorage.setItem("fb_clickableBar", JSON.stringify(clickableBar));
    applyToAllVideos();
  });

  // 面板快捷鍵
  let visible = true;
  document.addEventListener("keydown", (e) => {
    if (e.key.toLowerCase() === "h") {
      visible = !visible;
      panel.style.opacity = visible ? "1" : "0";
      panel.style.pointerEvents = visible ? "auto" : "none";
    }
  });

  // 進度條樣式(覆蓋在影片上)
  function ensureClickableBar(video) {
    if (!clickableBar || video.__fbHasBar) return;
    const container = video.parentElement;
    if (!container) return;

    const bar = document.createElement("div");
    Object.assign(bar.style, {
      position: "absolute",
      left: "0",
      right: "0",
      bottom: "6px",
      height: "6px",
      background: "rgba(255,255,255,0.28)",
      cursor: "pointer",
      zIndex: 99998,
      borderRadius: "999px",
      overflow: "hidden",
      backdropFilter: "blur(1px)"
    });

    const prog = document.createElement("div");
    Object.assign(prog.style, {
      height: "100%",
      width: "0%",
      background: "rgba(255,255,255,0.9)"
    });
    bar.appendChild(prog);

    // 點擊跳轉
    bar.addEventListener("click", (e) => {
      const rect = bar.getBoundingClientRect();
      const percent = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
      if (isFinite(video.duration) && video.duration > 0) {
        video.currentTime = video.duration * percent;
      }
    });

    // 跟隨時間更新
    const onTime = () => {
      if (!isFinite(video.duration) || video.duration <= 0) return;
      const p = (video.currentTime / video.duration) * 100;
      prog.style.width = p + "%";
    };
    video.addEventListener("timeupdate", onTime);
    video.addEventListener("progress", onTime);
    video.addEventListener("loadedmetadata", onTime);
    onTime();

    // 確保父容器能定位
    const prevPos = container.style.position;
    if (getComputedStyle(container).position === "static") {
      container.style.position = "relative";
    }
    container.appendChild(bar);

    // 標記並保存清理器
    video.__fbHasBar = true;
    video.__fbBar = bar;
    video.__fbBarCleanup = () => {
      video.removeEventListener("timeupdate", onTime);
      video.removeEventListener("progress", onTime);
      video.removeEventListener("loadedmetadata", onTime);
      if (bar.isConnected) bar.remove();
      if (prevPos) container.style.position = prevPos;
      video.__fbHasBar = false;
      video.__fbBar = null;
      video.__fbBarCleanup = null;
    };
  }

  function removeClickableBar(video) {
    if (video.__fbBarCleanup) video.__fbBarCleanup();
  }

  // 對單一 video 套用規則
  function applyRules(video) {
    try {
      if (disableLoop) video.loop = false;
      if (unmuteVideo) {
        video.muted = false;
        video.defaultMuted = false;
        // 強化一次,避免 FB 程式碼又設回去
        video.volume = 1.0;
        // 嘗試開啟音軌(有些瀏覽器需互動才允許播放聲音)
        const resume = () => {
          video.muted = false;
          video.defaultMuted = false;
          video.volume = 1.0;
        };
        video.addEventListener("play", resume, { once: true });
      }
      if (clickableBar) {
        ensureClickableBar(video);
      } else {
        removeClickableBar(video);
      }
    } catch (_) {}
  }

  // 初次套用
  function applyToAllVideos() {
    document.querySelectorAll("video").forEach((v) => applyRules(v));
  }
  applyToAllVideos();

  // 監聽動態載入(Facebook 是 SPA)
  const obs = new MutationObserver((muts) => {
    for (const mut of muts) {
      mut.addedNodes.forEach((node) => {
        if (node instanceof HTMLVideoElement) {
          applyRules(node);
        } else if (node.querySelectorAll) {
          node.querySelectorAll("video").forEach((v) => applyRules(v));
        }
      });
      // 若節點被移除,清理進度條
      mut.removedNodes.forEach((node) => {
        if (node instanceof HTMLVideoElement) {
          removeClickableBar(node);
        } else if (node.querySelectorAll) {
          node.querySelectorAll("video").forEach(removeClickableBar);
        }
      });
    }
  });
  obs.observe(document.documentElement, { childList: true, subtree: true });

  // 小提示:滾動時有新影片載入也會自動套用
  console.log("[FB Video Helper] 已啟用:停用重播 =", disableLoop, "解除靜音 =", unmuteVideo, "可點擊進度條 =", clickableBar);
})();