91 Plus M

打造行動裝置看91譜的最好體驗。

目前為 2023-08-11 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         91 Plus M
// @namespace    https://github.com/DonkeyBear
// @version      0.100.0
// @description  打造行動裝置看91譜的最好體驗。
// @author       DonkeyBear
// @match        https://www.91pu.com.tw/m/*
// @match        https://www.91pu.com.tw/song/*
// @icon         https://www.91pu.com.tw/icons/favicon-32x32.png
// @grant        none
// ==/UserScript==

const currentUrl = window.location.href;
if (currentUrl.match(/\/song\//)) {
  const sheetId = currentUrl.match(/(?<=\/)\d+(?=\.)/)[0];
  const newUrl = `https://www.91pu.com.tw/m/tone.shtml?id=${sheetId}`;
  window.location.replace(newUrl);
}

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);

class Chord {
  sharps = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
  flats = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];

  constructor (chordString) {
    this.chordString = chordString;
    return this;
  }

  transpose (delta = 0) {
    this.chordString = this.chordString.replaceAll(/[A-G][#|b]?/g, (note) => {
      const isSharp = this.sharps.includes(note);
      const scale = isSharp ? this.sharps : this.flats;
      const noteIndex = scale.indexOf(note);
      const transposedIndex = (noteIndex + delta + 12) % 12;
      const transposedChord = scale[transposedIndex];
      return transposedChord;
    });
    return this;
  }

  text () {
    return this.chordString;
  }
}

const observerCheckList = {
  modifyTitle: false,
  // modifyHeaderFunction: false,
  modifyTransposeButton: false,
  archiveChordSheet: false
};

const observer = new MutationObserver(() => {
  /* 更改網頁標題 */
  if (!observerCheckList.modifyTitle) {
    const songTitle = document.querySelector('#mtitle');
    if (songTitle?.innerText.trim()) {
      observerCheckList.modifyTitle = true;
      document.title = `${songTitle.innerText} | 91+ M`;
    }
  }

  /* 修改頁首功能鈕(下排) */
  /* if (!observerCheckList.modifyHeaderFunction) {
    if (document.querySelectorAll('.setint .hr').length === 6) {
      // 隱藏頁首部分功能鈕
      observerCheckList.modifyHeaderFunction = true;
      for (let i = 3; i < 6; i++) {
        if (document.querySelectorAll('.setint .hr')[i]) {
          document.querySelectorAll('.setint .hr')[i].style.display = 'none';
        }
      }
      // 新增功能鈕
      const newFunctionDiv = document.createElement('div');
      newFunctionDiv.classList.add('hr', 'select-all');
      newFunctionDiv.innerHTML = html`<button class="scf">全選</button>`; // eslint-disable-line quotes
      newFunctionDiv.onclick = () => {
        if (window.getSelection) {
          const range = document.createRange();
          range.selectNode(document.querySelector('#tone_z'));
          window.getSelection().removeAllRanges();
          window.getSelection().addRange(range);
        }
      };
      document.querySelector('.setint').appendChild(newFunctionDiv);
    }
  } */

  /* 刪除內建的移調鈕,建立自製的 */
  if (!observerCheckList.modifyTransposeButton) {
    if (document.querySelector('.capo .select')) {
      observerCheckList.modifyTransposeButton = true;
      const stringCapo = document.querySelector('.capo .select').innerText.split(' / ')[0]; // CAPO
      const stringKey = document.querySelector('.capo .select').innerText.split(' / ')[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, '<sup>$&</sup>')}</span>)
        </button>
        <button class="scf capo-button increase">▶</button>
      `;
      const spanCapo = newFunctionDiv.querySelector('.text-capo');
      const spanKey = newFunctionDiv.querySelector('.text-key');
      const orginalCapo = Number(spanCapo.innerText);
      function transposeSheet (delta) {
        spanCapo.innerText = (Number(spanCapo.innerText) + delta) % 12;
        const keyName = new Chord(spanKey.innerText);
        spanKey.innerHTML = keyName.transpose(-delta).text().replaceAll(/(#|b)/g, '<sup>$&</sup>');
        for (const chordEl of document.querySelectorAll('#tone_z .tf')) {
          const chord = new Chord(chordEl.innerText);
          chordEl.innerHTML = chord.transpose(-delta).text().replaceAll(/(#|b)/g, '<sup>$&</sup>');
        }
      };
      newFunctionDiv.querySelector('.capo-button.decrease').onclick = () => { transposeSheet(-1) };
      newFunctionDiv.querySelector('.capo-button.increase').onclick = () => { transposeSheet(1) };
      newFunctionDiv.querySelector('.capo-button.info').onclick = () => {
        transposeSheet(orginalCapo - Number(spanCapo.innerText));
      };
      document.querySelector('.setint').appendChild(newFunctionDiv);
    }
  }

  /* 發送請求至 API,雲端備份樂譜 */
  if (!observerCheckList.archiveChordSheet) {
    const sheet = document.getElementById('tone_z');
    if (sheet) {
      observerCheckList.archiveChordSheet = true;
      const underlineEl = sheet.querySelectorAll('u');
      for (const u of underlineEl) { u.innerText = `{_${u.innerText}_}` }
      const urlParams = new URLSearchParams(window.location.search);
      const formBody = {
        id: Number(urlParams.get('id')),
        title: document.getElementById('mtitle').innerText.trim(),
        key: document.querySelector('.tkinfo').innerText.match(/(?<=原調:)\w*/)[0],
        play: document.querySelector('.capo .select').innerText.split(' / ')[1],
        capo: Number(document.querySelector('.capo .select').innerText.split(' / ')[0]),
        singer: document.querySelector('.tinfo').innerText.match(/(?<=演唱:).*(?=(\n|$))/)[0].trim(),
        composer: document.querySelector('.tinfo').innerText.match(/(?<=曲:).*?(?=(詞:|$))/)[0].trim(),
        lyricist: document.querySelector('.tinfo').innerText.match(/(?<=詞:).*?(?=(曲:|$))/)[0].trim(),
        bpm: Number(document.querySelector('.tkinfo')?.innerText.match(/\d+/)[0]),
        sheet_text:
          sheet.innerText
            .replaceAll(/\s+?\n/g, '\n')
            .replaceAll('\n\n', '\n')
            .trim()
            .replaceAll(/\s+/g, (match) => { return `{%${match.length}%}` })
      };
      for (const u of underlineEl) { u.innerText = u.innerText.replaceAll(/{_|_}/g, '') }
      fetch('https://91-plus-plus-api.fly.dev/archive', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formBody)
      })
        .then(() => { console.log('91 Plus 已完成雲端樂譜紀錄!') })
        .catch(error => { console.error(error) });
    }
  }
});

observer.observe(document.body, { childList: true, subtree: true });