mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-07-07 10:35:09 +00:00
Add download location settings
This commit is contained in:
@@ -24,7 +24,7 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<h2 class="card-title">{{ download.chartName }}</h2>
|
<h2 class="card-title">{{ getDownloadName(download) }}</h2>
|
||||||
<progress
|
<progress
|
||||||
[attr.value]="download.percent"
|
[attr.value]="download.percent"
|
||||||
max="100"
|
max="100"
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Component, HostBinding } from '@angular/core'
|
import { Component, HostBinding } from '@angular/core'
|
||||||
|
|
||||||
|
import { SettingsService } from 'src-angular/app/core/services/settings.service'
|
||||||
|
import { DownloadProgress } from 'src-shared/interfaces/download.interface'
|
||||||
|
import { resolveChartFolderName } from 'src-shared/UtilFunctions'
|
||||||
|
|
||||||
import { DownloadService } from '../../../../core/services/download.service'
|
import { DownloadService } from '../../../../core/services/download.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -9,9 +13,16 @@ import { DownloadService } from '../../../../core/services/download.service'
|
|||||||
export class DownloadsModalComponent {
|
export class DownloadsModalComponent {
|
||||||
@HostBinding('class.contents') contents = true
|
@HostBinding('class.contents') contents = true
|
||||||
|
|
||||||
constructor(public downloadService: DownloadService) { }
|
constructor(
|
||||||
|
public downloadService: DownloadService,
|
||||||
|
public settingsService: SettingsService,
|
||||||
|
) { }
|
||||||
|
|
||||||
showFile(filepath: string) {
|
showFile(filepath: string) {
|
||||||
window.electron.emit.showFile(filepath)
|
window.electron.emit.showFile(filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDownloadName(download: DownloadProgress) {
|
||||||
|
return resolveChartFolderName(this.settingsService.chartFolderName, download.chart)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">
|
||||||
|
Chart folder name
|
||||||
|
<button class="btn btn-xs btn-circle btn-ghost" (click)="chartFolderNameModal.showModal()">
|
||||||
|
<i class="bi bi-info-circle text-sm hover:border-b-secondary-focus"></i>
|
||||||
|
</button>
|
||||||
|
<dialog #chartFolderNameModal class="modal whitespace-normal">
|
||||||
|
<div class="modal-box bg-base-100 text-base-content flex flex-col gap-2">
|
||||||
|
<form method="dialog">
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
|
<i class="bi bi-x-lg text-lg"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div class="flex-1">
|
||||||
|
<span class="font-bold text-lg">Chart Folder Name</span>
|
||||||
|
<ul class="list-disc pl-5">
|
||||||
|
<li>Describes where Bridge will put the chart inside the chart library directory</li>
|
||||||
|
<li>Use "/" to describe subfolders</li>
|
||||||
|
<li>Use "{{ '{tag}' }}" as a placeholder for chart-specific properties</li>
|
||||||
|
<br />
|
||||||
|
Available tags:
|
||||||
|
<div class="text-xs">{{ '{name}, {artist}, {album}, {genre}, {year}, {charter}' }}</div>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop">
|
||||||
|
<button>close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input [formControl]="chartFolderName" class="input input-bordered" type="text" placeholder="{artist} - {name} ({charter})" />
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="form-control w-full max-w-xs">
|
<div class="form-control w-full max-w-xs">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Appearance</span>
|
<span class="label-text">Appearance</span>
|
||||||
@@ -76,7 +111,7 @@
|
|||||||
<button class="btn btn-xs btn-circle btn-ghost" (click)="selectSngModal.showModal()">
|
<button class="btn btn-xs btn-circle btn-ghost" (click)="selectSngModal.showModal()">
|
||||||
<i class="bi bi-info-circle text-sm hover:border-b-secondary-focus"></i>
|
<i class="bi bi-info-circle text-sm hover:border-b-secondary-focus"></i>
|
||||||
</button>
|
</button>
|
||||||
<dialog #selectSngModal id="report_modal" class="modal whitespace-normal">
|
<dialog #selectSngModal class="modal whitespace-normal">
|
||||||
<div class="modal-box bg-base-100 text-base-content flex flex-col gap-2">
|
<div class="modal-box bg-base-100 text-base-content flex flex-col gap-2">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { themes } from 'src-shared/Settings'
|
|||||||
export class SettingsComponent implements OnInit {
|
export class SettingsComponent implements OnInit {
|
||||||
@ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef
|
@ViewChild('themeDropdown', { static: true }) themeDropdown: ElementRef
|
||||||
|
|
||||||
|
public chartFolderName: FormControl<string>
|
||||||
public isSng: FormControl<boolean>
|
public isSng: FormControl<boolean>
|
||||||
public isCompactTable: FormControl<boolean>
|
public isCompactTable: FormControl<boolean>
|
||||||
|
|
||||||
@@ -37,6 +38,10 @@ export class SettingsComponent implements OnInit {
|
|||||||
private ref: ChangeDetectorRef
|
private ref: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
const ss = settingsService
|
const ss = settingsService
|
||||||
|
|
||||||
|
this.chartFolderName = new FormControl<string>(ss.chartFolderName, { nonNullable: true })
|
||||||
|
this.chartFolderName.valueChanges.subscribe(value => ss.chartFolderName = value)
|
||||||
|
|
||||||
this.isSng = new FormControl<boolean>(ss.isSng, { nonNullable: true })
|
this.isSng = new FormControl<boolean>(ss.isSng, { nonNullable: true })
|
||||||
this.isSng.valueChanges.subscribe(value => settingsService.isSng = value)
|
this.isSng.valueChanges.subscribe(value => settingsService.isSng = value)
|
||||||
this.isCompactTable = new FormControl<boolean>(settingsService.isCompactTable, { nonNullable: true })
|
this.isCompactTable = new FormControl<boolean>(settingsService.isCompactTable, { nonNullable: true })
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { EventEmitter, Injectable, NgZone } from '@angular/core'
|
|||||||
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||||
import { removeStyleTags } from 'src-shared/UtilFunctions'
|
import { resolveChartFolderName } from 'src-shared/UtilFunctions'
|
||||||
|
|
||||||
import { DownloadProgress } from '../../../../src-shared/interfaces/download.interface'
|
import { DownloadProgress } from '../../../../src-shared/interfaces/download.interface'
|
||||||
|
import { SettingsService } from './settings.service'
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -14,7 +15,7 @@ export class DownloadService {
|
|||||||
public downloadCountChanges = new EventEmitter<number>()
|
public downloadCountChanges = new EventEmitter<number>()
|
||||||
public downloads: DownloadProgress[] = []
|
public downloads: DownloadProgress[] = []
|
||||||
|
|
||||||
constructor(zone: NgZone) {
|
constructor(zone: NgZone, private settingsService: SettingsService) {
|
||||||
window.electron.on.downloadQueueUpdate(download => zone.run(() => {
|
window.electron.on.downloadQueueUpdate(download => zone.run(() => {
|
||||||
const downloadIndex = this.downloads.findIndex(d => d.md5 === download.md5)
|
const downloadIndex = this.downloads.findIndex(d => d.md5 === download.md5)
|
||||||
if (download.type === 'cancel') {
|
if (download.type === 'cancel') {
|
||||||
@@ -50,7 +51,12 @@ export class DownloadService {
|
|||||||
|
|
||||||
get currentDownloadText() {
|
get currentDownloadText() {
|
||||||
const download = this.downloads.find(d => !d.stale && d.type === 'good')
|
const download = this.downloads.find(d => !d.stale && d.type === 'good')
|
||||||
return download ? `Downloading: ${_.truncate(download.chartName, { length: 80 })}` : ''
|
if (download) {
|
||||||
|
return 'Downloading: '
|
||||||
|
+ _.truncate(resolveChartFolderName(this.settingsService.chartFolderName, download.chart), { length: 80 })
|
||||||
|
} else {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get anyErrorsExist() {
|
get anyErrorsExist() {
|
||||||
@@ -62,19 +68,24 @@ export class DownloadService {
|
|||||||
if (this.downloads.every(d => d.type === 'done')) { // Reset overall progress bar if it finished
|
if (this.downloads.every(d => d.type === 'done')) { // Reset overall progress bar if it finished
|
||||||
this.downloads.forEach(d => d.stale = true)
|
this.downloads.forEach(d => d.stale = true)
|
||||||
}
|
}
|
||||||
const chartName = `${removeStyleTags(chart.artist ?? 'Unknown Artist')
|
const newChart = {
|
||||||
} - ${removeStyleTags(chart.name ?? 'Unknown Name')
|
name: chart.name ?? 'Unknown Name',
|
||||||
} (${removeStyleTags(chart.charter ?? 'Unknown Charter')})`
|
artist: chart.artist ?? 'Unknown Artist',
|
||||||
|
album: chart.album ?? 'Unknown Album',
|
||||||
|
genre: chart.genre ?? 'Unknown Genre',
|
||||||
|
year: chart.year ?? 'Unknown Year',
|
||||||
|
charter: chart.charter ?? 'Unknown Charter',
|
||||||
|
}
|
||||||
this.downloads.push({
|
this.downloads.push({
|
||||||
md5: chart.md5,
|
md5: chart.md5,
|
||||||
chartName,
|
chart: newChart,
|
||||||
header: 'Waiting for other downloads to finish...',
|
header: 'Waiting for other downloads to finish...',
|
||||||
body: '',
|
body: '',
|
||||||
percent: 0,
|
percent: 0,
|
||||||
type: 'good',
|
type: 'good',
|
||||||
isPath: false,
|
isPath: false,
|
||||||
})
|
})
|
||||||
window.electron.emit.download({ action: 'add', md5: chart.md5, chartName })
|
window.electron.emit.download({ action: 'add', md5: chart.md5, chart: newChart })
|
||||||
}
|
}
|
||||||
this.downloadCountChanges.emit(this.downloadCount)
|
this.downloadCountChanges.emit(this.downloadCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,15 @@ export class SettingsService {
|
|||||||
get libraryDirectory() {
|
get libraryDirectory() {
|
||||||
return this.settings.libraryPath
|
return this.settings.libraryPath
|
||||||
}
|
}
|
||||||
set libraryDirectory(newValue: string | undefined) {
|
set libraryDirectory(value: string | undefined) {
|
||||||
this.settings.libraryPath = newValue
|
this.settings.libraryPath = value
|
||||||
|
this.saveSettings()
|
||||||
|
}
|
||||||
|
get chartFolderName() {
|
||||||
|
return this.settings.chartFolderName
|
||||||
|
}
|
||||||
|
set chartFolderName(value: string) {
|
||||||
|
this.settings.chartFolderName = value
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
import { basename, parse } from 'path'
|
import { parse } from 'path'
|
||||||
import sanitize from 'sanitize-filename'
|
import sanitize from 'sanitize-filename'
|
||||||
import { inspect } from 'util'
|
import { inspect } from 'util'
|
||||||
|
|
||||||
import { lower } from '../src-shared/UtilFunctions.js'
|
import { lower } from '../src-shared/UtilFunctions.js'
|
||||||
import { settings } from './ipc/SettingsHandler.ipc.js'
|
|
||||||
import { emitIpcEvent } from './main.js'
|
import { emitIpcEvent } from './main.js'
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns The relative filepath from the library folder to `absoluteFilepath`.
|
|
||||||
*/
|
|
||||||
export async function getRelativeFilepath(absoluteFilepath: string) {
|
|
||||||
if (!settings.libraryPath) { throw 'getRelativeFilepath() failed; libraryPath is undefined' }
|
|
||||||
return basename(settings.libraryPath) + absoluteFilepath.substring(settings.libraryPath.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns `true` if `name` has a valid video file extension.
|
* @returns `true` if `name` has a valid video file extension.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const downloadQueue: DownloadQueue = new DownloadQueue()
|
|||||||
|
|
||||||
export async function download(data: Download) {
|
export async function download(data: Download) {
|
||||||
switch (data.action) {
|
switch (data.action) {
|
||||||
case 'add': downloadQueue.add(data.md5, data.chartName!); break
|
case 'add': downloadQueue.add(data.md5, data.chart!); break
|
||||||
case 'retry': downloadQueue.retry(data.md5); break
|
case 'retry': downloadQueue.retry(data.md5); break
|
||||||
case 'remove': downloadQueue.remove(data.md5); break
|
case 'remove': downloadQueue.remove(data.md5); break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { ReadableStream } from 'stream/web'
|
|||||||
import { inspect } from 'util'
|
import { inspect } from 'util'
|
||||||
|
|
||||||
import { tempPath } from '../../../src-shared/Paths.js'
|
import { tempPath } from '../../../src-shared/Paths.js'
|
||||||
import { sanitizeFilename } from '../../ElectronUtilFunctions.js'
|
import { resolveChartFolderName } from '../../../src-shared/UtilFunctions.js'
|
||||||
import { getSettings } from '../SettingsHandler.ipc.js'
|
import { getSettings } from '../SettingsHandler.ipc.js'
|
||||||
|
|
||||||
export interface DownloadMessage {
|
export interface DownloadMessage {
|
||||||
@@ -58,14 +58,17 @@ export class ChartDownload {
|
|||||||
private stepCompletedCount = 0
|
private stepCompletedCount = 0
|
||||||
private tempPath: string
|
private tempPath: string
|
||||||
|
|
||||||
private destinationName: string
|
private chartFolderPath: string
|
||||||
private isSng: boolean
|
private isSng: boolean
|
||||||
|
|
||||||
private showProgress = _.throttle((description: string, percent: number | null = null) => {
|
private showProgress = _.throttle((description: string, percent: number | null = null) => {
|
||||||
this.eventEmitter.emit('progress', { header: description, body: '' }, percent)
|
this.eventEmitter.emit('progress', { header: description, body: '' }, percent)
|
||||||
}, 10, { leading: true, trailing: true })
|
}, 10, { leading: true, trailing: true })
|
||||||
|
|
||||||
constructor(public readonly md5: string, private chartName: string) { }
|
constructor(
|
||||||
|
public readonly md5: string,
|
||||||
|
private chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string },
|
||||||
|
) { }
|
||||||
|
|
||||||
on<T extends keyof ChartDownloadEvents>(event: T, listener: ChartDownloadEvents[T]) {
|
on<T extends keyof ChartDownloadEvents>(event: T, listener: ChartDownloadEvents[T]) {
|
||||||
this.eventEmitter.on(event, listener)
|
this.eventEmitter.on(event, listener)
|
||||||
@@ -125,23 +128,16 @@ export class ChartDownload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.isSng = settings.isSng
|
this.isSng = settings.isSng
|
||||||
this.destinationName = sanitizeFilename(this.isSng ? `${this.chartName}.sng` : this.chartName)
|
this.chartFolderPath = resolveChartFolderName(settings.chartFolderName, this.chart) + (this.isSng ? '.sng' : '')
|
||||||
this.showProgress('Checking for any duplicate charts...')
|
this.showProgress('Checking for any duplicate charts...')
|
||||||
const destinationPath = join(settings.libraryPath, this.destinationName)
|
const destinationPath = join(settings.libraryPath, this.chartFolderPath)
|
||||||
const isDuplicate = await access(destinationPath, constants.F_OK).then(() => true).catch(() => false)
|
const isDuplicate = await access(destinationPath, constants.F_OK).then(() => true).catch(() => false)
|
||||||
if (this._canceled) { return }
|
if (this._canceled) { return }
|
||||||
if (isDuplicate) {
|
if (isDuplicate) {
|
||||||
throw { header: 'This chart already exists in your library folder', body: destinationPath, isPath: true }
|
throw { header: 'This chart already exists in your library folder', body: destinationPath, isPath: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tempPath = join(tempPath, randomUUID())
|
this.tempPath = join(tempPath, randomUUID()) + (this.isSng ? '.sng' : '')
|
||||||
try {
|
|
||||||
this.showProgress('Creating temporary download folder...')
|
|
||||||
await ensureDir(this.tempPath)
|
|
||||||
if (this._canceled) { return }
|
|
||||||
} catch (err) {
|
|
||||||
throw { header: 'Failed to create temporary download folder', body: inspect(err) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async downloadChart() {
|
private async downloadChart() {
|
||||||
@@ -149,7 +145,7 @@ export class ChartDownload {
|
|||||||
const fileSize = BigInt(response.headers['content-length']!)
|
const fileSize = BigInt(response.headers['content-length']!)
|
||||||
|
|
||||||
if (this.isSng) {
|
if (this.isSng) {
|
||||||
response.pipe(createWriteStream(join(this.tempPath, this.destinationName), { highWaterMark: 2e+9 }))
|
response.pipe(createWriteStream(this.tempPath, { highWaterMark: 2e+9 }))
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
let downloadedByteCount = BigInt(0)
|
let downloadedByteCount = BigInt(0)
|
||||||
@@ -169,12 +165,12 @@ export class ChartDownload {
|
|||||||
const sngStream = new SngStream(Readable.toWeb(response) as any, { generateSongIni: true })
|
const sngStream = new SngStream(Readable.toWeb(response) as any, { generateSongIni: true })
|
||||||
let downloadedByteCount = BigInt(0)
|
let downloadedByteCount = BigInt(0)
|
||||||
|
|
||||||
await ensureDir(join(this.tempPath, this.destinationName))
|
await ensureDir(this.tempPath)
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
sngStream.on('file', async (fileName, fileStream, nextFile) => {
|
sngStream.on('file', async (fileName, fileStream, nextFile) => {
|
||||||
const nodeFileStream = Readable.fromWeb(fileStream as ReadableStream<Uint8Array>, { highWaterMark: 2e+9 })
|
const nodeFileStream = Readable.fromWeb(fileStream as ReadableStream<Uint8Array>, { highWaterMark: 2e+9 })
|
||||||
nodeFileStream.pipe(createWriteStream(join(this.tempPath, this.destinationName, fileName), { highWaterMark: 2e+9 }))
|
nodeFileStream.pipe(createWriteStream(join(this.tempPath, fileName), { highWaterMark: 2e+9 }))
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
nodeFileStream.on('end', resolve)
|
nodeFileStream.on('end', resolve)
|
||||||
@@ -220,8 +216,8 @@ export class ChartDownload {
|
|||||||
await new Promise<void>(resolve => setTimeout(resolve, 200)) // Delay for OS file processing
|
await new Promise<void>(resolve => setTimeout(resolve, 200)) // Delay for OS file processing
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
if (settings.libraryPath) {
|
if (settings.libraryPath) {
|
||||||
const destinationPath = join(settings.libraryPath, this.destinationName)
|
const destinationPath = join(settings.libraryPath, this.chartFolderPath)
|
||||||
move(join(this.tempPath, this.destinationName), destinationPath, { overwrite: true }, err => {
|
move(this.tempPath, destinationPath, { overwrite: true }, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject({ header: 'Failed to move chart to library folder', body: inspect(err) })
|
reject({ header: 'Failed to move chart to library folder', body: inspect(err) })
|
||||||
} else {
|
} else {
|
||||||
@@ -240,9 +236,8 @@ export class ChartDownload {
|
|||||||
throw { header: 'Failed to delete temporary folder', body: inspect(err) }
|
throw { header: 'Failed to delete temporary folder', body: inspect(err) }
|
||||||
}
|
}
|
||||||
|
|
||||||
const destinationPath = join(settings.libraryPath!, this.destinationName)
|
|
||||||
this.showProgress.cancel()
|
this.showProgress.cancel()
|
||||||
this.eventEmitter.emit('end', destinationPath)
|
this.eventEmitter.emit('end', join(settings.libraryPath!, this.chartFolderPath))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ export class DownloadQueue {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
add(md5: string, chartName: string) {
|
add(md5: string, chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string }) {
|
||||||
if (!this.isChartInQueue(md5)) {
|
if (!this.isChartInQueue(md5)) {
|
||||||
const chartDownload = new ChartDownload(md5, chartName)
|
const chartDownload = new ChartDownload(md5, chart)
|
||||||
this.downloadQueue.push(chartDownload)
|
this.downloadQueue.push(chartDownload)
|
||||||
|
|
||||||
chartDownload.on('progress', (message, percent) => emitIpcEvent('downloadQueueUpdate', {
|
chartDownload.on('progress', (message, percent) => emitIpcEvent('downloadQueueUpdate', {
|
||||||
md5,
|
md5,
|
||||||
chartName,
|
chart,
|
||||||
header: message.header,
|
header: message.header,
|
||||||
body: message.body,
|
body: message.body,
|
||||||
percent,
|
percent,
|
||||||
@@ -33,7 +33,7 @@ export class DownloadQueue {
|
|||||||
chartDownload.on('error', err => {
|
chartDownload.on('error', err => {
|
||||||
emitIpcEvent('downloadQueueUpdate', {
|
emitIpcEvent('downloadQueueUpdate', {
|
||||||
md5,
|
md5,
|
||||||
chartName,
|
chart,
|
||||||
header: err.header,
|
header: err.header,
|
||||||
body: err.body,
|
body: err.body,
|
||||||
percent: null,
|
percent: null,
|
||||||
@@ -49,7 +49,7 @@ export class DownloadQueue {
|
|||||||
chartDownload.on('end', destinationPath => {
|
chartDownload.on('end', destinationPath => {
|
||||||
emitIpcEvent('downloadQueueUpdate', {
|
emitIpcEvent('downloadQueueUpdate', {
|
||||||
md5,
|
md5,
|
||||||
chartName,
|
chart,
|
||||||
header: 'Download complete',
|
header: 'Download complete',
|
||||||
body: destinationPath,
|
body: destinationPath,
|
||||||
percent: 100,
|
percent: 100,
|
||||||
@@ -81,7 +81,7 @@ export class DownloadQueue {
|
|||||||
|
|
||||||
emitIpcEvent('downloadQueueUpdate', {
|
emitIpcEvent('downloadQueueUpdate', {
|
||||||
md5,
|
md5,
|
||||||
chartName: 'Canceled',
|
chart: { name: '', artist: '', album: '', genre: '', year: '', charter: '' },
|
||||||
header: '',
|
header: '',
|
||||||
body: '',
|
body: '',
|
||||||
percent: null,
|
percent: null,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface Settings {
|
|||||||
customTheme: ThemeColors | null // The colors of a custom theme
|
customTheme: ThemeColors | null // The colors of a custom theme
|
||||||
customThemePath: string | null // The last folder that contained the `customTheme`'s file
|
customThemePath: string | null // The last folder that contained the `customTheme`'s file
|
||||||
libraryPath: string | undefined // The path to the user's library
|
libraryPath: string | undefined // The path to the user's library
|
||||||
|
chartFolderName: string // The relative path and name of the chart that is saved in `libraryPath`
|
||||||
isSng: boolean // If the chart should be downloaded as a .sng file or as a chart folder
|
isSng: boolean // If the chart should be downloaded as a .sng file or as a chart folder
|
||||||
isCompactTable: boolean // If the search result table should have reduced padding
|
isCompactTable: boolean // If the search result table should have reduced padding
|
||||||
visibleColumns: string[] // The search result columns to include
|
visibleColumns: string[] // The search result columns to include
|
||||||
@@ -45,6 +46,7 @@ export const defaultSettings: Settings = {
|
|||||||
customTheme: null,
|
customTheme: null,
|
||||||
customThemePath: null,
|
customThemePath: null,
|
||||||
libraryPath: undefined,
|
libraryPath: undefined,
|
||||||
|
chartFolderName: '{artist} - {name} ({charter})',
|
||||||
isSng: false,
|
isSng: false,
|
||||||
isCompactTable: false,
|
isCompactTable: false,
|
||||||
visibleColumns: ['artist', 'album', 'genre', 'year'],
|
visibleColumns: ['artist', 'album', 'genre', 'year'],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { interpolate as culoriInterpolate, oklch, wcagContrast } from 'culori'
|
import { interpolate as culoriInterpolate, oklch, wcagContrast } from 'culori'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import sanitize from 'sanitize-filename'
|
||||||
import { Difficulty, Instrument } from 'scan-chart'
|
import { Difficulty, Instrument } from 'scan-chart'
|
||||||
|
|
||||||
import { ChartData } from './interfaces/search.interface'
|
import { ChartData } from './interfaces/search.interface'
|
||||||
@@ -169,6 +170,63 @@ export function hasIssues(chart: Pick<ChartData, 'metadataIssues' | 'folderIssue
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveChartFolderName(
|
||||||
|
chartFolderName: string,
|
||||||
|
chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string },
|
||||||
|
) {
|
||||||
|
if (_.sumBy(chartFolderName.split('/'), n => n.length) === 0) {
|
||||||
|
chartFolderName = '{artist} - {name} ({charter})'
|
||||||
|
}
|
||||||
|
const pathParts = chartFolderName.split('/')
|
||||||
|
const resolvedPathParts: string[] = []
|
||||||
|
for (const pathPart of pathParts) {
|
||||||
|
const resolvedPath = sanitizeNonemptyFilename(pathPart
|
||||||
|
.replace(/\{name\}/g, chart.name)
|
||||||
|
.replace(/\{artist\}/g, chart.artist)
|
||||||
|
.replace(/\{album\}/g, chart.album)
|
||||||
|
.replace(/\{genre\}/g, chart.genre)
|
||||||
|
.replace(/\{year\}/g, chart.year)
|
||||||
|
.replace(/\{charter\}/g, chart.charter))
|
||||||
|
|
||||||
|
if (resolvedPath.length > 0) {
|
||||||
|
resolvedPathParts.push(resolvedPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvedPathParts.join('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns `filename` with all invalid filename characters replaced. Assumes `filename` has at least one valid filename character already.
|
||||||
|
*/
|
||||||
|
export function sanitizeNonemptyFilename(filename: string) {
|
||||||
|
return 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 '_'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
export const colorNames = {
|
export const colorNames = {
|
||||||
"primary": "--p",
|
"primary": "--p",
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
export interface Download {
|
export interface Download {
|
||||||
action: 'add' | 'remove' | 'retry'
|
action: 'add' | 'remove' | 'retry'
|
||||||
md5: string
|
md5: string
|
||||||
chartName?: string // Should be defined if action === 'add'
|
// Should be defined if action === 'add'
|
||||||
|
chart?: { name: string; artist: string; album: string; genre: string; year: string; charter: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,7 +14,7 @@ export interface Download {
|
|||||||
*/
|
*/
|
||||||
export interface DownloadProgress {
|
export interface DownloadProgress {
|
||||||
md5: string
|
md5: string
|
||||||
chartName: string
|
chart: { name: string; artist: string; album: string; genre: string; year: string; charter: string }
|
||||||
header: string
|
header: string
|
||||||
body: string
|
body: string
|
||||||
percent: number | null
|
percent: number | null
|
||||||
|
|||||||
Reference in New Issue
Block a user