Fix compiler issues

This commit is contained in:
Geomitron
2023-11-27 18:49:30 -06:00
parent cddec0d9d4
commit 558d76f582
16 changed files with 580 additions and 566 deletions

View File

@@ -1,12 +0,0 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5%
last 2 versions
Firefox ESR
not dead
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@@ -15,8 +15,8 @@
"main": "dist/electron/main.js", "main": "dist/electron/main.js",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "run-p serve:angular serve:electron", "start": "concurrently \"npm run serve:angular\" \"npm run serve:electron\" -n angular,electron -c red,yellow",
"serve:electron": "wait-on http-get://localhost:4200/ && nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts", "serve:electron": "nodemon --exec \"tsc -p tsconfig.electron.json && electron . --dev\" --watch src/electron -e ts",
"lint": "ng lint", "lint": "ng lint",
"clean": "rimraf dist release", "clean": "rimraf dist release",
"build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows", "build:windows": "ng build -c production && tsc -p tsconfig.electron.json && electron-builder build --windows",
@@ -44,6 +44,7 @@
"fomantic-ui": "^2.8.3", "fomantic-ui": "^2.8.3",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
"lodash": "^4.17.21",
"mv": "^2.1.1", "mv": "^2.1.1",
"randombytes": "^2.1.0", "randombytes": "^2.1.0",
"rimraf": "^5.0.5", "rimraf": "^5.0.5",
@@ -64,6 +65,7 @@
"@angular/language-service": "^17.0.4", "@angular/language-service": "^17.0.4",
"@types/cli-color": "^2.0.0", "@types/cli-color": "^2.0.0",
"@types/jsonfile": "^6.0.0", "@types/jsonfile": "^6.0.0",
"@types/lodash": "^4.14.202",
"@types/mv": "^2.1.0", "@types/mv": "^2.1.0",
"@types/node": "^18.16.0", "@types/node": "^18.16.0",
"@types/randombytes": "^2.0.0", "@types/randombytes": "^2.0.0",

10
pnpm-lock.yaml generated
View File

@@ -59,6 +59,9 @@ dependencies:
jsonfile: jsonfile:
specifier: ^6.0.1 specifier: ^6.0.1
version: 6.1.0 version: 6.1.0
lodash:
specifier: ^4.17.21
version: 4.17.21
mv: mv:
specifier: ^2.1.1 specifier: ^2.1.1
version: 2.1.1 version: 2.1.1
@@ -115,6 +118,9 @@ devDependencies:
'@types/jsonfile': '@types/jsonfile':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.1.4 version: 6.1.4
'@types/lodash':
specifier: ^4.14.202
version: 4.14.202
'@types/mv': '@types/mv':
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.4 version: 2.1.4
@@ -3294,6 +3300,10 @@ packages:
dependencies: dependencies:
'@types/node': 14.18.63 '@types/node': 14.18.63
/@types/lodash@4.14.202:
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
dev: true
/@types/mime@1.3.5: /@types/mime@1.3.5:
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
dev: true dev: true

View File

@@ -173,7 +173,7 @@ export class ChartSidebarComponent implements OnInit {
* @returns a string describing the difficulty number in the selected version. * @returns a string describing the difficulty number in the selected version.
*/ */
private getDiffNumber(instrument: Instrument) { private getDiffNumber(instrument: Instrument) {
const diffNumber: number = this.selectedVersion[`diff_${instrument}`] const diffNumber: number = (this.selectedVersion as any)[`diff_${instrument}`]
return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber) return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber)
} }

View File

@@ -1,12 +1,15 @@
import { Directive, ElementRef, Input } from '@angular/core' import { Directive, ElementRef, Input } from '@angular/core'
import * as _ from 'underscore' import * as _ from 'lodash'
@Directive({ @Directive({
selector: '[appProgressBar]', selector: '[appProgressBar]',
}) })
export class ProgressBarDirective { export class ProgressBarDirective {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _$progressBar: any
progress: (percent: number) => void progress: (percent: number) => void
@Input() set percent(percent: number) { @Input() set percent(percent: number) {
@@ -23,5 +26,4 @@ export class ProgressBarDirective {
} }
return this._$progressBar return this._$progressBar
} }
private _$progressBar: any
} }

View File

@@ -7,10 +7,10 @@ import { ElectronService } from './electron.service'
}) })
export class AlbumArtService { export class AlbumArtService {
constructor(private electronService: ElectronService) { }
private imageCache: { [songID: number]: string } = {} private imageCache: { [songID: number]: string } = {}
constructor(private electronService: ElectronService) { }
async getImage(songID: number): Promise<string | null> { async getImage(songID: number): Promise<string | null> {
if (this.imageCache[songID] == undefined) { if (this.imageCache[songID] == undefined) {
const albumArtResult = await this.electronService.invoke('album-art', songID) const albumArtResult = await this.electronService.invoke('album-art', songID)

View File

@@ -5,11 +5,6 @@ import { SearchService } from './search.service'
// Note: this class prevents event cycles by only emitting events if the checkbox changes // Note: this class prevents event cycles by only emitting events if the checkbox changes
interface SelectionEvent {
songID: number
selected: boolean
}
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })

View File

@@ -1,14 +1,13 @@
import { join, parse } from 'path' import { parse } from 'path'
import { rimraf } from 'rimraf' import { rimraf } from 'rimraf'
import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface' import { NewDownload, ProgressType } from 'src/electron/shared/interfaces/download.interface'
import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface' import { DriveFile } from 'src/electron/shared/interfaces/songDetails.interface'
import { emitIPCEvent } from '../../main' import { emitIPCEvent } from '../../main'
import { hasVideoExtension } from '../../shared/ElectronUtilFunctions' import { hasVideoExtension } from '../../shared/ElectronUtilFunctions'
import { interpolate, sanitizeFilename } from '../../shared/UtilFunctions' import { sanitizeFilename } from '../../shared/UtilFunctions'
import { getSettings } from '../SettingsHandler.ipc' import { getSettings } from '../SettingsHandler.ipc'
import { FileDownloader, getDownloader } from './FileDownloader' // import { FileDownloader, getDownloader } from './FileDownloader'
import { FileExtractor } from './FileExtractor'
import { FilesystemChecker } from './FilesystemChecker' import { FilesystemChecker } from './FilesystemChecker'
import { FileTransfer } from './FileTransfer' import { FileTransfer } from './FileTransfer'
@@ -143,23 +142,23 @@ export class ChartDownload {
for (let i = 0; i < this.files.length; i++) { for (let i = 0; i < this.files.length; i++) {
let wasCanceled = false let wasCanceled = false
this.cancelFn = () => { wasCanceled = true } this.cancelFn = () => { wasCanceled = true }
const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name)) // const downloader = getDownloader(this.files[i].webContentLink, join(this.tempPath, this.files[i].name))
if (wasCanceled) { return } if (wasCanceled) { return }
this.cancelFn = () => downloader.cancelDownload() // this.cancelFn = () => downloader.cancelDownload()
const downloadComplete = this.addDownloadEventListeners(downloader, i) // const downloadComplete = this.addDownloadEventListeners(downloader, i)
downloader.beginDownload() // downloader.beginDownload()
await downloadComplete // await downloadComplete
} }
// EXTRACT FILES // EXTRACT FILES
if (this.isArchive) { if (this.isArchive) {
const extractor = new FileExtractor(this.tempPath) // const extractor = new FileExtractor(this.tempPath)
this.cancelFn = () => extractor.cancelExtract() // this.cancelFn = () => extractor.cancelExtract()
const extractComplete = this.addExtractorEventListeners(extractor) // const extractComplete = this.addExtractorEventListeners(extractor)
extractor.beginExtract() // extractor.beginExtract()
await extractComplete // await extractComplete
} }
// TRANSFER FILES // TRANSFER FILES
@@ -196,68 +195,68 @@ export class ChartDownload {
* Defines what happens in response to `FileDownloader` events. * Defines what happens in response to `FileDownloader` events.
* @returns a `Promise` that resolves when the download finishes. * @returns a `Promise` that resolves when the download finishes.
*/ */
private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) { // private addDownloadEventListeners(downloader: FileDownloader, fileIndex: number) {
let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` // let downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts // let downloadStartPoint = 0 // How far into the individual file progress portion the download progress starts
let fileProgress = 0 // let fileProgress = 0
downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => { // downloader.on('waitProgress', (remainingSeconds: number, totalSeconds: number) => {
downloadStartPoint = this.individualFileProgressPortion / 2 // downloadStartPoint = this.individualFileProgressPortion / 2
this.percent = // this.percent =
this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2) // this._allFilesProgress + interpolate(remainingSeconds, totalSeconds, 0, 0, this.individualFileProgressPortion / 2)
this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good') // this.updateGUI(downloadHeader, `Waiting for Google rate limit... (${remainingSeconds}s)`, 'good')
}) // })
downloader.on('requestSent', () => { // downloader.on('requestSent', () => {
fileProgress = downloadStartPoint // fileProgress = downloadStartPoint
this.percent = this._allFilesProgress + fileProgress // this.percent = this._allFilesProgress + fileProgress
this.updateGUI(downloadHeader, 'Sending request...', 'good') // this.updateGUI(downloadHeader, 'Sending request...', 'good')
}) // })
downloader.on('downloadProgress', (bytesDownloaded: number) => { // downloader.on('downloadProgress', (bytesDownloaded: number) => {
downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})` // downloadHeader = `[${this.files[fileIndex].name}] (file ${fileIndex + 1}/${this.files.length})`
const size = Number(this.files[fileIndex].size) // const size = Number(this.files[fileIndex].size)
fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion) // fileProgress = interpolate(bytesDownloaded, 0, size, downloadStartPoint, this.individualFileProgressPortion)
this.percent = this._allFilesProgress + fileProgress // this.percent = this._allFilesProgress + fileProgress
this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good') // this.updateGUI(downloadHeader, `Downloading... (${Math.round(1000 * bytesDownloaded / size) / 10}%)`, 'good')
}) // })
downloader.on('error', this.handleError.bind(this)) // downloader.on('error', this.handleError.bind(this))
return new Promise<void>(resolve => { // return new Promise<void>(resolve => {
downloader.on('complete', () => { // downloader.on('complete', () => {
this._allFilesProgress += this.individualFileProgressPortion // this._allFilesProgress += this.individualFileProgressPortion
resolve() // resolve()
}) // })
}) // })
} // }
/** /**
* Defines what happens in response to `FileExtractor` events. * Defines what happens in response to `FileExtractor` events.
* @returns a `Promise` that resolves when the extraction finishes. * @returns a `Promise` that resolves when the extraction finishes.
*/ */
private addExtractorEventListeners(extractor: FileExtractor) { // private addExtractorEventListeners(extractor: FileExtractor) {
let archive = '' // let archive = ''
extractor.on('start', filename => { // extractor.on('start', filename => {
archive = filename // archive = filename
this.updateGUI(`[${archive}]`, 'Extracting...', 'good') // this.updateGUI(`[${archive}]`, 'Extracting...', 'good')
}) // })
extractor.on('extractProgress', (percent, filecount) => { // extractor.on('extractProgress', (percent, filecount) => {
this.percent = interpolate(percent, 0, 100, 80, 95) // this.percent = interpolate(percent, 0, 100, 80, 95)
this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good') // this.updateGUI(`[${archive}] (${filecount} file${filecount == 1 ? '' : 's'} extracted)`, `Extracting... (${percent}%)`, 'good')
}) // })
extractor.on('error', this.handleError.bind(this)) // extractor.on('error', this.handleError.bind(this))
return new Promise<void>(resolve => { // return new Promise<void>(resolve => {
extractor.on('complete', () => { // extractor.on('complete', () => {
this.percent = 95 // this.percent = 95
resolve() // resolve()
}) // })
}) // })
} // }
/** /**
* Defines what happens in response to `FileTransfer` events. * Defines what happens in response to `FileTransfer` events.

View File

@@ -1,368 +1,368 @@
import Bottleneck from 'bottleneck' // import Bottleneck from 'bottleneck'
import { createWriteStream, writeFile as _writeFile } from 'fs' // import { createWriteStream, writeFile as _writeFile } from 'fs'
import { google } from 'googleapis' // import { google } from 'googleapis'
import * as needle from 'needle' // import * as needle from 'needle'
import { join } from 'path' // import { join } from 'path'
import { Readable } from 'stream' // import { Readable } from 'stream'
import { inspect, promisify } from 'util' // import { inspect, promisify } from 'util'
import { devLog } from '../../shared/ElectronUtilFunctions' // import { devLog } from '../../shared/ElectronUtilFunctions'
import { tempPath } from '../../shared/Paths' // import { tempPath } from '../../shared/Paths'
import { AnyFunction } from '../../shared/UtilFunctions' // import { AnyFunction } from '../../shared/UtilFunctions'
import { DownloadError } from './ChartDownload' // import { DownloadError } from './ChartDownload'
// TODO: replace needle with got (for cancel() method) (if before-headers event is possible?) // // TODO: replace needle with got (for cancel() method) (if before-headers event is possible?)
import { googleTimer } from './GoogleTimer' // import { googleTimer } from './GoogleTimer'
const drive = google.drive('v3') // const drive = google.drive('v3')
const limiter = new Bottleneck({ // const limiter = new Bottleneck({
minTime: 200, // Wait 200 ms between API requests // minTime: 200, // Wait 200 ms between API requests
}) // })
const RETRY_MAX = 2 // const RETRY_MAX = 2
const writeFile = promisify(_writeFile) // const writeFile = promisify(_writeFile)
interface EventCallback { // interface EventCallback {
'waitProgress': (remainingSeconds: number, totalSeconds: number) => void // 'waitProgress': (remainingSeconds: number, totalSeconds: number) => void
/** Note: this event can be called multiple times if the connection times out or a large file is downloaded */ // /** Note: this event can be called multiple times if the connection times out or a large file is downloaded */
'requestSent': () => void // 'requestSent': () => void
'downloadProgress': (bytesDownloaded: number) => void // 'downloadProgress': (bytesDownloaded: number) => void
/** Note: after calling retry, the event lifecycle restarts */ // /** Note: after calling retry, the event lifecycle restarts */
'error': (err: DownloadError, retry: () => void) => void // 'error': (err: DownloadError, retry: () => void) => void
'complete': () => void // 'complete': () => void
} // }
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } // type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
export type FileDownloader = APIFileDownloader | SlowFileDownloader // export type FileDownloader = APIFileDownloader | SlowFileDownloader
const downloadErrors = { // const downloadErrors = {
timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } }, // timeout: (type: string) => { return { header: 'Timeout', body: `The download server could not be reached. (type=${type})` } },
connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } }, // connectionError: (err: Error) => { return { header: 'Connection Error', body: `${err.name}: ${err.message}` } },
responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } }, // responseError: (statusCode: string) => { return { header: 'Connection failed', body: `Server returned status code: ${statusCode}` } },
htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } }, // htmlError: (path: string) => { return { header: 'Download server returned HTML instead of a file.', body: path, isLink: true } },
linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } }, // linkError: (url: string) => { return { header: 'Invalid link', body: `The download link is not formatted correctly: ${url}` } },
} // }
/** // /**
* Downloads a file from `url` to `fullPath`. // * Downloads a file from `url` to `fullPath`.
* Will handle google drive virus scan warnings. Provides event listeners for download progress. // * Will handle google drive virus scan warnings. Provides event listeners for download progress.
* On error, provides the ability to retry. // * On error, provides the ability to retry.
* Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated. // * Will only send download requests once every `getSettings().rateLimitDelay` seconds.
* @param url The download link. // */
* @param fullPath The full path to where this file should be stored (including the filename). // class SlowFileDownloader {
*/
export function getDownloader(url: string, fullPath: string): FileDownloader {
return new SlowFileDownloader(url, fullPath)
}
/** // private callbacks = {} as Callbacks
* Downloads a file from `url` to `fullPath`. // private retryCount: number
* On error, provides the ability to retry. // private wasCanceled = false
*/ // private req: NodeJS.ReadableStream
class APIFileDownloader {
private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
private callbacks = {} as Callbacks // /**
private retryCount: number // * @param url The download link.
private wasCanceled = false // * @param fullPath The full path to where this file should be stored (including the filename).
private fileID: string // */
private downloadStream: Readable // constructor(private url: string, private fullPath: string) { }
/** // /**
* @param url The download link. // * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
* @param fullPath The full path to where this file should be stored (including the filename). // */
*/ // on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
constructor(private url: string, private fullPath: string) { // this.callbacks[event] = callback
// url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download" // }
this.fileID = this.URL_REGEX.exec(url)[1]
}
/** // /**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) // * Download the file after waiting for the google rate limit.
*/ // */
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) { // beginDownload() {
this.callbacks[event] = callback // googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
} // this.callbacks.waitProgress(remainingSeconds, totalSeconds)
// }))
/** // googleTimer.on('complete', this.cancelable(() => {
* Download the file after waiting for the google rate limit. // this.requestDownload()
*/ // }))
beginDownload() { // }
if (this.fileID == undefined) {
this.failDownload(downloadErrors.linkError(this.url))
}
this.startDownloadStream() // /**
} // * Sends a request to download the file at `this.url`.
// * @param cookieHeader the "cookie=" header to include this request.
// */
// private requestDownload(cookieHeader?: string) {
// this.callbacks.requestSent()
// this.req = needle.get(this.url, {
// 'follow_max': 10,
// 'open_timeout': 5000,
// 'headers': Object.assign({
// 'Referer': this.url,
// 'Accept': '*/*',
// },
// (cookieHeader ? { 'Cookie': cookieHeader } : undefined)
// ),
// })
/** // this.req.on('timeout', this.cancelable((type: string) => {
* Uses the Google Drive API to start a download stream for the file with `this.fileID`. // this.retryCount++
*/ // if (this.retryCount <= RETRY_MAX) {
private startDownloadStream() { // devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`)
limiter.schedule(this.cancelable(async () => { // this.requestDownload(cookieHeader)
this.callbacks.requestSent() // } else {
try { // this.failDownload(downloadErrors.timeout(type))
this.downloadStream = (await drive.files.get({ // }
fileId: this.fileID, // }))
alt: 'media',
}, {
responseType: 'stream',
})).data
if (this.wasCanceled) { return } // this.req.on('err', this.cancelable((err: Error) => {
// this.failDownload(downloadErrors.connectionError(err))
// }))
this.handleDownloadResponse() // this.req.on('header', this.cancelable((statusCode, headers: Headers) => {
} catch (err) { // if (statusCode != 200) {
this.retryCount++ // this.failDownload(downloadErrors.responseError(statusCode))
if (this.retryCount <= RETRY_MAX) { // return
devLog(`Failed to get file: Retry attempt ${this.retryCount}...`) // }
if (this.wasCanceled) { return }
this.startDownloadStream()
} else {
devLog(inspect(err))
if (err?.code && err?.response?.statusText) {
this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
} else {
this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
}
}
}
}))
}
/** // if (headers['content-type'].startsWith('text/html')) {
* Pipes the data from a download response to `this.fullPath`. // this.handleHTMLResponse(headers['set-cookie'])
* @param req The download request. // } else {
*/ // this.handleDownloadResponse()
private handleDownloadResponse() { // }
this.callbacks.downloadProgress(0) // }))
let downloadedSize = 0 // }
const writeStream = createWriteStream(this.fullPath)
try { // /**
this.downloadStream.pipe(writeStream) // * A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning.
} catch (err) { // * This function sends the request that results from clicking "download anyway", or generates an error if it can't be found.
this.failDownload(downloadErrors.connectionError(err)) // * @param cookieHeader The "cookie=" header of this request.
} // */
// private handleHTMLResponse(cookieHeader: string) {
// let virusScanHTML = ''
// this.req.on('data', this.cancelable(data => virusScanHTML += data))
// this.req.on('done', this.cancelable((err: Error) => {
// if (err) {
// this.failDownload(downloadErrors.connectionError(err))
// } else {
// try {
// const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
// const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
// const confirmToken = confirmTokenResults[1]
// const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
// this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
// const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
// const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
// const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
// this.requestDownload(newHeader)
// } catch (e) {
// this.saveHTMLError(virusScanHTML).then(path => {
// this.failDownload(downloadErrors.htmlError(path))
// })
// }
// }
// }))
// }
this.downloadStream.on('data', this.cancelable((chunk: Buffer) => { // /**
downloadedSize += chunk.length // * Pipes the data from a download response to `this.fullPath`.
})) // * @param req The download request.
// */
// private handleDownloadResponse() {
// this.callbacks.downloadProgress(0)
// let downloadedSize = 0
// this.req.pipe(createWriteStream(this.fullPath))
// this.req.on('data', this.cancelable(data => {
// downloadedSize += data.length
// this.callbacks.downloadProgress(downloadedSize)
// }))
const progressUpdater = setInterval(() => { // this.req.on('err', this.cancelable((err: Error) => {
this.callbacks.downloadProgress(downloadedSize) // this.failDownload(downloadErrors.connectionError(err))
}, 100) // }))
this.downloadStream.on('error', this.cancelable((err: Error) => { // this.req.on('end', this.cancelable(() => {
clearInterval(progressUpdater) // this.callbacks.complete()
this.failDownload(downloadErrors.connectionError(err)) // }))
})) // }
this.downloadStream.on('end', this.cancelable(() => { // private async saveHTMLError(text: string) {
clearInterval(progressUpdater) // const errorPath = join(tempPath, 'HTMLError.html')
writeStream.end() // await writeFile(errorPath, text)
this.downloadStream.destroy() // return errorPath
this.downloadStream = null // }
this.callbacks.complete() // /**
})) // * Display an error message and provide a function to retry the download.
} // */
// private failDownload(error: DownloadError) {
// this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
// }
/** // /**
* Display an error message and provide a function to retry the download. // * Stop the process of downloading the file. (no more events will be fired after this is called)
*/ // */
private failDownload(error: DownloadError) { // cancelDownload() {
this.callbacks.error(error, this.cancelable(() => this.beginDownload())) // this.wasCanceled = true
} // googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
// if (this.req) {
// // TODO: destroy request
// }
// }
/** // /**
* Stop the process of downloading the file. (no more events will be fired after this is called) // * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
*/ // */
cancelDownload() { // private cancelable<F extends AnyFunction>(fn: F) {
this.wasCanceled = true // return (...args: Parameters<F>): ReturnType<F> => {
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting // if (this.wasCanceled) { return }
if (this.downloadStream) { // return fn(...Array.from(args))
this.downloadStream.destroy() // }
} // }
} // }
/** // /**
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called. // * Downloads a file from `url` to `fullPath`.
*/ // * On error, provides the ability to retry.
private cancelable<F extends AnyFunction>(fn: F) { // */
return (...args: Parameters<F>): ReturnType<F> => { // class APIFileDownloader {
if (this.wasCanceled) { return } // private readonly URL_REGEX = /uc\?id=([^&]*)&export=download/u
return fn(...Array.from(args))
}
}
}
/** // private callbacks = {} as Callbacks
* Downloads a file from `url` to `fullPath`. // private retryCount: number
* Will handle google drive virus scan warnings. Provides event listeners for download progress. // private wasCanceled = false
* On error, provides the ability to retry. // private fileID: string
* Will only send download requests once every `getSettings().rateLimitDelay` seconds. // private downloadStream: Readable
*/
class SlowFileDownloader {
private callbacks = {} as Callbacks // /**
private retryCount: number // * @param url The download link.
private wasCanceled = false // * @param fullPath The full path to where this file should be stored (including the filename).
private req: NodeJS.ReadableStream // */
// constructor(private url: string, private fullPath: string) {
// // url looks like: "https://drive.google.com/uc?id=1TlxtOZlVgRgX7-1tyW0d5QzXVfL-MC3Q&export=download"
// this.fileID = this.URL_REGEX.exec(url)[1]
// }
/** // /**
* @param url The download link. // * Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called)
* @param fullPath The full path to where this file should be stored (including the filename). // */
*/ // on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
constructor(private url: string, private fullPath: string) { } // this.callbacks[event] = callback
// }
/** // /**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelDownload()` is called) // * Download the file after waiting for the google rate limit.
*/ // */
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) { // beginDownload() {
this.callbacks[event] = callback // if (this.fileID == undefined) {
} // this.failDownload(downloadErrors.linkError(this.url))
// }
/** // this.startDownloadStream()
* Download the file after waiting for the google rate limit. // }
*/
beginDownload() {
googleTimer.on('waitProgress', this.cancelable((remainingSeconds, totalSeconds) => {
this.callbacks.waitProgress(remainingSeconds, totalSeconds)
}))
googleTimer.on('complete', this.cancelable(() => { // /**
this.requestDownload() // * Uses the Google Drive API to start a download stream for the file with `this.fileID`.
})) // */
} // private startDownloadStream() {
// limiter.schedule(this.cancelable(async () => {
// this.callbacks.requestSent()
// try {
// this.downloadStream = (await drive.files.get({
// fileId: this.fileID,
// alt: 'media',
// }, {
// responseType: 'stream',
// })).data
/** // if (this.wasCanceled) { return }
* Sends a request to download the file at `this.url`.
* @param cookieHeader the "cookie=" header to include this request.
*/
private requestDownload(cookieHeader?: string) {
this.callbacks.requestSent()
this.req = needle.get(this.url, {
'follow_max': 10,
'open_timeout': 5000,
'headers': Object.assign({
'Referer': this.url,
'Accept': '*/*',
},
(cookieHeader ? { 'Cookie': cookieHeader } : undefined)
),
})
this.req.on('timeout', this.cancelable((type: string) => { // this.handleDownloadResponse()
this.retryCount++ // } catch (err) {
if (this.retryCount <= RETRY_MAX) { // this.retryCount++
devLog(`TIMEOUT: Retry attempt ${this.retryCount}...`) // if (this.retryCount <= RETRY_MAX) {
this.requestDownload(cookieHeader) // devLog(`Failed to get file: Retry attempt ${this.retryCount}...`)
} else { // if (this.wasCanceled) { return }
this.failDownload(downloadErrors.timeout(type)) // this.startDownloadStream()
} // } else {
})) // devLog(inspect(err))
// if (err?.code && err?.response?.statusText) {
// this.failDownload(downloadErrors.responseError(`${err.code} (${err.response.statusText})`))
// } else {
// this.failDownload(downloadErrors.responseError(err?.code ?? 'unknown'))
// }
// }
// }
// }))
// }
this.req.on('err', this.cancelable((err: Error) => { // /**
this.failDownload(downloadErrors.connectionError(err)) // * Pipes the data from a download response to `this.fullPath`.
})) // * @param req The download request.
// */
// private handleDownloadResponse() {
// this.callbacks.downloadProgress(0)
// let downloadedSize = 0
// const writeStream = createWriteStream(this.fullPath)
this.req.on('header', this.cancelable((statusCode, headers: Headers) => { // try {
if (statusCode != 200) { // this.downloadStream.pipe(writeStream)
this.failDownload(downloadErrors.responseError(statusCode)) // } catch (err) {
return // this.failDownload(downloadErrors.connectionError(err))
} // }
if (headers['content-type'].startsWith('text/html')) { // this.downloadStream.on('data', this.cancelable((chunk: Buffer) => {
this.handleHTMLResponse(headers['set-cookie']) // downloadedSize += chunk.length
} else { // }))
this.handleDownloadResponse()
}
}))
}
/** // const progressUpdater = setInterval(() => {
* A Google Drive HTML response to a download request usually means this is the "file too large to scan for viruses" warning. // this.callbacks.downloadProgress(downloadedSize)
* This function sends the request that results from clicking "download anyway", or generates an error if it can't be found. // }, 100)
* @param cookieHeader The "cookie=" header of this request.
*/
private handleHTMLResponse(cookieHeader: string) {
let virusScanHTML = ''
this.req.on('data', this.cancelable(data => virusScanHTML += data))
this.req.on('done', this.cancelable((err: Error) => {
if (err) {
this.failDownload(downloadErrors.connectionError(err))
} else {
try {
const confirmTokenRegex = /confirm=([0-9A-Za-z\-_]+)&/g
const confirmTokenResults = confirmTokenRegex.exec(virusScanHTML)
const confirmToken = confirmTokenResults[1]
const downloadID = this.url.substr(this.url.indexOf('id=') + 'id='.length)
this.url = `https://drive.google.com/uc?confirm=${confirmToken}&id=${downloadID}`
const warningCode = /download_warning_([^=]*)=/.exec(cookieHeader)[1]
const NID = /NID=([^;]*);/.exec(cookieHeader)[1].replace('=', '%')
const newHeader = `download_warning_${warningCode}=${confirmToken}; NID=${NID}`
this.requestDownload(newHeader)
} catch (e) {
this.saveHTMLError(virusScanHTML).then(path => {
this.failDownload(downloadErrors.htmlError(path))
})
}
}
}))
}
/** // this.downloadStream.on('error', this.cancelable((err: Error) => {
* Pipes the data from a download response to `this.fullPath`. // clearInterval(progressUpdater)
* @param req The download request. // this.failDownload(downloadErrors.connectionError(err))
*/ // }))
private handleDownloadResponse() {
this.callbacks.downloadProgress(0)
let downloadedSize = 0
this.req.pipe(createWriteStream(this.fullPath))
this.req.on('data', this.cancelable(data => {
downloadedSize += data.length
this.callbacks.downloadProgress(downloadedSize)
}))
this.req.on('err', this.cancelable((err: Error) => { // this.downloadStream.on('end', this.cancelable(() => {
this.failDownload(downloadErrors.connectionError(err)) // clearInterval(progressUpdater)
})) // writeStream.end()
// this.downloadStream.destroy()
// this.downloadStream = null
this.req.on('end', this.cancelable(() => { // this.callbacks.complete()
this.callbacks.complete() // }))
})) // }
}
private async saveHTMLError(text: string) { // /**
const errorPath = join(tempPath, 'HTMLError.html') // * Display an error message and provide a function to retry the download.
await writeFile(errorPath, text) // */
return errorPath // private failDownload(error: DownloadError) {
} // this.callbacks.error(error, this.cancelable(() => this.beginDownload()))
// }
/** // /**
* Display an error message and provide a function to retry the download. // * Stop the process of downloading the file. (no more events will be fired after this is called)
*/ // */
private failDownload(error: DownloadError) { // cancelDownload() {
this.callbacks.error(error, this.cancelable(() => this.beginDownload())) // this.wasCanceled = true
} // googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting
// if (this.downloadStream) {
// this.downloadStream.destroy()
// }
// }
/** // /**
* Stop the process of downloading the file. (no more events will be fired after this is called) // * Wraps a function that is able to be prevented if `this.cancelDownload()` was called.
*/ // */
cancelDownload() { // private cancelable<F extends AnyFunction>(fn: F) {
this.wasCanceled = true // return (...args: Parameters<F>): ReturnType<F> => {
googleTimer.cancelTimer() // Prevents timer from trying to activate a download and resetting // if (this.wasCanceled) { return }
if (this.req) { // return fn(...Array.from(args))
// TODO: destroy request // }
} // }
} // }
/** // /**
* Wraps a function that is able to be prevented if `this.cancelDownload()` was called. // * Downloads a file from `url` to `fullPath`.
*/ // * Will handle google drive virus scan warnings. Provides event listeners for download progress.
private cancelable<F extends AnyFunction>(fn: F) { // * On error, provides the ability to retry.
return (...args: Parameters<F>): ReturnType<F> => { // * Will only send download requests once every `getSettings().rateLimitDelay` seconds if a Google account has not been authenticated.
if (this.wasCanceled) { return } // * @param url The download link.
return fn(...Array.from(args)) // * @param fullPath The full path to where this file should be stored (including the filename).
} // */
} // export function getDownloader(url: string, fullPath: string): FileDownloader {
} // return new SlowFileDownloader(url, fullPath)
// }

View File

@@ -1,164 +1,170 @@
import * as zipBin from '7zip-bin' // import * as zipBin from '7zip-bin'
import { mkdir as _mkdir, readdir, unlink } from 'fs' // import { mkdir as _mkdir, readdir, unlink } from 'fs'
import * as node7z from 'node-7z' // import * as node7z from 'node-7z'
import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction // import * as unrarjs from 'node-unrar-js' // TODO find better rar library that has async extraction
import { FailReason } from 'node-unrar-js/dist/js/extractor' // import { FailReason } from 'node-unrar-js/dist/js/extractor'
import { extname, join } from 'path' // import { extname, join } from 'path'
import { promisify } from 'util' // import { promisify } from 'util'
import { devLog } from '../../shared/ElectronUtilFunctions' // import { devLog } from '../../shared/ElectronUtilFunctions'
import { AnyFunction } from '../../shared/UtilFunctions' // import { AnyFunction } from '../../shared/UtilFunctions'
import { DownloadError } from './ChartDownload' // import { DownloadError } from './ChartDownload'
const mkdir = promisify(_mkdir) // const mkdir = promisify(_mkdir)
interface EventCallback { // interface EventCallback {
'start': (filename: string) => void // 'start': (filename: string) => void
'extractProgress': (percent: number, fileCount: number) => void // 'extractProgress': (percent: number, fileCount: number) => void
'error': (err: DownloadError, retry: () => void | Promise<void>) => void // 'error': (err: DownloadError, retry: () => void | Promise<void>) => void
'complete': () => void // 'complete': () => void
} // }
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } // type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const extractErrors = { // const extractErrors = {
readError: (err: NodeJS.ErrnoException) => { return { header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` } }, // readError: (err: NodeJS.ErrnoException) => ({ header: `Failed to read file (${err.code})`, body: `${err.name}: ${err.message}` }),
emptyError: () => { return { header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' } }, // emptyError: () => ({ header: 'Failed to extract archive', body: 'File archive was downloaded but could not be found' }),
rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => { // rarmkdirError: (err: NodeJS.ErrnoException, sourceFile: string) => {
return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` } // return { header: `Extracting archive failed. (${err.code})`, body: `${err.name}: ${err.message} (${sourceFile})` }
}, // },
rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => { // rarextractError: (result: { reason: FailReason; msg: string }, sourceFile: string) => {
return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` } // return { header: `Extracting archive failed: ${result.reason}`, body: `${result.msg} (${sourceFile})` }
}, // },
} // }
export class FileExtractor { // export class FileExtractor {
private callbacks = {} as Callbacks // private callbacks = {} as Callbacks
private wasCanceled = false // private wasCanceled = false
constructor(private sourceFolder: string) { } // constructor(private sourceFolder: string) { }
/** // /**
* Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called) // * Calls `callback` when `event` fires. (no events will be fired after `this.cancelExtract()` is called)
*/ // */
on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) { // on<E extends keyof EventCallback>(event: E, callback: EventCallback[E]) {
this.callbacks[event] = callback // this.callbacks[event] = callback
} // }
/** // /**
* Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder) // * Extract the chart from `this.sourceFolder`. (assumes there is exactly one archive file in that folder)
*/ // */
beginExtract() { // beginExtract() {
setTimeout(this.cancelable(() => { // setTimeout(this.cancelable(() => {
readdir(this.sourceFolder, (err, files) => { // readdir(this.sourceFolder, (err, files) => {
if (err) { // if (err) {
this.callbacks.error(extractErrors.readError(err), () => this.beginExtract()) // this.callbacks.error(extractErrors.readError(err), () => this.beginExtract())
} else if (files.length == 0) { // } else if (files.length == 0) {
this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract()) // this.callbacks.error(extractErrors.emptyError(), () => this.beginExtract())
} else { // } else {
this.callbacks.start(files[0]) // this.callbacks.start(files[0])
this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar') // this.extract(join(this.sourceFolder, files[0]), extname(files[0]) == '.rar')
} // }
}) // })
}), 100) // Wait for filesystem to process downloaded file // }), 100) // Wait for filesystem to process downloaded file
} // }
/** // /**
* Extracts the file at `fullPath` to `this.sourceFolder`. // * Extracts the file at `fullPath` to `this.sourceFolder`.
*/ // */
private async extract(fullPath: string, useRarExtractor: boolean) { // private async extract(fullPath: string, useRarExtractor: boolean) {
if (useRarExtractor) { // if (useRarExtractor) {
await this.extractRar(fullPath) // Use node-unrar-js to extract the archive // await this.extractRar(fullPath) // Use node-unrar-js to extract the archive
} else { // } else {
this.extract7z(fullPath) // Use node-7z to extract the archive // this.extract7z(fullPath) // Use node-7z to extract the archive
} // }
} // }
/** // /**
* Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. // * Extracts a .rar archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
* @throws an `ExtractError` if this fails. // * @throws an `ExtractError` if this fails.
*/ // */
private async extractRar(fullPath: string) { // private async extractRar(fullPath: string) {
const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder) // const extractor = unrarjs.createExtractorFromFile(fullPath, this.sourceFolder)
const fileList = extractor.getFileList() // const fileList = extractor.getFileList()
if (fileList[0].state != 'FAIL') { // if (fileList[0].state != 'FAIL') {
// Create directories for nested archives (because unrarjs didn't feel like handling that automatically) // // Create directories for nested archives (because unrarjs didn't feel like handling that automatically)
const headers = fileList[1].fileHeaders // const headers = fileList[1].fileHeaders
for (const header of headers) { // for (const header of headers) {
if (header.flags.directory) { // if (header.flags.directory) {
try { // try {
await mkdir(join(this.sourceFolder, header.name), { recursive: true }) // await mkdir(join(this.sourceFolder, header.name), { recursive: true })
} catch (err) { // } catch (err) {
this.callbacks.error(extractErrors.rarmkdirError(err, fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) // this.callbacks.error(
return // extractErrors.rarmkdirError(err, fullPath),
} // () => this.extract(fullPath, extname(fullPath) == '.rar'),
} // )
} // return
} // }
// }
// }
// }
const extractResult = extractor.extractAll() // const extractResult = extractor.extractAll()
if (extractResult[0].state == 'FAIL') { // if (extractResult[0].state == 'FAIL') {
this.callbacks.error(extractErrors.rarextractError(extractResult[0], fullPath), () => this.extract(fullPath, extname(fullPath) == '.rar')) // this.callbacks.error(
} else { // extractErrors.rarextractError(extractResult[0], fullPath),
this.deleteArchive(fullPath) // () => this.extract(fullPath, extname(fullPath) == '.rar'),
} // )
} // } else {
// this.deleteArchive(fullPath)
// }
// }
/** // /**
* Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`. // * Extracts a .zip or .7z archive found at `fullPath` and puts the extracted results in `this.sourceFolder`.
*/ // */
private extract7z(fullPath: string) { // private extract7z(fullPath: string) {
const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :) // const zipBinPath = zipBin.path7za.replace('app.asar', 'app.asar.unpacked') // I love electron-builder packaging :)
const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath }) // const stream = node7z.extractFull(fullPath, this.sourceFolder, { $progress: true, $bin: zipBinPath })
stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => { // stream.on('progress', this.cancelable((progress: { percent: number; fileCount: number }) => {
this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount) // this.callbacks.extractProgress(progress.percent, isNaN(progress.fileCount) ? 0 : progress.fileCount)
})) // }))
let extractErrorOccured = false // let extractErrorOccured = false
stream.on('error', this.cancelable(() => { // stream.on('error', this.cancelable(() => {
extractErrorOccured = true // extractErrorOccured = true
devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`) // devLog(`Failed to extract [${fullPath}]; retrying with .rar extractor...`)
this.extract(fullPath, true) // this.extract(fullPath, true)
})) // }))
stream.on('end', this.cancelable(() => { // stream.on('end', this.cancelable(() => {
if (!extractErrorOccured) { // if (!extractErrorOccured) {
this.deleteArchive(fullPath) // this.deleteArchive(fullPath)
} // }
})) // }))
} // }
/** // /**
* Tries to delete the archive at `fullPath`. // * Tries to delete the archive at `fullPath`.
*/ // */
private deleteArchive(fullPath: string) { // private deleteArchive(fullPath: string) {
unlink(fullPath, this.cancelable(err => { // unlink(fullPath, this.cancelable(err => {
if (err && err.code != 'ENOENT') { // if (err && err.code != 'ENOENT') {
devLog(`Warning: failed to delete archive at [${fullPath}]`) // devLog(`Warning: failed to delete archive at [${fullPath}]`)
} // }
this.callbacks.complete() // this.callbacks.complete()
})) // }))
} // }
/** // /**
* Stop the process of extracting the file. (no more events will be fired after this is called) // * Stop the process of extracting the file. (no more events will be fired after this is called)
*/ // */
cancelExtract() { // cancelExtract() {
this.wasCanceled = true // this.wasCanceled = true
} // }
/** // /**
* Wraps a function that is able to be prevented if `this.cancelExtract()` was called. // * Wraps a function that is able to be prevented if `this.cancelExtract()` was called.
*/ // */
private cancelable<F extends AnyFunction>(fn: F) { // private cancelable<F extends AnyFunction>(fn: F) {
return (...args: Parameters<F>): ReturnType<F> => { // return (...args: Parameters<F>): ReturnType<F> => {
if (this.wasCanceled) { return } // if (this.wasCanceled) { return }
return fn(...Array.from(args)) // return fn(...Array.from(args))
} // }
} // }
} // }

View File

@@ -1,5 +1,5 @@
import { Dirent, readdir as _readdir } from 'fs' import { Dirent, readdir as _readdir } from 'fs'
import * as mv from 'mv' import mv from 'mv'
import { join } from 'path' import { join } from 'path'
import { rimraf } from 'rimraf' import { rimraf } from 'rimraf'
import { promisify } from 'util' import { promisify } from 'util'
@@ -20,7 +20,10 @@ const transferErrors = {
readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'), readError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to read file.'),
deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'), deleteError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete file.'),
rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'), rimrafError: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to delete folder.'),
mvError: (err: NodeJS.ErrnoException) => fsError(err, `Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`), mvError: (err: NodeJS.ErrnoException) => fsError(
err,
`Failed to move folder to library.${err.code == 'EPERM' ? ' (does the chart already exist?)' : ''}`,
),
} }
function fsError(err: NodeJS.ErrnoException, description: string) { function fsError(err: NodeJS.ErrnoException, description: string) {

View File

@@ -19,7 +19,7 @@ interface EventCallback {
type Callbacks = { [E in keyof EventCallback]: EventCallback[E] } type Callbacks = { [E in keyof EventCallback]: EventCallback[E] }
const filesystemErrors = { const filesystemErrors = {
libraryFolder: () => { return { header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' } }, libraryFolder: () => ({ header: 'Library folder not specified', body: 'Please go to the settings to set your library folder.' }),
libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'), libraryAccess: (err: NodeJS.ErrnoException) => fsError(err, 'Failed to access library folder.'),
destinationFolderExists: (destinationPath: string) => { destinationFolderExists: (destinationPath: string) => {
return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true } return { header: 'This chart already exists in your library folder.', body: destinationPath, isLink: true }

View File

@@ -1,5 +1,5 @@
import { app, BrowserWindow, ipcMain } from 'electron' import { app, BrowserWindow, ipcMain } from 'electron'
import * as windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import * as path from 'path' import * as path from 'path'
import * as url from 'url' import * as url from 'url'
@@ -9,12 +9,13 @@ import { updateChecker } from './ipc/UpdateHandler.ipc'
import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler' import { getIPCEmitHandlers, getIPCInvokeHandlers, IPCEmitEvents } from './shared/IPCHandler'
import { dataPath } from './shared/Paths' import { dataPath } from './shared/Paths'
require('electron-unhandled')({ showDialog: true }) import unhandled = require('electron-unhandled')
unhandled({ showDialog: true })
export let mainWindow: BrowserWindow export let mainWindow: BrowserWindow
const args = process.argv.slice(1) const args = process.argv.slice(1)
const isDevBuild = args.some(val => val == '--dev') const isDevBuild = args.some(val => val == '--dev')
const remote = require('@electron/remote/main') import remote = require('@electron/remote/main')
remote.initialize() remote.initialize()
@@ -66,7 +67,7 @@ function handleOSXWindowClosed() {
/** /**
* Launches and initializes Bridge's main window. * Launches and initializes Bridge's main window.
*/ */
function createBridgeWindow() { async function createBridgeWindow() {
// Load window size and maximized/restored state from previous session // Load window size and maximized/restored state from previous session
const windowState = windowStateKeeper({ const windowState = windowStateKeeper({
@@ -89,7 +90,7 @@ function createBridgeWindow() {
getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0]))) getIPCEmitHandlers().map(handler => ipcMain.on(handler.event, (_event, ...args) => handler.handler(args[0])))
// Load angular app // Load angular app
mainWindow.loadURL(getLoadUrl()) await loadWindow()
if (isDevBuild) { if (isDevBuild) {
mainWindow.webContents.openDevTools() mainWindow.webContents.openDevTools()
@@ -115,10 +116,9 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
frame: false, frame: false,
title: 'Bridge', title: 'Bridge',
webPreferences: { webPreferences: {
nodeIntegration: true, // preload:
allowRunningInsecureContent: (isDevBuild) ? true : false, allowRunningInsecureContent: (isDevBuild) ? true : false,
textAreasAreResizable: false, textAreasAreResizable: false,
contextIsolation: false,
}, },
simpleFullscreen: true, simpleFullscreen: true,
fullscreenable: false, fullscreenable: false,
@@ -132,6 +132,15 @@ function createBrowserWindow(windowState: windowStateKeeper.State) {
return new BrowserWindow(options) return new BrowserWindow(options)
} }
async function loadWindow(retries = 0) {
if (retries > 10) { throw new Error('Angular frontend did not load') }
try {
await mainWindow.loadURL(getLoadUrl())
} catch (err) {
await loadWindow(retries + 1)
}
}
/** /**
* Load from localhost during development; load from index.html in production * Load from localhost during development; load from index.html in production
*/ */

View File

@@ -23,6 +23,6 @@ export function hasVideoExtension(name: string) {
* Log a message in the main BrowserWindow's console. * 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. * Note: Error objects can't be serialized by this; use inspect(err) before passing it here.
*/ */
export function devLog(...messages: any[]) { export function devLog(...messages: unknown[]) {
emitIPCEvent('log', messages) emitIPCEvent('log', messages)
} }

View File

@@ -1,6 +1,5 @@
import * as randomBytes from 'randombytes' import randomBytes from 'randombytes'
import sanitize from 'sanitize-filename'
const sanitize = require('sanitize-filename')
// WARNING: do not import anything related to Electron; the code will not compile correctly. // WARNING: do not import anything related to Electron; the code will not compile correctly.

View File

@@ -7,6 +7,7 @@
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"target": "ES2022", "target": "ES2022",