91 Plus

自由轉調、輕鬆練歌,打造 91 譜的最佳體驗!

目前为 2024-03-28 提交的版本。查看 最新版本

// ==UserScript==
// @name         91 Plus
// @namespace    https://github.com/DonkeyBear
// @version      1.8.2
// @author       DonkeyBear
// @description  自由轉調、輕鬆練歌,打造 91 譜的最佳體驗!
// @icon         https://www.91pu.com.tw/icons/favicon-32x32.png
// @match        *://www.91pu.com.tw/m/*
// @match        *://www.91pu.com.tw/song/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/pinia.iife.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/zipson.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/html2canvas.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vexchords.dev.min.js
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @antifeature  tracking  使用 Google Analytics 了解使用情況
// ==/UserScript==

(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const a=document.createElement("style");a.textContent=e,document.head.append(a)})(' @import"https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css";#trigger-overlay[data-v-77b574f3]{position:fixed;top:0;left:0;right:0;bottom:0;z-index:500}.bi[data-v-ff0eef21]{color:var(--4f7e9938);font-size:var(--3ce130e8);-webkit-text-stroke:var(--882971fa) var(--4f7e9938)}.bi[data-v-ff0eef21]:before{transition:text-shadow .2s}.bi[active=true][data-v-ff0eef21]:before{text-shadow:0 0 .5rem rgb(75,156,169)}.toolbar-icon[data-v-a789fc6b]{cursor:pointer;padding:.25rem .75rem}.adjust-widget[data-v-cb8ab81d]{display:flex}.adjust-widget .adjust-button[data-v-cb8ab81d]{border:0;border-radius:.25rem;background:transparent}.adjust-widget .adjust-button[data-v-cb8ab81d]:hover{background:rgba(0,0,0,.025)}.adjust-widget .adjust-button[data-v-cb8ab81d]:disabled{opacity:.25}.adjust-widget .adjust-button.adjust-button-middle[data-v-cb8ab81d]{flex-grow:1;color:var(--13e75dc8);font-size:calc(var(--10392328) * .75);font-weight:700}.adjust-widget .adjust-button.adjust-button-left[data-v-cb8ab81d]{padding-right:1rem}.adjust-widget .adjust-button.adjust-button-right[data-v-cb8ab81d]{padding-left:1rem}.slide-and-fade-enter-active[data-v-364cc4c8],.slide-and-fade-leave-active[data-v-364cc4c8]{transition:all .2s}.slide-and-fade-enter-from[data-v-364cc4c8],.slide-and-fade-leave-to[data-v-364cc4c8]{transform:translateY(10%);opacity:0}#plus91-sheet-popup[data-v-364cc4c8]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;padding:1rem 2rem;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll}#plus91-sheet-popup[data-v-364cc4c8]::-webkit-scrollbar{display:none}.transpose-range-container[data-v-364cc4c8]{margin-top:1rem}.transpose-range-container input[type=range][data-v-364cc4c8]{width:100%}.instrument-select-container[data-v-364cc4c8]{display:flex;border:1px solid lightgray;border-radius:.25rem;margin-top:1rem;background:white}.instrument-select-container .instrument-select-button[data-v-364cc4c8]{width:33.3333333333%;border:0;border-right:1px solid lightgray;background:transparent;color:#666;padding:.5rem;font-size:.65rem;font-weight:700;cursor:pointer!important}.instrument-select-container .instrument-select-button[data-v-364cc4c8]:last-child{border:0;border-radius:0 .25rem .25rem 0}.instrument-select-container .instrument-select-button[data-v-364cc4c8]:first-child{border-radius:.25rem 0 0 .25rem}.instrument-select-container .instrument-select-button[data-v-364cc4c8]:hover{background:whitesmoke}.chord-container .chord-name[data-v-735734f6]{font-size:.5rem;font-weight:900;color:#666;text-align:center}.chord-container .chord-chart[data-v-735734f6]{margin:-.6rem 0 -.25rem}.slide-and-fade-enter-active[data-v-110e9a91],.slide-and-fade-leave-active[data-v-110e9a91]{transition:all .2s}.slide-and-fade-enter-from[data-v-110e9a91],.slide-and-fade-leave-to[data-v-110e9a91]{transform:translateY(10%);opacity:0}#plus91-chord-popup[data-v-110e9a91]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll;padding:1rem}#plus91-chord-popup[data-v-110e9a91]::-webkit-scrollbar{display:none}#plus91-chord-popup .banner[data-v-110e9a91]{display:flex;align-items:center;background:rgba(75,156,169,.25);color:color-mix(in srgb,rgba(75,156,169,.65) 50%,black 50%);border-radius:.5rem;padding:.5rem .75rem;margin-bottom:.25rem}#plus91-chord-popup .banner section[data-v-110e9a91]{flex-grow:1;margin-left:.5rem}#plus91-chord-popup .chord-popup-container[data-v-110e9a91]{display:grid;grid-template-columns:repeat(6,1fr);column-gap:.5rem;padding-top:.4rem}.slide-and-fade-enter-active[data-v-3be5b338],.slide-and-fade-leave-active[data-v-3be5b338]{transition:all .2s}.slide-and-fade-enter-from[data-v-3be5b338],.slide-and-fade-leave-to[data-v-3be5b338]{transform:translateY(10%);opacity:0}#plus91-font-popup[data-v-3be5b338]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;padding:1rem 2rem;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll}#plus91-font-popup[data-v-3be5b338]::-webkit-scrollbar{display:none}.slide-and-fade-enter-active[data-v-430d5768],.slide-and-fade-leave-active[data-v-430d5768]{transition:all .2s}.slide-and-fade-enter-from[data-v-430d5768],.slide-and-fade-leave-to[data-v-430d5768]{transform:translateY(10%);opacity:0}#plus91-settings-popup[data-v-430d5768]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll;padding:1rem}#plus91-settings-popup[data-v-430d5768]::-webkit-scrollbar{display:none}#plus91-settings-popup .setting-item[data-v-430d5768]{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;border-radius:.5rem;cursor:pointer}#plus91-settings-popup .setting-item[data-v-430d5768]:hover{background:rgba(0,0,0,.05)}.icon-button[data-v-e9653884]{display:flex;flex-direction:column;align-items:center;cursor:pointer;padding:0 .6rem .4rem;border-radius:.25rem}.icon-button[data-v-e9653884]:hover{background:rgba(0,0,0,.025)}.icon-button .button-text[data-v-e9653884]{font-size:.5rem;color:var(--5908aad9)}.slide-and-fade-enter-active[data-v-fca8d708],.slide-and-fade-leave-active[data-v-fca8d708]{transition:all .2s}.slide-and-fade-enter-from[data-v-fca8d708],.slide-and-fade-leave-to[data-v-fca8d708]{transform:translateY(10%);opacity:0}#plus91-menu-popup[data-v-fca8d708]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;padding:1rem 2rem;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll}#plus91-menu-popup[data-v-fca8d708]::-webkit-scrollbar{display:none}#plus91-menu-popup .menu-popup-container[data-v-fca8d708]{display:flex;justify-content:space-around}.hotkey-item[data-v-3c43f6cf]{display:flex;justify-content:space-between;align-items:center;padding:0 .25rem;border-radius:.25rem;height:1.4rem}.hotkey-item[data-v-3c43f6cf]:nth-child(odd){background:rgba(0,0,0,.025)}.desc.title[data-v-3c43f6cf]{font-size:.55rem;color:#999}.hotkeys[data-v-3c43f6cf]{display:flex}.hr[data-v-3c43f6cf]{display:flex;flex-grow:1;border-top:1px solid lightgray;margin-left:.25rem}kbd[data-v-3c43f6cf]{font-size:.6rem;border:solid lightgray;border-width:1px .1rem .15rem;border-radius:.2rem;padding:0 .2rem;letter-spacing:-.025rem;color:#666;margin-left:.15rem}.slide-and-fade-enter-active[data-v-eb86b87c],.slide-and-fade-leave-active[data-v-eb86b87c]{transition:all .2s}.slide-and-fade-enter-from[data-v-eb86b87c],.slide-and-fade-leave-to[data-v-eb86b87c]{transform:translateY(10%);opacity:0}#plus91-hotkey-popup[data-v-eb86b87c]{position:absolute;left:0;right:0;bottom:100%;background:#fafafa;border:1px solid lightgray;padding:1rem 2rem;border-radius:1rem;margin:.5rem 1rem;max-height:50vh;overflow-y:scroll}#plus91-hotkey-popup[data-v-eb86b87c]::-webkit-scrollbar{display:none}#plus91-hotkey-popup .hotkey-popup-container[data-v-eb86b87c]{display:flex;color:#444}#plus91-hotkey-popup section[data-v-eb86b87c]{flex-grow:1;width:50%;margin:-.1rem 0}#plus91-hotkey-popup section.left-part[data-v-eb86b87c]{border-right:1px solid lightgray;margin-left:-.5rem;padding-right:.5rem}#plus91-hotkey-popup section.right-part[data-v-eb86b87c]{padding-left:.5rem;margin-right:-.5rem}#plus91-hotkey-popup kbd[data-v-eb86b87c]{font-size:.65rem;border:solid lightgray;border-width:1px .1rem .15rem;border-radius:.2rem;padding:0 .2rem;letter-spacing:-.025rem;color:#666}.slide-enter-active[data-v-d7417138],.slide-leave-active[data-v-d7417138]{transition:transform .2s}.slide-enter-from[data-v-d7417138],.slide-leave-to[data-v-d7417138]{transform:translateY(100%)}#plus91-footer[data-v-d7417138]{z-index:1000;display:flex;justify-content:center;position:fixed;left:0;right:0;bottom:0}.footer-container[data-v-d7417138]{width:min(100vw,768px);background:rgba(75,156,169,.65);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);padding:.25rem .5rem .75rem;display:flex;justify-content:space-between;align-items:center;border-radius:1rem 1rem 0 0;border:1px solid rgb(90,140,160);border-bottom:0}.slide-enter-active[data-v-a1982e9d],.slide-leave-active[data-v-a1982e9d]{transition:transform .2s}.slide-enter-from[data-v-a1982e9d],.slide-leave-to[data-v-a1982e9d]{transform:translateY(-100%)}#plus91-header[data-v-a1982e9d]{z-index:1000;display:flex;justify-content:center;position:fixed;left:0;right:0;top:0}.header-container[data-v-a1982e9d]{width:min(100vw,768px);background:rgba(75,156,169,.65);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);padding:.25rem .5rem;display:flex;justify-content:space-between;align-items:center;border-radius:0 0 1rem 1rem;border:1px solid rgb(90,140,160);border-top:0}.header-container input[data-v-a1982e9d]{flex-grow:1;width:100%;border-radius:50rem;border:0;font-size:.8rem;font-weight:700;padding:.35rem 1.25rem;background:rgba(255,255,255,.6666666667);color:#0009;opacity:.5;transition:all .2s}.header-container input[data-v-a1982e9d]:focus-visible{outline:0;opacity:1}.fade-enter-active[data-v-a9f152b4],.fade-leave-active[data-v-a9f152b4]{transition:opacity .2s}.fade-enter-from[data-v-a9f152b4],.fade-leave-to[data-v-a9f152b4]{opacity:0}#dark-mode-overlay[data-v-a9f152b4]{position:fixed;top:0;left:0;right:0;bottom:0;z-index:800;-webkit-backdrop-filter:invert(1) hue-rotate(145deg) saturate(.75);backdrop-filter:invert(1) hue-rotate(145deg) saturate(.75);pointer-events:none}html{background:#fafafa url(/templets/pu/images/tone-bg.gif)}#vue-91plus{font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif}.tfunc2{margin:10px}#mtitle{font-family:system-ui}input[type=range],input[type=range]::-webkit-slider-thumb,input[type=range]::-webkit-slider-runnable-track{-webkit-appearance:none;box-shadow:none}input[type=range]::-webkit-slider-thumb,input[type=range]::-webkit-slider-runnable-track{border:1px solid rgba(68,68,68,.25)}input[type=range]::-webkit-slider-thumb{background:#60748d}#viptoneWindow.window,#bottomad,.update_vip_bar,.wmask,header,footer,.autoscroll,.backplace,.set .keys,.set .plays,.set .clear,.setint .hr:nth-child(4),.setint .hr:nth-child(5),.setint .hr:nth-child(6),.adsbygoogle{display:none!important} ');

(function (vue, pinia$1, piniaPluginPersistedstate, zipson, vexchords, html2canvas) {
  'use strict';

  var __defProp = Object.defineProperty;
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  var __publicField = (obj, key, value) => {
    __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
    return value;
  };
  var __accessCheck = (obj, member, msg) => {
    if (!member.has(obj))
      throw TypeError("Cannot " + msg);
  };
  var __privateGet = (obj, member, getter) => {
    __accessCheck(obj, member, "read from private field");
    return getter ? getter.call(obj) : member.get(obj);
  };
  var __privateAdd = (obj, member, value) => {
    if (member.has(obj))
      throw TypeError("Cannot add the same private member more than once");
    member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
  };
  var __privateSet = (obj, member, value, setter) => {
    __accessCheck(obj, member, "write to private field");
    setter ? setter.call(obj, value) : member.set(obj, value);
    return value;
  };
  var __privateMethod = (obj, member, method) => {
    __accessCheck(obj, member, "access private method");
    return method;
  };
  var _unformat, unformat_fn, _store, _watchTranspose, watchTranspose_fn, _watchFontSize, watchFontSize_fn;
  const _export_sfc = (sfc, props) => {
    const target = sfc.__vccOpts || sfc;
    for (const [key, val] of props) {
      target[key] = val;
    }
    return target;
  };
  const _sfc_main$g = {};
  const _hoisted_1$f = { id: "trigger-overlay" };
  function _sfc_render(_ctx, _cache) {
    return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$f);
  }
  const TriggerOverlay = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["render", _sfc_render], ["__scopeId", "data-v-77b574f3"]]);
  const _Chord = class _Chord {
    /** @param {string} chordString  */
    constructor(chordString) {
      this.chordString = chordString;
    }
    /**
     * @param {number} delta
     * @returns {Chord}
     */
    transpose(delta) {
      this.chordString = this.chordString.replaceAll(/[A-G][#b]?/g, (note) => {
        const isSharp = _Chord.sharps.includes(note);
        const scale = isSharp ? _Chord.sharps : _Chord.flats;
        const noteIndex = scale.indexOf(note);
        const transposedIndex = (noteIndex + delta + 12) % 12;
        const transposedNote = scale[transposedIndex];
        return transposedNote;
      });
      return this;
    }
    /** @returns {Chord} */
    switchModifier() {
      this.chordString = this.chordString.replaceAll(/[A-G][#b]/g, (note) => {
        const scale = note.includes("#") ? _Chord.sharps : _Chord.flats;
        const newScale = note.includes("#") ? _Chord.flats : _Chord.sharps;
        const noteIndex = scale.indexOf(note);
        return newScale[noteIndex];
      });
      return this;
    }
    /** @returns {Chord} */
    useSharpModifier() {
      this.chordString = this.chordString.replaceAll(/[A-G]b/g, (note) => {
        const noteIndex = _Chord.flats.indexOf(note);
        return _Chord.sharps[noteIndex];
      });
      return this;
    }
    /** @returns {Chord} */
    useFlatModifier() {
      this.chordString = this.chordString.replaceAll(/[A-G]#/g, (note) => {
        const noteIndex = _Chord.sharps.indexOf(note);
        return _Chord.flats[noteIndex];
      });
      return this;
    }
    /** @returns {string} */
    toString() {
      return this.chordString;
    }
    /** @returns {string} */
    toFormattedString() {
      return this.chordString.replaceAll(
        /[#b]/g,
        /* html */
        `<sup>$&</sup>`
      );
    }
  };
  __publicField(_Chord, "sharps", ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]);
  __publicField(_Chord, "flats", ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]);
  let Chord = _Chord;
  var _GM_getValue = /* @__PURE__ */ (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)();
  var _GM_setValue = /* @__PURE__ */ (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)();
  var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)();
  class MonkeyStorage {
    /**
     * @param {String} key 
     * @returns {String|null}
     */
    static getItem(key) {
      if (_GM_getValue) {
        return _GM_getValue(key, null);
      } else {
        return localStorage.getItem(key);
      }
    }
    /**
     * @param {String} key 
     * @param {String} value 
     * @returns {void}
     */
    static setItem(key, value) {
      if (_GM_setValue) {
        _GM_setValue(key, value);
      } else {
        localStorage.setItem(key, value);
      }
    }
  }
  const useStore = pinia$1.defineStore("store", {
    state() {
      return {
        // ####################
        // 元件相關狀態
        // ####################
        isDarkMode: false,
        isToolbarsShow: false,
        isPopupShow: {
          sheet: false,
          chord: false,
          font: false,
          settings: false,
          menu: false,
          // 選單內功能
          hotkey: false
        },
        // ####################
        // 偏好設定相關狀態
        // ####################
        agreeToArchiveSheet: true,
        // ####################
        // 譜面相關狀態
        // ####################
        transpose: 0,
        /** 在 `StoreHandler` 裡賦值 */
        originalCapo: 0,
        /** 在 `StoreHandler` 裡賦值,HTML 格式 */
        originalKey: "",
        /** `font-size` 的變化值 */
        fontSizeDelta: 0,
        /** 在 `StoreHandler` 裡賦值,單位為 px */
        originalFontSize: 0,
        /** 在 `StoreHandler` 裡賦值,單位為 px */
        originalLineHeight: 0
      };
    },
    persist: {
      key: "plus91-preferences",
      storage: MonkeyStorage,
      deserialize: zipson.parse,
      serialize: zipson.stringify,
      paths: ["isDarkMode", "agreeToArchiveSheet"],
      beforeRestore() {
        console.log("[91 Plus] 讀取偏好設置中");
      },
      afterRestore() {
        console.log("[91 Plus] 偏好設置讀取完畢");
      },
      debug: true
    },
    getters: {
      currentCapo() {
        return this.originalCapo + this.transpose;
      },
      currentKey() {
        return new Chord(this.originalKey).transpose(-this.transpose).toFormattedString();
      }
    },
    actions: {
      toggleToolbars() {
        if (this.isToolbarsShow) {
          this.closePopups();
        } else {
          this.isPopupShow.sheet = true;
        }
        this.isToolbarsShow = !this.isToolbarsShow;
      },
      closePopups() {
        for (const popup in this.isPopupShow) {
          this.isPopupShow[popup] = false;
        }
      },
      /** @param {'sheet'|'chord'|'font'|'settings'|'menu'|'hotkey'} name */
      togglePopup(name) {
        for (const popup in this.isPopupShow) {
          if (popup === name) {
            this.isPopupShow[popup] = !this.isPopupShow[popup];
          } else {
            this.isPopupShow[popup] = false;
          }
        }
      },
      plusTranspose(numberToPlus) {
        let newTranspose = this.transpose + numberToPlus;
        const newCapo = this.originalCapo + newTranspose;
        if (newCapo === 12 || newCapo === -12) {
          newTranspose = -this.originalCapo;
        }
        this.transpose = newTranspose;
      }
    }
  });
  const _hoisted_1$e = ["active"];
  const _sfc_main$f = {
    __name: "BootstrapIcon",
    props: {
      icon: {
        type: String,
        required: true
      },
      color: {
        type: String,
        default: "whitesmoke"
      },
      size: {
        type: String,
        default: "1rem"
      },
      stroke: {
        type: String,
        default: "0"
      },
      active: {
        type: Boolean,
        default: false
      }
    },
    setup(__props) {
      vue.useCssVars((_ctx) => ({
        "4f7e9938": __props.color,
        "3ce130e8": __props.size,
        "882971fa": __props.stroke
      }));
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("i", {
          class: vue.normalizeClass(`bi bi-${props.icon}`),
          active: props.active
        }, null, 10, _hoisted_1$e);
      };
    }
  };
  const BootstrapIcon = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["__scopeId", "data-v-ff0eef21"]]);
  const _hoisted_1$d = { class: "toolbar-icon" };
  const _sfc_main$e = {
    __name: "ToolbarIcon",
    props: {
      icon: {
        type: String,
        required: true
      },
      stroke: {
        type: String,
        default: "0"
      },
      active: {
        type: Boolean,
        default: false
      },
      color: {
        type: String,
        default: "whitesmoke"
      }
    },
    setup(__props) {
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$d, [
          vue.createVNode(BootstrapIcon, {
            icon: props.icon,
            color: props.color,
            size: "1.3rem",
            stroke: props.stroke,
            active: props.active
          }, null, 8, ["icon", "color", "stroke", "active"])
        ]);
      };
    }
  };
  const ToolbarIcon = /* @__PURE__ */ _export_sfc(_sfc_main$e, [["__scopeId", "data-v-a789fc6b"]]);
  const _hoisted_1$c = { class: "adjust-widget" };
  const _hoisted_2$b = ["disabled"];
  const _hoisted_3$6 = ["disabled"];
  const _hoisted_4$4 = ["disabled"];
  const _sfc_main$d = {
    __name: "AdjustWidget",
    props: {
      iconLeft: {
        type: String,
        default: "caret-left-fill"
      },
      iconRight: {
        type: String,
        default: "caret-right-fill"
      },
      disabledLeft: {
        type: Boolean,
        default: false
      },
      disabledMiddle: {
        type: Boolean,
        default: false
      },
      disabledRight: {
        type: Boolean,
        default: false
      },
      color: {
        type: String,
        default: "#444"
      },
      size: {
        type: String,
        default: "1.25rem"
      },
      onclickLeft: Function,
      onclickMiddle: Function,
      onclickRight: Function
    },
    setup(__props) {
      vue.useCssVars((_ctx) => ({
        "13e75dc8": __props.color,
        "10392328": __props.size
      }));
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$c, [
          vue.createElementVNode("button", {
            class: "adjust-button adjust-button-left",
            onClick: _cache[0] || (_cache[0] = (...args) => props.onclickLeft && props.onclickLeft(...args)),
            disabled: props.disabledLeft
          }, [
            vue.createVNode(BootstrapIcon, {
              icon: props.iconLeft,
              color: props.color,
              size: props.size
            }, null, 8, ["icon", "color", "size"])
          ], 8, _hoisted_2$b),
          vue.createElementVNode("button", {
            class: "adjust-button adjust-button-middle",
            onClick: _cache[1] || (_cache[1] = (...args) => props.onclickMiddle && props.onclickMiddle(...args)),
            disabled: props.disabledMiddle
          }, [
            vue.renderSlot(_ctx.$slots, "default", {}, void 0, true)
          ], 8, _hoisted_3$6),
          vue.createElementVNode("button", {
            class: "adjust-button adjust-button-right",
            onClick: _cache[2] || (_cache[2] = (...args) => props.onclickRight && props.onclickRight(...args)),
            disabled: props.disabledRight
          }, [
            vue.createVNode(BootstrapIcon, {
              icon: props.iconRight,
              color: props.color,
              size: props.size
            }, null, 8, ["icon", "color", "size"])
          ], 8, _hoisted_4$4)
        ]);
      };
    }
  };
  const AdjustWidget = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["__scopeId", "data-v-cb8ab81d"]]);
  class ChordSheetElement {
    /** @param {HTMLElement} chordSheetElement  */
    constructor(chordSheetElement) {
      /** @param {NodeList} nodeList */
      __privateAdd(this, _unformat);
      this.chordSheetElement = chordSheetElement;
    }
    /**
     * 將 Header 和譜上的和弦移調,並實質修改於 DOM
     * @param {number} delta 相對於當前調的移調值
     */
    static transposeSheet(delta) {
      $("#tone_z .tf").each(function() {
        const chord = new Chord($(this).text());
        const newChordHTML = chord.transpose(-delta).toFormattedString();
        $(this).html(newChordHTML);
      });
    }
    /** @returns {ChordSheetElement} */
    formatUnderlines() {
      const underlineEl = this.chordSheetElement.querySelectorAll("u");
      const doubleUnderlineEl = this.chordSheetElement.querySelectorAll("abbr");
      underlineEl.forEach((el) => {
        el.innerText = `{_${el.innerText}_}`;
      });
      doubleUnderlineEl.forEach((el) => {
        el.innerText = `{=${el.innerText}=}`;
      });
      return this;
    }
    /** @returns {ChordSheetElement} */
    unformatUnderlines() {
      const underlineEl = this.chordSheetElement.querySelectorAll("u");
      const doubleUnderlineEl = this.chordSheetElement.querySelectorAll("abbr");
      __privateMethod(this, _unformat, unformat_fn).call(this, underlineEl);
      __privateMethod(this, _unformat, unformat_fn).call(this, doubleUnderlineEl);
      return this;
    }
  }
  _unformat = new WeakSet();
  unformat_fn = function(nodeList) {
    nodeList.forEach((el) => {
      el.innerHTML = el.innerText.replaceAll(/{_|{=|=}|_}/g, "").replaceAll(
        /[a-zA-Z0-9#/]+/g,
        /* html */
        `<span class="tf">$&</span>`
      );
    });
  };
  class ChordSheetDocument {
    constructor() {
      this.el = {
        mtitle: document.getElementById("mtitle"),
        tkinfo: document.querySelector(".tkinfo"),
        capoSelect: document.querySelector(".capo .select"),
        tinfo: document.querySelector(".tinfo"),
        tone_z: document.getElementById("tone_z")
      };
    }
    getId() {
      const urlParams = new URLSearchParams(window.location.search);
      return Number(urlParams.get("id"));
    }
    getTitle() {
      return this.el.mtitle.innerText.trim();
    }
    getKey() {
      var _a;
      const match = (_a = this.el.tkinfo) == null ? void 0 : _a.innerText.match(new RegExp("(?<=原調:)\\w*"));
      return match ? match[0].trim() : "";
    }
    getPlay() {
      var _a;
      const match = (_a = this.el.capoSelect) == null ? void 0 : _a.innerText.split(/\s*\/\s*/);
      return match ? match[1].trim() : "";
    }
    getCapo() {
      var _a;
      const match = (_a = this.el.capoSelect) == null ? void 0 : _a.innerText.split(/\s*\/\s*/);
      return match ? Number(match[0]) : 0;
    }
    getSinger() {
      var _a;
      const match = (_a = this.el.tinfo) == null ? void 0 : _a.innerText.match(new RegExp("(?<=演唱:).*(?=\\n|$)"));
      return match ? match[0].trim() : "";
    }
    getComposer() {
      var _a;
      const match = (_a = this.el.tinfo) == null ? void 0 : _a.innerText.match(new RegExp("(?<=曲:).*?(?=詞:|$)"));
      return match ? match[0].trim() : "";
    }
    getLyricist() {
      var _a;
      const match = (_a = this.el.tinfo) == null ? void 0 : _a.innerText.match(new RegExp("(?<=詞:).*?(?=曲:|$)"));
      return match ? match[0].trim() : "";
    }
    getBpm() {
      var _a;
      const match = (_a = this.el.tkinfo) == null ? void 0 : _a.innerText.match(/\d+/);
      return match ? Number(match[0]) : 0;
    }
    getSheetText() {
      const formattedChordSheet = this.el.tone_z.innerText.replaceAll(/\s+?\n/g, "\n").replaceAll("\n\n", "\n").trim().replaceAll(/\s+/g, (match) => {
        return `{%${match.length}%}`;
      });
      return formattedChordSheet;
    }
  }
  class StoreHandler {
    constructor() {
      /** 當 `#store.transpose` 變動時,將譜面上的和弦進行移調 */
      __privateAdd(this, _watchTranspose);
      __privateAdd(this, _watchFontSize);
      // 命 `#store` 為私有屬性,在建立實例時再賦值,避免衝突
      __privateAdd(this, _store, void 0);
      __privateSet(this, _store, useStore());
    }
    initState() {
      const capoSelected = $(".capo .select").eq(0).text().trim();
      const originalCapo = +capoSelected.split(/\s*\/\s*/)[0];
      const originalKey = capoSelected.split(/\s*\/\s*/)[1];
      __privateGet(this, _store).originalCapo = originalCapo;
      __privateGet(this, _store).originalKey = originalKey;
      const fontSize = +$("#tone_z").css("font-size").match(/^\d+/)[0];
      const lineHeight = +$("#tone_z > p").css("line-height").match(/^\d+/)[0];
      __privateGet(this, _store).originalFontSize = fontSize;
      __privateGet(this, _store).originalLineHeight = lineHeight;
      const params = getQueryParams();
      if (params.transpose) {
        __privateGet(this, _store).transpose = params.transpose;
      }
    }
    start() {
      __privateMethod(this, _watchTranspose, watchTranspose_fn).call(this);
      __privateMethod(this, _watchFontSize, watchFontSize_fn).call(this);
      return this;
    }
    static handleKeydown(key) {
      const store = useStore();
      switch (key) {
        case " ": {
          store.toggleToolbars();
          break;
        }
        case "/": {
          if (!store.isToolbarsShow) {
            store.toggleToolbars();
            store.closePopups();
          }
          setTimeout(() => {
            $("#plus91-header input").get(0).focus();
          }, 0);
          break;
        }
        case "Escape": {
          if (store.isToolbarsShow) {
            store.toggleToolbars();
          }
          break;
        }
      }
      if (store.isPopupShow.sheet) {
        switch (key) {
          case "ArrowLeft": {
            store.plusTranspose(-1);
            break;
          }
          case "ArrowRight": {
            store.plusTranspose(1);
            break;
          }
          case "ArrowDown": {
            store.transpose = 0;
            break;
          }
        }
      }
    }
  }
  _store = new WeakMap();
  _watchTranspose = new WeakSet();
  watchTranspose_fn = function() {
    vue.watch(() => {
      return __privateGet(this, _store).transpose;
    }, (newValue, oldValue) => {
      ChordSheetElement.transposeSheet((newValue - oldValue) % 12);
    });
  };
  _watchFontSize = new WeakSet();
  watchFontSize_fn = function() {
    vue.watch(() => {
      return __privateGet(this, _store).fontSizeDelta;
    }, (newValue) => {
      const oFontSize = __privateGet(this, _store).originalFontSize;
      const oLineHeight = __privateGet(this, _store).originalLineHeight;
      $("#tone_z").css("font-size", `${oFontSize + newValue}px`);
      $("#tone_z > p").css("line-height", `${oLineHeight + newValue}px`);
    });
  };
  function redirect() {
    const currentUrl = window.location.href;
    if (/\/song\//.test(currentUrl)) {
      const sheetId = currentUrl.match(new RegExp("(?<=\\/)\\d+(?=\\.)"))[0];
      const newUrl = `https://www.91pu.com.tw/m/tone.shtml?id=${sheetId}`;
      window.location.replace(newUrl);
    }
  }
  function injectGtag() {
    const newScript = document.createElement("script");
    newScript.src = "https://www.googletagmanager.com/gtag/js?id=G-JF4S3HZY31";
    newScript.async = true;
    document.head.appendChild(newScript);
    newScript.onload = () => {
      window.dataLayer = window.dataLayer || [];
      function gtag() {
        window.dataLayer.push(arguments);
      }
      gtag("js", /* @__PURE__ */ new Date());
      gtag("config", "G-JF4S3HZY31");
    };
  }
  function getQueryParams() {
    const url = new URL(window.location.href);
    const params = {
      transpose: +url.searchParams.get("transpose"),
      darkMode: !!url.searchParams.get("darkmode")
    };
    return params;
  }
  function changeTitle() {
    const newTitle = $("#mtitle").text().trim();
    document.title = `${newTitle} | 91+`;
  }
  function archiveChordSheet() {
    const sheet = document.getElementById("tone_z");
    const chordSheetDocument = new ChordSheetDocument();
    try {
      const chordSheetElement = new ChordSheetElement(sheet);
      chordSheetElement.formatUnderlines();
      const formBody = {
        id: chordSheetDocument.getId(),
        title: chordSheetDocument.getTitle(),
        key: chordSheetDocument.getKey(),
        play: chordSheetDocument.getPlay(),
        capo: chordSheetDocument.getCapo(),
        singer: chordSheetDocument.getSinger(),
        composer: chordSheetDocument.getComposer(),
        lyricist: chordSheetDocument.getLyricist(),
        bpm: chordSheetDocument.getBpm(),
        sheet_text: chordSheetDocument.getSheetText()
      };
      chordSheetElement.unformatUnderlines();
      fetch("https://91-plus-plus-api.fly.dev/archive", {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(formBody)
      }).then((response) => {
        console.log("[91 Plus] 雲端樂譜備份成功:", response);
      }).catch((error) => {
        console.error("[91 Plus] 雲端樂譜備份失敗:", error);
      });
    } catch {
      console.warn("[91 Plus] 樂譜解析失敗,無法備份");
      fetch(
        `https://91-plus-plus-api.fly.dev/report?id=${chordSheetDocument.getId()}`
      );
    }
  }
  function initMutationObserver() {
    return new MutationObserver((records, observer) => {
      const isMutationDone = !!document.querySelector("#tone_z").childElementCount;
      if (!isMutationDone) {
        return;
      }
      $("body").trigger("mutation.done");
      observer.disconnect();
    }).observe(document.body, { childList: true, subtree: true });
  }
  function onDomReady(callback) {
    $("body").on("mutation.done", callback);
  }
  function handleEvents() {
    $("html").on("keydown", (event) => {
      const excludedTags = ["input"];
      const tagName = event.target.tagName.toLowerCase();
      if (excludedTags.includes(tagName)) {
        return;
      }
      StoreHandler.handleKeydown(event.key);
    });
  }
  function switchInstrument(instrument) {
    switch (instrument) {
      case "guitar": {
        $(".schord").trigger("click");
        break;
      }
      case "ukulele": {
        $(".ukschord").trigger("click");
        break;
      }
      default: {
        $(".nsChord").trigger("click");
        break;
      }
    }
  }
  function getChordShapes() {
    const chordShapes = _unsafeWindow.chord_shapes;
    return chordShapes;
  }
  function getChordList() {
    const chordList = [];
    $("#tone_z .tf").each(function() {
      chordList.push($(this).text());
    });
    return [...new Set(chordList)];
  }
  function convertChordName(chordName) {
    const root = chordName.match(/^[A-G]#?/)[0];
    const rest = chordName.replace(/^[A-G]#?/, "");
    return `${rest} ${root}`;
  }
  const _hoisted_1$b = { id: "plus91-sheet-popup" };
  const _hoisted_2$a = { class: "sheet-popup-container" };
  const _hoisted_3$5 = { class: "text-capo" };
  const _hoisted_4$3 = ["innerHTML"];
  const _hoisted_5$1 = { class: "transpose-range-container" };
  const _hoisted_6$1 = ["value"];
  const _hoisted_7 = { class: "instrument-select-container" };
  const _sfc_main$c = {
    __name: "SheetPopup",
    setup(__props) {
      const store = useStore();
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$b, [
              vue.createElementVNode("div", _hoisted_2$a, [
                vue.createVNode(AdjustWidget, {
                  "onclick-left": () => {
                    vue.unref(store).plusTranspose(-1);
                  },
                  "onclick-middle": () => {
                    vue.unref(store).transpose = 0;
                  },
                  "onclick-right": () => {
                    vue.unref(store).plusTranspose(1);
                  }
                }, {
                  default: vue.withCtx(() => [
                    vue.createTextVNode(" CAPO:"),
                    vue.createElementVNode("span", _hoisted_3$5, vue.toDisplayString(vue.unref(store).currentCapo), 1),
                    vue.createTextVNode(" ("),
                    vue.createElementVNode("span", {
                      class: "text-key",
                      innerHTML: vue.unref(store).currentKey
                    }, null, 8, _hoisted_4$3),
                    vue.createTextVNode(") ")
                  ]),
                  _: 1
                }, 8, ["onclick-left", "onclick-middle", "onclick-right"]),
                vue.createElementVNode("div", _hoisted_5$1, [
                  vue.createElementVNode("input", {
                    type: "range",
                    min: "-11",
                    max: "11",
                    value: vue.unref(store).currentCapo,
                    onInput: _cache[0] || (_cache[0] = ($event) => {
                      vue.unref(store).transpose = $event.target.value - vue.unref(store).originalCapo;
                    })
                  }, null, 40, _hoisted_6$1)
                ]),
                vue.createElementVNode("div", _hoisted_7, [
                  vue.createElementVNode("button", {
                    class: "instrument-select-button",
                    onClick: _cache[1] || (_cache[1] = () => {
                      vue.unref(switchInstrument)("");
                    })
                  }, " 無 "),
                  vue.createElementVNode("button", {
                    class: "instrument-select-button",
                    onClick: _cache[2] || (_cache[2] = () => {
                      vue.unref(switchInstrument)("guitar");
                    })
                  }, " 吉他 "),
                  vue.createElementVNode("button", {
                    class: "instrument-select-button",
                    onClick: _cache[3] || (_cache[3] = () => {
                      vue.unref(switchInstrument)("ukulele");
                    })
                  }, " 烏克莉莉 ")
                ])
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.sheet]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const SheetPopup = /* @__PURE__ */ _export_sfc(_sfc_main$c, [["__scopeId", "data-v-364cc4c8"]]);
  const _hoisted_1$a = {
    key: 0,
    class: "chord-container"
  };
  const _hoisted_2$9 = { class: "chord-name" };
  const _hoisted_3$4 = ["chord-name"];
  const _sfc_main$b = {
    __name: "ChordChart",
    props: {
      chord: String
    },
    setup(__props) {
      const props = __props;
      const chordRef = vue.ref(void 0);
      const chordShapes = getChordShapes();
      const isChordExist = vue.ref(true);
      vue.onMounted(() => {
        var _a;
        const formattedChordKey = convertChordName(props.chord);
        const chordShape = chordShapes[formattedChordKey];
        if (!chordShape) {
          return isChordExist.value = false;
        }
        const chordObject = {
          ...chordShape,
          // position: chordShape.position,
          // positionText: chordShape.position_text,
          barres: (_a = chordShape.bars) == null ? void 0 : _a.map((barre) => {
            return {
              ...barre,
              fromString: barre.from_string,
              toString: barre.to_string
            };
          }),
          chord: chordShape.chord.map(([stringNum, fretNum]) => {
            const raw = [stringNum, fretNum];
            if (isNaN(+fretNum)) {
              return raw;
            }
            let newFretNum = fretNum;
            newFretNum += chordShape.position || 0;
            newFretNum -= chordShape.position_text || 0;
            return [stringNum, newFretNum];
          })
        };
        vue.nextTick(() => {
          const width = chordRef.value.clientWidth;
          const chordBoxSelector = `.chord-chart[chord-name="${props.chord}"]`;
          const chordBox = new vexchords.ChordBox(chordBoxSelector, {
            width,
            height: width * 1.25,
            circleRadius: 5,
            numStrings: 6,
            numFrets: 5,
            showTuning: false,
            defaultColor: "#444",
            bgColor: "transparent"
          });
          chordBox.draw(chordObject);
        });
      });
      return (_ctx, _cache) => {
        return isChordExist.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$a, [
          vue.createElementVNode("div", _hoisted_2$9, vue.toDisplayString(props.chord), 1),
          vue.createElementVNode("div", {
            class: "chord-chart",
            "chord-name": props.chord,
            ref_key: "chordRef",
            ref: chordRef
          }, null, 8, _hoisted_3$4)
        ])) : vue.createCommentVNode("", true);
      };
    }
  };
  const ChordChart = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "data-v-735734f6"]]);
  const _withScopeId$1 = (n) => (vue.pushScopeId("data-v-110e9a91"), n = n(), vue.popScopeId(), n);
  const _hoisted_1$9 = { id: "plus91-chord-popup" };
  const _hoisted_2$8 = { class: "banner" };
  const _hoisted_3$3 = /* @__PURE__ */ _withScopeId$1(() => /* @__PURE__ */ vue.createElementVNode("section", null, "此處的和弦圖示僅供參考,尤其把位部分較常出現錯誤,請注意。", -1));
  const _hoisted_4$2 = { class: "chord-popup-container" };
  const _sfc_main$a = {
    __name: "ChordPopup",
    setup(__props) {
      const store = useStore();
      const chordList = vue.ref([]);
      vue.watch(store.isPopupShow, () => {
        if (!store.isPopupShow.chord) {
          return;
        }
        chordList.value = getChordList();
      });
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$9, [
              vue.createElementVNode("div", _hoisted_2$8, [
                vue.createVNode(BootstrapIcon, {
                  icon: "info-circle-fill",
                  color: "inherit",
                  size: "inherit"
                }),
                _hoisted_3$3
              ]),
              vue.createElementVNode("div", _hoisted_4$2, [
                (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(chordList.value, (chord) => {
                  return vue.openBlock(), vue.createBlock(ChordChart, {
                    key: `${chord}_${( new Date()).getTime()}`,
                    chord
                  }, null, 8, ["chord"]);
                }), 128))
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.chord]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const ChordPopup = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-110e9a91"]]);
  const _hoisted_1$8 = { id: "plus91-font-popup" };
  const _hoisted_2$7 = { class: "font-popup-container" };
  const _sfc_main$9 = {
    __name: "FontSizePopup",
    setup(__props) {
      const store = useStore();
      const getFontSize = vue.computed(() => {
        return store.originalFontSize + store.fontSizeDelta;
      });
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$8, [
              vue.createElementVNode("div", _hoisted_2$7, [
                vue.createVNode(AdjustWidget, {
                  "onclick-left": () => {
                    vue.unref(store).fontSizeDelta--;
                  },
                  "onclick-middle": () => {
                    vue.unref(store).fontSizeDelta = 0;
                  },
                  "onclick-right": () => {
                    vue.unref(store).fontSizeDelta++;
                  },
                  "disabled-left": getFontSize.value <= 8,
                  "disabled-right": getFontSize.value >= 30
                }, {
                  default: vue.withCtx(() => [
                    vue.createTextVNode(vue.toDisplayString(getFontSize.value) + "px ", 1)
                  ]),
                  _: 1
                }, 8, ["onclick-left", "onclick-middle", "onclick-right", "disabled-left", "disabled-right"])
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.font]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const FontSizePopup = /* @__PURE__ */ _export_sfc(_sfc_main$9, [["__scopeId", "data-v-3be5b338"]]);
  const _withScopeId = (n) => (vue.pushScopeId("data-v-430d5768"), n = n(), vue.popScopeId(), n);
  const _hoisted_1$7 = { id: "plus91-settings-popup" };
  const _hoisted_2$6 = { class: "settings-popup-container" };
  const _hoisted_3$2 = { class: "setting-item" };
  const _hoisted_4$1 = /* @__PURE__ */ _withScopeId(() => /* @__PURE__ */ vue.createElementVNode("span", null, "深色模式", -1));
  const _hoisted_5 = { class: "setting-item" };
  const _hoisted_6 = /* @__PURE__ */ _withScopeId(() => /* @__PURE__ */ vue.createElementVNode("span", null, "協助測試雲端備份樂譜功能", -1));
  const _sfc_main$8 = {
    __name: "SettingsPopup",
    setup(__props) {
      const store = useStore();
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$7, [
              vue.createElementVNode("div", _hoisted_2$6, [
                vue.createElementVNode("label", _hoisted_3$2, [
                  _hoisted_4$1,
                  vue.withDirectives(vue.createElementVNode("input", {
                    type: "checkbox",
                    "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(store).isDarkMode = $event)
                  }, null, 512), [
                    [vue.vModelCheckbox, vue.unref(store).isDarkMode]
                  ])
                ]),
                vue.createElementVNode("label", _hoisted_5, [
                  _hoisted_6,
                  vue.withDirectives(vue.createElementVNode("input", {
                    type: "checkbox",
                    "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(store).agreeToArchiveSheet = $event)
                  }, null, 512), [
                    [vue.vModelCheckbox, vue.unref(store).agreeToArchiveSheet]
                  ])
                ])
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.settings]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const SettingsPopup = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-430d5768"]]);
  const _hoisted_1$6 = { class: "icon-button" };
  const _hoisted_2$5 = { class: "button-text" };
  const _sfc_main$7 = {
    __name: "MenuButton",
    props: {
      icon: String,
      name: String,
      color: String
    },
    setup(__props) {
      vue.useCssVars((_ctx) => ({
        "5908aad9": __props.color
      }));
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$6, [
          vue.createVNode(ToolbarIcon, {
            icon: props.icon,
            color: props.color
          }, null, 8, ["icon", "color"]),
          vue.createElementVNode("div", _hoisted_2$5, vue.toDisplayString(props.name), 1)
        ]);
      };
    }
  };
  const MenuButton = /* @__PURE__ */ _export_sfc(_sfc_main$7, [["__scopeId", "data-v-e9653884"]]);
  const _hoisted_1$5 = { id: "plus91-menu-popup" };
  const _hoisted_2$4 = { class: "menu-popup-container" };
  const BUTTON_COLOR = "#555";
  const _sfc_main$6 = {
    __name: "MenuPopup",
    setup(__props) {
      const store = useStore();
      const captureAsImage = () => {
        const content = document.querySelector("section.content");
        html2canvas(content).then((canvas) => {
          const newWindow = window.open();
          newWindow.document.write(`<img src="${canvas.toDataURL()}" />`);
        });
      };
      const searchOnYoutube = () => {
        const chordSheetDocument = new ChordSheetDocument();
        const title = chordSheetDocument.getTitle();
        const artist = chordSheetDocument.getSinger();
        const url = `https://www.youtube.com/results?search_query=${title}+${artist}`;
        window.open(url, "_blank").focus();
      };
      const goToGithubPage = () => {
        const url = "https://github.com/DonkeyBear/91Plus/blob/main/README.md";
        window.open(url, "_blank").focus();
      };
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$5, [
              vue.createElementVNode("div", _hoisted_2$4, [
                vue.createVNode(MenuButton, {
                  icon: "keyboard",
                  name: "快捷鍵",
                  color: BUTTON_COLOR,
                  onClick: _cache[0] || (_cache[0] = () => {
                    vue.unref(store).togglePopup("hotkey");
                  })
                }),
                vue.createVNode(MenuButton, {
                  icon: "file-earmark-image",
                  name: "擷取為圖片",
                  color: BUTTON_COLOR,
                  onClick: captureAsImage
                }),
                vue.createVNode(MenuButton, {
                  icon: "youtube",
                  name: "搜尋 YouTube",
                  color: BUTTON_COLOR,
                  onClick: searchOnYoutube
                }),
                vue.createVNode(MenuButton, {
                  icon: "github",
                  name: "關於 91 Plus",
                  color: BUTTON_COLOR,
                  onClick: goToGithubPage
                })
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.menu]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const MenuPopup = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-fca8d708"]]);
  const _hoisted_1$4 = { class: "hotkey-item" };
  const _hoisted_2$3 = {
    key: 0,
    class: "hotkeys"
  };
  const _hoisted_3$1 = {
    key: 1,
    class: "hr"
  };
  const _sfc_main$5 = {
    __name: "HotkeyItem",
    props: {
      hotkey: {
        type: String,
        required: false
      },
      desc: String
    },
    setup(__props) {
      const props = __props;
      const hotkeyList = props.hotkey.split(" ");
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [
          vue.createElementVNode("div", {
            class: vue.normalizeClass(["desc", { "title": !__props.hotkey }])
          }, vue.toDisplayString(__props.desc), 3),
          __props.hotkey ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$3, [
            (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(hotkeyList), (key) => {
              return vue.openBlock(), vue.createElementBlock("kbd", {
                key: `${key}_${__props.hotkey}_${__props.desc}`
              }, vue.toDisplayString(key), 1);
            }), 128))
          ])) : (vue.openBlock(), vue.createElementBlock("div", _hoisted_3$1))
        ]);
      };
    }
  };
  const HotkeyItem = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-3c43f6cf"]]);
  const hotkeysLeft = [
    {
      hotkey: "空白鍵",
      desc: "開啟 / 關閉功能選單"
    },
    {
      hotkey: "ESC",
      desc: "關閉功能選單"
    },
    {
      hotkey: "/",
      desc: "切換至搜尋框"
    }
  ];
  const hotkeysRight = [
    {
      hotkey: "",
      desc: "移調選單開啟時"
    },
    {
      hotkey: "← →",
      desc: "移調"
    },
    {
      hotkey: "↓",
      desc: "移回初始調"
    },
    {
      hotkey: "",
      desc: "在搜尋框內"
    },
    {
      hotkey: "Enter",
      desc: "搜尋"
    },
    {
      hotkey: "ESC",
      desc: "跳出搜尋框"
    }
  ];
  const hotkeyData = {
    hotkeysLeft,
    hotkeysRight
  };
  const _hoisted_1$3 = { id: "plus91-hotkey-popup" };
  const _hoisted_2$2 = { class: "hotkey-popup-container" };
  const _hoisted_3 = { class: "left-part" };
  const _hoisted_4 = { class: "right-part" };
  const _sfc_main$4 = {
    __name: "HotkeyPopup",
    setup(__props) {
      const store = useStore();
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$3, [
              vue.createElementVNode("div", _hoisted_2$2, [
                vue.createElementVNode("section", _hoisted_3, [
                  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(hotkeyData).hotkeysLeft, (item, index) => {
                    return vue.openBlock(), vue.createBlock(HotkeyItem, {
                      key: `${item.hotkey}_${item.desc}_${index}`,
                      hotkey: item.hotkey,
                      desc: item.desc
                    }, null, 8, ["hotkey", "desc"]);
                  }), 128))
                ]),
                vue.createElementVNode("section", _hoisted_4, [
                  (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(vue.unref(hotkeyData).hotkeysRight, (item, index) => {
                    return vue.openBlock(), vue.createBlock(HotkeyItem, {
                      key: `${item.hotkey}_${item.desc}_${index}`,
                      hotkey: item.hotkey,
                      desc: item.desc
                    }, null, 8, ["hotkey", "desc"]);
                  }), 128))
                ])
              ])
            ], 512), [
              [vue.vShow, vue.unref(store).isPopupShow.hotkey]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const HotkeyPopup = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["__scopeId", "data-v-eb86b87c"]]);
  const _hoisted_1$2 = { id: "plus91-footer" };
  const _hoisted_2$1 = { class: "footer-container" };
  const _sfc_main$3 = {
    __name: "AppFooter",
    props: {
      active: Boolean
    },
    setup(__props) {
      const store = useStore();
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$2, [
              vue.createElementVNode("div", _hoisted_2$1, [
                vue.createVNode(ToolbarIcon, {
                  icon: "music-note-beamed",
                  stroke: ".05rem",
                  active: vue.unref(store).isPopupShow.sheet,
                  onClick: _cache[0] || (_cache[0] = ($event) => vue.unref(store).togglePopup("sheet"))
                }, null, 8, ["active"]),
                vue.createVNode(ToolbarIcon, {
                  icon: "table",
                  active: vue.unref(store).isPopupShow.chord,
                  onClick: _cache[1] || (_cache[1] = ($event) => vue.unref(store).togglePopup("chord"))
                }, null, 8, ["active"]),
                vue.createVNode(ToolbarIcon, {
                  icon: "type",
                  stroke: ".05rem",
                  active: vue.unref(store).isPopupShow.font,
                  onClick: _cache[2] || (_cache[2] = ($event) => vue.unref(store).togglePopup("font"))
                }, null, 8, ["active"]),
                vue.createVNode(ToolbarIcon, {
                  icon: "gear-wide-connected",
                  active: vue.unref(store).isPopupShow.settings,
                  onClick: _cache[3] || (_cache[3] = ($event) => vue.unref(store).togglePopup("settings"))
                }, null, 8, ["active"]),
                vue.createVNode(ToolbarIcon, {
                  icon: "list",
                  stroke: ".05rem",
                  active: vue.unref(store).isPopupShow.menu,
                  onClick: _cache[4] || (_cache[4] = ($event) => vue.unref(store).togglePopup("menu"))
                }, null, 8, ["active"]),
                vue.createVNode(SheetPopup),
                vue.createVNode(ChordPopup),
                vue.createVNode(FontSizePopup),
                vue.createVNode(SettingsPopup),
                vue.createVNode(MenuPopup),
                vue.createVNode(HotkeyPopup)
              ])
            ], 512), [
              [vue.vShow, props.active]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const AppFooter = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-d7417138"]]);
  const _hoisted_1$1 = { id: "plus91-header" };
  const _hoisted_2 = { class: "header-container" };
  const _sfc_main$2 = {
    __name: "AppHeader",
    props: {
      active: Boolean
    },
    setup(__props) {
      const props = __props;
      const searchText = vue.ref("");
      const search = () => {
        if (!searchText.value) {
          return;
        }
        const url = `https://www.91pu.com.tw/plus/search.php?keyword=${searchText.value}`;
        window.open(url, "_blank").focus();
        searchText.value = "";
      };
      const backToPreviousPage = () => {
        history.back();
      };
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1$1, [
              vue.createElementVNode("div", _hoisted_2, [
                vue.createVNode(ToolbarIcon, {
                  icon: "chevron-left",
                  stroke: ".04rem",
                  onClick: backToPreviousPage
                }),
                vue.withDirectives(vue.createElementVNode("input", {
                  type: "text",
                  placeholder: "91 Plus",
                  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchText.value = $event),
                  onKeydown: [
                    vue.withKeys(search, ["enter"]),
                    _cache[1] || (_cache[1] = vue.withKeys((event) => {
                      event.target.blur();
                    }, ["esc"]))
                  ]
                }, null, 544), [
                  [
                    vue.vModelText,
                    searchText.value,
                    void 0,
                    { trim: true }
                  ]
                ]),
                vue.createVNode(ToolbarIcon, {
                  icon: "search",
                  stroke: ".03rem",
                  onClick: _cache[2] || (_cache[2] = ($event) => search())
                })
              ])
            ], 512), [
              [vue.vShow, props.active]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const AppHeader = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-a1982e9d"]]);
  const _hoisted_1 = { id: "dark-mode-overlay" };
  const _sfc_main$1 = {
    __name: "DarkModeOverlay",
    props: {
      active: Boolean
    },
    setup(__props) {
      const props = __props;
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(vue.Transition, { name: "fade" }, {
          default: vue.withCtx(() => [
            vue.withDirectives(vue.createElementVNode("div", _hoisted_1, null, 512), [
              [vue.vShow, props.active]
            ])
          ]),
          _: 1
        });
      };
    }
  };
  const DarkModeOverlay = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-a9f152b4"]]);
  const _sfc_main = {
    __name: "App",
    setup(__props) {
      const store = useStore();
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [
          vue.createVNode(TriggerOverlay, {
            onClick: vue.unref(store).toggleToolbars
          }, null, 8, ["onClick"]),
          vue.createVNode(AppHeader, {
            active: vue.unref(store).isToolbarsShow
          }, null, 8, ["active"]),
          vue.createVNode(AppFooter, {
            active: vue.unref(store).isToolbarsShow
          }, null, 8, ["active"]),
          vue.createVNode(DarkModeOverlay, {
            active: vue.unref(store).isDarkMode
          }, null, 8, ["active"])
        ], 64);
      };
    }
  };
  function init() {
    redirect();
    injectGtag();
    initMutationObserver();
    handleEvents();
    const storeHandler = new StoreHandler().start();
    onDomReady(() => {
      changeTitle();
      storeHandler.initState();
      const store = useStore();
      if (store.agreeToArchiveSheet) {
        archiveChordSheet();
      }
    });
  }
  const pinia = pinia$1.createPinia();
  pinia.use(piniaPluginPersistedstate);
  vue.createApp(_sfc_main).use(pinia).mount(
    (() => {
      const app = document.createElement("div");
      app.id = "vue-91plus";
      document.body.append(app);
      return app;
    })()
  );
  init();

})(Vue, pinia, pinia-plugin-persistedstate, zipson, vexchords, html2canvas);

QingJ © 2025

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