Greasy Fork

来自缓存

Greasy Fork is available in English.

JAVM M3U8 Helper

检测页面中的 m3u8 链接,一键唤起 JAVM 下载或调用播放器

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         JAVM M3U8 Helper
// @namespace    bidbadegg/javm-m3u8-helper
// @version      1.1.2
// @author       BidBadEgg
// @description  检测页面中的 m3u8 链接,一键唤起 JAVM 下载或调用播放器
// @license      MIT
// @icon         https://raw.githubusercontent.com/BidBadEgg/javm/main/src-tauri/icons/32x32.png
// @match        *://*/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_info
// @grant        GM_openInTab
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// ==/UserScript==

(function (vue) {
  'use strict';

  const d=new Set;const t = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));};

  t(' .javm-root[data-v-51d933fe]{all:initial;position:fixed!important;z-index:2147483647!important;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Microsoft YaHei,sans-serif!important;font-size:13px!important;line-height:1.4!important;display:flex!important;flex-direction:column!important;align-items:flex-end!important;gap:12px!important;-webkit-font-smoothing:antialiased!important}.javm-bubble-wrap[data-v-51d933fe]{position:relative!important;width:48px!important;height:48px!important}.javm-bubble-ring[data-v-51d933fe]{position:absolute!important;inset:-4px!important;border-radius:50%!important;border:2px solid rgba(99,102,241,.4)!important;animation:javm-ring-pulse-51d933fe 2s ease-in-out infinite!important;pointer-events:none!important}@keyframes javm-ring-pulse-51d933fe{0%,to{transform:scale(1);opacity:.5}50%{transform:scale(1.15);opacity:0}}.javm-bubble[data-v-51d933fe]{all:unset;position:relative!important;width:48px!important;height:48px!important;border-radius:16px!important;background:linear-gradient(135deg,#6366f1,#8b5cf6,#a855f7)!important;color:#fff!important;cursor:pointer!important;display:flex!important;align-items:center!important;justify-content:center!important;box-shadow:0 4px 15px #6366f166,0 1px 3px #0003,inset 0 1px #ffffff26!important;transition:transform .25s cubic-bezier(.34,1.56,.64,1),box-shadow .25s ease,border-radius .25s ease!important;-webkit-user-select:none!important;user-select:none!important}.javm-bubble[data-v-51d933fe]:before{content:""!important;position:absolute!important;inset:0!important;border-radius:inherit!important;background:linear-gradient(135deg,rgba(255,255,255,.2) 0%,transparent 50%)!important;pointer-events:none!important}.javm-bubble[data-v-51d933fe]:hover{transform:translateY(-2px) scale(1.08)!important;box-shadow:0 8px 25px #6366f180,0 2px 8px #0003,inset 0 1px #fff3!important;border-radius:14px!important}.javm-bubble.pressed[data-v-51d933fe]{transform:translateY(0) scale(.95)!important;box-shadow:0 2px 8px #6366f14d,inset 0 2px 4px #00000026!important}.javm-bubble.active[data-v-51d933fe]{background:linear-gradient(135deg,#4f46e5,#7c3aed)!important;border-radius:14px!important}.javm-bubble.dragging[data-v-51d933fe]{cursor:grabbing!important;transform:scale(1.12)!important;box-shadow:0 10px 30px #6366f180,0 2px 8px #0000004d,inset 0 1px #fff3!important;transition:none!important}.javm-bubble-icon[data-v-51d933fe]{width:20px!important;height:20px!important;flex-shrink:0!important;opacity:.9!important}.javm-bubble-num[data-v-51d933fe]{position:absolute!important;top:-6px!important;right:-6px!important;min-width:20px!important;height:20px!important;border-radius:10px!important;background:#ef4444!important;color:#fff!important;font-size:11px!important;font-weight:700!important;display:flex!important;align-items:center!important;justify-content:center!important;padding:0 5px!important;box-shadow:0 2px 6px #ef444466!important;animation:javm-num-pop-51d933fe .3s cubic-bezier(.34,1.56,.64,1)!important;z-index:1!important}@keyframes javm-num-pop-51d933fe{0%{transform:scale(0)}to{transform:scale(1)}}.javm-panel[data-v-51d933fe]{all:unset;display:block!important;width:500px!important;max-height:440px!important;background:#0f0f1aeb!important;backdrop-filter:blur(20px) saturate(1.5)!important;-webkit-backdrop-filter:blur(20px) saturate(1.5)!important;border:1px solid rgba(99,102,241,.15)!important;border-radius:16px!important;box-shadow:0 20px 60px #00000080,0 0 0 1px #ffffff0d,0 0 80px -20px #6366f126!important;overflow:hidden!important;color:#e2e8f0!important;position:relative!important}.javm-panel-glow[data-v-51d933fe]{position:absolute!important;top:0!important;left:0!important;right:0!important;height:80px!important;background:linear-gradient(180deg,rgba(99,102,241,.08),transparent)!important;pointer-events:none!important}.javm-panel-header[data-v-51d933fe]{position:relative!important;display:flex!important;align-items:center!important;justify-content:space-between!important;padding:14px 16px!important;border-bottom:1px solid rgba(255,255,255,.06)!important}.javm-panel-title-area[data-v-51d933fe]{display:flex!important;align-items:center!important;gap:8px!important}.javm-panel-title[data-v-51d933fe]{font-weight:600!important;font-size:14px!important;color:#f1f5f9!important;letter-spacing:-.01em!important}.javm-panel-count[data-v-51d933fe]{font-size:11px!important;font-weight:600!important;color:#a5b4fc!important;background:#6366f126!important;padding:2px 8px!important;border-radius:10px!important;letter-spacing:.02em!important}.javm-header-actions[data-v-51d933fe]{display:flex!important;gap:4px!important}.javm-icon-btn[data-v-51d933fe]{all:unset;width:30px!important;height:30px!important;border-radius:8px!important;display:flex!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;color:#64748b!important;transition:all .2s ease!important}.javm-icon-btn svg[data-v-51d933fe]{width:16px!important;height:16px!important;transition:transform .3s ease!important}.javm-icon-btn[data-v-51d933fe]:hover{color:#e2e8f0!important;background:#ffffff14!important}.javm-icon-btn:hover svg[data-v-51d933fe]{transform:rotate(30deg)!important}.javm-icon-btn.is-active[data-v-51d933fe]{color:#a5b4fc!important;background:#6366f126!important}.javm-icon-btn.is-active svg[data-v-51d933fe]{transform:rotate(90deg)!important}.javm-close-btn[data-v-51d933fe]:hover{color:#fca5a5!important;background:#ef44441f!important}.javm-close-btn:hover svg[data-v-51d933fe]{transform:rotate(0) scale(1.1)!important}.javm-panel-body[data-v-51d933fe]{overflow-y:auto!important;max-height:370px!important;scroll-behavior:smooth!important}.javm-panel-body[data-v-51d933fe]::-webkit-scrollbar{width:5px!important}.javm-panel-body[data-v-51d933fe]::-webkit-scrollbar-track{background:transparent!important}.javm-panel-body[data-v-51d933fe]::-webkit-scrollbar-thumb{background:#6366f140!important;border-radius:5px!important}.javm-panel-body[data-v-51d933fe]::-webkit-scrollbar-thumb:hover{background:#6366f173!important}.javm-item[data-v-51d933fe]{display:flex!important;align-items:center!important;gap:10px!important;padding:10px 16px!important;border-bottom:1px solid rgba(255,255,255,.04)!important;transition:all .2s ease!important;position:relative!important}.javm-item[data-v-51d933fe]:before{content:""!important;position:absolute!important;left:0!important;top:0!important;bottom:0!important;width:3px!important;background:linear-gradient(180deg,#6366f1,#a855f7)!important;border-radius:0 2px 2px 0!important;opacity:0!important;transform:scaleY(.5)!important;transition:opacity .2s,transform .2s!important}.javm-item[data-v-51d933fe]:last-child{border-bottom:none!important}.javm-item[data-v-51d933fe]:hover{background:#6366f10f!important}.javm-item[data-v-51d933fe]:hover:before{opacity:1!important;transform:scaleY(1)!important}.javm-item-info[data-v-51d933fe]{display:flex!important;align-items:center!important;gap:8px!important;flex:1!important;min-width:0!important}.javm-badge[data-v-51d933fe]{flex-shrink:0!important;background:#6366f11f!important;color:#a5b4fc!important;font-size:11px!important;font-weight:600!important;padding:3px 8px!important;border-radius:6px!important;white-space:nowrap!important;border:1px solid rgba(99,102,241,.1)!important;letter-spacing:.02em!important}.javm-filename[data-v-51d933fe]{color:#94a3b8!important;font-size:12px!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.javm-actions[data-v-51d933fe]{display:flex!important;gap:2px!important;flex-shrink:0!important;opacity:.6!important;transition:opacity .2s!important}.javm-item:hover .javm-actions[data-v-51d933fe]{opacity:1!important}.javm-act-btn[data-v-51d933fe]{all:unset;width:30px!important;height:30px!important;border-radius:8px!important;display:flex!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;transition:all .2s cubic-bezier(.34,1.56,.64,1)!important;position:relative!important;color:#94a3b8!important}.javm-act-btn svg[data-v-51d933fe]{width:15px!important;height:15px!important}.javm-act-btn[data-v-51d933fe]:hover{transform:translateY(-2px)!important}.javm-act-btn[data-v-51d933fe]:active{transform:translateY(0) scale(.92)!important}.javm-act-copy[data-v-51d933fe]:hover{background:#3b82f626!important;color:#60a5fa!important}.javm-act-dl[data-v-51d933fe]:hover{background:#ef444426!important;color:#f87171!important}.javm-act-player[data-v-51d933fe]{font-size:15px!important}.javm-act-player[data-v-51d933fe]:hover{background:#a855f726!important}.javm-settings[data-v-51d933fe]{padding:4px 0!important}.javm-settings-section[data-v-51d933fe]{padding:10px 16px 12px!important}.javm-settings-section+.javm-settings-section[data-v-51d933fe]{border-top:1px solid rgba(255,255,255,.05)!important}.javm-settings-label[data-v-51d933fe]{font-size:11px!important;color:#64748b!important;font-weight:600!important;margin-bottom:10px!important;text-transform:uppercase!important;letter-spacing:.08em!important}.javm-setting-row[data-v-51d933fe]{display:flex!important;align-items:center!important;gap:10px!important;padding:6px 8px!important;border-radius:8px!important;margin:2px -8px!important;transition:background .15s!important}.javm-setting-row[data-v-51d933fe]:hover{background:#ffffff08!important}.javm-setting-icon[data-v-51d933fe]{font-size:16px!important;width:22px!important;text-align:center!important}.javm-setting-name[data-v-51d933fe]{font-size:13px!important;font-weight:500!important;color:#e2e8f0!important;min-width:70px!important}.javm-setting-tpl[data-v-51d933fe]{font-size:11px!important;color:#475569!important;flex:1!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important;font-family:SF Mono,Monaco,Cascadia Code,Consolas,monospace!important}.javm-setting-del[data-v-51d933fe]{all:unset;cursor:pointer!important;color:#475569!important;width:22px!important;height:22px!important;display:flex!important;align-items:center!important;justify-content:center!important;border-radius:6px!important;transition:all .2s!important}.javm-setting-del[data-v-51d933fe]:hover{color:#fca5a5!important;background:#ef44441f!important}.javm-setting-tag[data-v-51d933fe]{font-size:10px!important;color:#64748b!important;background:#6366f11a!important;padding:2px 8px!important;border-radius:6px!important;font-weight:500!important}.javm-reset-btn[data-v-51d933fe]{all:unset;cursor:pointer!important;font-size:11px!important;color:#a5b4fc!important;background:#6366f11f!important;padding:3px 10px!important;border-radius:6px!important;font-weight:500!important;transition:all .2s ease!important}.javm-reset-btn[data-v-51d933fe]:hover{background:#6366f140!important;color:#c7d2fe!important}.javm-switch[data-v-51d933fe]{position:relative!important;display:inline-block!important;width:36px!important;height:20px!important;flex-shrink:0!important}.javm-switch input[data-v-51d933fe]{opacity:0!important;width:0!important;height:0!important;position:absolute!important}.javm-slider[data-v-51d933fe]{position:absolute!important;cursor:pointer!important;inset:0!important;background:#ffffff14!important;border-radius:20px!important;transition:all .3s cubic-bezier(.34,1.56,.64,1)!important}.javm-slider[data-v-51d933fe]:before{content:""!important;position:absolute!important;width:16px!important;height:16px!important;left:2px!important;bottom:2px!important;background:#475569!important;border-radius:50%!important;transition:all .3s cubic-bezier(.34,1.56,.64,1)!important;box-shadow:0 1px 3px #0000004d!important}.javm-switch input:checked+.javm-slider[data-v-51d933fe]{background:#6366f140!important}.javm-switch input:checked+.javm-slider[data-v-51d933fe]:before{transform:translate(16px)!important;background:#818cf8!important;box-shadow:0 0 8px #818cf866!important}.javm-add-form[data-v-51d933fe]{display:flex!important;gap:6px!important;align-items:center!important}.javm-input[data-v-51d933fe]{all:unset;background:#ffffff0a!important;border:1px solid rgba(255,255,255,.08)!important;border-radius:8px!important;padding:7px 10px!important;font-size:12px!important;color:#e2e8f0!important;transition:all .2s ease!important}.javm-input[data-v-51d933fe]::placeholder{color:#475569!important}.javm-input[data-v-51d933fe]:focus{border-color:#6366f166!important;box-shadow:0 0 0 3px #6366f11a!important;background:#ffffff0f!important}.javm-input-icon[data-v-51d933fe]{width:32px!important;text-align:center!important;font-size:15px!important}.javm-input-name[data-v-51d933fe]{width:76px!important}.javm-input-tpl[data-v-51d933fe]{flex:1!important;font-family:SF Mono,Monaco,Cascadia Code,Consolas,monospace!important;font-size:11px!important}.javm-add-btn[data-v-51d933fe]{all:unset;width:32px!important;height:32px!important;border-radius:8px!important;background:#6366f126!important;color:#a5b4fc!important;display:flex!important;align-items:center!important;justify-content:center!important;cursor:pointer!important;transition:all .2s ease!important;flex-shrink:0!important}.javm-add-btn svg[data-v-51d933fe]{width:16px!important;height:16px!important}.javm-add-btn[data-v-51d933fe]:hover{background:#6366f140!important;color:#c7d2fe!important;transform:scale(1.05)!important}.javm-add-btn[data-v-51d933fe]:active{transform:scale(.95)!important}.javm-hint[data-v-51d933fe]{font-size:11px!important;color:#475569!important;margin-top:8px!important;line-height:1.6!important}.javm-hint code[data-v-51d933fe]{color:#a5b4fc!important;background:#6366f11a!important;padding:1px 5px!important;border-radius:4px!important;font-size:10px!important;font-family:SF Mono,Monaco,Cascadia Code,Consolas,monospace!important}.javm-about[data-v-51d933fe]{display:flex!important;flex-direction:column!important;align-items:center!important;padding:28px 24px 24px!important;gap:6px!important}.javm-about-logo[data-v-51d933fe]{width:48px!important;height:48px!important;border-radius:14px!important;background:linear-gradient(135deg,#6366f1,#a855f7)!important;display:flex!important;align-items:center!important;justify-content:center!important;margin-bottom:4px!important;box-shadow:0 4px 15px #6366f14d!important}.javm-about-logo svg[data-v-51d933fe]{width:24px!important;height:24px!important;color:#fff!important}.javm-about-name[data-v-51d933fe]{font-size:16px!important;font-weight:700!important;color:#f1f5f9!important;letter-spacing:-.02em!important}.javm-about-ver[data-v-51d933fe]{font-size:11px!important;color:#64748b!important;background:#6366f11a!important;padding:2px 10px!important;border-radius:10px!important;font-weight:500!important}.javm-about-desc[data-v-51d933fe]{font-size:12px!important;color:#94a3b8!important;text-align:center!important;margin:4px 0 12px!important;line-height:1.5!important}.javm-about-links[data-v-51d933fe]{display:flex!important;flex-direction:column!important;gap:8px!important;width:100%!important}.javm-about-link[data-v-51d933fe]{all:unset;display:flex!important;align-items:center!important;gap:12px!important;padding:12px 14px!important;border-radius:10px!important;background:#ffffff0a!important;border:1px solid rgba(255,255,255,.06)!important;cursor:pointer!important;color:#94a3b8!important;transition:all .2s ease!important;text-decoration:none!important}.javm-about-link[data-v-51d933fe]:hover{background:#6366f114!important;border-color:#6366f133!important;transform:translateY(-1px)!important}.javm-about-link-text[data-v-51d933fe]{flex:1!important;display:flex!important;flex-direction:column!important;gap:2px!important;min-width:0!important}.javm-about-link-title[data-v-51d933fe]{font-size:13px!important;font-weight:600!important;color:#e2e8f0!important}.javm-about-link-sub[data-v-51d933fe]{font-size:11px!important;color:#64748b!important;overflow:hidden!important;text-overflow:ellipsis!important;white-space:nowrap!important}.javm-toast[data-v-51d933fe]{position:absolute!important;bottom:12px!important;left:50%!important;transform:translate(-50%)!important;background:#0f172ae6!important;backdrop-filter:blur(8px)!important;-webkit-backdrop-filter:blur(8px)!important;border:1px solid rgba(99,102,241,.15)!important;color:#e2e8f0!important;font-size:12px!important;font-weight:500!important;padding:6px 14px!important;border-radius:10px!important;pointer-events:none!important;white-space:nowrap!important;display:flex!important;align-items:center!important;gap:6px!important;box-shadow:0 4px 15px #0000004d!important}.bubble-enter-active[data-v-51d933fe]{transition:opacity .4s ease,transform .5s cubic-bezier(.34,1.56,.64,1)!important}.bubble-leave-active[data-v-51d933fe]{transition:opacity .25s ease,transform .25s ease!important}.bubble-enter-from[data-v-51d933fe]{opacity:0!important;transform:scale(.3) rotate(-30deg)!important}.bubble-leave-to[data-v-51d933fe]{opacity:0!important;transform:scale(.5)!important}.panel-enter-active[data-v-51d933fe]{transition:opacity .3s ease,transform .4s cubic-bezier(.34,1.56,.64,1)!important}.panel-leave-active[data-v-51d933fe]{transition:opacity .2s ease,transform .2s ease!important}.panel-enter-from[data-v-51d933fe]{opacity:0!important;transform:translateY(20px) scale(.95)!important}.panel-leave-to[data-v-51d933fe]{opacity:0!important;transform:translateY(10px) scale(.98)!important}.title-swap-enter-active[data-v-51d933fe],.title-swap-leave-active[data-v-51d933fe]{transition:opacity .15s ease,transform .15s ease!important}.title-swap-enter-from[data-v-51d933fe]{opacity:0!important;transform:translateY(-6px)!important}.title-swap-leave-to[data-v-51d933fe]{opacity:0!important;transform:translateY(6px)!important}.view-swap-enter-active[data-v-51d933fe]{transition:opacity .2s ease .05s,transform .25s ease!important}.view-swap-leave-active[data-v-51d933fe]{transition:opacity .15s ease,transform .15s ease!important}.view-swap-enter-from[data-v-51d933fe]{opacity:0!important;transform:translate(12px)!important}.view-swap-leave-to[data-v-51d933fe]{opacity:0!important;transform:translate(-12px)!important}.list-item-enter-active[data-v-51d933fe]{transition:opacity .3s ease,transform .3s cubic-bezier(.34,1.56,.64,1)!important;transition-delay:calc(var(--i, 0) * .04s)!important}.list-item-leave-active[data-v-51d933fe]{transition:opacity .2s ease,transform .2s ease!important}.list-item-enter-from[data-v-51d933fe]{opacity:0!important;transform:translate(16px)!important}.list-item-leave-to[data-v-51d933fe]{opacity:0!important;transform:translate(-16px) scale(.95)!important}.list-item-move[data-v-51d933fe]{transition:transform .3s ease!important}.toast-enter-active[data-v-51d933fe]{transition:opacity .25s ease,transform .3s cubic-bezier(.34,1.56,.64,1)!important}.toast-leave-active[data-v-51d933fe]{transition:opacity .2s ease,transform .2s ease!important}.toast-enter-from[data-v-51d933fe]{opacity:0!important;transform:translate(-50%) translateY(8px) scale(.9)!important}.toast-leave-to[data-v-51d933fe]{opacity:0!important;transform:translate(-50%) translateY(-4px)!important} ');

  const STORAGE_KEY = "javm-players";
  function storageGet(key) {
    try {
      if (typeof GM_getValue === "function") {
        const v = GM_getValue(key, "");
        if (v) return typeof v === "string" ? v : JSON.stringify(v);
      }
    } catch {
    }
    try {
      return localStorage.getItem(key) ?? "";
    } catch {
    }
    return "";
  }
  function storageSet(key, value) {
    try {
      if (typeof GM_setValue === "function") GM_setValue(key, value);
    } catch {
    }
    try {
      localStorage.setItem(key, value);
    } catch {
    }
  }
  const DEFAULT_PLAYERS = [
    { id: "potplayer", name: "PotPlayer", icon: "🎬", template: "potplayer://{url}", enabled: true, builtin: true },
    { id: "vlc", name: "VLC", icon: "📡", template: "vlc://{encoded_url}", enabled: true, builtin: true },
    { id: "mpc-hc", name: "MPC-HC", icon: "🖥", template: "mpc-hc://open/file?url={encoded_url}", enabled: true, builtin: true },
    { id: "mpv", name: "MPV", icon: "🎵", template: "mpv://play/?url={encoded_url}", enabled: true, builtin: true }
  ];
  function formatPlayerUrl(template, rawUrl) {
    return template.replace(/\{url\}/g, rawUrl).replace(/\{encoded_url\}/g, encodeURIComponent(rawUrl));
  }
  function loadPlayers() {
    try {
      const raw = storageGet(STORAGE_KEY);
      if (!raw) return structuredClone(DEFAULT_PLAYERS);
      const saved = JSON.parse(raw);
      const map = new Map(saved.map((p) => [p.id, p]));
      for (const def of DEFAULT_PLAYERS) {
        if (!map.has(def.id)) {
          saved.push({ ...def });
        }
      }
      return saved;
    } catch {
      return structuredClone(DEFAULT_PLAYERS);
    }
  }
  function savePlayers(players) {
    storageSet(STORAGE_KEY, JSON.stringify(players));
  }
  const POS_KEY = "javm-bubble-pos";
  const REMEMBER_KEY = "javm-remember-pos";
  function loadRememberPosition() {
    const v = storageGet(REMEMBER_KEY);
    return v === "true";
  }
  function saveRememberPosition(val) {
    storageSet(REMEMBER_KEY, val ? "true" : "false");
  }
  function loadBubblePosition() {
    try {
      const raw = storageGet(POS_KEY);
      if (!raw) return null;
      return JSON.parse(raw);
    } catch {
      return null;
    }
  }
  function saveBubblePosition(pos) {
    storageSet(POS_KEY, JSON.stringify(pos));
  }
  const _hoisted_1 = {
    key: 0,
    class: "javm-bubble-wrap"
  };
  const _hoisted_2 = ["title"];
  const _hoisted_3 = { class: "javm-bubble-num" };
  const _hoisted_4 = { class: "javm-panel-header" };
  const _hoisted_5 = { class: "javm-panel-title-area" };
  const _hoisted_6 = {
    key: "list",
    class: "javm-panel-title"
  };
  const _hoisted_7 = {
    key: "settings",
    class: "javm-panel-title"
  };
  const _hoisted_8 = {
    key: "about",
    class: "javm-panel-title"
  };
  const _hoisted_9 = {
    key: 0,
    class: "javm-panel-count"
  };
  const _hoisted_10 = { class: "javm-header-actions" };
  const _hoisted_11 = {
    key: "list",
    class: "javm-panel-body"
  };
  const _hoisted_12 = { class: "javm-item-info" };
  const _hoisted_13 = { class: "javm-badge" };
  const _hoisted_14 = ["title"];
  const _hoisted_15 = { class: "javm-actions" };
  const _hoisted_16 = ["onClick"];
  const _hoisted_17 = ["onClick"];
  const _hoisted_18 = ["onClick", "title"];
  const _hoisted_19 = {
    key: "settings",
    class: "javm-panel-body javm-settings"
  };
  const _hoisted_20 = { class: "javm-settings-section" };
  const _hoisted_21 = { class: "javm-switch" };
  const _hoisted_22 = ["checked", "onChange"];
  const _hoisted_23 = { class: "javm-setting-icon" };
  const _hoisted_24 = { class: "javm-setting-name" };
  const _hoisted_25 = { class: "javm-setting-tpl" };
  const _hoisted_26 = ["onClick"];
  const _hoisted_27 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "12px", "height": "12px" }
  };
  const _hoisted_28 = {
    key: 1,
    class: "javm-setting-tag"
  };
  const _hoisted_29 = { class: "javm-settings-section" };
  const _hoisted_30 = { class: "javm-setting-row" };
  const _hoisted_31 = { class: "javm-switch" };
  const _hoisted_32 = ["checked"];
  const _hoisted_33 = { class: "javm-settings-section" };
  const _hoisted_34 = { class: "javm-add-form" };
  const _hoisted_35 = {
    key: "about",
    class: "javm-panel-body javm-about"
  };
  const _hoisted_36 = { class: "javm-about-ver" };
  const _hoisted_37 = { class: "javm-about-links" };
  const _hoisted_38 = {
    class: "javm-about-link",
    href: "https://github.com/ddmoyu/javm",
    target: "_blank",
    rel: "noopener noreferrer"
  };
  const _hoisted_39 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "16px", "height": "16px", "flex-shrink": "0" }
  };
  const _hoisted_40 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "14px", "height": "14px", "flex-shrink": "0", "opacity": "0.4" }
  };
  const _hoisted_41 = {
    class: "javm-about-link",
    href: "https://github.com/ddmoyu/javm_us",
    target: "_blank",
    rel: "noopener noreferrer"
  };
  const _hoisted_42 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "16px", "height": "16px", "flex-shrink": "0" }
  };
  const _hoisted_43 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "14px", "height": "14px", "flex-shrink": "0", "opacity": "0.4" }
  };
  const _hoisted_44 = {
    key: 0,
    class: "javm-toast"
  };
  const _hoisted_45 = {
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": "2",
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    style: { "width": "14px", "height": "14px", "flex-shrink": "0" }
  };
  const _sfc_main = vue.defineComponent({
    __name: "App",
    setup(__props) {
      const collector2 = vue.inject("collector");
      const version = typeof GM_info !== "undefined" ? GM_info.script.version : "1.0.1";
      const items = vue.ref([]);
      const showPanel = vue.ref(false);
      const showSettings = vue.ref(false);
      const showAbout = vue.ref(false);
      const toastMsg = vue.ref("");
      const bubblePressed = vue.ref(false);
      let toastTimer = 0;
      let offChange = null;
      const players = vue.ref(loadPlayers());
      const enabledPlayers = vue.computed(() => players.value.filter((p) => p.enabled));
      const newPlayer = vue.ref({ name: "", icon: "🔗", template: "" });
      const rememberPos = vue.ref(loadRememberPosition());
      const bubblePos = vue.ref(loadBubblePosition() ?? { bottom: 24, right: 24 });
      const windowWidth = vue.ref(typeof window !== "undefined" ? window.innerWidth : 1920);
      const isDragging = vue.ref(false);
      let dragStartX = 0;
      let dragStartY = 0;
      let dragStartRight = 0;
      let dragStartBottom = 0;
      let hasMoved = false;
      function onBubblePointerDown(e) {
        bubblePressed.value = true;
        isDragging.value = false;
        hasMoved = false;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        dragStartRight = bubblePos.value.right;
        dragStartBottom = bubblePos.value.bottom;
        window.addEventListener("pointermove", onPointerMove);
        window.addEventListener("pointerup", onPointerUp);
      }
      function onPointerMove(e) {
        const dx = e.clientX - dragStartX;
        const dy = e.clientY - dragStartY;
        if (!hasMoved && Math.abs(dx) < 4 && Math.abs(dy) < 4) return;
        hasMoved = true;
        isDragging.value = true;
        const maxRight = window.innerWidth - 56;
        const maxBottom = window.innerHeight - 56;
        bubblePos.value = {
          right: Math.max(0, Math.min(maxRight, dragStartRight - dx)),
          bottom: Math.max(0, Math.min(maxBottom, dragStartBottom - dy))
        };
      }
      function onPointerUp() {
        bubblePressed.value = false;
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("pointerup", onPointerUp);
        if (isDragging.value && rememberPos.value) {
          saveBubblePosition(bubblePos.value);
        }
        setTimeout(() => {
          isDragging.value = false;
        }, 0);
      }
      function onBubbleClick() {
        if (hasMoved) return;
        togglePanel();
      }
      const panelStyle = vue.computed(() => {
        const panelWidth = 500;
        const margin = 8;
        const panelLeftEdge = windowWidth.value - bubblePos.value.right - panelWidth;
        if (panelLeftEdge < margin) {
          const offset = margin - panelLeftEdge;
          return { transform: `translateX(${offset}px)` };
        }
        return {};
      });
      function toggleRememberPos() {
        rememberPos.value = !rememberPos.value;
        saveRememberPosition(rememberPos.value);
        if (rememberPos.value) {
          saveBubblePosition(bubblePos.value);
        }
      }
      function resetPosition() {
        bubblePos.value = { bottom: 24, right: 24 };
        if (rememberPos.value) {
          saveBubblePosition(bubblePos.value);
        }
        toast("已重置位置");
      }
      function onWindowResize() {
        windowWidth.value = window.innerWidth;
      }
      vue.onMounted(() => {
        items.value = collector2.getItems();
        offChange = collector2.onChange(() => {
          items.value = collector2.getItems();
        });
        window.addEventListener("resize", onWindowResize);
      });
      vue.onUnmounted(() => {
        offChange?.();
        window.removeEventListener("resize", onWindowResize);
      });
      function togglePanel() {
        showPanel.value = !showPanel.value;
        showSettings.value = false;
        showAbout.value = false;
      }
      function toast(msg) {
        toastMsg.value = msg;
        clearTimeout(toastTimer);
        toastTimer = window.setTimeout(() => toastMsg.value = "", 2e3);
      }
      function copyUrl(url) {
        GM_setClipboard(url);
        toast("已复制链接");
      }
      const downloadedUrls = new Set();
      function getPageTitle() {
        try {
          if (window.top && window.top.document) {
            return window.top.document.title;
          }
        } catch {
        }
        try {
          const topTitle = GM_getValue("javm_top_title", "");
          if (topTitle) return topTitle;
        } catch {
        }
        return document.title;
      }
      function downloadJavm(item, index) {
        if (downloadedUrls.has(item.url)) {
          toast("该任务已发送,请勿重复下载");
          return;
        }
        let title = getPageTitle() || item.filename;
        if (items.value.length > 1) {
          title += `_${index + 1}`;
        }
        const deeplink = `javm://download?url=${encodeURIComponent(item.url)}&title=${encodeURIComponent(title)}`;
        downloadedUrls.add(item.url);
        const isTop = (() => {
          try {
            return window.self === window.top;
          } catch {
            return false;
          }
        })();
        if (isTop) {
          location.href = deeplink;
        } else {
          GM_openInTab(deeplink, { active: false });
        }
        toast("已发送到 JAVM");
      }
      function launchPlayer(player, url) {
        const href = formatPlayerUrl(player.template, url);
        const a = document.createElement("a");
        a.href = href;
        a.style.display = "none";
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        toast(`已调用 ${player.name}`);
      }
      function togglePlayer(id) {
        const p = players.value.find((x) => x.id === id);
        if (p) {
          p.enabled = !p.enabled;
          savePlayers(players.value);
        }
      }
      function removePlayer(id) {
        players.value = players.value.filter((x) => x.id !== id);
        savePlayers(players.value);
      }
      function addPlayer() {
        const { name, icon, template } = newPlayer.value;
        if (!name.trim() || !template.trim()) {
          toast("名称和协议模板不能为空");
          return;
        }
        const id = `custom-${Date.now()}`;
        players.value.push({ id, name: name.trim(), icon: icon.trim() || "🔗", template: template.trim(), enabled: true, builtin: false });
        savePlayers(players.value);
        newPlayer.value = { name: "", icon: "🔗", template: "" };
        toast(`已添加 ${name}`);
      }
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", {
          class: "javm-root",
          style: vue.normalizeStyle({ bottom: bubblePos.value.bottom + "px", right: bubblePos.value.right + "px" })
        }, [
          vue.createVNode(vue.Transition, { name: "bubble" }, {
            default: vue.withCtx(() => [
              items.value.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
                _cache[7] || (_cache[7] = vue.createElementVNode("div", { class: "javm-bubble-ring" }, null, -1)),
                vue.createElementVNode("button", {
                  class: vue.normalizeClass(["javm-bubble", { active: showPanel.value, pressed: bubblePressed.value, dragging: isDragging.value }]),
                  onClick: onBubbleClick,
                  onPointerdown: onBubblePointerDown,
                  title: `发现 ${items.value.length} 个 m3u8 流`
                }, [
                  _cache[6] || (_cache[6] = vue.createElementVNode("svg", {
                    class: "javm-bubble-icon",
                    viewBox: "0 0 24 24",
                    fill: "none",
                    stroke: "currentColor",
                    "stroke-width": "2",
                    "stroke-linecap": "round",
                    "stroke-linejoin": "round"
                  }, [
                    vue.createElementVNode("polygon", { points: "23 7 16 12 23 17 23 7" }),
                    vue.createElementVNode("rect", {
                      x: "1",
                      y: "5",
                      width: "15",
                      height: "14",
                      rx: "2",
                      ry: "2"
                    })
                  ], -1)),
                  vue.createElementVNode("span", _hoisted_3, vue.toDisplayString(items.value.length), 1)
                ], 42, _hoisted_2)
              ])) : vue.createCommentVNode("", true)
            ]),
            _: 1
          }),
          vue.createVNode(vue.Transition, { name: "panel" }, {
            default: vue.withCtx(() => [
              showPanel.value && items.value.length > 0 ? (vue.openBlock(), vue.createElementBlock("div", {
                key: 0,
                class: "javm-panel",
                style: vue.normalizeStyle(panelStyle.value)
              }, [
                _cache[33] || (_cache[33] = vue.createElementVNode("div", { class: "javm-panel-glow" }, null, -1)),
                vue.createElementVNode("div", _hoisted_4, [
                  vue.createElementVNode("div", _hoisted_5, [
                    vue.createVNode(vue.Transition, {
                      name: "title-swap",
                      mode: "out-in"
                    }, {
                      default: vue.withCtx(() => [
                        !showSettings.value && !showAbout.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_6, "M3U8 流")) : showSettings.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_7, "播放器设置")) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_8, "关于"))
                      ]),
                      _: 1
                    }),
                    !showSettings.value && !showAbout.value ? (vue.openBlock(), vue.createElementBlock("span", _hoisted_9, vue.toDisplayString(items.value.length), 1)) : vue.createCommentVNode("", true)
                  ]),
                  vue.createElementVNode("div", _hoisted_10, [
                    vue.createElementVNode("button", {
                      class: vue.normalizeClass(["javm-icon-btn", { "is-active": showSettings.value }]),
                      onClick: _cache[0] || (_cache[0] = ($event) => {
                        showSettings.value = !showSettings.value;
                        showAbout.value = false;
                      }),
                      title: "设置"
                    }, [..._cache[8] || (_cache[8] = [
                      vue.createElementVNode("svg", {
                        viewBox: "0 0 24 24",
                        fill: "none",
                        stroke: "currentColor",
                        "stroke-width": "2",
                        "stroke-linecap": "round",
                        "stroke-linejoin": "round"
                      }, [
                        vue.createElementVNode("circle", {
                          cx: "12",
                          cy: "12",
                          r: "3"
                        }),
                        vue.createElementVNode("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
                      ], -1)
                    ])], 2),
                    vue.createElementVNode("button", {
                      class: vue.normalizeClass(["javm-icon-btn", { "is-active": showAbout.value }]),
                      onClick: _cache[1] || (_cache[1] = ($event) => {
                        showAbout.value = !showAbout.value;
                        showSettings.value = false;
                      }),
                      title: "关于"
                    }, [..._cache[9] || (_cache[9] = [
                      vue.createElementVNode("svg", {
                        viewBox: "0 0 24 24",
                        fill: "none",
                        stroke: "currentColor",
                        "stroke-width": "2",
                        "stroke-linecap": "round",
                        "stroke-linejoin": "round"
                      }, [
                        vue.createElementVNode("circle", {
                          cx: "12",
                          cy: "12",
                          r: "10"
                        }),
                        vue.createElementVNode("line", {
                          x1: "12",
                          y1: "16",
                          x2: "12",
                          y2: "12"
                        }),
                        vue.createElementVNode("line", {
                          x1: "12",
                          y1: "8",
                          x2: "12.01",
                          y2: "8"
                        })
                      ], -1)
                    ])], 2),
                    vue.createElementVNode("button", {
                      class: "javm-icon-btn javm-close-btn",
                      onClick: _cache[2] || (_cache[2] = ($event) => showPanel.value = false),
                      title: "关闭"
                    }, [..._cache[10] || (_cache[10] = [
                      vue.createElementVNode("svg", {
                        viewBox: "0 0 24 24",
                        fill: "none",
                        stroke: "currentColor",
                        "stroke-width": "2",
                        "stroke-linecap": "round",
                        "stroke-linejoin": "round"
                      }, [
                        vue.createElementVNode("line", {
                          x1: "18",
                          y1: "6",
                          x2: "6",
                          y2: "18"
                        }),
                        vue.createElementVNode("line", {
                          x1: "6",
                          y1: "6",
                          x2: "18",
                          y2: "18"
                        })
                      ], -1)
                    ])])
                  ])
                ]),
                vue.createVNode(vue.Transition, {
                  name: "view-swap",
                  mode: "out-in"
                }, {
                  default: vue.withCtx(() => [
                    !showSettings.value && !showAbout.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_11, [
                      vue.createVNode(vue.TransitionGroup, {
                        name: "list-item",
                        tag: "div"
                      }, {
                        default: vue.withCtx(() => [
                          (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(items.value, (item, i) => {
                            return vue.openBlock(), vue.createElementBlock("div", {
                              key: item.url,
                              class: "javm-item",
                              style: vue.normalizeStyle({ "--i": i })
                            }, [
                              vue.createElementVNode("div", _hoisted_12, [
                                vue.createElementVNode("span", _hoisted_13, vue.toDisplayString(item.resolution), 1),
                                vue.createElementVNode("span", {
                                  class: "javm-filename",
                                  title: item.url
                                }, vue.toDisplayString(item.filename), 9, _hoisted_14)
                              ]),
                              vue.createElementVNode("div", _hoisted_15, [
                                vue.createElementVNode("button", {
                                  class: "javm-act-btn javm-act-copy",
                                  onClick: ($event) => copyUrl(item.url),
                                  title: "复制链接"
                                }, [..._cache[11] || (_cache[11] = [
                                  vue.createElementVNode("svg", {
                                    viewBox: "0 0 24 24",
                                    fill: "none",
                                    stroke: "currentColor",
                                    "stroke-width": "2",
                                    "stroke-linecap": "round",
                                    "stroke-linejoin": "round"
                                  }, [
                                    vue.createElementVNode("rect", {
                                      x: "9",
                                      y: "9",
                                      width: "13",
                                      height: "13",
                                      rx: "2",
                                      ry: "2"
                                    }),
                                    vue.createElementVNode("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" })
                                  ], -1)
                                ])], 8, _hoisted_16),
                                vue.createElementVNode("button", {
                                  class: "javm-act-btn javm-act-dl",
                                  onClick: ($event) => downloadJavm(item, i),
                                  title: "JAVM 下载"
                                }, [..._cache[12] || (_cache[12] = [
                                  vue.createElementVNode("svg", {
                                    viewBox: "0 0 24 24",
                                    fill: "none",
                                    stroke: "currentColor",
                                    "stroke-width": "2",
                                    "stroke-linecap": "round",
                                    "stroke-linejoin": "round"
                                  }, [
                                    vue.createElementVNode("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
                                    vue.createElementVNode("polyline", { points: "7 10 12 15 17 10" }),
                                    vue.createElementVNode("line", {
                                      x1: "12",
                                      y1: "15",
                                      x2: "12",
                                      y2: "3"
                                    })
                                  ], -1)
                                ])], 8, _hoisted_17),
                                (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(enabledPlayers.value, (p) => {
                                  return vue.openBlock(), vue.createElementBlock("button", {
                                    key: p.id,
                                    class: "javm-act-btn javm-act-player",
                                    onClick: ($event) => launchPlayer(p, item.url),
                                    title: p.name
                                  }, vue.toDisplayString(p.icon), 9, _hoisted_18);
                                }), 128))
                              ])
                            ], 4);
                          }), 128))
                        ]),
                        _: 1
                      })
                    ])) : showSettings.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_19, [
                      vue.createElementVNode("div", _hoisted_20, [
                        _cache[15] || (_cache[15] = vue.createElementVNode("div", { class: "javm-settings-label" }, "播放器列表", -1)),
                        vue.createVNode(vue.TransitionGroup, {
                          name: "list-item",
                          tag: "div"
                        }, {
                          default: vue.withCtx(() => [
                            (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(players.value, (p, i) => {
                              return vue.openBlock(), vue.createElementBlock("div", {
                                key: p.id,
                                class: "javm-setting-row",
                                style: vue.normalizeStyle({ "--i": i })
                              }, [
                                vue.createElementVNode("label", _hoisted_21, [
                                  vue.createElementVNode("input", {
                                    type: "checkbox",
                                    checked: p.enabled,
                                    onChange: ($event) => togglePlayer(p.id)
                                  }, null, 40, _hoisted_22),
                                  _cache[13] || (_cache[13] = vue.createElementVNode("span", { class: "javm-slider" }, null, -1))
                                ]),
                                vue.createElementVNode("span", _hoisted_23, vue.toDisplayString(p.icon), 1),
                                vue.createElementVNode("span", _hoisted_24, vue.toDisplayString(p.name), 1),
                                vue.createElementVNode("span", _hoisted_25, vue.toDisplayString(p.template), 1),
                                !p.builtin ? (vue.openBlock(), vue.createElementBlock("button", {
                                  key: 0,
                                  class: "javm-setting-del",
                                  onClick: ($event) => removePlayer(p.id),
                                  title: "删除"
                                }, [
                                  (vue.openBlock(), vue.createElementBlock("svg", _hoisted_27, [..._cache[14] || (_cache[14] = [
                                    vue.createElementVNode("line", {
                                      x1: "18",
                                      y1: "6",
                                      x2: "6",
                                      y2: "18"
                                    }, null, -1),
                                    vue.createElementVNode("line", {
                                      x1: "6",
                                      y1: "6",
                                      x2: "18",
                                      y2: "18"
                                    }, null, -1)
                                  ])]))
                                ], 8, _hoisted_26)) : (vue.openBlock(), vue.createElementBlock("span", _hoisted_28, "内置"))
                              ], 4);
                            }), 128))
                          ]),
                          _: 1
                        })
                      ]),
                      vue.createElementVNode("div", _hoisted_29, [
                        _cache[18] || (_cache[18] = vue.createElementVNode("div", { class: "javm-settings-label" }, "气泡位置", -1)),
                        vue.createElementVNode("div", _hoisted_30, [
                          vue.createElementVNode("label", _hoisted_31, [
                            vue.createElementVNode("input", {
                              type: "checkbox",
                              checked: rememberPos.value,
                              onChange: toggleRememberPos
                            }, null, 40, _hoisted_32),
                            _cache[16] || (_cache[16] = vue.createElementVNode("span", { class: "javm-slider" }, null, -1))
                          ]),
                          _cache[17] || (_cache[17] = vue.createElementVNode("span", {
                            class: "javm-setting-name",
                            style: { "flex": "1" }
                          }, "记住位置", -1)),
                          vue.createElementVNode("button", {
                            class: "javm-reset-btn",
                            onClick: resetPosition,
                            title: "重置到右下角"
                          }, "重置")
                        ]),
                        _cache[19] || (_cache[19] = vue.createElementVNode("div", { class: "javm-hint" }, "拖拽气泡按钮可自由移动位置", -1))
                      ]),
                      vue.createElementVNode("div", _hoisted_33, [
                        _cache[21] || (_cache[21] = vue.createElementVNode("div", { class: "javm-settings-label" }, "添加自定义播放器", -1)),
                        vue.createElementVNode("div", _hoisted_34, [
                          vue.withDirectives(vue.createElementVNode("input", {
                            "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => newPlayer.value.icon = $event),
                            class: "javm-input javm-input-icon",
                            placeholder: "📦",
                            maxlength: "2"
                          }, null, 512), [
                            [vue.vModelText, newPlayer.value.icon]
                          ]),
                          vue.withDirectives(vue.createElementVNode("input", {
                            "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => newPlayer.value.name = $event),
                            class: "javm-input javm-input-name",
                            placeholder: "名称"
                          }, null, 512), [
                            [vue.vModelText, newPlayer.value.name]
                          ]),
                          vue.withDirectives(vue.createElementVNode("input", {
                            "onUpdate:modelValue": _cache[5] || (_cache[5] = ($event) => newPlayer.value.template = $event),
                            class: "javm-input javm-input-tpl",
                            placeholder: "proto://{url}"
                          }, null, 512), [
                            [vue.vModelText, newPlayer.value.template]
                          ]),
                          vue.createElementVNode("button", {
                            class: "javm-add-btn",
                            onClick: addPlayer
                          }, [..._cache[20] || (_cache[20] = [
                            vue.createElementVNode("svg", {
                              viewBox: "0 0 24 24",
                              fill: "none",
                              stroke: "currentColor",
                              "stroke-width": "2.5",
                              "stroke-linecap": "round",
                              "stroke-linejoin": "round"
                            }, [
                              vue.createElementVNode("line", {
                                x1: "12",
                                y1: "5",
                                x2: "12",
                                y2: "19"
                              }),
                              vue.createElementVNode("line", {
                                x1: "5",
                                y1: "12",
                                x2: "19",
                                y2: "12"
                              })
                            ], -1)
                          ])])
                        ]),
                        _cache[22] || (_cache[22] = vue.createElementVNode("div", { class: "javm-hint" }, [
                          vue.createTextVNode(" 模板变量: "),
                          vue.createElementVNode("code", null, "{url}"),
                          vue.createTextVNode(" 原始链接 · "),
                          vue.createElementVNode("code", null, "{encoded_url}"),
                          vue.createTextVNode(" 编码后链接 ")
                        ], -1))
                      ])
                    ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_35, [
                      _cache[29] || (_cache[29] = vue.createElementVNode("div", { class: "javm-about-logo" }, [
                        vue.createElementVNode("svg", {
                          viewBox: "0 0 24 24",
                          fill: "none",
                          stroke: "currentColor",
                          "stroke-width": "1.5",
                          "stroke-linecap": "round",
                          "stroke-linejoin": "round"
                        }, [
                          vue.createElementVNode("polygon", { points: "23 7 16 12 23 17 23 7" }),
                          vue.createElementVNode("rect", {
                            x: "1",
                            y: "5",
                            width: "15",
                            height: "14",
                            rx: "2",
                            ry: "2"
                          })
                        ])
                      ], -1)),
                      _cache[30] || (_cache[30] = vue.createElementVNode("div", { class: "javm-about-name" }, "JAVM M3U8 Helper", -1)),
                      vue.createElementVNode("div", _hoisted_36, "v" + vue.toDisplayString(vue.unref(version)), 1),
                      _cache[31] || (_cache[31] = vue.createElementVNode("div", { class: "javm-about-desc" }, "自动检测页面中的 m3u8 视频流,一键下载或播放。", -1)),
                      vue.createElementVNode("div", _hoisted_37, [
                        vue.createElementVNode("a", _hoisted_38, [
                          (vue.openBlock(), vue.createElementBlock("svg", _hoisted_39, [..._cache[23] || (_cache[23] = [
                            vue.createElementVNode("path", { d: "M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" }, null, -1)
                          ])])),
                          _cache[25] || (_cache[25] = vue.createElementVNode("div", { class: "javm-about-link-text" }, [
                            vue.createElementVNode("span", { class: "javm-about-link-title" }, "JAVM"),
                            vue.createElementVNode("span", { class: "javm-about-link-sub" }, "Jav 视频管理工具,包含:刮削,下载,播放")
                          ], -1)),
                          (vue.openBlock(), vue.createElementBlock("svg", _hoisted_40, [..._cache[24] || (_cache[24] = [
                            vue.createElementVNode("line", {
                              x1: "7",
                              y1: "17",
                              x2: "17",
                              y2: "7"
                            }, null, -1),
                            vue.createElementVNode("polyline", { points: "7 7 17 7 17 17" }, null, -1)
                          ])]))
                        ]),
                        vue.createElementVNode("a", _hoisted_41, [
                          (vue.openBlock(), vue.createElementBlock("svg", _hoisted_42, [..._cache[26] || (_cache[26] = [
                            vue.createElementVNode("path", { d: "M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" }, null, -1)
                          ])])),
                          _cache[28] || (_cache[28] = vue.createElementVNode("div", { class: "javm-about-link-text" }, [
                            vue.createElementVNode("span", { class: "javm-about-link-title" }, "JAVM UserScript"),
                            vue.createElementVNode("span", { class: "javm-about-link-sub" }, "M3U8 检测油猴脚本,配合 JAVM 使用")
                          ], -1)),
                          (vue.openBlock(), vue.createElementBlock("svg", _hoisted_43, [..._cache[27] || (_cache[27] = [
                            vue.createElementVNode("line", {
                              x1: "7",
                              y1: "17",
                              x2: "17",
                              y2: "7"
                            }, null, -1),
                            vue.createElementVNode("polyline", { points: "7 7 17 7 17 17" }, null, -1)
                          ])]))
                        ])
                      ])
                    ]))
                  ]),
                  _: 1
                }),
                vue.createVNode(vue.Transition, { name: "toast" }, {
                  default: vue.withCtx(() => [
                    toastMsg.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_44, [
                      (vue.openBlock(), vue.createElementBlock("svg", _hoisted_45, [..._cache[32] || (_cache[32] = [
                        vue.createElementVNode("path", { d: "M22 11.08V12a10 10 0 1 1-5.93-9.14" }, null, -1),
                        vue.createElementVNode("polyline", { points: "22 4 12 14.01 9 11.01" }, null, -1)
                      ])])),
                      vue.createTextVNode(" " + vue.toDisplayString(toastMsg.value), 1)
                    ])) : vue.createCommentVNode("", true)
                  ]),
                  _: 1
                })
              ], 4)) : vue.createCommentVNode("", true)
            ]),
            _: 1
          })
        ], 4);
      };
    }
  });
  const _export_sfc = (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key, val] of props) {
      target[key] = val;
    }
    return target;
  };
  const App = _export_sfc(_sfc_main, [["__scopeId", "data-v-51d933fe"]]);
  function checkUrl(url) {
    try {
      const u = new URL(url, location.href);
      return u.pathname.endsWith(".m3u8") || u.pathname.endsWith(".m3u");
    } catch {
      return /\.m3u8?(?:[?#]|$)/i.test(url);
    }
  }
  function checkContent(content) {
    return content.trim().startsWith("#EXTM3U");
  }
  function guessResolution(url, index) {
    const m = url.match(/[_\-/](\d{3,4})[pP]|(\d{3,4})x\d{3,4}|[_\-](hd|sd|fhd|uhd|4k|2k|1080|720|480|360)[_\-/]/i);
    if (m) {
      if (m[1]) return `${m[1]}p`;
      if (m[2]) return `${m[2]}p`;
      if (m[3]) return m[3].toUpperCase();
    }
    return `流 ${index + 1}`;
  }
  function urlToFilename(url) {
    try {
      const path = new URL(url).pathname;
      return decodeURIComponent(path.split("/").pop() ?? url);
    } catch {
      return url;
    }
  }
  function buildItems(urls) {
    return urls.map((url, i) => ({
      url,
      resolution: guessResolution(url, i),
      filename: urlToFilename(url)
    }));
  }
  function scanVideoElements() {
    const urls = [];
    document.querySelectorAll("video, source, video source").forEach((el) => {
      const src = el.src || el.getAttribute("src") || "";
      if (checkUrl(src)) urls.push(src);
      const dataSrc = el.getAttribute("data-src") || "";
      if (checkUrl(dataSrc)) urls.push(dataSrc);
    });
    return urls;
  }
  function hookNetwork(onFound) {
    const w = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
    const origResponseText = w.Response.prototype.text;
    w.Response.prototype.text = function() {
      return new Promise((resolve, reject) => {
        origResponseText.call(this).then((text) => {
          resolve(text);
          if (checkContent(text)) {
            onFound(this.url);
          }
        }).catch(reject);
      });
    };
    const origOpen = w.XMLHttpRequest.prototype.open;
    w.XMLHttpRequest.prototype.open = function(...args) {
      this.addEventListener("load", () => {
        try {
          const content = this.responseText;
          if (checkContent(content)) {
            onFound(args[1]);
          }
        } catch {
        }
        try {
          const reqUrl = String(args[1]);
          if (checkUrl(reqUrl)) {
            onFound(reqUrl);
          }
        } catch {
        }
      });
      return origOpen.apply(this, args);
    };
  }
  function createCollector() {
    const found = new Set();
    const listeners = new Set();
    function notify() {
      for (const fn of listeners) fn();
    }
    function add(url) {
      const clean = url.trim();
      if (!clean || found.has(clean)) return;
      try {
        const full = new URL(clean, location.href).href;
        if (found.has(full)) return;
        found.add(full);
      } catch {
        if (found.has(clean)) return;
        found.add(clean);
      }
      notify();
    }
    function onChange(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    }
    function getItems() {
      return buildItems([...found]);
    }
    hookNetwork(add);
    let scanTimer = 0;
    function debouncedScan() {
      if (scanTimer) return;
      scanTimer = window.setTimeout(() => {
        scanTimer = 0;
        scanVideoElements().forEach(add);
      }, 300);
    }
    setInterval(debouncedScan, 3e3);
    const observer = new MutationObserver(debouncedScan);
    if (document.documentElement) {
      observer.observe(document.documentElement, { childList: true, subtree: true });
    } else {
      const waitDOM = setInterval(() => {
        if (document.documentElement) {
          clearInterval(waitDOM);
          observer.observe(document.documentElement, { childList: true, subtree: true });
        }
      }, 50);
    }
    return { add, onChange, getItems };
  }
  function isTopFrame() {
    try {
      return window.self === window.top;
    } catch {
      return false;
    }
  }
  if (isTopFrame()) {
    const storeTitle = () => {
      if (document.title) {
        GM_setValue("javm_top_title", document.title);
        GM_setValue("javm_top_origin", location.origin);
      }
    };
    document.addEventListener("DOMContentLoaded", storeTitle);
    window.addEventListener("load", storeTitle);
    const obs = new MutationObserver(storeTitle);
    const tryObserve = () => {
      const titleEl = document.querySelector("title");
      if (titleEl) {
        obs.observe(titleEl, { childList: true, characterData: true, subtree: true });
      } else if (document.head) {
        obs.observe(document.head, { childList: true, subtree: true });
      }
    };
    if (document.head) tryObserve();
    else document.addEventListener("DOMContentLoaded", tryObserve);
  }
  const collector = createCollector();
  function mountUI() {
    const host = document.createElement("div");
    host.id = "javm-us-host";
    document.documentElement.appendChild(host);
    const app = vue.createApp(App);
    app.provide("collector", collector);
    app.mount(host);
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", mountUI);
  } else {
    mountUI();
  }

})(Vue);