import _ from 'lodash' import { Difficulty, Instrument } from 'scan-chart' import { ChartData } from './interfaces/search.interface' // WARNING: do not import anything related to Electron; the code will not compile correctly. // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyFunction = (...args: any) => any /** Overwrites the type of a nested property in `T` with `U`. */ export type Overwrite = U extends object ? ( T extends object ? { [K in keyof T]: K extends keyof U ? Overwrite : T[K]; } : U ) : U export type RequireMatchingProps = T & { [P in K]-?: NonNullable } /** * @returns `https://drive.google.com/open?id=${fileID}` */ export function driveLink(fileId: string) { return `https://drive.google.com/open?id=${fileId}` } /** * @returns `text` converted to lower case. */ export function lower(text: string) { return text.toLowerCase() } /** * Converts `val` from the range (`fromStart`, `fromEnd`) to the range (`toStart`, `toEnd`). */ export function interpolate(val: number, fromStart: number, fromEnd: number, toStart: number, toEnd: number) { return ((val - fromStart) / (fromEnd - fromStart)) * (toEnd - toStart) + toStart } /** * @returns `objectList` split into multiple groups, where each group contains objects where every one of its values in `keys` matches. */ export function groupBy(objectList: T[], ...keys: (keyof T)[]) { const results: T[][] = [] for (const object of objectList) { const matchingGroup = results.find(result => keys.every(key => result[0][key] === object[key])) if (matchingGroup !== undefined) { matchingGroup.push(object) } else { results.push([object]) } } return results } export const instruments = [ 'guitar', 'guitarcoop', 'rhythm', 'bass', 'drums', 'keys', 'guitarghl', 'guitarcoopghl', 'rhythmghl', 'bassghl', ] as const satisfies Readonly export const difficulties = ['expert', 'hard', 'medium', 'easy'] as const satisfies Readonly export function instrumentDisplay(instrument: Instrument | null) { switch (instrument) { case 'guitar': return 'Lead Guitar' case 'guitarcoop': return 'Co-op Guitar' case 'rhythm': return 'Rhythm Guitar' case 'bass': return 'Bass Guitar' case 'drums': return 'Drums' case 'keys': return 'Keys' case 'guitarghl': return 'GHL (6-fret) Lead Guitar' case 'guitarcoopghl': return 'GHL (6-fret) Co-op Guitar' case 'rhythmghl': return 'GHL (6-fret) Rhythm Guitar' case 'bassghl': return 'GHL (6-fret) Bass Guitar' case null: return 'Any Instrument' } } export function shortInstrumentDisplay(instrument: Instrument | null) { switch (instrument) { case 'guitar': return 'Guitar' case 'guitarcoop': return 'Co-op' case 'rhythm': return 'Rhythm' case 'bass': return 'Bass' case 'drums': return 'Drums' case 'keys': return 'Keys' case 'guitarghl': return 'GHL Guitar' case 'guitarcoopghl': return 'GHL Co-op' case 'rhythmghl': return 'GHL Rhythm' case 'bassghl': return 'GHL Bass' case null: return 'Any Instrument' } } export function difficultyDisplay(difficulty: Difficulty | null) { switch (difficulty) { case 'expert': return 'Expert' case 'hard': return 'Hard' case 'medium': return 'Medium' case 'easy': return 'Easy' case null: return 'Any Difficulty' } } export function instrumentToDiff(instrument: Instrument | 'vocals') { switch (instrument) { case 'guitar': return 'diff_guitar' case 'guitarcoop': return 'diff_guitar_coop' case 'rhythm': return 'diff_rhythm' case 'bass': return 'diff_bass' case 'drums': return 'diff_drums' case 'keys': return 'diff_keys' case 'guitarghl': return 'diff_guitarghl' case 'guitarcoopghl': return 'diff_guitar_coop_ghl' case 'rhythmghl': return 'diff_rhythm_ghl' case 'bassghl': return 'diff_bassghl' case 'vocals': return 'diff_vocals' } } /** * @returns a string representation of `ms` that looks like HH:MM:SS */ export function msToRoughTime(ms: number) { const seconds = _.floor((ms / 1000) % 60) const minutes = _.floor((ms / 1000 / 60) % 60) const hours = _.floor((ms / 1000 / 60 / 60) % 24) return `${hours ? `${hours}:` : ''}${minutes}:${_.padStart(String(seconds), 2, '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. "Aren Eternal & 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 } export function hasIssues(chart: Pick) { if (chart.metadataIssues.length > 0) { return true } for (const folderIssue of chart.folderIssues) { if (!['albumArtSize', 'invalidIni', 'multipleVideo', 'badIniLine'].includes(folderIssue.folderIssue)) { return true } } for (const chartIssue of chart.notesData?.chartIssues ?? []) { if (chartIssue !== 'isDefaultBPM') { return true } } for (const trackIssue of chart.notesData?.trackIssues ?? []) { for (const ti of trackIssue.trackIssues) { if (ti !== 'noNotesOnNonemptyTrack') { return true } } } for (const noteIssue of chart.notesData?.noteIssues ?? []) { for (const ni of noteIssue.noteIssues) { if (ni.issueType !== 'babySustain') { return true } } } return false }