mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Add "Tools" tab with chart issue scanner
This commit is contained in:
240
src-electron/ipc/issue-scan/ExcelBuilder.ts
Normal file
240
src-electron/ipc/issue-scan/ExcelBuilder.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import exceljs, { Borders } from 'exceljs'
|
||||
import _ from 'lodash'
|
||||
import { FolderIssueType, ScannedChart } from 'scan-chart'
|
||||
|
||||
export function getChartIssues(charts: { chart: ScannedChart; path: string }[]) {
|
||||
const chartIssues: {
|
||||
path: string
|
||||
artist: string
|
||||
name: string
|
||||
charter: string
|
||||
errorName: string
|
||||
errorDescription: string
|
||||
fixMandatory: boolean
|
||||
}[] = []
|
||||
|
||||
for (const chart of charts) {
|
||||
const addIssue = (
|
||||
errorName: string,
|
||||
errorDescription: string,
|
||||
fixMandatory: boolean,
|
||||
) => {
|
||||
|
||||
chartIssues.push({
|
||||
path: chart.path,
|
||||
artist: removeStyleTags(chart.chart.artist ?? ''),
|
||||
name: removeStyleTags(chart.chart.name ?? ''),
|
||||
charter: removeStyleTags(chart.chart.charter ?? ''),
|
||||
errorName,
|
||||
errorDescription,
|
||||
fixMandatory,
|
||||
})
|
||||
}
|
||||
|
||||
if (chart.chart.folderIssues.length > 0) {
|
||||
for (const folderIssue of chart.chart.folderIssues) {
|
||||
if (folderIssue.folderIssue === 'albumArtSize') {
|
||||
continue
|
||||
} // Ignored; .sng conversion fixes this
|
||||
addIssue(
|
||||
folderIssue.folderIssue,
|
||||
folderIssue.description,
|
||||
(
|
||||
[
|
||||
'noMetadata',
|
||||
'invalidMetadata',
|
||||
'noAudio',
|
||||
'badAudio',
|
||||
'noChart',
|
||||
'invalidChart',
|
||||
'badChart',
|
||||
] satisfies FolderIssueType[] as FolderIssueType[]
|
||||
).includes(folderIssue.folderIssue),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const metadataIssue of chart.chart.metadataIssues) {
|
||||
addIssue(
|
||||
metadataIssue.metadataIssue,
|
||||
metadataIssue.description,
|
||||
['"name"', '"artist"', '"charter"'].some(property => metadataIssue.description.includes(property)),
|
||||
)
|
||||
}
|
||||
|
||||
if (chart.chart.notesData) {
|
||||
for (const issue of chart.chart.notesData.chartIssues) {
|
||||
addIssue(
|
||||
issue.noteIssue,
|
||||
`${issue.instrument ? `[${issue.instrument}]` : ''}${issue.difficulty ? `[${issue.difficulty}]` : ''} ${issue.description}`,
|
||||
issue.noteIssue === 'noNotes',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return chartIssues
|
||||
}
|
||||
|
||||
export async function getIssuesXLSX(
|
||||
chartIssues: Awaited<ReturnType<typeof getChartIssues>>,
|
||||
) {
|
||||
const chartIssueHeaders = [
|
||||
{ text: 'Artist', width: 160 / 7 },
|
||||
{ text: 'Name', width: 400 / 7 },
|
||||
{ text: 'Charter', width: 160 / 7 },
|
||||
{ text: 'Issue Name', width: 160 / 7 },
|
||||
{
|
||||
text: 'Issue Description (a more detailed description of issue types can be '
|
||||
+ 'found at https://drive.google.com/open?id=1UK7GsP4ZHJkOg8uREFRMY72svySaDlf0QRTGlk-ruYQ)',
|
||||
width: 650 / 7,
|
||||
},
|
||||
{ text: 'Fix Mandatory?', width: 120 / 7 },
|
||||
{ text: 'Path', width: 600 / 7 },
|
||||
]
|
||||
const chartIssueRows: (string | { text: string; hyperlink: string })[][] = []
|
||||
for (const issue of chartIssues) {
|
||||
chartIssueRows.push([
|
||||
issue.artist,
|
||||
issue.name,
|
||||
issue.charter,
|
||||
issue.errorName,
|
||||
issue.errorDescription,
|
||||
issue.fixMandatory ? 'yes' : 'no',
|
||||
issue.path,
|
||||
])
|
||||
}
|
||||
|
||||
const gridlineBorderStyle = {
|
||||
top: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
left: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
bottom: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
right: { style: 'thin', color: { argb: 'FFD0D0D0' } },
|
||||
} satisfies Partial<Borders>
|
||||
const workbook = new exceljs.Workbook()
|
||||
workbook.creator = 'Chorus'
|
||||
workbook.created = new Date()
|
||||
workbook.modified = new Date()
|
||||
|
||||
const chartIssuesWorksheet = workbook.addWorksheet('Chart Issues', {
|
||||
views: [{ state: 'frozen', ySplit: 1 }], // Sticky header row
|
||||
})
|
||||
chartIssuesWorksheet.autoFilter = {
|
||||
from: { row: 1, column: 1 },
|
||||
to: { row: chartIssueRows.length + 1, column: chartIssueHeaders.length },
|
||||
}
|
||||
chartIssueHeaders.forEach((header, index) => {
|
||||
const cell = chartIssuesWorksheet.getCell(1, index + 1)
|
||||
cell.value = header.text
|
||||
cell.fill = {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFD3D3D3' },
|
||||
}
|
||||
cell.font = { bold: true }
|
||||
const column = chartIssuesWorksheet.getColumn(index + 1)
|
||||
column.width = header.width
|
||||
column.border = gridlineBorderStyle
|
||||
})
|
||||
chartIssuesWorksheet.addRows(chartIssueRows)
|
||||
chartIssuesWorksheet.addConditionalFormatting({
|
||||
ref: `A2:${columnNumberToLetter(chartIssueHeaders.length)}${chartIssueRows.length + 1
|
||||
}`,
|
||||
rules: [
|
||||
{
|
||||
type: 'expression',
|
||||
priority: 99999,
|
||||
formulae: ['MOD(ROW(),2)=0'],
|
||||
style: {
|
||||
fill: {
|
||||
type: 'pattern',
|
||||
pattern: 'solid',
|
||||
fgColor: { argb: 'FFF7F7F7' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return await workbook.xlsx.writeBuffer({ useStyles: true })
|
||||
}
|
||||
|
||||
export function columnNumberToLetter(column: number) {
|
||||
let temp,
|
||||
letter = ''
|
||||
while (column > 0) {
|
||||
temp = (column - 1) % 26
|
||||
letter = String.fromCharCode(temp + 65) + letter
|
||||
column = (column - temp - 1) / 26
|
||||
}
|
||||
return letter
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns an string representation of `ms` that looks like HH:MM:SS.mm
|
||||
*/
|
||||
export function msToExactTime(ms: number) {
|
||||
const seconds = _.round((ms / 1000) % 60, 2)
|
||||
const minutes = Math.floor((ms / 1000 / 60) % 60)
|
||||
const hours = Math.floor((ms / 1000 / 60 / 60) % 24)
|
||||
return `${hours ? `${hours}:` : ''}${_.padStart(
|
||||
minutes + '',
|
||||
2,
|
||||
'0',
|
||||
)}:${_.padStart(seconds.toFixed(2), 5, '0')}`
|
||||
}
|
||||
|
||||
const allowedTags = [
|
||||
'align',
|
||||
'allcaps',
|
||||
'alpha',
|
||||
'b',
|
||||
'br',
|
||||
'color',
|
||||
'cspace',
|
||||
'font',
|
||||
'font-weight',
|
||||
'gradient',
|
||||
'i',
|
||||
'indent',
|
||||
'line-height',
|
||||
'line-indent',
|
||||
'link',
|
||||
'lowercase',
|
||||
'margin',
|
||||
'mark',
|
||||
'mspace',
|
||||
'nobr',
|
||||
'noparse',
|
||||
'page',
|
||||
'pos',
|
||||
'rotate',
|
||||
's',
|
||||
'size',
|
||||
'smallcaps',
|
||||
'space',
|
||||
'sprite',
|
||||
'strikethrough',
|
||||
'style',
|
||||
'sub',
|
||||
'sup',
|
||||
'u',
|
||||
'uppercase',
|
||||
'voffset',
|
||||
'width',
|
||||
]
|
||||
const tagPattern = allowedTags.map(tag => `\\b${tag}\\b`).join('|')
|
||||
/**
|
||||
* @returns `text` with all style tags removed. (e.g. "<color=#AEFFFF>Aren Eternal</color> & Geo" -> "Aren Eternal & Geo")
|
||||
*/
|
||||
export function removeStyleTags(text: string) {
|
||||
let oldText = text
|
||||
let newText = text
|
||||
do {
|
||||
oldText = newText
|
||||
newText = newText
|
||||
.replace(new RegExp(`<\\s*\\/?\\s*(?:#|${tagPattern})[^>]*>`, 'gi'), '')
|
||||
.trim()
|
||||
} while (newText !== oldText)
|
||||
return newText
|
||||
}
|
||||
Reference in New Issue
Block a user