在您安裝前,Greasy Fork镜像希望您了解本腳本包含“可能不受歡迎的功能”,可能幫助腳本的作者獲利,而不能給你帶來任何收益。
此腳本內的代碼會追蹤您的瀏覽行為。
打造91譜的最佳體驗
// ==UserScript== // @name 91 Plus M // @namespace https://github.com/DonkeyBear // @version 1.0.2 // @description 打造91譜的最佳體驗 // @author DonkeyBear // @match *://www.91pu.com.tw/m/* // @match *://www.91pu.com.tw/song/* // @icon https://www.91pu.com.tw/icons/favicon-32x32.png // @antifeature tracking // @grant none // ==/UserScript== /* global $ */ /** 若樂譜頁面為電腦版,跳轉至行動版 */ function redirect () { const currentUrl = window.location.href; if ((/\/song\//).test(currentUrl)) { const sheetId = currentUrl.match(/(?<=\/)\d+(?=\.)/)[0]; const newUrl = `https://www.91pu.com.tw/m/tone.shtml?id=${sheetId}`; window.location.replace(newUrl); } } /** 引入 Google Analytics */ 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 = () => { // 此區塊由 Google Analytics 生成 window.dataLayer = window.dataLayer || []; function gtag () { window.dataLayer.push(arguments) } gtag('js', new Date()); gtag('config', 'G-JF4S3HZY31'); }; } /** 注入頁面樣式 */ function injectStyle () { const stylesheet = /* css */` html { background: #fafafa url(/templets/pu/images/tone-bg.gif); } header { background-color: rgba(25, 20, 90, 0.5); backdrop-filter: blur(5px) saturate(80%); -webkit-backdrop-filter: blur(5px) saturate(80%); display: flex; justify-content: center; font-family: system-ui; } header > .set { width: 768px; } .tfunc2 { margin: 10px; } .setint { border-top: 1px solid rgba(255, 255, 255, 0.2); } .setint, .plays .capo { display: flex; justify-content: space-between; } #mtitle { font-family: system-ui; } .setint { border-top: 0; padding: 10px; } .setint > .hr { margin-right: 15px; padding: 0 15px; } .capo-section { flex-grow: 1; margin-right: 0 !important; display: flex !important; justify-content: space-between !important; } .capo-button.decrease { padding-right: 20px; } .capo-button.increase { padding-left: 20px; } /* 需要倒數才能關閉的蓋版廣告 */ #viptoneWindow.window, /* 在頁面最底部的廣告 */ #bottomad, /* 最上方提醒升級VIP的廣告 */ .update_vip_bar, /* 譜上的LOGO和浮水印 */ .wmask, /* 彈出式頁尾 */ footer, /* 自動滾動頁面捲軸 */ .autoscroll, /* 頁首的返回列 */ .backplace, /* 頁首的多餘列 */ .set .keys, .set .plays, .set .clear, /* 功能列上多餘的按鈕 */ .setint .hr:nth-child(4), .setint .hr:nth-child(5), .setint .hr:nth-child(6), /* 其餘的Google廣告 */ .adsbygoogle { display: none !important; } `; const style = document.createElement('style'); style.innerText = stylesheet; document.head.appendChild(style); } /** * @typedef {object} Params * @prop {number} transpose * @prop {boolean} darkMode */ /** * 從 URL 取得參數 * @returns {Params} */ function getQueryParams () { const url = new URL(window.location.href); const params = { transpose: +url.searchParams.get('transpose'), darkMode: !!url.searchParams.get('darkmode') }; return params; } /** 用於操作和弦字串 */ 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']; /** @param {string} chordString */ constructor (chordString) { this.chordString = chordString; } /** @param {number} delta */ 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, /* html */`<sup>$&</sup>`); // eslint-disable-line quotes } } /** 用於修改樂譜 */ class ChordSheetElement { /** @param {HTMLElement} chordSheetElement */ constructor (chordSheetElement) { this.chordSheetElement = 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; } unformatUnderlines () { const underlineEl = this.chordSheetElement.querySelectorAll('u'); const doubleUnderlineEl = this.chordSheetElement.querySelectorAll('abbr'); const unformat = (nodeList) => { nodeList.forEach((el) => { el.innerHTML = el.innerText .replaceAll(/{_|{=|=}|_}/g, '') .replaceAll(/[a-zA-Z0-9#/]+/g, /* html */`<span class="tf">$&</span>`); // eslint-disable-line quotes }); }; unformat(underlineEl); unformat(doubleUnderlineEl); return this; } } /** 用於取得樂譜相關資訊 */ 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 () { const match = this.el.tkinfo?.innerText.match(/(?<=原調:)\w*/); return match ? match[0].trim() : ''; } getPlay () { const match = this.el.capoSelect?.innerText.split(/\s*\/\s*/); return match ? match[1].trim() : ''; } getCapo () { const match = this.el.capoSelect?.innerText.split(/\s*\/\s*/); return match ? Number(match[0]) : 0; } getSinger () { const match = this.el.tinfo?.innerText.match(/(?<=演唱:).*(?=\n|$)/); return match ? match[0].trim() : ''; } getComposer () { const match = this.el.tinfo?.innerText.match(/(?<=曲:).*?(?=詞:|$)/); return match ? match[0].trim() : ''; } getLyricist () { const match = this.el.tinfo?.innerText.match(/(?<=詞:).*?(?=曲:|$)/); return match ? match[0].trim() : ''; } getBpm () { const match = this.el.tkinfo?.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; } } /** * 將 Header 和譜上的和弦移調,並實質修改於 DOM * @param {number} delta */ function transposeSheet (delta) { // 修改 Header 上的 Capo const $spanCapo = $('.capo-button > .text-capo'); const newSpanCapoText = (+$spanCapo.text() + delta) % 12; $spanCapo.text(newSpanCapoText); // 修改 Header 上的 Key const $spanKey = $('.capo-button > .text-key'); const keyName = new Chord($spanKey.text()); const newSpanCapoHTML = keyName.transpose(-delta).toFormattedString(); $spanKey.html(newSpanCapoHTML); // 修改譜上的和弦 $('#tone_z .tf').each(function () { const chord = new Chord($(this).text()); const newChordHTML = chord.transpose(-delta).toFormattedString(); $(this).html(newChordHTML); }); }; /** 初始化並綁定大部分事件 */ function initEventHandlers () { /** @type {number} */ let originalCapo; // 頁面動態讀取完成時觸發 $('body').on('mutation.done', () => { // 記錄原調 const $textCapo = $('.capo-button > .text-capo'); originalCapo = +$textCapo.text(); // 依照 URL 參數進行移調 if (getQueryParams().transpose) { transposeSheet(getQueryParams().transpose); } }); // 點擊移調按鈕時進行移調 $('body').on('click', '.capo-section > .capo-button.decrease', () => { transposeSheet(-1) }); $('body').on('click', '.capo-section > .capo-button.increase', () => { transposeSheet(1) }); $('body').on('click', '.capo-section > .capo-button.info', () => { const $textCapo = $('.capo-button > .text-capo'); const currentCapo = +$textCapo.text(); transposeSheet(originalCapo - currentCapo); }); } /** * 將網頁標題替換為自訂格式 * @returns {boolean} 是否完成 */ function changeTitle () { const $mtitle = $('#mtitle'); const newTitle = $mtitle.text().trim(); if (newTitle) { document.title = `${newTitle} | 91+ M`; return true; } else { return false; } } /** * 修改 Header:替換移調按鈕、增加自訂按鈕等 * @returns {boolean} 是否完成 */ function modifyHeader () { const capoSelectText = $('.capo .select').eq(0).text().trim(); if (!capoSelectText) { return false } const stringCapo = capoSelectText.split(/\s*\/\s*/)[0]; // CAPO const stringKey = capoSelectText.split(/\s*\/\s*/)[1]; // 調 // 新增功能鈕 const newFunctionDiv = document.createElement('div'); newFunctionDiv.classList.add('hr', 'capo-section'); newFunctionDiv.innerHTML = /* html */` <button class="scf capo-button decrease">◀</button> <button class="scf capo-button info"> CAPO:<span class="text-capo">${stringCapo}</span> (<span class="text-key">${ stringKey.replaceAll(/[#b]/g, /* html */`<sup>$&</sup>`) // eslint-disable-line quotes }</span>) </button> <button class="scf capo-button increase">▶</button> `; document.querySelector('.setint').appendChild(newFunctionDiv); return true; } /** * 發送請求至 API,雲端備份樂譜 * @returns {boolean} 是否完成 */ function archiveChordSheet () { const sheet = document.getElementById('tone_z'); if (!sheet?.innerText.trim()) { return false } 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('雲端樂譜備份成功:', response) }) .catch(error => { console.error('雲端樂譜備份失敗:', error) }); } catch { console.warn('樂譜解析失敗,無法備份'); fetch(`https://91-plus-plus-api.fly.dev/report?id=${chordSheetDocument.getId()}`); } return true; } /** * @typedef {object} ObserverCheckList * @prop {boolean} changeTitle 是否已替換頁面標題 * @prop {boolean} modifyHeader 是否已替換 Header * @prop {boolean} archiveChordSheet 是否已將樂譜進行雲端備份 */ /** * 透過 MutationObserver 觸發的處理函式 * @param {ObserverCheckList} checkList */ function observerHandler (checkList) { if (!checkList.changeTitle) { checkList.changeTitle = changeTitle(); } if (!checkList.modifyHeader) { checkList.modifyHeader = modifyHeader(); } if (!checkList.archiveChordSheet) { checkList.archiveChordSheet = archiveChordSheet(); } // 如果已全數完成,則觸發 body 上的 mutation.done 事件 let isAllClear = true; for (const checked of Object.values(checkList)) { if (!checked) { isAllClear = false } } if (isAllClear) { $('body').trigger('mutation.done') } } /** 初始化 MutationObserver */ function initMutationObserver () { /** @type {ObserverCheckList} */ const observerCheckList = { changeTitle: false, modifyHeader: false, archiveChordSheet: false }; const observer = new MutationObserver(() => { observerHandler(observerCheckList); }); observer.observe(document.body, { childList: true, subtree: true }); $('body').on('mutation.done', () => { observer.disconnect() }); } /** 於每天第一次使用時跳出升級建議 */ function askToUpdate () { const currentUrl = window.location.href; if ((/\/song\//).test(currentUrl)) { return } const storageKey = 'plus91-last-visit'; const lastVisit = localStorage.getItem(storageKey); const formatDate = (date) => { const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}-${month}-${day}`; }; const currentDate = formatDate(new Date()); if (currentDate !== lastVisit) { localStorage.setItem(storageKey, currentDate); const ans = confirm('91 Plus M 已經停止更新和維護了,\n建議升級至全新版本的 91 Plus!\n\n(本訊息僅會在每天第一次使用時跳出)'); if (ans) { window.location.replace('https://github.com/DonkeyBear/91Plus/wiki/91-Plus-%E8%88%87-91-Plus-M'); } } } /** 主程式進入點 */ function main () { redirect(); injectGtag(); injectStyle(); initEventHandlers(); initMutationObserver(); askToUpdate(); } main();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址