style: use JavaScript Standard Style
This commit is contained in:
parent
0fc5b0d990
commit
435c4427f3
|
@ -1 +1,2 @@
|
||||||
dist/*
|
dist/*
|
||||||
|
rollup*
|
||||||
|
|
54
.eslintrc
54
.eslintrc
|
@ -5,31 +5,41 @@
|
||||||
"es6": true,
|
"es6": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": "eslint:recommended",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"plugins": [
|
||||||
"ecmaVersion": 2019,
|
"@typescript-eslint"
|
||||||
"sourceType": "module",
|
],
|
||||||
"ecmaFeatures": {
|
"extends": [
|
||||||
"jsx": true
|
"standard",
|
||||||
}
|
"plugin:@typescript-eslint/recommended",
|
||||||
},
|
"plugin:@typescript-eslint/recommended-requiring-type-checking"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"dot-notation": "off",
|
||||||
|
"no-useless-constructor": "off",
|
||||||
|
"@typescript-eslint/no-useless-constructor": "error",
|
||||||
|
"no-dupe-class-members": "off",
|
||||||
|
"@typescript-eslint/no-dupe-class-members": "error",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/member-delimiter-style": "warn",
|
||||||
|
"@typescript-eslint/ban-ts-ignore": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/prefer-regexp-exec": "off",
|
||||||
|
"no-trailing-spaces": [
|
||||||
"error",
|
"error",
|
||||||
4
|
{
|
||||||
|
"ignoreComments": true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"linebreak-style": [
|
"comma-dangle": [
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"warn",
|
"warn",
|
||||||
"double"
|
"always-multiline"
|
||||||
],
|
]
|
||||||
"semi": [
|
},
|
||||||
"warn",
|
"parserOptions": {
|
||||||
"never"
|
"project": [
|
||||||
],
|
"./tsconfig.json"
|
||||||
"no-console": "off"
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"eslint.options": {},
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
],
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"javascript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||||
|
"typescript.format.insertSpaceBeforeFunctionParenthesis": true,
|
||||||
|
"javascript.format.insertSpaceAfterConstructor": true,
|
||||||
|
"typescript.format.insertSpaceAfterConstructor": true,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
}
|
417
src/main.ts
417
src/main.ts
|
@ -1,259 +1,262 @@
|
||||||
import "./meta"
|
import './meta'
|
||||||
|
|
||||||
import { ScorePlayerData } from "./types"
|
import { ScorePlayerData } from './types'
|
||||||
import { waitForDocumentLoaded } from "./utils"
|
import { waitForDocumentLoaded } from './utils'
|
||||||
import * as recaptcha from "./recaptcha"
|
import * as recaptcha from './recaptcha'
|
||||||
|
|
||||||
import { PDFWorkerHelper } from "./worker-helper"
|
import { PDFWorkerHelper } from './worker-helper'
|
||||||
import FileSaver from "file-saver/dist/FileSaver.js"
|
import FileSaver from 'file-saver/dist/FileSaver.js'
|
||||||
|
|
||||||
const saveAs: typeof import("file-saver").saveAs = FileSaver.saveAs
|
const saveAs: typeof import('file-saver').saveAs = FileSaver.saveAs
|
||||||
|
|
||||||
const PROCESSING_TEXT = "Processing…"
|
const PROCESSING_TEXT = 'Processing…'
|
||||||
const FAILED_TEXT = "❌Download Failed!"
|
const FAILED_TEXT = '❌Download Failed!'
|
||||||
const WEBMSCORE_URL = "https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js"
|
const WEBMSCORE_URL = 'https://cdn.jsdelivr.net/npm/webmscore@0.5/webmscore.js'
|
||||||
|
|
||||||
let pdfBlob: Blob
|
let pdfBlob: Blob
|
||||||
let msczBufferP: Promise<ArrayBuffer>
|
let msczBufferP: Promise<ArrayBuffer> | undefined
|
||||||
|
|
||||||
const generatePDF = async (imgURLs: string[], imgType: "svg" | "png", name?: string) => {
|
const generatePDF = async (imgURLs: string[], imgType: 'svg' | 'png', name?: string): Promise<void> => {
|
||||||
if (pdfBlob) {
|
if (pdfBlob) {
|
||||||
return saveAs(pdfBlob, `${name}.pdf`)
|
return saveAs(pdfBlob, `${name}.pdf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const cachedImg = document.querySelector("img[src*=score_]") as HTMLImageElement
|
const cachedImg: HTMLImageElement = document.querySelector('img[src*=score_]')
|
||||||
const { naturalWidth: width, naturalHeight: height } = cachedImg
|
const { naturalWidth: width, naturalHeight: height } = cachedImg
|
||||||
|
|
||||||
const worker = new PDFWorkerHelper()
|
const worker = new PDFWorkerHelper()
|
||||||
const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height)
|
const pdfArrayBuffer = await worker.generatePDF(imgURLs, imgType, width, height)
|
||||||
worker.terminate()
|
worker.terminate()
|
||||||
|
|
||||||
pdfBlob = new Blob([pdfArrayBuffer])
|
pdfBlob = new Blob([pdfArrayBuffer])
|
||||||
|
|
||||||
saveAs(pdfBlob, `${name}.pdf`)
|
saveAs(pdfBlob, `${name}.pdf`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPagesNumber = (scorePlayerData: ScorePlayerData) => {
|
const getPagesNumber = (scorePlayerData: ScorePlayerData): number => {
|
||||||
try {
|
try {
|
||||||
return scorePlayerData.json.metadata.pages
|
return scorePlayerData.json.metadata.pages
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return document.querySelectorAll("img[src*=score_]").length
|
return document.querySelectorAll('img[src*=score_]').length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getImgType = (): "svg" | "png" => {
|
const getImgType = (): 'svg' | 'png' => {
|
||||||
try {
|
try {
|
||||||
const imgE: HTMLImageElement = document.querySelector("img[src*=score_]")
|
const imgE: HTMLImageElement = document.querySelector('img[src*=score_]')
|
||||||
const { pathname } = new URL(imgE.src)
|
const { pathname } = new URL(imgE.src)
|
||||||
const imgtype = pathname.match(/\.(\w+)$/)[1]
|
const imgtype = pathname.match(/\.(\w+)$/)[1]
|
||||||
return imgtype as "svg" | "png"
|
return imgtype as 'svg' | 'png'
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTitle = (scorePlayerData: ScorePlayerData) => {
|
const getTitle = (scorePlayerData: ScorePlayerData): string => {
|
||||||
try {
|
try {
|
||||||
return scorePlayerData.json.metadata.title
|
return scorePlayerData.json.metadata.title
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return ""
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getScoreFileName = (scorePlayerData: ScorePlayerData) => {
|
const getScoreFileName = (scorePlayerData: ScorePlayerData): string => {
|
||||||
return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, "_")
|
return getTitle(scorePlayerData).replace(/[\s<>:{}"/\\|?*~.\0\cA-\cZ]+/g, '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMscz = async (url: string): Promise<ArrayBuffer> => {
|
const fetchMscz = async (url: string): Promise<ArrayBuffer> => {
|
||||||
if (!msczBufferP) {
|
if (!msczBufferP) {
|
||||||
msczBufferP = (async () => {
|
msczBufferP = (async (): Promise<ArrayBuffer> => {
|
||||||
const token = await recaptcha.execute()
|
const token = await recaptcha.execute()
|
||||||
const r = await fetch(url + token)
|
const r = await fetch(url + token)
|
||||||
const data = await r.arrayBuffer()
|
const data = await r.arrayBuffer()
|
||||||
return data
|
return data
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
return msczBufferP
|
return msczBufferP
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = () => {
|
const main = (): void => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return }
|
||||||
|
|
||||||
// @ts-ignore
|
// init recaptcha
|
||||||
if (!window.UGAPP || !window.UGAPP.store || !window.UGAPP.store.jmuse_settings) { return }
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
recaptcha.init()
|
||||||
|
|
||||||
// init recaptcha
|
// @ts-ignore
|
||||||
recaptcha.init()
|
const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player
|
||||||
|
|
||||||
// @ts-ignore
|
const { id } = scorePlayer.json
|
||||||
const scorePlayer: ScorePlayerData = window.UGAPP.store.jmuse_settings.score_player
|
const baseURL = scorePlayer.urls.image_path
|
||||||
|
|
||||||
const { id } = scorePlayer.json
|
const filename = getScoreFileName(scorePlayer)
|
||||||
const baseURL = scorePlayer.urls.image_path
|
|
||||||
|
|
||||||
const filename = getScoreFileName(scorePlayer)
|
// https://github.com/Xmader/cloudflare-worker-musescore-mscz
|
||||||
|
const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=`
|
||||||
|
|
||||||
// https://github.com/Xmader/cloudflare-worker-musescore-mscz
|
const mxlURL = baseURL + 'score.mxl'
|
||||||
const msczURL = `https://musescore.now.sh/api/mscz?id=${id}&token=`
|
const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls
|
||||||
|
|
||||||
const mxlURL = baseURL + "score.mxl"
|
const btnsDiv = document.querySelector('.score-right .buttons-wrapper') || document.querySelectorAll('aside section > div')[4]
|
||||||
const { midi: midiURL, mp3: mp3URL } = scorePlayer.urls
|
const downloadBtn = btnsDiv.querySelector('button, .button') as HTMLElement
|
||||||
|
downloadBtn.onclick = null
|
||||||
|
|
||||||
const btnsDiv = document.querySelector(".score-right .buttons-wrapper") || document.querySelectorAll("aside section > div")[4]
|
// fix the icon of the download btn
|
||||||
const downloadBtn = btnsDiv.querySelector("button, .button") as HTMLElement
|
// if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
|
||||||
downloadBtn.onclick = null
|
const svgPath: SVGPathElement = downloadBtn.querySelector('svg > path')
|
||||||
|
if (svgPath) {
|
||||||
|
svgPath.setAttribute('d', 'M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z')
|
||||||
|
}
|
||||||
|
|
||||||
// fix the icon of the download btn
|
const imgType = getImgType() || 'svg'
|
||||||
// if the `downloadBtn` seleted was a `Print` btn, replace the `print` icon with the `download` icon
|
|
||||||
const svgPath: SVGPathElement = downloadBtn.querySelector("svg > path")
|
const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => {
|
||||||
if (svgPath) {
|
return baseURL + `score_${i}.${imgType}`
|
||||||
svgPath.setAttribute("d", "M9.6 2.4h4.8V12h2.784l-5.18 5.18L6.823 12H9.6V2.4zM19.2 19.2H4.8v2.4h14.4v-2.4z")
|
})
|
||||||
|
|
||||||
|
const downloadURLs = {
|
||||||
|
MSCZ: null,
|
||||||
|
PDF: null,
|
||||||
|
MusicXML: mxlURL,
|
||||||
|
MIDI: midiURL,
|
||||||
|
MP3: mp3URL,
|
||||||
|
Parts: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||||
|
const createBtn = (name: string) => {
|
||||||
|
const btn: HTMLButtonElement = downloadBtn.cloneNode(true) as any
|
||||||
|
|
||||||
|
if (btn.nodeName.toLowerCase() === 'button') {
|
||||||
|
btn.setAttribute('style', 'width: 205px !important')
|
||||||
|
} else {
|
||||||
|
btn.dataset.target = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const imgType = getImgType() || "svg"
|
const textNode = [...btn.childNodes].find((x) => {
|
||||||
|
return x.textContent.includes('Download') || x.textContent.includes('Print')
|
||||||
const sheetImgURLs = Array.from({ length: getPagesNumber(scorePlayer) }).fill(null).map((_, i) => {
|
|
||||||
return baseURL + `score_${i}.${imgType}`
|
|
||||||
})
|
})
|
||||||
|
textNode.textContent = `Download ${name}`
|
||||||
|
|
||||||
const downloadURLs = {
|
return {
|
||||||
"MSCZ": null,
|
btn,
|
||||||
"PDF": null,
|
textNode,
|
||||||
"MusicXML": mxlURL,
|
|
||||||
"MIDI": midiURL,
|
|
||||||
"MP3": mp3URL,
|
|
||||||
"Parts": null,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const createBtn = (name: string) => {
|
const newDownloadBtns = Object.keys(downloadURLs).map((name) => {
|
||||||
const btn = downloadBtn.cloneNode(true) as HTMLElement
|
const url = downloadURLs[name]
|
||||||
|
const { btn, textNode } = createBtn(name)
|
||||||
|
|
||||||
if (btn.nodeName.toLowerCase() == "button") {
|
if (name === 'PDF') {
|
||||||
btn.setAttribute("style", "width: 205px !important")
|
btn.onclick = async (): Promise<void> => {
|
||||||
} else {
|
const filename = getScoreFileName(scorePlayer)
|
||||||
btn.dataset.target = ""
|
|
||||||
|
textNode.textContent = PROCESSING_TEXT
|
||||||
|
|
||||||
|
try {
|
||||||
|
await generatePDF(sheetImgURLs, imgType, filename)
|
||||||
|
textNode.textContent = 'Download PDF'
|
||||||
|
} catch (err) {
|
||||||
|
textNode.textContent = FAILED_TEXT
|
||||||
|
console.error(err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else if (name === 'MSCZ') {
|
||||||
|
btn.onclick = async (): Promise<void> => {
|
||||||
|
textNode.textContent = PROCESSING_TEXT
|
||||||
|
|
||||||
const textNode = [...btn.childNodes].find((x) => {
|
try {
|
||||||
return x.textContent.includes("Download") || x.textContent.includes("Print")
|
const data = new Blob([await fetchMscz(msczURL)])
|
||||||
|
textNode.textContent = 'Download MSCZ'
|
||||||
|
saveAs(data, `${filename}.mscz`)
|
||||||
|
} catch (err) {
|
||||||
|
textNode.textContent = FAILED_TEXT
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (name === 'Parts') { // download individual parts
|
||||||
|
btn.title = 'Download individual parts (BETA)'
|
||||||
|
const cb = btn.onclick = async (): Promise<void> => {
|
||||||
|
btn.onclick = null
|
||||||
|
textNode.textContent = PROCESSING_TEXT
|
||||||
|
|
||||||
|
const w = window.open('')
|
||||||
|
const txt = document.createTextNode(PROCESSING_TEXT)
|
||||||
|
w.document.body.append(txt)
|
||||||
|
|
||||||
|
// set page hooks
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let score: any
|
||||||
|
const destroy = (): void => {
|
||||||
|
score.destroy()
|
||||||
|
w.close()
|
||||||
|
}
|
||||||
|
window.addEventListener('unload', destroy)
|
||||||
|
w.addEventListener('beforeunload', () => {
|
||||||
|
score.destroy()
|
||||||
|
window.removeEventListener('unload', destroy)
|
||||||
|
textNode.textContent = 'Download Parts'
|
||||||
|
btn.onclick = cb
|
||||||
})
|
})
|
||||||
textNode.textContent = `Download ${name}`
|
|
||||||
|
|
||||||
return {
|
// load webmscore (https://github.com/LibreScore/webmscore)
|
||||||
btn,
|
const script = w.document.createElement('script')
|
||||||
textNode,
|
script.src = WEBMSCORE_URL
|
||||||
|
w.document.body.append(script)
|
||||||
|
await new Promise(resolve => { script.onload = resolve })
|
||||||
|
|
||||||
|
// parse mscz data
|
||||||
|
const data = new Uint8Array(
|
||||||
|
new Uint8Array(await fetchMscz(msczURL)) // copy its ArrayBuffer
|
||||||
|
)
|
||||||
|
score = await w['WebMscore'].load('mscz', data)
|
||||||
|
await score.generateExcerpts()
|
||||||
|
const metadata = await score.metadata()
|
||||||
|
console.log('score metadata loaded by webmscore', metadata)
|
||||||
|
|
||||||
|
// render the part selection page
|
||||||
|
txt.remove()
|
||||||
|
const fieldset = w.document.createElement('fieldset')
|
||||||
|
for (const excerpt of metadata.excerpts) {
|
||||||
|
const e = w.document.createElement('input')
|
||||||
|
e.name = 'score-part'
|
||||||
|
e.type = 'radio'
|
||||||
|
e.value = excerpt.id
|
||||||
|
const label = w.document.createElement('label')
|
||||||
|
label.innerText = excerpt.title
|
||||||
|
const br = w.document.createElement('br')
|
||||||
|
fieldset.append(e, label, br)
|
||||||
}
|
}
|
||||||
|
const submitBtn = w.document.createElement('input')
|
||||||
|
submitBtn.type = 'submit'
|
||||||
|
submitBtn.value = 'Download PDF'
|
||||||
|
fieldset.append(submitBtn)
|
||||||
|
w.document.body.append(fieldset)
|
||||||
|
|
||||||
|
submitBtn.onclick = async (): Promise<void> => {
|
||||||
|
const checked: HTMLInputElement = fieldset.querySelector('input:checked')
|
||||||
|
const id = checked.value
|
||||||
|
|
||||||
|
await score.setExcerptId(id)
|
||||||
|
|
||||||
|
const data = new Blob([await score.savePdf()])
|
||||||
|
saveAs(data, `${filename}-part-${id}.pdf`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
btn.onclick = (): void => {
|
||||||
|
window.open(url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDownloadBtns = Object.keys(downloadURLs).map((name) => {
|
return btn
|
||||||
const url = downloadURLs[name]
|
})
|
||||||
const { btn, textNode } = createBtn(name)
|
|
||||||
|
|
||||||
if (name == "PDF") {
|
|
||||||
btn.onclick = async () => {
|
|
||||||
const filename = getScoreFileName(scorePlayer)
|
|
||||||
|
|
||||||
textNode.textContent = PROCESSING_TEXT
|
|
||||||
|
|
||||||
try {
|
|
||||||
await generatePDF(sheetImgURLs, imgType, filename)
|
|
||||||
textNode.textContent = "Download PDF"
|
|
||||||
} catch (err) {
|
|
||||||
textNode.textContent = FAILED_TEXT
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (name == "MSCZ") {
|
|
||||||
btn.onclick = async () => {
|
|
||||||
textNode.textContent = PROCESSING_TEXT
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = new Blob([await fetchMscz(msczURL)])
|
|
||||||
textNode.textContent = "Download MSCZ"
|
|
||||||
saveAs(data, `${filename}.mscz`)
|
|
||||||
} catch (err) {
|
|
||||||
textNode.textContent = FAILED_TEXT
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (name == "Parts") { // download individual parts
|
|
||||||
btn.title = "Download individual parts (BETA)"
|
|
||||||
const cb = btn.onclick = async () => {
|
|
||||||
btn.onclick = null
|
|
||||||
textNode.textContent = PROCESSING_TEXT
|
|
||||||
|
|
||||||
const w = window.open("")
|
|
||||||
const txt = document.createTextNode(PROCESSING_TEXT)
|
|
||||||
w.document.body.append(txt)
|
|
||||||
|
|
||||||
// set page hooks
|
|
||||||
const destroy = () => {
|
|
||||||
score.destroy()
|
|
||||||
w.close()
|
|
||||||
}
|
|
||||||
window.addEventListener("unload", destroy)
|
|
||||||
w.addEventListener("beforeunload", () => {
|
|
||||||
score.destroy()
|
|
||||||
window.removeEventListener("unload", destroy)
|
|
||||||
textNode.textContent = "Download Parts"
|
|
||||||
btn.onclick = cb
|
|
||||||
})
|
|
||||||
|
|
||||||
// load webmscore (https://github.com/LibreScore/webmscore)
|
|
||||||
const script = w.document.createElement("script")
|
|
||||||
script.src = WEBMSCORE_URL
|
|
||||||
w.document.body.append(script)
|
|
||||||
await new Promise(resolve => { script.onload = resolve })
|
|
||||||
|
|
||||||
// parse mscz data
|
|
||||||
const data = new Uint8Array(
|
|
||||||
new Uint8Array(await fetchMscz(msczURL)) // copy its ArrayBuffer
|
|
||||||
)
|
|
||||||
const score = await w["WebMscore"].load("mscz", data)
|
|
||||||
await score.generateExcerpts()
|
|
||||||
const metadata = await score.metadata()
|
|
||||||
console.log("score metadata loaded by webmscore", metadata)
|
|
||||||
|
|
||||||
// render the part selection page
|
|
||||||
txt.remove()
|
|
||||||
const fieldset = w.document.createElement("fieldset")
|
|
||||||
for (const excerpt of metadata.excerpts) {
|
|
||||||
const e = w.document.createElement("input")
|
|
||||||
e.name = "score-part"
|
|
||||||
e.type = "radio"
|
|
||||||
e.value = excerpt.id
|
|
||||||
const label = w.document.createElement("label")
|
|
||||||
label.innerText = excerpt.title
|
|
||||||
const br = w.document.createElement("br")
|
|
||||||
fieldset.append(e, label, br)
|
|
||||||
}
|
|
||||||
const submitBtn = w.document.createElement("input")
|
|
||||||
submitBtn.type = "submit"
|
|
||||||
submitBtn.value = "Download PDF"
|
|
||||||
fieldset.append(submitBtn)
|
|
||||||
w.document.body.append(fieldset)
|
|
||||||
|
|
||||||
submitBtn.onclick = async () => {
|
|
||||||
const checked: HTMLInputElement = w.document.querySelector("input:checked")
|
|
||||||
const id = checked.value
|
|
||||||
|
|
||||||
await score.setExcerptId(id)
|
|
||||||
|
|
||||||
const data = new Blob([await score.savePdf()])
|
|
||||||
saveAs(data, `${filename}-part-${id}.pdf`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
btn.onclick = () => {
|
|
||||||
window.open(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return btn
|
|
||||||
})
|
|
||||||
|
|
||||||
downloadBtn.replaceWith(...newDownloadBtns)
|
|
||||||
|
|
||||||
|
downloadBtn.replaceWith(...newDownloadBtns)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
waitForDocumentLoaded().then(main)
|
waitForDocumentLoaded().then(main)
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
/**
|
/**
|
||||||
* the site key for Google reCAPTCHA v3
|
* the site key for Google reCAPTCHA v3
|
||||||
*/
|
*/
|
||||||
const SITE_KEY = "6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM"
|
const SITE_KEY = '6Ldxtt8UAAAAALvcRqWTlVOVIB7MmEWwN-zw_9fM'
|
||||||
|
|
||||||
type token = string;
|
type token = string;
|
||||||
interface GRecaptcha {
|
interface GRecaptcha {
|
||||||
ready(cb: Function): void;
|
ready (cb: Function): void;
|
||||||
execute(siteKey: string, opts: { action: string }): Promise<token>;
|
execute (siteKey: string, opts: { action: string }): Promise<token>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gr: GRecaptcha | Promise<GRecaptcha>
|
let gr: GRecaptcha | Promise<GRecaptcha>
|
||||||
|
@ -16,33 +16,33 @@ let gr: GRecaptcha | Promise<GRecaptcha>
|
||||||
* load reCAPTCHA
|
* load reCAPTCHA
|
||||||
*/
|
*/
|
||||||
const load = (): Promise<GRecaptcha> => {
|
const load = (): Promise<GRecaptcha> => {
|
||||||
// load script
|
// load script
|
||||||
const script = document.createElement("script")
|
const script = document.createElement('script')
|
||||||
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}`
|
script.src = `https://www.recaptcha.net/recaptcha/api.js?render=${SITE_KEY}`
|
||||||
script.async = true
|
script.async = true
|
||||||
document.body.appendChild(script)
|
document.body.appendChild(script)
|
||||||
|
|
||||||
// add css
|
// add css
|
||||||
const style = document.createElement("style")
|
const style = document.createElement('style')
|
||||||
style.innerHTML = ".grecaptcha-badge { display: none !important; }"
|
style.innerHTML = '.grecaptcha-badge { display: none !important; }'
|
||||||
document.head.appendChild(style)
|
document.head.appendChild(style)
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
script.onload = () => {
|
script.onload = (): void => {
|
||||||
const grecaptcha: GRecaptcha = window["grecaptcha"]
|
const grecaptcha: GRecaptcha = window['grecaptcha']
|
||||||
grecaptcha.ready(() => resolve(grecaptcha))
|
grecaptcha.ready(() => resolve(grecaptcha))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const init = () => {
|
export const init = (): GRecaptcha | Promise<GRecaptcha> => {
|
||||||
if (!gr) {
|
if (!gr) {
|
||||||
gr = load()
|
gr = load()
|
||||||
}
|
}
|
||||||
return gr
|
return gr
|
||||||
}
|
}
|
||||||
|
|
||||||
export const execute = async (): Promise<token> => {
|
export const execute = async (): Promise<token> => {
|
||||||
const captcha = await init()
|
const captcha = await init()
|
||||||
return captcha.execute(SITE_KEY, { action: "downloadmscz" })
|
return captcha.execute(SITE_KEY, { action: 'downloadmscz' })
|
||||||
}
|
}
|
||||||
|
|
110
src/types.ts
110
src/types.ts
|
@ -1,84 +1,84 @@
|
||||||
|
|
||||||
interface SourceData {
|
interface SourceData {
|
||||||
type: string; // "audio"
|
type: string; // "audio"
|
||||||
title: string; // "Musescore audio"
|
title: string; // "Musescore audio"
|
||||||
nid: number;
|
nid: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommentData = any;
|
type CommentData = any;
|
||||||
|
|
||||||
interface PartData {
|
interface PartData {
|
||||||
part: {
|
part: {
|
||||||
name: string;
|
name: string;
|
||||||
program: number;
|
program: number;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Metadata {
|
interface Metadata {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
composer?: string;
|
composer?: string;
|
||||||
poet?: string;
|
poet?: string;
|
||||||
pages: number;
|
pages: number;
|
||||||
measures: number;
|
measures: number;
|
||||||
lyrics: number;
|
lyrics: number;
|
||||||
chordnames: number;
|
chordnames: number;
|
||||||
keysig: number;
|
keysig: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
parts: PartData[];
|
parts: PartData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScoreJson {
|
interface ScoreJson {
|
||||||
id: number;
|
id: number;
|
||||||
vid: number;
|
vid: number;
|
||||||
dates: {
|
dates: {
|
||||||
revised: number;
|
revised: number;
|
||||||
};
|
};
|
||||||
secret: string;
|
secret: string;
|
||||||
permalink: string;
|
permalink: string;
|
||||||
custom_url: string;
|
custom_url: string;
|
||||||
format: string; // "0"
|
format: string; // "0"
|
||||||
has_custom_audio: 0 | 1;
|
has_custom_audio: 0 | 1;
|
||||||
metadata: Metadata;
|
metadata: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UrlsData {
|
interface UrlsData {
|
||||||
midi: string;
|
midi: string;
|
||||||
mp3: string;
|
mp3: string;
|
||||||
space: string;
|
space: string;
|
||||||
image_path: string;
|
image_path: string;
|
||||||
media?: string[];
|
media?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccessControlData {
|
interface AccessControlData {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
hasAccess: boolean;
|
hasAccess: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PianoKeyboardData extends AccessControlData {
|
interface PianoKeyboardData extends AccessControlData {
|
||||||
midiUrl: string;
|
midiUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PianoRollData extends AccessControlData {
|
interface PianoRollData extends AccessControlData {
|
||||||
resourcesUrl: string;
|
resourcesUrl: string;
|
||||||
feedbackUrl: string;
|
feedbackUrl: string;
|
||||||
forceShow: boolean;
|
forceShow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScorePlayerData {
|
export interface ScorePlayerData {
|
||||||
embed: boolean;
|
embed: boolean;
|
||||||
sources: SourceData[];
|
sources: SourceData[];
|
||||||
default_source?: SourceData;
|
default_source?: SourceData;
|
||||||
mixer?: string;
|
mixer?: string;
|
||||||
secondaryMixer?: string;
|
secondaryMixer?: string;
|
||||||
bucket?: string; // "https://musescore.com/static/musescore/scoredata"
|
bucket?: string; // "https://musescore.com/static/musescore/scoredata"
|
||||||
json: ScoreJson;
|
json: ScoreJson;
|
||||||
render_vector: boolean;
|
render_vector: boolean;
|
||||||
comments: CommentData[];
|
comments: CommentData[];
|
||||||
score_id: number;
|
score_id: number;
|
||||||
urls: UrlsData;
|
urls: UrlsData;
|
||||||
sendEvents?: boolean;
|
sendEvents?: boolean;
|
||||||
pianoKeyboard: PianoKeyboardData;
|
pianoKeyboard: PianoKeyboardData;
|
||||||
pianoRoll: PianoRollData;
|
pianoRoll: PianoRollData;
|
||||||
}
|
}
|
||||||
|
|
38
src/utils.ts
38
src/utils.ts
|
@ -1,24 +1,24 @@
|
||||||
|
|
||||||
export const getIndexPath = (id: number) => {
|
export const getIndexPath = (id: number): string => {
|
||||||
const idStr = String(id)
|
const idStr = String(id)
|
||||||
// 获取最后三位,倒序排列
|
// 获取最后三位,倒序排列
|
||||||
// x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7
|
// x, y, z are the reversed last digits of the score id. Example: id 123456789, x = 9, y = 8, z = 7
|
||||||
// https://developers.musescore.com/#/file-urls
|
// https://developers.musescore.com/#/file-urls
|
||||||
// "5449062" -> ["2", "6", "0"]
|
// "5449062" -> ["2", "6", "0"]
|
||||||
const indexN = idStr.split("").reverse().slice(0, 3)
|
const indexN = idStr.split('').reverse().slice(0, 3)
|
||||||
return indexN.join("/")
|
return indexN.join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const waitForDocumentLoaded = (): Promise<void> => {
|
export const waitForDocumentLoaded = (): Promise<void> => {
|
||||||
if (document.readyState !== "complete") {
|
if (document.readyState !== 'complete') {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
document.addEventListener("readystatechange", () => {
|
document.addEventListener('readystatechange', () => {
|
||||||
if (document.readyState == "complete") {
|
if (document.readyState === 'complete') {
|
||||||
resolve()
|
resolve()
|
||||||
}
|
}
|
||||||
}, { once: true })
|
}, { once: true })
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
|
|
||||||
import { PDFWorkerMessage } from "./worker"
|
import { PDFWorkerMessage } from './worker'
|
||||||
import { PDFWorker } from "../dist/cache/worker"
|
import { PDFWorker } from '../dist/cache/worker'
|
||||||
|
|
||||||
const scriptUrlFromFunction = (fn: Function) => {
|
const scriptUrlFromFunction = (fn: Function): string => {
|
||||||
const blob = new Blob(["(" + fn.toString() + ")()"], { type: "application/javascript" })
|
const blob = new Blob(['(' + fn.toString() + ')()'], { type: 'application/javascript' })
|
||||||
return URL.createObjectURL(blob)
|
return URL.createObjectURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PDFWorkerHelper extends Worker {
|
export class PDFWorkerHelper extends Worker {
|
||||||
constructor() {
|
constructor () {
|
||||||
const url = scriptUrlFromFunction(PDFWorker)
|
const url = scriptUrlFromFunction(PDFWorker)
|
||||||
super(url)
|
super(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
generatePDF(imgURLs: string[], imgType: "svg" | "png", width: number, height: number): Promise<ArrayBuffer> {
|
generatePDF (imgURLs: string[], imgType: 'svg' | 'png', width: number, height: number): Promise<ArrayBuffer> {
|
||||||
const msg: PDFWorkerMessage = [
|
const msg: PDFWorkerMessage = [
|
||||||
imgURLs,
|
imgURLs,
|
||||||
imgType,
|
imgType,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
]
|
]
|
||||||
this.postMessage(msg)
|
this.postMessage(msg)
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
this.addEventListener("message", (e) => {
|
this.addEventListener('message', (e) => {
|
||||||
resolve(e.data)
|
resolve(e.data)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
137
src/worker.ts
137
src/worker.ts
|
@ -1,88 +1,87 @@
|
||||||
|
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
import PDFDocument from "pdfkit/lib/document"
|
import PDFDocument from 'pdfkit/lib/document'
|
||||||
import SVGtoPDF from "svg-to-pdfkit"
|
import SVGtoPDF from 'svg-to-pdfkit'
|
||||||
|
|
||||||
type ImgType = "svg" | "png"
|
type ImgType = 'svg' | 'png'
|
||||||
|
|
||||||
const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const pdf = new (PDFDocument as typeof import("pdfkit"))({
|
|
||||||
// compress: true,
|
|
||||||
size: [width, height],
|
|
||||||
autoFirstPage: false,
|
|
||||||
margin: 0,
|
|
||||||
layout: "portrait",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (imgType == "png") {
|
|
||||||
const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL))
|
|
||||||
|
|
||||||
imgDataUrlList.forEach((data) => {
|
|
||||||
pdf.addPage()
|
|
||||||
pdf.image(data, {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
} else { // imgType == "svg"
|
|
||||||
const svgList = await Promise.all(imgURLs.map(fetchText))
|
|
||||||
|
|
||||||
svgList.forEach((svg) => {
|
|
||||||
pdf.addPage()
|
|
||||||
SVGtoPDF(pdf, svg, 0, 0, {
|
|
||||||
preserveAspectRatio: "none",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
const buf: Uint8Array = await pdf.getBuffer()
|
|
||||||
|
|
||||||
return buf.buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
const getDataURL = (blob: Blob): Promise<string> => {
|
const getDataURL = (blob: Blob): Promise<string> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.onload = () => {
|
reader.onload = (): void => {
|
||||||
const result = reader.result
|
const result = reader.result
|
||||||
resolve(result as string)
|
resolve(result as string)
|
||||||
}
|
}
|
||||||
reader.onerror = reject
|
reader.onerror = reject
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchDataURL = async (imgUrl: string): Promise<string> => {
|
const fetchDataURL = async (imgUrl: string): Promise<string> => {
|
||||||
const r = await fetch(imgUrl)
|
const r = await fetch(imgUrl)
|
||||||
const blob = await r.blob()
|
const blob = await r.blob()
|
||||||
return getDataURL(blob)
|
return getDataURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchText = async (imgUrl: string): Promise<string> => {
|
const fetchText = async (imgUrl: string): Promise<string> => {
|
||||||
const r = await fetch(imgUrl)
|
const r = await fetch(imgUrl)
|
||||||
return r.text()
|
return r.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatePDF = async (imgURLs: string[], imgType: ImgType, width: number, height: number): Promise<ArrayBuffer> => {
|
||||||
|
// @ts-ignore
|
||||||
|
const pdf = new (PDFDocument as typeof import('pdfkit'))({
|
||||||
|
// compress: true,
|
||||||
|
size: [width, height],
|
||||||
|
autoFirstPage: false,
|
||||||
|
margin: 0,
|
||||||
|
layout: 'portrait',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (imgType === 'png') {
|
||||||
|
const imgDataUrlList: string[] = await Promise.all(imgURLs.map(fetchDataURL))
|
||||||
|
|
||||||
|
imgDataUrlList.forEach((data) => {
|
||||||
|
pdf.addPage()
|
||||||
|
pdf.image(data, {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else { // imgType == "svg"
|
||||||
|
const svgList = await Promise.all(imgURLs.map(fetchText))
|
||||||
|
|
||||||
|
svgList.forEach((svg) => {
|
||||||
|
pdf.addPage()
|
||||||
|
SVGtoPDF(pdf, svg, 0, 0, {
|
||||||
|
preserveAspectRatio: 'none',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const buf: Uint8Array = await pdf.getBuffer()
|
||||||
|
|
||||||
|
return buf.buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PDFWorkerMessage = [string[], ImgType, number, number];
|
export type PDFWorkerMessage = [string[], ImgType, number, number];
|
||||||
|
|
||||||
onmessage = async (e) => {
|
onmessage = async (e): Promise<void> => {
|
||||||
const [
|
const [
|
||||||
imgURLs,
|
imgURLs,
|
||||||
imgType,
|
imgType,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
] = e.data as PDFWorkerMessage
|
] = e.data as PDFWorkerMessage
|
||||||
|
|
||||||
const pdfBuf = await generatePDF(
|
const pdfBuf = await generatePDF(
|
||||||
imgURLs,
|
imgURLs,
|
||||||
imgType,
|
imgType,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
)
|
)
|
||||||
|
|
||||||
postMessage(pdfBuf, [pdfBuf])
|
postMessage(pdfBuf, [pdfBuf])
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2019",
|
||||||
|
],
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"sourceMap": false,
|
||||||
|
"newLine": "lf",
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue