mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 06:09:39 +00:00
Restructure
This commit is contained in:
28
src-shared/ElectronUtilFunctions.ts
Normal file
28
src-shared/ElectronUtilFunctions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { basename, parse } from 'path'
|
||||
|
||||
import { getSettingsHandler } from '../ipc/SettingsHandler.ipc'
|
||||
import { emitIPCEvent } from '../main'
|
||||
import { lower } from './UtilFunctions'
|
||||
|
||||
/**
|
||||
* @returns The relative filepath from the library folder to `absoluteFilepath`.
|
||||
*/
|
||||
export function getRelativeFilepath(absoluteFilepath: string) {
|
||||
const settings = getSettingsHandler.getSettings()
|
||||
return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if `name` has a valid video file extension.
|
||||
*/
|
||||
export function hasVideoExtension(name: string) {
|
||||
return (['.mp4', '.avi', '.webm', '.ogv', '.mpeg'].includes(parse(lower(name)).ext))
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message in the main BrowserWindow's console.
|
||||
* Note: Error objects can't be serialized by this; use inspect(err) before passing it here.
|
||||
*/
|
||||
export function devLog(...messages: unknown[]) {
|
||||
emitIPCEvent('log', messages)
|
||||
}
|
||||
123
src-shared/IPCHandler.ts
Normal file
123
src-shared/IPCHandler.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { UpdateInfo } from 'electron-updater'
|
||||
|
||||
import { albumArtHandler } from '../ipc/browse/AlbumArtHandler.ipc'
|
||||
import { batchSongDetailsHandler } from '../ipc/browse/BatchSongDetailsHandler.ipc'
|
||||
import { searchHandler } from '../ipc/browse/SearchHandler.ipc'
|
||||
import { songDetailsHandler } from '../ipc/browse/SongDetailsHandler.ipc'
|
||||
import { clearCacheHandler } from '../ipc/CacheHandler.ipc'
|
||||
import { downloadHandler } from '../ipc/download/DownloadHandler'
|
||||
import { openURLHandler } from '../ipc/OpenURLHandler.ipc'
|
||||
import { getSettingsHandler, setSettingsHandler } from '../ipc/SettingsHandler.ipc'
|
||||
import { downloadUpdateHandler, getCurrentVersionHandler, getUpdateAvailableHandler, quitAndInstallHandler, updateChecker, UpdateProgress } from '../ipc/UpdateHandler.ipc'
|
||||
import { Download, DownloadProgress } from './interfaces/download.interface'
|
||||
import { SongResult, SongSearch } from './interfaces/search.interface'
|
||||
import { AlbumArtResult, VersionResult } from './interfaces/songDetails.interface'
|
||||
import { Settings } from './Settings'
|
||||
|
||||
/**
|
||||
* To add a new IPC listener:
|
||||
* 1.) Write input/output interfaces
|
||||
* 2.) Add the event to IPCEvents
|
||||
* 3.) Write a class that implements IPCHandler
|
||||
* 4.) Add the class to getIPCHandlers
|
||||
*/
|
||||
|
||||
export function getIPCInvokeHandlers(): IPCInvokeHandler<keyof IPCInvokeEvents>[] {
|
||||
return [
|
||||
getSettingsHandler,
|
||||
clearCacheHandler,
|
||||
searchHandler,
|
||||
songDetailsHandler,
|
||||
batchSongDetailsHandler,
|
||||
albumArtHandler,
|
||||
getCurrentVersionHandler,
|
||||
getUpdateAvailableHandler,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of possible async IPC events that return values, mapped to their input and output types.
|
||||
*/
|
||||
export interface IPCInvokeEvents {
|
||||
'get-settings': {
|
||||
input: undefined
|
||||
output: Settings
|
||||
}
|
||||
'clear-cache': {
|
||||
input: undefined
|
||||
output: void
|
||||
}
|
||||
'song-search': {
|
||||
input: SongSearch
|
||||
output: SongResult[]
|
||||
}
|
||||
'album-art': {
|
||||
input: SongResult['id']
|
||||
output: AlbumArtResult
|
||||
}
|
||||
'song-details': {
|
||||
input: SongResult['id']
|
||||
output: VersionResult[]
|
||||
}
|
||||
'batch-song-details': {
|
||||
input: number[]
|
||||
output: VersionResult[]
|
||||
}
|
||||
'get-current-version': {
|
||||
input: undefined
|
||||
output: string
|
||||
}
|
||||
'get-update-available': {
|
||||
input: undefined
|
||||
output: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an object that handles the `E` async IPC event that will return a value.
|
||||
*/
|
||||
export interface IPCInvokeHandler<E extends keyof IPCInvokeEvents> {
|
||||
event: E
|
||||
handler(data: IPCInvokeEvents[E]['input']): Promise<IPCInvokeEvents[E]['output']> | IPCInvokeEvents[E]['output']
|
||||
}
|
||||
|
||||
|
||||
export function getIPCEmitHandlers(): IPCEmitHandler<keyof IPCEmitEvents>[] {
|
||||
return [
|
||||
downloadHandler,
|
||||
setSettingsHandler,
|
||||
downloadUpdateHandler,
|
||||
updateChecker,
|
||||
quitAndInstallHandler,
|
||||
openURLHandler,
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of possible async IPC events that don't return values, mapped to their input types.
|
||||
*/
|
||||
export interface IPCEmitEvents {
|
||||
'log': any[]
|
||||
|
||||
'download': Download
|
||||
'download-updated': DownloadProgress
|
||||
'set-settings': Settings
|
||||
'queue-updated': number[]
|
||||
|
||||
'update-error': Error
|
||||
'update-available': UpdateInfo
|
||||
'update-progress': UpdateProgress
|
||||
'update-downloaded': undefined
|
||||
'download-update': undefined
|
||||
'retry-update': undefined
|
||||
'quit-and-install': undefined
|
||||
'open-url': string
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an object that handles the `E` async IPC event that will not return a value.
|
||||
*/
|
||||
export interface IPCEmitHandler<E extends keyof IPCEmitEvents> {
|
||||
event: E
|
||||
handler(data: IPCEmitEvents[E]): void
|
||||
}
|
||||
18
src-shared/Paths.ts
Normal file
18
src-shared/Paths.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { app } from 'electron'
|
||||
import { join } from 'path'
|
||||
|
||||
// Data paths
|
||||
export const dataPath = join(app.getPath('userData'), 'bridge_data')
|
||||
export const libraryPath = join(dataPath, 'library.db')
|
||||
export const settingsPath = join(dataPath, 'settings.json')
|
||||
export const tempPath = join(dataPath, 'temp')
|
||||
export const themesPath = join(dataPath, 'themes')
|
||||
|
||||
// URL
|
||||
export const serverURL = 'bridge-db.net'
|
||||
|
||||
// OAuth callback server
|
||||
export const SERVER_PORT = 42813
|
||||
export const REDIRECT_BASE = `http://127.0.0.1:${SERVER_PORT}`
|
||||
export const REDIRECT_PATH = `/oauth2callback`
|
||||
export const REDIRECT_URI = `${REDIRECT_BASE}${REDIRECT_PATH}`
|
||||
19
src-shared/Settings.ts
Normal file
19
src-shared/Settings.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Represents Bridge's user settings.
|
||||
*/
|
||||
export interface Settings {
|
||||
rateLimitDelay: number // Number of seconds to wait between each file download from Google servers
|
||||
downloadVideos: boolean // If background videos should be downloaded
|
||||
theme: string // The name of the currently enabled UI theme
|
||||
libraryPath: string // The path to the user's library
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridge's default user settings.
|
||||
*/
|
||||
export const defaultSettings: Settings = {
|
||||
rateLimitDelay: 31,
|
||||
downloadVideos: true,
|
||||
theme: 'Default',
|
||||
libraryPath: undefined,
|
||||
}
|
||||
61
src-shared/UtilFunctions.ts
Normal file
61
src-shared/UtilFunctions.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import randomBytes from 'randombytes'
|
||||
import sanitize from 'sanitize-filename'
|
||||
|
||||
// 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
|
||||
|
||||
/**
|
||||
* @returns `filename` with all invalid filename characters replaced.
|
||||
*/
|
||||
export function sanitizeFilename(filename: string): string {
|
||||
const newFilename = sanitize(filename, {
|
||||
replacement: ((invalidChar: string) => {
|
||||
switch (invalidChar) {
|
||||
case '<': return '❮'
|
||||
case '>': return '❯'
|
||||
case ':': return '꞉'
|
||||
case '"': return "'"
|
||||
case '/': return '/'
|
||||
case '\\': return '⧵'
|
||||
case '|': return '⏐'
|
||||
case '?': return '?'
|
||||
case '*': return '⁎'
|
||||
default: return '_'
|
||||
}
|
||||
}),
|
||||
})
|
||||
return (newFilename == '' ? randomBytes(5).toString('hex') : newFilename)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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<T>(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
|
||||
}
|
||||
38
src-shared/interfaces/download.interface.ts
Normal file
38
src-shared/interfaces/download.interface.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DriveChart } from './songDetails.interface'
|
||||
|
||||
/**
|
||||
* Represents a user's request to interact with the download system.
|
||||
*/
|
||||
export interface Download {
|
||||
action: 'add' | 'retry' | 'cancel'
|
||||
versionID: number
|
||||
data?: NewDownload // Should be defined if action == 'add'
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the data required to start downloading a single chart.
|
||||
*/
|
||||
export interface NewDownload {
|
||||
chartName: string
|
||||
artist: string
|
||||
charter: string
|
||||
driveData: DriveChart & { inChartPack: boolean }
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the download progress of a single chart.
|
||||
*/
|
||||
export interface DownloadProgress {
|
||||
versionID: number
|
||||
title: string
|
||||
header: string
|
||||
description: string
|
||||
percent: number
|
||||
type: ProgressType
|
||||
/** If `description` contains a filepath that can be clicked */
|
||||
isLink: boolean
|
||||
/** If the download should not appear in the total download progress */
|
||||
stale?: boolean
|
||||
}
|
||||
|
||||
export type ProgressType = 'good' | 'error' | 'cancel' | 'done'
|
||||
91
src-shared/interfaces/search.interface.ts
Normal file
91
src-shared/interfaces/search.interface.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Represents a user's song search query.
|
||||
*/
|
||||
export interface SongSearch { // TODO: make limit a setting that's not always 51
|
||||
query: string
|
||||
quantity: 'all' | 'any'
|
||||
similarity: 'similar' | 'exact'
|
||||
fields: SearchFields
|
||||
tags: SearchTags
|
||||
instruments: SearchInstruments
|
||||
difficulties: SearchDifficulties
|
||||
minDiff: number
|
||||
maxDiff: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export function getDefaultSearch(): SongSearch {
|
||||
return {
|
||||
query: '',
|
||||
quantity: 'all',
|
||||
similarity: 'similar',
|
||||
fields: { name: true, artist: true, album: true, genre: true, year: true, charter: true, tag: true },
|
||||
tags: {
|
||||
'sections': false, 'star power': false, 'forcing': false, 'taps': false, 'lyrics': false,
|
||||
'video': false, 'stems': false, 'solo sections': false, 'open notes': false
|
||||
},
|
||||
instruments: {
|
||||
guitar: false, bass: false, rhythm: false, keys: false,
|
||||
drums: false, guitarghl: false, bassghl: false, vocals: false
|
||||
},
|
||||
difficulties: { expert: false, hard: false, medium: false, easy: false },
|
||||
minDiff: 0,
|
||||
maxDiff: 6,
|
||||
limit: 50 + 1,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
export interface SearchFields {
|
||||
name: boolean
|
||||
artist: boolean
|
||||
album: boolean
|
||||
genre: boolean
|
||||
year: boolean
|
||||
charter: boolean
|
||||
tag: boolean
|
||||
}
|
||||
|
||||
export interface SearchTags {
|
||||
'sections': boolean // Tag inverted
|
||||
'star power': boolean // Tag inverted
|
||||
'forcing': boolean // Tag inverted
|
||||
'taps': boolean
|
||||
'lyrics': boolean
|
||||
'video': boolean
|
||||
'stems': boolean
|
||||
'solo sections': boolean
|
||||
'open notes': boolean
|
||||
}
|
||||
|
||||
export interface SearchInstruments {
|
||||
guitar: boolean
|
||||
bass: boolean
|
||||
rhythm: boolean
|
||||
keys: boolean
|
||||
drums: boolean
|
||||
guitarghl: boolean
|
||||
bassghl: boolean
|
||||
vocals: boolean
|
||||
}
|
||||
|
||||
export interface SearchDifficulties {
|
||||
expert: boolean
|
||||
hard: boolean
|
||||
medium: boolean
|
||||
easy: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single song search result.
|
||||
*/
|
||||
export interface SongResult {
|
||||
id: number
|
||||
chartCount: number
|
||||
name: string
|
||||
artist: string
|
||||
album: string
|
||||
genre: string
|
||||
year: string
|
||||
}
|
||||
101
src-shared/interfaces/songDetails.interface.ts
Normal file
101
src-shared/interfaces/songDetails.interface.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* The image data for a song's album art.
|
||||
*/
|
||||
export interface AlbumArtResult {
|
||||
base64Art: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single chart version.
|
||||
*/
|
||||
export interface VersionResult {
|
||||
versionID: number
|
||||
chartID: number
|
||||
songID: number
|
||||
latestVersionID: number
|
||||
latestSetlistVersionID: number
|
||||
name: string
|
||||
chartName: string
|
||||
artist: string
|
||||
album: string
|
||||
genre: string
|
||||
year: string
|
||||
songDataIncorrect: boolean
|
||||
driveData: DriveChart & { inChartPack: boolean }
|
||||
md5: string
|
||||
lastModified: string
|
||||
icon: string
|
||||
charters: string
|
||||
charterIDs: string
|
||||
tags: string | null
|
||||
songLength: number
|
||||
diff_band: number
|
||||
diff_guitar: number
|
||||
diff_bass: number
|
||||
diff_rhythm: number
|
||||
diff_drums: number
|
||||
diff_keys: number
|
||||
diff_guitarghl: number
|
||||
diff_bassghl: number
|
||||
chartData: ChartData
|
||||
}
|
||||
|
||||
export interface DriveChart {
|
||||
source: DriveSource
|
||||
isArchive: boolean
|
||||
downloadPath: string
|
||||
filesHash: string
|
||||
folderName: string
|
||||
folderID: string
|
||||
files: DriveFile[]
|
||||
}
|
||||
|
||||
export interface DriveSource {
|
||||
isSetlistSource: boolean
|
||||
isDriveFileSource?: boolean
|
||||
setlistIcon?: string
|
||||
sourceUserIDs: number[]
|
||||
sourceName: string
|
||||
sourceDriveID: string
|
||||
proxyLink?: string
|
||||
}
|
||||
|
||||
export interface DriveFile {
|
||||
id: string
|
||||
name: string
|
||||
mimeType: string
|
||||
webContentLink: string
|
||||
modifiedTime: string
|
||||
md5Checksum: string
|
||||
size: string
|
||||
}
|
||||
|
||||
export interface ChartData {
|
||||
hasSections: boolean
|
||||
hasStarPower: boolean
|
||||
hasForced: boolean
|
||||
hasTap: boolean
|
||||
hasOpen: {
|
||||
[instrument: string]: boolean
|
||||
}
|
||||
hasSoloSections: boolean
|
||||
hasLyrics: boolean
|
||||
is120: boolean
|
||||
hasBrokenNotes: boolean
|
||||
noteCounts: {
|
||||
[instrument in Instrument]: {
|
||||
[difficulty in ChartedDifficulty]: number
|
||||
}
|
||||
}
|
||||
/** number of seconds */
|
||||
length: number
|
||||
/** number of seconds */
|
||||
effectiveLength: number
|
||||
}
|
||||
|
||||
export type Instrument = 'guitar' | 'bass' | 'rhythm' | 'keys' | 'drums' | 'guitarghl' | 'bassghl' | 'vocals' | 'undefined'
|
||||
export type ChartedDifficulty = 'x' | 'h' | 'm' | 'e'
|
||||
|
||||
export function getInstrumentIcon(instrument: Instrument) {
|
||||
return `${instrument}.png`
|
||||
}
|
||||
Reference in New Issue
Block a user