自由轉調、輕鬆練歌,打造 91 譜的最佳體驗!
// ==UserScript== // @name 91 Plus // @namespace https://github.com/DonkeyBear // @version 1.9.1 // @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/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 // ==/UserScript== (function (vue, vexchords, zipson, html2canvas) { 'use strict'; const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):document.head.appendChild(document.createElement("style")).append(t);})(e));}; importCSS(" .bi[data-v-a9f332df]{color:var(--c22eb872);font-size:var(--v70824f1d);-webkit-text-stroke:var(--v59c28e54) var(--c22eb872);line-height:0}.bi[data-v-a9f332df]:before{transition:text-shadow .2s}.bi[active=true][data-v-a9f332df]:before{text-shadow:0 0 .5rem color-mix(in srgb,var(--theme-color) 50%,white)}.chord-container .chord-name[data-v-128703be]{font-size:.5rem;font-weight:900;color:#666;text-align:center}.chord-container .chord-chart[data-v-128703be]{margin:-.6rem 0 -.25rem}html[data-v-ed81246a]{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}.slide-and-fade-enter-active[data-v-ed81246a],.slide-and-fade-leave-active[data-v-ed81246a]{transition:all .2s}.slide-and-fade-enter-from[data-v-ed81246a],.slide-and-fade-leave-to[data-v-ed81246a]{transform:translateY(10%);opacity:0}.plus91-popup[data-v-ed81246a]{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-popup[data-v-ed81246a]::-webkit-scrollbar{display:none}.plus91-popup[data-v-ed81246a]{padding-left:1rem;padding-right:1rem}#plus91-chord-popup .banner[data-v-858a1989]{display:flex;align-items:center;background:color-mix(in srgb,var(--toolbar-bg-color) 25%,transparent);color:color-mix(in srgb,var(--toolbar-bg-color) 50%,black);border-radius:.5rem;padding:.5rem .75rem;margin-bottom:.25rem}#plus91-chord-popup .banner section[data-v-858a1989]{flex-grow:1;margin-left:.5rem}#plus91-chord-popup .chord-popup-container[data-v-858a1989]{display:grid;grid-template-columns:repeat(6,1fr);column-gap:.5rem;padding-top:.4rem}#plus91-chord-popup.banner-only .banner[data-v-858a1989]{margin-bottom:0;background:#f6d26640;color:color-mix(in srgb,#f6d266 50%,black 35%)}#plus91-chord-popup.banner-only .chord-popup-container[data-v-858a1989]{padding-top:0}.toolbar-icon[data-v-cbf0cf0b]{cursor:pointer;padding:.25rem .75rem;display:flex;flex-direction:column;align-items:center;gap:.15rem}.toolbar-icon-text[data-v-cbf0cf0b]{color:color-mix(in srgb,var(--v08ff4922) 70%,var(--toolbar-bg-color));font-size:.5rem;letter-spacing:.15rem;margin-right:-.15rem}.adjust-widget[data-v-0178875e]{display:flex}.adjust-widget .adjust-button[data-v-0178875e]{border:0;border-radius:.25rem;background:transparent}.adjust-widget .adjust-button[data-v-0178875e]:hover{background:#00000006}.adjust-widget .adjust-button[data-v-0178875e]:not(:disabled){cursor:pointer}.adjust-widget .adjust-button[data-v-0178875e]:disabled{opacity:.25}.adjust-widget .adjust-button.adjust-button-middle[data-v-0178875e]{flex-grow:1;color:var(--v5e7bf01c);font-size:calc(var(--v1bd9a428) * .75);font-weight:700}.adjust-widget .adjust-button.adjust-button-left[data-v-0178875e]{padding-right:1rem}.adjust-widget .adjust-button.adjust-button-right[data-v-0178875e]{padding-left:1rem}.hotkey-item[data-v-851f225f]{display:flex;justify-content:space-between;align-items:center;padding:0 .25rem;border-radius:.25rem;height:1.4rem}.hotkey-item[data-v-851f225f]:nth-child(odd){background:#00000006}.desc.title[data-v-851f225f]{font-size:.55rem;color:#999}.hotkeys[data-v-851f225f]{display:flex}.hr[data-v-851f225f]{display:flex;flex-grow:1;border-top:1px solid lightgray;margin-left:.25rem}kbd[data-v-851f225f]{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}#plus91-hotkey-popup .hotkey-popup-container[data-v-07402c98]{display:flex;color:#444}#plus91-hotkey-popup section[data-v-07402c98]{flex-grow:1;width:50%;margin:-.1rem 0}#plus91-hotkey-popup section.left-part[data-v-07402c98]{border-right:1px solid lightgray;margin-left:-.5rem;padding-right:.5rem}#plus91-hotkey-popup section.right-part[data-v-07402c98]{padding-left:.5rem;margin-right:-.5rem}#plus91-hotkey-popup kbd[data-v-07402c98]{font-size:.65rem;border:solid lightgray;border-width:1px .1rem .15rem;border-radius:.2rem;padding:0 .2rem;letter-spacing:-.025rem;color:#666}.icon-button[data-v-cb0cf859]{display:flex;flex-direction:column;align-items:center;cursor:pointer;padding:0 .6rem .4rem;border-radius:.25rem}.icon-button[data-v-cb0cf859]:hover{background:#00000006}.icon-button .button-text[data-v-cb0cf859]{font-size:.5rem;color:var(--v12c3e3a5)}#plus91-menu-popup .menu-popup-container[data-v-f8df8357]{display:flex;justify-content:space-around}.color-switcher-container[data-v-9499f72a]{display:flex}.color-switcher-container .color-switcher-option[data-v-9499f72a]{display:flex;align-items:center;justify-content:center;width:1.25em;height:1em;cursor:pointer;border-style:solid;border-width:0;border-top-width:1px;border-bottom-width:1px}.color-switcher-container .color-switcher-option[data-v-9499f72a]:first-child{border-radius:50rem 0 0 50rem;border-left-width:1px;width:1.3em}.color-switcher-container .color-switcher-option[data-v-9499f72a]:last-child{border-radius:0 50rem 50rem 0;border-right-width:1px;width:1.3em}.toggle-switch[data-v-1cf8e431]{display:inline-flex;cursor:pointer}.switch-track[data-v-1cf8e431]{position:relative;width:2.25em;height:1.25em;background:#ccc;border-radius:50rem;transition:background .3s}.switch-track.active[data-v-1cf8e431]{background:#1e90ff}.switch-track.active .switch-thumb[data-v-1cf8e431]{transform:translate(100%)}.switch-thumb[data-v-1cf8e431]{position:absolute;top:.125em;left:.125em;width:1em;height:1em;background:#fff;border-radius:50%;transition:transform .3s}#plus91-settings-popup .bi[data-v-3259211c]{color:#a9a9a9;margin-right:.25em;font-size:1em}#plus91-settings-popup .setting-item[data-v-3259211c]{display:flex;align-items:center;justify-content:space-between;padding:.5rem 1rem;border-radius:.5rem;color:#000c;cursor:pointer}#plus91-settings-popup .setting-item[data-v-3259211c]:hover{background:#0000000d}#plus91-sheet-popup .transpose-range-container[data-v-18539399]{margin-top:1rem}#plus91-sheet-popup .transpose-range-container input[type=range][data-v-18539399]{width:100%}#plus91-sheet-popup .instrument-select-container[data-v-18539399]{display:flex;border:1px solid lightgray;border-radius:.25rem;margin-top:1rem;background:#fff}#plus91-sheet-popup .instrument-select-container .instrument-select-button[data-v-18539399]{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}#plus91-sheet-popup .instrument-select-container .instrument-select-button[data-v-18539399]:last-child{border:0;border-radius:0 .25rem .25rem 0}#plus91-sheet-popup .instrument-select-container .instrument-select-button[data-v-18539399]:first-child{border-radius:.25rem 0 0 .25rem}#plus91-sheet-popup .instrument-select-container .instrument-select-button[data-v-18539399]:hover{background:#f5f5f5}html[data-v-c2303173]{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}.slide-enter-active[data-v-c2303173],.slide-leave-active[data-v-c2303173]{transition:transform .2s}.slide-enter-from[data-v-c2303173],.slide-leave-to[data-v-c2303173]{transform:translateY(100%)}#plus91-footer[data-v-c2303173]{z-index:1000;display:flex;justify-content:center;position:fixed;left:0;right:0;bottom:0}.footer-container[data-v-c2303173]{width:min(100vw,768px);background-color:var(--toolbar-bg-color);background-image:linear-gradient(transparent,color-mix(in srgb,var(--toolbar-bg-color) 99%,white) 250%);background-blend-mode:multiply;transition:background-color .3s ease,border-color .3s ease;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);padding:.25rem .75rem;display:flex;justify-content:space-between;align-items:center}@media (min-width: 768px){.footer-container[data-v-c2303173]{border-radius:1rem 1rem 0 0}}.footer-container[data-v-c2303173]{padding-bottom:.75rem;border-top:1px solid var(--toolbar-border-color)}html[data-v-c3427f68]{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}.slide-enter-active[data-v-c3427f68],.slide-leave-active[data-v-c3427f68]{transition:transform .2s}.slide-enter-from[data-v-c3427f68],.slide-leave-to[data-v-c3427f68]{transform:translateY(-100%)}#plus91-header[data-v-c3427f68]{z-index:1000;display:flex;justify-content:center;position:fixed;left:0;right:0;top:0}.header-container[data-v-c3427f68]{width:min(100vw,768px);background-color:var(--toolbar-bg-color);background-image:linear-gradient(transparent,color-mix(in srgb,var(--toolbar-bg-color) 99%,white) 250%);background-blend-mode:multiply;transition:background-color .3s ease,border-color .3s ease;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);padding:.25rem .75rem;display:flex;justify-content:space-between;align-items:center}@media (min-width: 768px){.header-container[data-v-c3427f68]{border-radius:0 0 1rem 1rem}}.header-container[data-v-c3427f68]{border-bottom:1px solid var(--toolbar-border-color)}.header-container form[data-v-c3427f68],.header-container .search-container[data-v-c3427f68]{display:flex;flex:1;height:100%}.header-container .search-container[data-v-c3427f68]{position:relative}.header-container .search-container .clear-input[data-v-c3427f68]{position:absolute;right:0;top:50%;transform:translateY(-50%)}.header-container input[data-v-c3427f68]{flex:1;border-radius:50rem;border:0;font-size:.8rem;font-weight:700;padding-left:1.25rem;padding-right:1.25rem;background:#fffa;color:#0009;opacity:.5;transition:all .2s}.header-container input[data-v-c3427f68]:focus-visible{outline:0;opacity:1}html[data-v-111379c3]{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}.fade-enter-active[data-v-111379c3],.fade-leave-active[data-v-111379c3]{transition:opacity .2s}.fade-enter-from[data-v-111379c3],.fade-leave-to[data-v-111379c3]{opacity:0}#dark-mode-overlay[data-v-111379c3]{position:fixed;inset: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} "); /*! * pinia v3.0.3 * (c) 2025 Eduardo San Martin Morote * @license MIT */ let activePinia; const setActivePinia = (pinia2) => activePinia = pinia2; const piniaSymbol = ( Symbol() ); function isPlainObject(o) { return o && typeof o === "object" && Object.prototype.toString.call(o) === "[object Object]" && typeof o.toJSON !== "function"; } var MutationType; (function(MutationType2) { MutationType2["direct"] = "direct"; MutationType2["patchObject"] = "patch object"; MutationType2["patchFunction"] = "patch function"; })(MutationType || (MutationType = {})); function createPinia() { const scope = vue.effectScope(true); const state = scope.run(() => vue.ref({})); let _p = []; let toBeInstalled = []; const pinia2 = vue.markRaw({ install(app) { setActivePinia(pinia2); pinia2._a = app; app.provide(piniaSymbol, pinia2); app.config.globalProperties.$pinia = pinia2; toBeInstalled.forEach((plugin) => _p.push(plugin)); toBeInstalled = []; }, use(plugin) { if (!this._a) { toBeInstalled.push(plugin); } else { _p.push(plugin); } return this; }, _p, _a: null, _e: scope, _s: new Map(), state }); return pinia2; } const noop$1 = () => { }; function addSubscription(subscriptions, callback, detached, onCleanup = noop$1) { subscriptions.push(callback); const removeSubscription = () => { const idx = subscriptions.indexOf(callback); if (idx > -1) { subscriptions.splice(idx, 1); onCleanup(); } }; if (!detached && vue.getCurrentScope()) { vue.onScopeDispose(removeSubscription); } return removeSubscription; } function triggerSubscriptions(subscriptions, ...args) { subscriptions.slice().forEach((callback) => { callback(...args); }); } const fallbackRunWithContext = (fn) => fn(); const ACTION_MARKER = Symbol(); const ACTION_NAME = Symbol(); function mergeReactiveObjects(target, patchToApply) { if (target instanceof Map && patchToApply instanceof Map) { patchToApply.forEach((value, key) => target.set(key, value)); } else if (target instanceof Set && patchToApply instanceof Set) { patchToApply.forEach(target.add, target); } for (const key in patchToApply) { if (!patchToApply.hasOwnProperty(key)) continue; const subPatch = patchToApply[key]; const targetValue = target[key]; if (isPlainObject(targetValue) && isPlainObject(subPatch) && target.hasOwnProperty(key) && !vue.isRef(subPatch) && !vue.isReactive(subPatch)) { target[key] = mergeReactiveObjects(targetValue, subPatch); } else { target[key] = subPatch; } } return target; } const skipHydrateSymbol = ( Symbol() ); function shouldHydrate(obj) { return !isPlainObject(obj) || !Object.prototype.hasOwnProperty.call(obj, skipHydrateSymbol); } const { assign } = Object; function isComputed(o) { return !!(vue.isRef(o) && o.effect); } function createOptionsStore(id, options, pinia2, hot) { const { state, actions, getters } = options; const initialState = pinia2.state.value[id]; let store; function setup() { if (!initialState && true) { pinia2.state.value[id] = state ? state() : {}; } const localState = vue.toRefs(pinia2.state.value[id]); return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => { computedGetters[name] = vue.markRaw(vue.computed(() => { setActivePinia(pinia2); const store2 = pinia2._s.get(id); return getters[name].call(store2, store2); })); return computedGetters; }, {})); } store = createSetupStore(id, setup, options, pinia2, hot, true); return store; } function createSetupStore($id, setup, options = {}, pinia2, hot, isOptionsStore) { let scope; const optionsForPlugin = assign({ actions: {} }, options); const $subscribeOptions = { deep: true }; let isListening; let isSyncListening; let subscriptions = []; let actionSubscriptions = []; let debuggerEvents; const initialState = pinia2.state.value[$id]; if (!isOptionsStore && !initialState && true) { pinia2.state.value[$id] = {}; } vue.ref({}); let activeListener; function $patch(partialStateOrMutator) { let subscriptionMutation; isListening = isSyncListening = false; if (typeof partialStateOrMutator === "function") { partialStateOrMutator(pinia2.state.value[$id]); subscriptionMutation = { type: MutationType.patchFunction, storeId: $id, events: debuggerEvents }; } else { mergeReactiveObjects(pinia2.state.value[$id], partialStateOrMutator); subscriptionMutation = { type: MutationType.patchObject, payload: partialStateOrMutator, storeId: $id, events: debuggerEvents }; } const myListenerId = activeListener = Symbol(); vue.nextTick().then(() => { if (activeListener === myListenerId) { isListening = true; } }); isSyncListening = true; triggerSubscriptions(subscriptions, subscriptionMutation, pinia2.state.value[$id]); } const $reset = isOptionsStore ? function $reset2() { const { state } = options; const newState = state ? state() : {}; this.$patch(($state) => { assign($state, newState); }); } : ( noop$1 ); function $dispose() { scope.stop(); subscriptions = []; actionSubscriptions = []; pinia2._s.delete($id); } const action = (fn, name = "") => { if (ACTION_MARKER in fn) { fn[ACTION_NAME] = name; return fn; } const wrappedAction = function() { setActivePinia(pinia2); const args = Array.from(arguments); const afterCallbackList = []; const onErrorCallbackList = []; function after(callback) { afterCallbackList.push(callback); } function onError(callback) { onErrorCallbackList.push(callback); } triggerSubscriptions(actionSubscriptions, { args, name: wrappedAction[ACTION_NAME], store, after, onError }); let ret; try { ret = fn.apply(this && this.$id === $id ? this : store, args); } catch (error) { triggerSubscriptions(onErrorCallbackList, error); throw error; } if (ret instanceof Promise) { return ret.then((value) => { triggerSubscriptions(afterCallbackList, value); return value; }).catch((error) => { triggerSubscriptions(onErrorCallbackList, error); return Promise.reject(error); }); } triggerSubscriptions(afterCallbackList, ret); return ret; }; wrappedAction[ACTION_MARKER] = true; wrappedAction[ACTION_NAME] = name; return wrappedAction; }; const partialStore = { _p: pinia2, $id, $onAction: addSubscription.bind(null, actionSubscriptions), $patch, $reset, $subscribe(callback, options2 = {}) { const removeSubscription = addSubscription(subscriptions, callback, options2.detached, () => stopWatcher()); const stopWatcher = scope.run(() => vue.watch(() => pinia2.state.value[$id], (state) => { if (options2.flush === "sync" ? isSyncListening : isListening) { callback({ storeId: $id, type: MutationType.direct, events: debuggerEvents }, state); } }, assign({}, $subscribeOptions, options2))); return removeSubscription; }, $dispose }; const store = vue.reactive(partialStore); pinia2._s.set($id, store); const runWithContext = pinia2._a && pinia2._a.runWithContext || fallbackRunWithContext; const setupStore = runWithContext(() => pinia2._e.run(() => (scope = vue.effectScope()).run(() => setup({ action })))); for (const key in setupStore) { const prop = setupStore[key]; if (vue.isRef(prop) && !isComputed(prop) || vue.isReactive(prop)) { if (!isOptionsStore) { if (initialState && shouldHydrate(prop)) { if (vue.isRef(prop)) { prop.value = initialState[key]; } else { mergeReactiveObjects(prop, initialState[key]); } } pinia2.state.value[$id][key] = prop; } } else if (typeof prop === "function") { const actionValue = action(prop, key); setupStore[key] = actionValue; optionsForPlugin.actions[key] = prop; } else ; } assign(store, setupStore); assign(vue.toRaw(store), setupStore); Object.defineProperty(store, "$state", { get: () => pinia2.state.value[$id], set: (state) => { $patch(($state) => { assign($state, state); }); } }); pinia2._p.forEach((extender) => { { assign(store, scope.run(() => extender({ store, app: pinia2._a, pinia: pinia2, options: optionsForPlugin }))); } }); if (initialState && isOptionsStore && options.hydrate) { options.hydrate(store.$state, initialState); } isListening = true; isSyncListening = true; return store; } function defineStore(id, setup, setupOptions) { let options; const isSetupStore = typeof setup === "function"; options = isSetupStore ? setupOptions : setup; function useStore2(pinia2, hot) { const hasContext = vue.hasInjectionContext(); pinia2 = pinia2 || (hasContext ? vue.inject(piniaSymbol, null) : null); if (pinia2) setActivePinia(pinia2); pinia2 = activePinia; if (!pinia2._s.has(id)) { if (isSetupStore) { createSetupStore(id, setup, options, pinia2); } else { createOptionsStore(id, options, pinia2); } } const store = pinia2._s.get(id); return store; } useStore2.$id = id; return useStore2; } const suspectProtoRx = /"(?:_|\\u0{2}5[Ff]){2}(?:p|\\u0{2}70)(?:r|\\u0{2}72)(?:o|\\u0{2}6[Ff])(?:t|\\u0{2}74)(?:o|\\u0{2}6[Ff])(?:_|\\u0{2}5[Ff]){2}"\s*:/; const suspectConstructorRx = /"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/; const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/; function jsonParseTransform(key, value) { if (key === "__proto__" || key === "constructor" && value && typeof value === "object" && "prototype" in value) { warnKeyDropped(key); return; } return value; } function warnKeyDropped(key) { console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`); } function destr(value, options = {}) { if (typeof value !== "string") { return value; } if (value[0] === '"' && value[value.length - 1] === '"' && value.indexOf("\\") === -1) { return value.slice(1, -1); } const _value = value.trim(); if (_value.length <= 9) { switch (_value.toLowerCase()) { case "true": { return true; } case "false": { return false; } case "undefined": { return void 0; } case "null": { return null; } case "nan": { return Number.NaN; } case "infinity": { return Number.POSITIVE_INFINITY; } case "-infinity": { return Number.NEGATIVE_INFINITY; } } } if (!JsonSigRx.test(value)) { if (options.strict) { throw new SyntaxError("[destr] Invalid JSON"); } return value; } try { if (suspectProtoRx.test(value) || suspectConstructorRx.test(value)) { if (options.strict) { throw new Error("[destr] Possible prototype pollution"); } return JSON.parse(value, jsonParseTransform); } return JSON.parse(value); } catch (error) { if (options.strict) { throw error; } return value; } } function get(obj, path) { if (obj == null) return void 0; let value = obj; for (let i = 0; i < path.length; i++) { if (value == null || value[path[i]] == null) return void 0; value = value[path[i]]; } return value; } function set(obj, value, path) { if (path.length === 0) return value; const idx = path[0]; if (path.length > 1) { value = set( typeof obj !== "object" || obj === null || !Object.prototype.hasOwnProperty.call(obj, idx) ? Number.isInteger(Number(path[1])) ? [] : {} : obj[idx], value, Array.prototype.slice.call(path, 1) ); } if (Number.isInteger(Number(idx)) && Array.isArray(obj)) return obj.slice()[idx]; return Object.assign({}, obj, { [idx]: value }); } function unset(obj, path) { if (obj == null || path.length === 0) return obj; if (path.length === 1) { if (obj == null) return obj; if (Number.isInteger(path[0]) && Array.isArray(obj)) return Array.prototype.slice.call(obj, 0).splice(path[0], 1); const result = {}; for (const p in obj) result[p] = obj[p]; delete result[path[0]]; return result; } if (obj[path[0]] == null) { if (Number.isInteger(path[0]) && Array.isArray(obj)) return Array.prototype.concat.call([], obj); const result = {}; for (const p in obj) result[p] = obj[p]; return result; } return set( obj, unset( obj[path[0]], Array.prototype.slice.call(path, 1) ), [path[0]] ); } function deepPickUnsafe(obj, paths) { return paths.map((p) => p.split(".")).map((p) => [p, get(obj, p)]).filter((t) => t[1] !== void 0).reduce((acc, cur) => set(acc, cur[1], cur[0]), {}); } function deepOmitUnsafe(obj, paths) { return paths.map((p) => p.split(".")).reduce((acc, cur) => unset(acc, cur), obj); } function hydrateStore(store, { storage, serializer, key, debug, pick, omit, beforeHydrate, afterHydrate }, context, runHooks = true) { try { if (runHooks) beforeHydrate?.(context); const fromStorage = storage.getItem(key); if (fromStorage) { const deserialized = serializer.deserialize(fromStorage); const picked = pick ? deepPickUnsafe(deserialized, pick) : deserialized; const omitted = omit ? deepOmitUnsafe(picked, omit) : picked; store.$patch(omitted); } if (runHooks) afterHydrate?.(context); } catch (error) { if (debug) console.error("[pinia-plugin-persistedstate]", error); } } function persistState(state, { storage, serializer, key, debug, pick, omit }) { try { const picked = pick ? deepPickUnsafe(state, pick) : state; const omitted = omit ? deepOmitUnsafe(picked, omit) : picked; const toStorage = serializer.serialize(omitted); storage.setItem(key, toStorage); } catch (error) { if (debug) console.error("[pinia-plugin-persistedstate]", error); } } function createPersistence(context, optionsParser, auto) { const { pinia: pinia2, store, options: { persist = auto } } = context; if (!persist) return; if (!(store.$id in pinia2.state.value)) { const originalStore = pinia2._s.get(store.$id.replace("__hot:", "")); if (originalStore) void Promise.resolve().then(() => originalStore.$persist()); return; } const persistenceOptions = Array.isArray(persist) ? persist : persist === true ? [{}] : [persist]; const persistences = persistenceOptions.map(optionsParser); store.$hydrate = ({ runHooks = true } = {}) => { persistences.forEach((p) => { hydrateStore(store, p, context, runHooks); }); }; store.$persist = () => { persistences.forEach((p) => { persistState(store.$state, p); }); }; persistences.forEach((p) => { hydrateStore(store, p, context); store.$subscribe( (_mutation, state) => persistState(state, p), { detached: true } ); }); } function createPersistedState(options = {}) { return function(context) { createPersistence( context, (p) => ({ key: (options.key ? options.key : (x) => x)(p.key ?? context.store.$id), debug: p.debug ?? options.debug ?? false, serializer: p.serializer ?? options.serializer ?? { serialize: (data) => JSON.stringify(data), deserialize: (data) => destr(data) }, storage: p.storage ?? options.storage ?? window.localStorage, beforeHydrate: p.beforeHydrate, afterHydrate: p.afterHydrate, pick: p.pick, omit: p.omit }), options.auto ?? false ); }; } var index_default = createPersistedState(); function computedWithControl(source, fn, options = {}) { let v = void 0; let track; let trigger; let dirty = true; const update = () => { dirty = true; trigger(); }; vue.watch(source, update, { flush: "sync", ...options }); const get$1 = typeof fn === "function" ? fn : fn.get; const set$1 = typeof fn === "function" ? void 0 : fn.set; const result = vue.customRef((_track, _trigger) => { track = _track; trigger = _trigger; return { get() { if (dirty) { v = get$1(v); dirty = false; } track(); return v; }, set(v$1) { set$1 === null || set$1 === void 0 || set$1(v$1); } }; }); result.trigger = update; return result; } function tryOnScopeDispose(fn, failSilently) { if (vue.getCurrentScope()) { vue.onScopeDispose(fn, failSilently); return true; } return false; } const isClient = typeof window !== "undefined" && typeof document !== "undefined"; typeof WorkerGlobalScope !== "undefined" && globalThis instanceof WorkerGlobalScope; const notNullish = (val) => val != null; const toString = Object.prototype.toString; const isObject = (val) => toString.call(val) === "[object Object]"; const noop = () => { }; function toArray(value) { return Array.isArray(value) ? value : [value]; } function getLifeCycleTarget(target) { return vue.getCurrentInstance(); } function tryOnMounted(fn, sync = true, target) { if (getLifeCycleTarget()) vue.onMounted(fn, target); else if (sync) fn(); else vue.nextTick(fn); } function tryOnUnmounted(fn, target) { if (getLifeCycleTarget()) vue.onUnmounted(fn, target); } function watchImmediate(source, cb, options) { return vue.watch(source, cb, { ...options, immediate: true }); } const defaultWindow = isClient ? window : void 0; const defaultDocument = isClient ? window.document : void 0; function unrefElement(elRef) { var _$el; const plain = vue.toValue(elRef); return (_$el = plain === null || plain === void 0 ? void 0 : plain.$el) !== null && _$el !== void 0 ? _$el : plain; } function useEventListener(...args) { const cleanups = []; const cleanup = () => { cleanups.forEach((fn) => fn()); cleanups.length = 0; }; const register = (el, event, listener, options) => { el.addEventListener(event, listener, options); return () => el.removeEventListener(event, listener, options); }; const firstParamTargets = vue.computed(() => { const test = toArray(vue.toValue(args[0])).filter((e) => e != null); return test.every((e) => typeof e !== "string") ? test : void 0; }); const stopWatch = watchImmediate(() => { var _firstParamTargets$va, _firstParamTargets$va2; return [ (_firstParamTargets$va = (_firstParamTargets$va2 = firstParamTargets.value) === null || _firstParamTargets$va2 === void 0 ? void 0 : _firstParamTargets$va2.map((e) => unrefElement(e))) !== null && _firstParamTargets$va !== void 0 ? _firstParamTargets$va : [defaultWindow].filter((e) => e != null), toArray(vue.toValue(firstParamTargets.value ? args[1] : args[0])), toArray(vue.unref(firstParamTargets.value ? args[2] : args[1])), vue.toValue(firstParamTargets.value ? args[3] : args[2]) ]; }, ([raw_targets, raw_events, raw_listeners, raw_options]) => { cleanup(); if (!(raw_targets === null || raw_targets === void 0 ? void 0 : raw_targets.length) || !(raw_events === null || raw_events === void 0 ? void 0 : raw_events.length) || !(raw_listeners === null || raw_listeners === void 0 ? void 0 : raw_listeners.length)) return; const optionsClone = isObject(raw_options) ? { ...raw_options } : raw_options; cleanups.push(...raw_targets.flatMap((el) => raw_events.flatMap((event) => raw_listeners.map((listener) => register(el, event, listener, optionsClone))))); }, { flush: "post" }); const stop = () => { stopWatch(); cleanup(); }; tryOnScopeDispose(cleanup); return stop; } function onClickOutside(target, handler, options = {}) { const { window: window$1 = defaultWindow, ignore = [], capture = true, detectIframe = false, controls = false } = options; if (!window$1) return controls ? { stop: noop, cancel: noop, trigger: noop } : noop; let shouldListen = true; const shouldIgnore = (event) => { return vue.toValue(ignore).some((target$1) => { if (typeof target$1 === "string") return Array.from(window$1.document.querySelectorAll(target$1)).some((el) => el === event.target || event.composedPath().includes(el)); else { const el = unrefElement(target$1); return el && (event.target === el || event.composedPath().includes(el)); } }); }; function hasMultipleRoots(target$1) { const vm = vue.toValue(target$1); return vm && vm.$.subTree.shapeFlag === 16; } function checkMultipleRoots(target$1, event) { const vm = vue.toValue(target$1); const children = vm.$.subTree && vm.$.subTree.children; if (children == null || !Array.isArray(children)) return false; return children.some((child) => child.el === event.target || event.composedPath().includes(child.el)); } const listener = (event) => { const el = unrefElement(target); if (event.target == null) return; if (!(el instanceof Element) && hasMultipleRoots(target) && checkMultipleRoots(target, event)) return; if (!el || el === event.target || event.composedPath().includes(el)) return; if ("detail" in event && event.detail === 0) shouldListen = !shouldIgnore(event); if (!shouldListen) { shouldListen = true; return; } handler(event); }; let isProcessingClick = false; const cleanup = [ useEventListener(window$1, "click", (event) => { if (!isProcessingClick) { isProcessingClick = true; setTimeout(() => { isProcessingClick = false; }, 0); listener(event); } }, { passive: true, capture }), useEventListener(window$1, "pointerdown", (e) => { const el = unrefElement(target); shouldListen = !shouldIgnore(e) && !!(el && !e.composedPath().includes(el)); }, { passive: true }), detectIframe && useEventListener(window$1, "blur", (event) => { setTimeout(() => { var _window$document$acti; const el = unrefElement(target); if (((_window$document$acti = window$1.document.activeElement) === null || _window$document$acti === void 0 ? void 0 : _window$document$acti.tagName) === "IFRAME" && !(el === null || el === void 0 ? void 0 : el.contains(window$1.document.activeElement))) handler(event); }, 0); }, { passive: true }) ].filter(Boolean); const stop = () => cleanup.forEach((fn) => fn()); if (controls) return { stop, cancel: () => { shouldListen = false; }, trigger: (event) => { shouldListen = true; listener(event); shouldListen = false; } }; return stop; } function useMounted() { const isMounted = vue.shallowRef(false); const instance = vue.getCurrentInstance(); if (instance) vue.onMounted(() => { isMounted.value = true; }, instance); return isMounted; } function useSupported(callback) { const isMounted = useMounted(); return vue.computed(() => { isMounted.value; return Boolean(callback()); }); } function useMutationObserver(target, callback, options = {}) { const { window: window$1 = defaultWindow, ...mutationOptions } = options; let observer; const isSupported = useSupported(() => window$1 && "MutationObserver" in window$1); const cleanup = () => { if (observer) { observer.disconnect(); observer = void 0; } }; const stopWatch = vue.watch(vue.computed(() => { const items = toArray(vue.toValue(target)).map(unrefElement).filter(notNullish); return new Set(items); }), (newTargets) => { cleanup(); if (isSupported.value && newTargets.size) { observer = new MutationObserver(callback); newTargets.forEach((el) => observer.observe(el, mutationOptions)); } }, { immediate: true, flush: "post" }); const takeRecords = () => { return observer === null || observer === void 0 ? void 0 : observer.takeRecords(); }; const stop = () => { stopWatch(); cleanup(); }; tryOnScopeDispose(stop); return { isSupported, stop, takeRecords }; } function onElementRemoval(target, callback, options = {}) { const { window: window$1 = defaultWindow, document: document$1 = window$1 === null || window$1 === void 0 ? void 0 : window$1.document, flush = "sync" } = options; if (!window$1 || !document$1) return noop; let stopFn; const cleanupAndUpdate = (fn) => { stopFn === null || stopFn === void 0 || stopFn(); stopFn = fn; }; const stopWatch = vue.watchEffect(() => { const el = unrefElement(target); if (el) { const { stop } = useMutationObserver(document$1, (mutationsList) => { if (mutationsList.map((mutation) => [...mutation.removedNodes]).flat().some((node) => node === el || node.contains(el))) callback(mutationsList); }, { window: window$1, childList: true, subtree: true }); cleanupAndUpdate(stop); } }, { flush }); const stopHandle = () => { stopWatch(); cleanupAndUpdate(); }; tryOnScopeDispose(stopHandle); return stopHandle; } function createKeyPredicate(keyFilter) { if (typeof keyFilter === "function") return keyFilter; else if (typeof keyFilter === "string") return (event) => event.key === keyFilter; else if (Array.isArray(keyFilter)) return (event) => keyFilter.includes(event.key); return () => true; } function onKeyStroke(...args) { let key; let handler; let options = {}; if (args.length === 3) { key = args[0]; handler = args[1]; options = args[2]; } else if (args.length === 2) if (typeof args[1] === "object") { key = true; handler = args[0]; options = args[1]; } else { key = args[0]; handler = args[1]; } else { key = true; handler = args[0]; } const { target = defaultWindow, eventName = "keydown", passive = false, dedupe = false } = options; const predicate = createKeyPredicate(key); const listener = (e) => { if (e.repeat && vue.toValue(dedupe)) return; if (predicate(e)) handler(e); }; return useEventListener(target, eventName, listener, passive); } function useActiveElement(options = {}) { var _options$document; const { window: window$1 = defaultWindow, deep = true, triggerOnRemoval = false } = options; const document$1 = (_options$document = options.document) !== null && _options$document !== void 0 ? _options$document : window$1 === null || window$1 === void 0 ? void 0 : window$1.document; const getDeepActiveElement = () => { let element = document$1 === null || document$1 === void 0 ? void 0 : document$1.activeElement; if (deep) { var _element$shadowRoot; while (element === null || element === void 0 ? void 0 : element.shadowRoot) element = element === null || element === void 0 || (_element$shadowRoot = element.shadowRoot) === null || _element$shadowRoot === void 0 ? void 0 : _element$shadowRoot.activeElement; } return element; }; const activeElement = vue.shallowRef(); const trigger = () => { activeElement.value = getDeepActiveElement(); }; if (window$1) { const listenerOptions = { capture: true, passive: true }; useEventListener(window$1, "blur", (event) => { if (event.relatedTarget !== null) return; trigger(); }, listenerOptions); useEventListener(window$1, "focus", trigger, listenerOptions); } if (triggerOnRemoval) onElementRemoval(activeElement, trigger, { document: document$1 }); trigger(); return activeElement; } function useCssVar(prop, target, options = {}) { const { window: window$1 = defaultWindow, initialValue, observe = false } = options; const variable = vue.shallowRef(initialValue); const elRef = vue.computed(() => { var _window$document; return unrefElement(target) || (window$1 === null || window$1 === void 0 || (_window$document = window$1.document) === null || _window$document === void 0 ? void 0 : _window$document.documentElement); }); function updateCssVar() { const key = vue.toValue(prop); const el = vue.toValue(elRef); if (el && window$1 && key) { var _window$getComputedSt; variable.value = ((_window$getComputedSt = window$1.getComputedStyle(el).getPropertyValue(key)) === null || _window$getComputedSt === void 0 ? void 0 : _window$getComputedSt.trim()) || variable.value || initialValue; } } if (observe) useMutationObserver(elRef, updateCssVar, { attributeFilter: ["style", "class"], window: window$1 }); vue.watch([elRef, () => vue.toValue(prop)], (_, old) => { if (old[0] && old[1]) old[0].style.removeProperty(old[1]); updateCssVar(); }, { immediate: true }); vue.watch([variable, elRef], ([val, el]) => { const raw_prop = vue.toValue(prop); if ((el === null || el === void 0 ? void 0 : el.style) && raw_prop) if (val == null) el.style.removeProperty(raw_prop); else el.style.setProperty(raw_prop, val); }, { immediate: true }); return variable; } function useCurrentElement(rootComponent) { const vm = vue.getCurrentInstance(); const currentElement = computedWithControl(() => null, () => vm.proxy.$el); vue.onUpdated(currentElement.trigger); vue.onMounted(currentElement.trigger); return currentElement; } function useParentElement(element = useCurrentElement()) { const parentElement = vue.shallowRef(); const update = () => { const el = unrefElement(element); if (el) parentElement.value = el.parentElement; }; tryOnMounted(update); vue.watch(() => vue.toValue(element), update); return parentElement; } function useScriptTag(src, onLoaded = noop, options = {}) { const { immediate = true, manual = false, type = "text/javascript", async = true, crossOrigin, referrerPolicy, noModule, defer, document: document$1 = defaultDocument, attrs = {}, nonce = void 0 } = options; const scriptTag = vue.shallowRef(null); let _promise = null; const loadScript = (waitForScriptLoad) => new Promise((resolve, reject) => { const resolveWithElement = (el$1) => { scriptTag.value = el$1; resolve(el$1); return el$1; }; if (!document$1) { resolve(false); return; } let shouldAppend = false; let el = document$1.querySelector(`script[src="${vue.toValue(src)}"]`); if (!el) { el = document$1.createElement("script"); el.type = type; el.async = async; el.src = vue.toValue(src); if (defer) el.defer = defer; if (crossOrigin) el.crossOrigin = crossOrigin; if (noModule) el.noModule = noModule; if (referrerPolicy) el.referrerPolicy = referrerPolicy; if (nonce) el.nonce = nonce; Object.entries(attrs).forEach(([name, value]) => el === null || el === void 0 ? void 0 : el.setAttribute(name, value)); shouldAppend = true; } else if (el.hasAttribute("data-loaded")) resolveWithElement(el); const listenerOptions = { passive: true }; useEventListener(el, "error", (event) => reject(event), listenerOptions); useEventListener(el, "abort", (event) => reject(event), listenerOptions); useEventListener(el, "load", () => { el.setAttribute("data-loaded", "true"); onLoaded(el); resolveWithElement(el); }, listenerOptions); if (shouldAppend) el = document$1.head.appendChild(el); if (!waitForScriptLoad) resolveWithElement(el); }); const load = (waitForScriptLoad = true) => { if (!_promise) _promise = loadScript(waitForScriptLoad); return _promise; }; const unload = () => { if (!document$1) return; _promise = null; if (scriptTag.value) scriptTag.value = null; const el = document$1.querySelector(`script[src="${vue.toValue(src)}"]`); if (el) document$1.head.removeChild(el); }; if (immediate && !manual) tryOnMounted(load); if (!manual) tryOnUnmounted(unload); return { scriptTag, load, unload }; } const _export_sfc = (sfc, props) => { const target = sfc.__vccOpts || sfc; for (const [key, val] of props) { target[key] = val; } return target; }; const _hoisted_1$i = ["active"]; const _sfc_main$j = { __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) => ({ "c22eb872": __props.color, "v70824f1d": __props.size, "v59c28e54": __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$i); }; } }; const BootstrapIcon = _export_sfc(_sfc_main$j, [["__scopeId", "data-v-a9f332df"]]); var _GM_getValue = (() => typeof GM_getValue != "undefined" ? GM_getValue : void 0)(); var _GM_setValue = (() => typeof GM_setValue != "undefined" ? GM_setValue : void 0)(); var _unsafeWindow = (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); 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.textContent.trim(); } getKey() { const match = this.el.tkinfo?.textContent.match(new RegExp("(?<=原調:)\\w*")); return match ? match[0].trim() : ""; } getPlay() { const match = this.el.capoSelect?.textContent.split(/\s*\/\s*/); return match ? match[1].trim() : ""; } getCapo() { const match = this.el.capoSelect?.textContent.split(/\s*\/\s*/); return match ? Number(match[0]) : 0; } getSinger() { const match = this.el.tinfo?.textContent.match(new RegExp("(?<=演唱:).*(?=\\n|$)")); return match ? match[0].trim() : ""; } getComposer() { const match = this.el.tinfo?.textContent.match(new RegExp("(?<=曲:).*?(?=詞:|$)")); return match ? match[0].trim() : ""; } getLyricist() { const match = this.el.tinfo?.textContent.match(new RegExp("(?<=詞:).*?(?=曲:|$)")); return match ? match[0].trim() : ""; } getBpm() { const match = this.el.tkinfo?.textContent.match(/\d+/); return match ? Number(match[0]) : 0; } getSheetText() { const formattedChordSheet = this.el.tone_z.textContent.replaceAll(/\s+?\n/g, "\n").replaceAll("\n\n", "\n").trim().replaceAll(/\s+/g, (match) => { return `{%${match.length}%}`; }); return formattedChordSheet; } } class Chord { static sharps = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; static flats = ["C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B"]; constructor(chordString) { this.chordString = chordString; } 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; } 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; } useSharpModifier() { this.chordString = this.chordString.replaceAll(/[A-G]b/g, (note) => { const noteIndex = Chord.flats.indexOf(note); return Chord.sharps[noteIndex]; }); return this; } useFlatModifier() { this.chordString = this.chordString.replaceAll(/[A-G]#/g, (note) => { const noteIndex = Chord.sharps.indexOf(note); return Chord.flats[noteIndex]; }); return this; } toString() { return this.chordString; } toFormattedString() { return this.chordString.replaceAll( /[#b]/g, `<sup>$&</sup>` ); } } class ChordSheetElement { constructor(chordSheetElement) { this.chordSheetElement = chordSheetElement; } static transposeSheet(delta) { $("#tone_z .tf").each(function() { const chord = new Chord($(this).text()); const newChordHTML = chord.transpose(-delta).toFormattedString(); $(this).html(newChordHTML); }); } formatUnderlines() { const underlineEl = this.chordSheetElement.querySelectorAll("u"); const doubleUnderlineEl = this.chordSheetElement.querySelectorAll("abbr"); underlineEl.forEach((el) => { el.textContent = `{_${el.textContent}_}`; }); doubleUnderlineEl.forEach((el) => { el.textContent = `{=${el.textContent}=}`; }); return this; } #unformat(nodeList) { nodeList.forEach((el) => { el.innerHTML = el.textContent.replaceAll(/\{_|\{=|=\}|_\}/g, "").replaceAll( /[a-z0-9#/]+/gi, `<span class="tf">$&</span>` ); }); } unformatUnderlines() { const underlineEl = this.chordSheetElement.querySelectorAll("u"); const doubleUnderlineEl = this.chordSheetElement.querySelectorAll("abbr"); this.#unformat(underlineEl); this.#unformat(doubleUnderlineEl); return this; } } 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 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 onSheetDomReady(callback) { return new MutationObserver((_records, observer) => { const isMutationDone = !!document.querySelector("#tone_z").childElementCount; if (isMutationDone) { observer.disconnect(); callback(); } }).observe(document.body, { childList: true, subtree: true }); } 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 thisWindow = _unsafeWindow ?? window; const chordShapes = thisWindow?.chord_shapes ?? {}; return chordShapes; } function getChordList() { const chordList = []; $("#tone_z .tf").each(function() { const chordName = $(this).text().trim(); if (chordName) { chordList.push(chordName); } }); 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$h = { key: 0, class: "chord-container" }; const _hoisted_2$b = { class: "chord-name" }; const _hoisted_3$6 = ["chord-name"]; const _sfc_main$i = { __name: "ChordChart", props: { chord: String }, setup(__props) { const props = __props; const chordRef = vue.useTemplateRef("chord"); const chordShapes = getChordShapes(); const isChordExist = vue.ref(true); vue.onMounted(() => { const formattedChordKey = convertChordName(props.chord); const chordShape = chordShapes[formattedChordKey]; if (!chordShape) { return isChordExist.value = false; } const chordObject = { ...chordShape, barres: chordShape.bars?.map((barre) => { return { ...barre, fromString: barre.from_string, toString: barre.to_string }; }), chord: chordShape.chord.map(([stringNum, fretNum]) => { const raw = [stringNum, fretNum]; if (Number.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$h, [ vue.createElementVNode("div", _hoisted_2$b, vue.toDisplayString(props.chord), 1), vue.createElementVNode("div", { ref: "chord", class: "chord-chart", "chord-name": props.chord }, null, 8, _hoisted_3$6) ])) : vue.createCommentVNode("", true); }; } }; const ChordChart = _export_sfc(_sfc_main$i, [["__scopeId", "data-v-128703be"]]); const _hoisted_1$g = { class: "plus91-popup" }; const _sfc_main$h = { __name: "PopupBase", props: { "modelValue": {}, "modelModifiers": {} }, emits: ["update:modelValue"], setup(__props) { const modelValue = vue.useModel(__props, "modelValue"); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide-and-fade" }, { default: vue.withCtx(() => [ vue.withDirectives(vue.createElementVNode("div", _hoisted_1$g, [ vue.renderSlot(_ctx.$slots, "default", {}, void 0, true) ], 512), [ [vue.vShow, modelValue.value] ]) ]), _: 3 }); }; } }; const PopupBase = _export_sfc(_sfc_main$h, [["__scopeId", "data-v-ed81246a"]]); class MonkeyStorage { static getStorageType() { if (_GM_getValue && _GM_setValue) { return "Greasemonkey"; } else { return "LocalStorage"; } } static getItem(key) { const storageType = this.getStorageType(); switch (storageType) { case "Greasemonkey": return _GM_getValue(key, null); case "LocalStorage": return localStorage.getItem(key); default: return null; } } static setItem(key, value) { const storageType = this.getStorageType(); switch (storageType) { case "Greasemonkey": _GM_setValue(key, value); break; case "LocalStorage": localStorage.setItem(key, value); break; } } } const useStore = defineStore("store", { state() { return { isDarkMode: false, isToolbarsShow: false, isPopupShow: { sheet: false, chord: false, font: false, settings: false, menu: false, hotkey: false }, agreeToArchiveSheet: true, isDevMode: false, themeColor: "#4b96a9", transpose: 0, originalCapo: 0, originalKey: "", fontSizeDelta: 0, originalFontSize: 0, originalLineHeight: 0 }; }, persist: { key: "plus91-preferences", storage: MonkeyStorage, deserialize: zipson.parse, serialize: zipson.stringify, pick: ["isDarkMode", "agreeToArchiveSheet", "isDevMode", "themeColor"], beforeHydrate() { console.log("[91Plus] 讀取偏好設置中"); }, afterHydrate() { console.log("[91Plus] 偏好設置讀取完畢"); }, 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; } }, 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$f = { class: "banner" }; const _hoisted_2$a = { class: "chord-popup-container" }; const _sfc_main$g = { __name: "ChordPopup", setup(__props) { const store = useStore(); const bannerText = vue.ref(""); const bannerTextList = [ "此處的和弦圖示僅供參考!由於技術問題,目前尚無法準確繪製,尤其在把位較常出現錯誤,請注意。", "在 91 譜中沒有資料的和弦是畫不出來的呦!" ]; function refreshBanner() { const randomIndex = Math.floor(Math.random() * bannerTextList.length); bannerText.value = bannerTextList[randomIndex]; } const chordList = vue.ref([]); vue.watch(store.isPopupShow, () => { if (!store.isPopupShow.chord) { return; } refreshBanner(); chordList.value = getChordList(); }); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(PopupBase, { id: "plus91-chord-popup", modelValue: vue.unref(store).isPopupShow.chord, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(store).isPopupShow.chord = $event), class: vue.normalizeClass({ "banner-only": !chordList.value.length }) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$f, [ vue.createVNode(BootstrapIcon, { icon: "info-circle-fill", color: "inherit", size: "inherit" }), vue.createElementVNode("section", null, vue.toDisplayString(bannerText.value), 1) ]), vue.createElementVNode("div", _hoisted_2$a, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(chordList.value, (chord) => { return vue.openBlock(), vue.createBlock(ChordChart, { key: chord, chord }, null, 8, ["chord"]); }), 128)) ]) ]), _: 1 }, 8, ["modelValue", "class"]); }; } }; const ChordPopup = _export_sfc(_sfc_main$g, [["__scopeId", "data-v-858a1989"]]); const _hoisted_1$e = { class: "toolbar-icon" }; const _hoisted_2$9 = { key: 0, class: "toolbar-icon-text" }; const _sfc_main$f = { __name: "ToolbarIcon", props: { icon: { type: String, required: true }, text: { type: String, default: "" }, stroke: { type: String, default: "0" }, active: { type: Boolean, default: false }, color: { type: String, default: "whitesmoke" } }, setup(__props) { vue.useCssVars((_ctx) => ({ "v08ff4922": __props.color })); const props = __props; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$e, [ vue.createVNode(BootstrapIcon, { size: "1.3rem", icon: props.icon, color: props.color, stroke: props.stroke, active: props.active }, null, 8, ["icon", "color", "stroke", "active"]), props.text ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$9, vue.toDisplayString(props.text), 1)) : vue.createCommentVNode("", true) ]); }; } }; const ToolbarIcon = _export_sfc(_sfc_main$f, [["__scopeId", "data-v-cbf0cf0b"]]); const _hoisted_1$d = { class: "adjust-widget" }; const _hoisted_2$8 = ["disabled"]; const _hoisted_3$5 = ["disabled"]; const _hoisted_4$2 = ["disabled"]; const _sfc_main$e = { __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) => ({ "v5e7bf01c": __props.color, "v1bd9a428": __props.size })); const props = __props; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$d, [ vue.createElementVNode("button", { class: "adjust-button adjust-button-left", disabled: props.disabledLeft, onClick: _cache[0] || (_cache[0] = (...args) => props.onclickLeft && props.onclickLeft(...args)) }, [ vue.createVNode(BootstrapIcon, { icon: props.iconLeft, color: props.color, size: props.size }, null, 8, ["icon", "color", "size"]) ], 8, _hoisted_2$8), vue.createElementVNode("button", { class: "adjust-button adjust-button-middle", disabled: props.disabledMiddle, onClick: _cache[1] || (_cache[1] = (...args) => props.onclickMiddle && props.onclickMiddle(...args)) }, [ vue.renderSlot(_ctx.$slots, "default", {}, void 0, true) ], 8, _hoisted_3$5), vue.createElementVNode("button", { class: "adjust-button adjust-button-right", disabled: props.disabledRight, onClick: _cache[2] || (_cache[2] = (...args) => props.onclickRight && props.onclickRight(...args)) }, [ vue.createVNode(BootstrapIcon, { icon: props.iconRight, color: props.color, size: props.size }, null, 8, ["icon", "color", "size"]) ], 8, _hoisted_4$2) ]); }; } }; const AdjustWidget = _export_sfc(_sfc_main$e, [["__scopeId", "data-v-0178875e"]]); const _hoisted_1$c = { class: "font-popup-container" }; const _sfc_main$d = { __name: "FontSizePopup", setup(__props) { const store = useStore(); const getFontSize = vue.computed(() => store.originalFontSize + store.fontSizeDelta); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(PopupBase, { id: "plus91-font-popup", modelValue: vue.unref(store).isPopupShow.font, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(store).isPopupShow.font = $event) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$c, [ 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"]) ]) ]), _: 1 }, 8, ["modelValue"]); }; } }; 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$b = { class: "hotkey-item" }; const _hoisted_2$7 = { key: 0, class: "hotkeys" }; const _hoisted_3$4 = { key: 1, class: "hr" }; const _sfc_main$c = { __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$b, [ vue.createElementVNode("div", { class: vue.normalizeClass(["desc", { title: !__props.hotkey }]) }, vue.toDisplayString(__props.desc), 3), __props.hotkey ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_2$7, [ (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$4)) ]); }; } }; const HotkeyItem = _export_sfc(_sfc_main$c, [["__scopeId", "data-v-851f225f"]]); const _hoisted_1$a = { class: "hotkey-popup-container" }; const _hoisted_2$6 = { class: "left-part" }; const _hoisted_3$3 = { class: "right-part" }; const _sfc_main$b = { __name: "HotkeyPopup", setup(__props) { const store = useStore(); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(PopupBase, { id: "plus91-hotkey-popup", modelValue: vue.unref(store).isPopupShow.hotkey, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(store).isPopupShow.hotkey = $event) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$a, [ vue.createElementVNode("section", _hoisted_2$6, [ (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_3$3, [ (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)) ]) ]) ]), _: 1 }, 8, ["modelValue"]); }; } }; const HotkeyPopup = _export_sfc(_sfc_main$b, [["__scopeId", "data-v-07402c98"]]); const _hoisted_1$9 = { class: "icon-button" }; const _hoisted_2$5 = { class: "button-text" }; const _sfc_main$a = { __name: "MenuButton", props: { icon: String, name: String, color: String }, setup(__props) { vue.useCssVars((_ctx) => ({ "v12c3e3a5": __props.color })); const props = __props; return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$9, [ 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 = _export_sfc(_sfc_main$a, [["__scopeId", "data-v-cb0cf859"]]); const _hoisted_1$8 = { class: "menu-popup-container" }; const BUTTON_COLOR = "#555"; const _sfc_main$9 = { __name: "MenuPopup", setup(__props) { const store = useStore(); async function captureAsImage() { const content = document.querySelector("section.content"); const canvas = await html2canvas(content); const blob = await new Promise((resolve) => { canvas.toBlob(resolve, "image/png"); }); const url = URL.createObjectURL(blob); window.open(url, "_blank"); setTimeout(() => URL.revokeObjectURL(url), 1e4); } function 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(); } function 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(PopupBase, { id: "plus91-menu-popup", modelValue: vue.unref(store).isPopupShow.menu, "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(store).isPopupShow.menu = $event) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$8, [ 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 }) ]) ]), _: 1 }, 8, ["modelValue"]); }; } }; const MenuPopup = _export_sfc(_sfc_main$9, [["__scopeId", "data-v-f8df8357"]]); const colors = [ "#4b96a9", "#a2b538", "#e181bf", "#6c59bb" ]; const _hoisted_1$7 = { class: "color-switcher-container" }; const _hoisted_2$4 = ["onClick"]; const _sfc_main$8 = { __name: "ColorSwitcher", props: vue.mergeModels({ options: { type: Array, required: true, validator: (options) => { return options.every((opt) => typeof opt === "string"); } } }, { "modelValue": {}, "modelModifiers": {} }), emits: ["update:modelValue"], setup(__props) { const props = __props; const modelValue = vue.useModel(__props, "modelValue"); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$7, [ (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(props.options, (option) => { return vue.openBlock(), vue.createElementBlock("div", { key: option, class: "color-switcher-option", style: vue.normalizeStyle({ background: `color-mix(in srgb, ${option} 75%, white)`, borderColor: `color-mix(in srgb, ${option} 80%, white)` }), onClick: () => modelValue.value = option }, [ modelValue.value === option ? (vue.openBlock(), vue.createBlock(BootstrapIcon, { key: 0, icon: "check", color: `color-mix(in srgb, ${option} 25%, white)` }, null, 8, ["color"])) : vue.createCommentVNode("", true) ], 12, _hoisted_2$4); }), 128)) ]); }; } }; const ColorSwitcher = _export_sfc(_sfc_main$8, [["__scopeId", "data-v-9499f72a"]]); const _hoisted_1$6 = { class: "toggle-switch" }; const _sfc_main$7 = { __name: "ToggleSwitch", props: { "modelValue": { type: Boolean, default: false }, "modelModifiers": {} }, emits: ["update:modelValue"], setup(__props) { const modelValue = vue.useModel(__props, "modelValue"); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("label", _hoisted_1$6, [ vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event), type: "checkbox", hidden: "" }, null, 512), [ [vue.vModelCheckbox, modelValue.value] ]), vue.createElementVNode("div", { class: vue.normalizeClass(["switch-track", { active: modelValue.value }]) }, [..._cache[1] || (_cache[1] = [ vue.createElementVNode("div", { class: "switch-thumb" }, null, -1) ])], 2) ]); }; } }; const ToggleSwitch = _export_sfc(_sfc_main$7, [["__scopeId", "data-v-1cf8e431"]]); const _hoisted_1$5 = { class: "settings-popup-container" }; const _hoisted_2$3 = { class: "setting-item" }; const _hoisted_3$2 = { class: "setting-item" }; const _hoisted_4$1 = { class: "setting-item" }; const _hoisted_5$1 = { class: "setting-item" }; const _sfc_main$6 = { __name: "SettingsPopup", setup(__props) { const store = useStore(); const themeColor = useCssVar("--theme-color", document.documentElement); vue.watch(() => store.themeColor, (newColor) => { themeColor.value = newColor; }, { immediate: true }); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(PopupBase, { id: "plus91-settings-popup", modelValue: vue.unref(store).isPopupShow.settings, "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(store).isPopupShow.settings = $event) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$5, [ vue.createElementVNode("div", _hoisted_2$3, [ vue.createElementVNode("div", null, [ vue.createVNode(BootstrapIcon, { icon: "palette" }), _cache[5] || (_cache[5] = vue.createTextVNode(" 主題色 ", -1)) ]), vue.createVNode(ColorSwitcher, { modelValue: vue.unref(store).themeColor, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => vue.unref(store).themeColor = $event), options: vue.unref(colors) }, null, 8, ["modelValue", "options"]) ]), vue.createElementVNode("label", _hoisted_3$2, [ vue.createElementVNode("div", null, [ vue.createVNode(BootstrapIcon, { icon: "moon" }), _cache[6] || (_cache[6] = vue.createTextVNode(" 深色模式 ", -1)) ]), vue.createVNode(ToggleSwitch, { modelValue: vue.unref(store).isDarkMode, "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => vue.unref(store).isDarkMode = $event) }, null, 8, ["modelValue"]) ]), vue.createElementVNode("label", _hoisted_4$1, [ vue.createElementVNode("div", null, [ vue.createVNode(BootstrapIcon, { icon: "cloudy" }), _cache[7] || (_cache[7] = vue.createTextVNode(" 協助測試雲端樂譜 ", -1)) ]), vue.createVNode(ToggleSwitch, { modelValue: vue.unref(store).agreeToArchiveSheet, "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => vue.unref(store).agreeToArchiveSheet = $event) }, null, 8, ["modelValue"]) ]), vue.createElementVNode("label", _hoisted_5$1, [ vue.createElementVNode("div", null, [ vue.createVNode(BootstrapIcon, { icon: "code-slash" }), _cache[8] || (_cache[8] = vue.createTextVNode(" 開發者模式 ", -1)) ]), vue.createVNode(ToggleSwitch, { modelValue: vue.unref(store).isDevMode, "onUpdate:modelValue": _cache[3] || (_cache[3] = ($event) => vue.unref(store).isDevMode = $event) }, null, 8, ["modelValue"]) ]) ]) ]), _: 1 }, 8, ["modelValue"]); }; } }; const SettingsPopup = _export_sfc(_sfc_main$6, [["__scopeId", "data-v-3259211c"]]); const _hoisted_1$4 = { class: "sheet-popup-container" }; const _hoisted_2$2 = { class: "text-capo" }; const _hoisted_3$1 = ["innerHTML"]; const _hoisted_4 = { class: "transpose-range-container" }; const _hoisted_5 = ["value"]; const _hoisted_6 = { class: "instrument-select-container" }; const _sfc_main$5 = { __name: "SheetPopup", setup(__props) { const store = useStore(); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(PopupBase, { id: "plus91-sheet-popup", modelValue: vue.unref(store).isPopupShow.sheet, "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => vue.unref(store).isPopupShow.sheet = $event) }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$4, [ 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(() => [ _cache[5] || (_cache[5] = vue.createTextVNode(" CAPO:", -1)), vue.createElementVNode("span", _hoisted_2$2, vue.toDisplayString(vue.unref(store).currentCapo), 1), _cache[6] || (_cache[6] = vue.createTextVNode(" (", -1)), vue.createElementVNode("span", { class: "text-key", innerHTML: vue.unref(store).currentKey }, null, 8, _hoisted_3$1), _cache[7] || (_cache[7] = vue.createTextVNode(") ", -1)) ]), _: 1 }, 8, ["onclick-left", "onclick-middle", "onclick-right"]), vue.createElementVNode("div", _hoisted_4, [ 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_5) ]), vue.createElementVNode("div", _hoisted_6, [ 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"); }) }, " 烏克莉莉 ") ]) ]) ]), _: 1 }, 8, ["modelValue"]); }; } }; const SheetPopup = _export_sfc(_sfc_main$5, [["__scopeId", "data-v-18539399"]]); const _hoisted_1$3 = { id: "plus91-footer" }; const _hoisted_2$1 = { class: "footer-container" }; const _sfc_main$4 = { __name: "AppFooter", props: { active: Boolean }, setup(__props) { const props = __props; const store = useStore(); return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(vue.Transition, { name: "slide" }, { default: vue.withCtx(() => [ vue.withDirectives(vue.createElementVNode("div", _hoisted_1$3, [ vue.createElementVNode("div", _hoisted_2$1, [ vue.createVNode(ToolbarIcon, { icon: "music-note-beamed", text: "譜面", 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", text: "和弦", 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", text: "字型", 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", text: "設定", 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", text: "其他", 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(_sfc_main$d), vue.createVNode(SettingsPopup), vue.createVNode(MenuPopup), vue.createVNode(HotkeyPopup) ]) ], 512), [ [vue.vShow, props.active] ]) ]), _: 1 }); }; } }; const AppFooter = _export_sfc(_sfc_main$4, [["__scopeId", "data-v-c2303173"]]); const _hoisted_1$2 = { id: "plus91-header" }; const _hoisted_2 = { class: "header-container" }; const _hoisted_3 = { class: "search-container" }; const _sfc_main$3 = { __name: "AppHeader", props: { active: Boolean }, setup(__props) { const props = __props; const isSearchInputFocused = vue.ref(false); const searchText = vue.ref(""); function search() { if (!searchText.value) { return; } const url = `https://www.91pu.com.tw/plus/search.php?keyword=${searchText.value}`; window.open(url, "_blank").focus(); } function 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$2, [ vue.createElementVNode("div", _hoisted_2, [ vue.createVNode(ToolbarIcon, { icon: "chevron-left", stroke: ".04rem", onClick: backToPreviousPage }), vue.createElementVNode("form", { onSubmit: vue.withModifiers(search, ["prevent"]) }, [ vue.createElementVNode("div", _hoisted_3, [ vue.withDirectives(vue.createElementVNode("input", { "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => searchText.value = $event), type: "text", placeholder: "搜尋樂譜 —— 91 Plus", onKeydown: _cache[1] || (_cache[1] = vue.withKeys(vue.withModifiers((event) => { event.target.blur(); }, ["stop"]), ["esc"])), onFocus: _cache[2] || (_cache[2] = ($event) => isSearchInputFocused.value = true), onBlur: _cache[3] || (_cache[3] = ($event) => isSearchInputFocused.value = false) }, null, 544), [ [ vue.vModelText, searchText.value, void 0, { trim: true } ] ]), searchText.value ? (vue.openBlock(), vue.createBlock(ToolbarIcon, { key: 0, class: "clear-input", icon: "x", color: isSearchInputFocused.value ? "#0007" : "#fffa", onClick: _cache[4] || (_cache[4] = () => { searchText.value = ""; }) }, null, 8, ["color"])) : vue.createCommentVNode("", true) ]) ], 32), vue.createVNode(ToolbarIcon, { icon: "search", stroke: ".03rem", onClick: search }) ]) ], 512), [ [vue.vShow, props.active] ]) ]), _: 1 }); }; } }; const AppHeader = _export_sfc(_sfc_main$3, [["__scopeId", "data-v-c3427f68"]]); const _hoisted_1$1 = { id: "dark-mode-overlay" }; const _sfc_main$2 = { __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$1, null, 512), [ [vue.vShow, props.active] ]) ]), _: 1 }); }; } }; const DarkModeOverlay = _export_sfc(_sfc_main$2, [["__scopeId", "data-v-111379c3"]]); const _hoisted_1 = { ref: "eruda-container" }; const _sfc_main$1 = { __name: "ErudaContainer", setup(__props) { const store = useStore(); const thisWindow = _unsafeWindow ?? window; const erudaContainer = vue.useTemplateRef("eruda-container"); function initEruda() { const erudaEl = document.createElement("div"); erudaContainer.value.appendChild(erudaEl); thisWindow.eruda.init({ container: erudaEl }); thisWindow.eruda.get("snippets").clear(); thisWindow.eruda.get("snippets").add("儲存模式", () => { console.log(`[91 Plus] 儲存模式:${MonkeyStorage.getStorageType()}`); }, "在控制台顯示目前的儲存模式"); } function handleEruda(isDevMode) { if (isDevMode) { if (!thisWindow.eruda) { useScriptTag("https://cdn.jsdelivr.net/npm/eruda/eruda.min.js", initEruda); } else { initEruda(); } } else { thisWindow.eruda?.destroy(); } } watchImmediate(() => store.isDevMode, handleEruda); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, null, 512); }; } }; const _sfc_main = { __name: "App", setup(__props) { const store = useStore(); const parent = useParentElement(); onClickOutside(parent, store.toggleToolbars); return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock(vue.Fragment, null, [ 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"]), vue.createVNode(_sfc_main$1) ], 64); }; } }; class StoreHandler { #store; constructor() { this.#store = useStore(); } initStateFromDom() { const capoSelected = $(".capo .select").eq(0).text().trim(); const originalCapo = +capoSelected.split(/\s*\/\s*/)[0]; const originalKey = capoSelected.split(/\s*\/\s*/)[1]; this.#store.originalCapo = originalCapo; 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]; this.#store.originalFontSize = fontSize; this.#store.originalLineHeight = lineHeight; const params = getQueryParams(); if (params.transpose) { this.#store.transpose = params.transpose; } } initWatchers() { this.#watchTranspose(); this.#watchFontSize(); } #watchTranspose() { vue.watch(() => this.#store.transpose, (newValue, oldValue) => { ChordSheetElement.transposeSheet((newValue - oldValue) % 12); }); } #watchFontSize() { vue.watch(() => this.#store.fontSizeDelta, (newValue) => { const oFontSize = this.#store.originalFontSize; const oLineHeight = this.#store.originalLineHeight; $("#tone_z").css("font-size", `${oFontSize + newValue}px`); $("#tone_z > p").css("line-height", `${oLineHeight + newValue}px`); }); } initKeyBindings() { const activeElement = useActiveElement(); function isInputFocused() { return activeElement.value?.tagName === "INPUT" || activeElement.value?.tagName === "TEXTAREA"; } function whenInputNotFocused(func) { return () => { if (!isInputFocused()) { func(); } }; } onKeyStroke(" ", whenInputNotFocused(() => { this.#store.toggleToolbars(); })); onKeyStroke("/", whenInputNotFocused(() => { if (!this.#store.isToolbarsShow) { this.#store.toggleToolbars(); this.#store.closePopups(); } setTimeout(() => { $("#plus91-header input")?.get(0)?.focus(); }); })); onKeyStroke("Escape", whenInputNotFocused(() => { if (this.#store.isToolbarsShow) { this.#store.toggleToolbars(); } })); onKeyStroke("ArrowLeft", whenInputNotFocused(() => { if (this.#store.isPopupShow.sheet) { this.#store.plusTranspose(-1); } })); onKeyStroke("ArrowRight", whenInputNotFocused(() => { if (this.#store.isPopupShow.sheet) { this.#store.plusTranspose(1); } })); onKeyStroke("ArrowDown", whenInputNotFocused(() => { if (this.#store.isPopupShow.sheet) { this.#store.transpose = 0; } })); } } function init() { redirect(); const storeHandler = new StoreHandler(); storeHandler.initWatchers(); storeHandler.initKeyBindings(); onSheetDomReady(() => { changeTitle(); storeHandler.initStateFromDom(); const store = useStore(); if (store.agreeToArchiveSheet) { archiveChordSheet(); } }); } const cdnsScss = '@import"https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css";'; importCSS(cdnsScss); const variablesScss = "html{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}"; importCSS(variablesScss); const stylesScss = "html{--toolbar-bg-color: color-mix(in srgb, var(--theme-color) 65%, transparent);--toolbar-border-color: color-mix(in srgb, var(--theme-color) 50%, rgba(255, 255, 255, .1))}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,[class^=AD2M],[id^=adGeek]{display:none!important}"; importCSS(stylesScss); const pinia = createPinia(); pinia.use(index_default); vue.createApp(_sfc_main).use(pinia).mount( (() => { const app = document.createElement("div"); app.id = "vue-91plus"; document.body.append(app); return app; })() ); init(); })(Vue, vexchords, zipson, html2canvas);