Restructure

This commit is contained in:
Geomitron
2023-11-27 18:53:09 -06:00
parent 558d76f582
commit 49c3f38f99
758 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { Injectable } from '@angular/core'
import { ElectronService } from './electron.service'
@Injectable({
providedIn: 'root',
})
export class AlbumArtService {
private imageCache: { [songID: number]: string } = {}
constructor(private electronService: ElectronService) { }
async getImage(songID: number): Promise<string | null> {
if (this.imageCache[songID] == undefined) {
const albumArtResult = await this.electronService.invoke('album-art', songID)
if (albumArtResult) {
this.imageCache[songID] = albumArtResult.base64Art
} else {
this.imageCache[songID] = null
}
}
return this.imageCache[songID]
}
}

View File

@@ -0,0 +1,89 @@
import { EventEmitter, Injectable } from '@angular/core'
import { DownloadProgress, NewDownload } from '../../../electron/shared/interfaces/download.interface'
import { ElectronService } from './electron.service'
@Injectable({
providedIn: 'root',
})
export class DownloadService {
private downloadUpdatedEmitter = new EventEmitter<DownloadProgress>()
private downloads: DownloadProgress[] = []
constructor(private electronService: ElectronService) {
this.electronService.receiveIPC('download-updated', result => {
// Update <this.downloads> with result
const thisDownloadIndex = this.downloads.findIndex(download => download.versionID == result.versionID)
if (result.type == 'cancel') {
this.downloads = this.downloads.filter(download => download.versionID != result.versionID)
} else if (thisDownloadIndex == -1) {
this.downloads.push(result)
} else {
this.downloads[thisDownloadIndex] = result
}
this.downloadUpdatedEmitter.emit(result)
})
}
get downloadCount() {
return this.downloads.length
}
get completedCount() {
return this.downloads.filter(download => download.type == 'done').length
}
get totalDownloadingPercent() {
let total = 0
let count = 0
for (const download of this.downloads) {
if (!download.stale) {
total += download.percent
count++
}
}
return total / count
}
get anyErrorsExist() {
return this.downloads.find(download => download.type == 'error') ? true : false
}
addDownload(versionID: number, newDownload: NewDownload) {
if (!this.downloads.find(download => download.versionID == versionID)) { // Don't download something twice
if (this.downloads.every(download => download.type == 'done')) { // Reset overall progress bar if it finished
this.downloads.forEach(download => download.stale = true)
}
this.electronService.sendIPC('download', { action: 'add', versionID, data: newDownload })
}
}
onDownloadUpdated(callback: (download: DownloadProgress) => void) {
this.downloadUpdatedEmitter.subscribe(callback)
}
cancelDownload(versionID: number) {
const removedDownload = this.downloads.find(download => download.versionID == versionID)
if (['error', 'done'].includes(removedDownload.type)) {
this.downloads = this.downloads.filter(download => download.versionID != versionID)
removedDownload.type = 'cancel'
this.downloadUpdatedEmitter.emit(removedDownload)
} else {
this.electronService.sendIPC('download', { action: 'cancel', versionID })
}
}
cancelCompleted() {
for (const download of this.downloads) {
if (download.type == 'done') {
this.cancelDownload(download.versionID)
}
}
}
retryDownload(versionID: number) {
this.electronService.sendIPC('download', { action: 'retry', versionID })
}
}

View File

@@ -0,0 +1,81 @@
import { Injectable } from '@angular/core'
// If you import a module but never use any of the imported values other than as TypeScript types,
// the resulting javascript file will look as if you never imported the module at all.
import * as electron from 'electron'
import { IPCEmitEvents, IPCInvokeEvents } from '../../../electron/shared/IPCHandler'
const { app, getCurrentWindow, dialog, session } = window.require('@electron/remote')
@Injectable({
providedIn: 'root',
})
export class ElectronService {
electron: typeof electron
get isElectron() {
return !!(window && window.process && window.process.type)
}
constructor() {
if (this.isElectron) {
this.electron = window.require('electron')
this.receiveIPC('log', results => results.forEach(result => console.log(result)))
}
}
get currentWindow() {
return getCurrentWindow()
}
/**
* Calls an async function in the main process.
* @param event The name of the IPC event to invoke.
* @param data The data object to send across IPC.
* @returns A promise that resolves to the output data.
*/
async invoke<E extends keyof IPCInvokeEvents>(event: E, data: IPCInvokeEvents[E]['input']) {
return this.electron.ipcRenderer.invoke(event, data) as Promise<IPCInvokeEvents[E]['output']>
}
/**
* Sends an IPC message to the main process.
* @param event The name of the IPC event to send.
* @param data The data object to send across IPC.
*/
sendIPC<E extends keyof IPCEmitEvents>(event: E, data: IPCEmitEvents[E]) {
this.electron.ipcRenderer.send(event, data)
}
/**
* Receives an IPC message from the main process.
* @param event The name of the IPC event to receive.
* @param callback The data object to receive across IPC.
*/
receiveIPC<E extends keyof IPCEmitEvents>(event: E, callback: (result: IPCEmitEvents[E]) => void) {
this.electron.ipcRenderer.on(event, (_event, ...results) => {
callback(results[0])
})
}
quit() {
app.exit()
}
openFolder(filepath: string) {
this.electron.shell.openPath(filepath)
}
showFolder(filepath: string) {
this.electron.shell.showItemInFolder(filepath)
}
showOpenDialog(options: Electron.OpenDialogOptions) {
return dialog.showOpenDialog(this.currentWindow, options)
}
get defaultSession() {
return session.defaultSession
}
}

View File

@@ -0,0 +1,119 @@
import { EventEmitter, Injectable } from '@angular/core'
import { SongResult, SongSearch } from 'src/electron/shared/interfaces/search.interface'
import { VersionResult } from 'src/electron/shared/interfaces/songDetails.interface'
import { ElectronService } from './electron.service'
@Injectable({
providedIn: 'root',
})
export class SearchService {
private resultsChangedEmitter = new EventEmitter<SongResult[]>() // For when any results change
private newResultsEmitter = new EventEmitter<SongResult[]>() // For when a new search happens
private errorStateEmitter = new EventEmitter<boolean>() // To indicate the search's error state
private results: SongResult[] = []
private awaitingResults = false
private currentQuery: SongSearch
private _allResultsVisible = true
constructor(private electronService: ElectronService) { }
async newSearch(query: SongSearch) {
if (this.awaitingResults) { return }
this.awaitingResults = true
this.currentQuery = query
try {
this.results = this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery))
this.errorStateEmitter.emit(false)
} catch (err) {
this.results = []
console.log(err.message)
this.errorStateEmitter.emit(true)
}
this.awaitingResults = false
this.newResultsEmitter.emit(this.results)
this.resultsChangedEmitter.emit(this.results)
}
isLoading() {
return this.awaitingResults
}
/**
* Event emitted when new search results are returned
* or when more results are added to an existing search.
* (emitted after `onNewSearch`)
*/
onSearchChanged(callback: (results: SongResult[]) => void) {
this.resultsChangedEmitter.subscribe(callback)
}
/**
* Event emitted when a new search query is typed in.
* (emitted before `onSearchChanged`)
*/
onNewSearch(callback: (results: SongResult[]) => void) {
this.newResultsEmitter.subscribe(callback)
}
/**
* Event emitted when the error state of the search changes.
* (emitted before `onSearchChanged`)
*/
onSearchErrorStateUpdate(callback: (isError: boolean) => void) {
this.errorStateEmitter.subscribe(callback)
}
get resultCount() {
return this.results.length
}
async updateScroll() {
if (!this.awaitingResults && !this._allResultsVisible) {
this.awaitingResults = true
this.currentQuery.offset += 50
this.results.push(...this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery)))
this.awaitingResults = false
this.resultsChangedEmitter.emit(this.results)
}
}
trimLastChart(results: SongResult[]) {
if (results.length > 50) {
results.splice(50, 1)
this._allResultsVisible = false
} else {
this._allResultsVisible = true
}
return results
}
get allResultsVisible() {
return this._allResultsVisible
}
/**
* Orders `versionResults` by lastModified date, but prefer the
* non-pack version if it's only a few days older.
*/
sortChart(versionResults: VersionResult[]) {
const dates: { [versionID: number]: number } = {}
versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime())
versionResults.sort((v1, v2) => {
const diff = dates[v2.versionID] - dates[v1.versionID]
if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack != v2.driveData.inChartPack) {
if (v1.driveData.inChartPack) {
return 1 // prioritize v2
} else {
return -1 // prioritize v1
}
} else {
return diff
}
})
}
}

View File

@@ -0,0 +1,85 @@
import { EventEmitter, Injectable } from '@angular/core'
import { SongResult } from '../../../electron/shared/interfaces/search.interface'
import { SearchService } from './search.service'
// Note: this class prevents event cycles by only emitting events if the checkbox changes
@Injectable({
providedIn: 'root',
})
export class SelectionService {
private searchResults: SongResult[] = []
private selectAllChangedEmitter = new EventEmitter<boolean>()
private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {}
private allSelected = false
private selections: { [songID: number]: boolean | undefined } = {}
constructor(searchService: SearchService) {
searchService.onSearchChanged(results => {
this.searchResults = results
if (this.allSelected) {
this.selectAll() // Select newly added rows if allSelected
}
})
searchService.onNewSearch(results => {
this.searchResults = results
this.selectionChangedCallbacks = {}
this.selections = {}
this.selectAllChangedEmitter.emit(false)
})
}
getSelectedResults() {
return this.searchResults.filter(result => this.selections[result.id] == true)
}
onSelectAllChanged(callback: (selected: boolean) => void) {
this.selectAllChangedEmitter.subscribe(callback)
}
/**
* Emits an event when the selection for `songID` needs to change.
* (note: only one emitter can be registered per `songID`)
*/
onSelectionChanged(songID: number, callback: (selection: boolean) => void) {
this.selectionChangedCallbacks[songID] = callback
}
deselectAll() {
if (this.allSelected) {
this.allSelected = false
this.selectAllChangedEmitter.emit(false)
}
setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
}
selectAll() {
if (!this.allSelected) {
this.allSelected = true
this.selectAllChangedEmitter.emit(true)
}
setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
}
deselectSong(songID: number) {
if (this.selections[songID]) {
this.selections[songID] = false
this.selectionChangedCallbacks[songID](false)
}
}
selectSong(songID: number) {
if (!this.selections[songID]) {
this.selections[songID] = true
this.selectionChangedCallbacks[songID](true)
}
}
}

View File

@@ -0,0 +1,82 @@
import { Injectable } from '@angular/core'
import { Settings } from 'src/electron/shared/Settings'
import { ElectronService } from './electron.service'
@Injectable({
providedIn: 'root',
})
export class SettingsService {
readonly builtinThemes = ['Default', 'Dark']
private settings: Settings
private currentThemeLink: HTMLLinkElement
constructor(private electronService: ElectronService) { }
async loadSettings() {
this.settings = await this.electronService.invoke('get-settings', undefined)
if (this.settings.theme != this.builtinThemes[0]) {
this.changeTheme(this.settings.theme)
}
}
saveSettings() {
this.electronService.sendIPC('set-settings', this.settings)
}
changeTheme(theme: string) {
if (this.currentThemeLink != undefined) this.currentThemeLink.remove()
if (theme == 'Default') { return }
const link = document.createElement('link')
link.type = 'text/css'
link.rel = 'stylesheet'
link.href = `./assets/themes/${theme.toLowerCase()}.css`
this.currentThemeLink = document.head.appendChild(link)
}
async getCacheSize() {
return this.electronService.defaultSession.getCacheSize()
}
async clearCache() {
this.saveSettings()
await this.electronService.defaultSession.clearCache()
await this.electronService.invoke('clear-cache', undefined)
}
// Individual getters/setters
get libraryDirectory() {
return this.settings.libraryPath
}
set libraryDirectory(newValue: string) {
this.settings.libraryPath = newValue
this.saveSettings()
}
get downloadVideos() {
return this.settings.downloadVideos
}
set downloadVideos(isChecked) {
this.settings.downloadVideos = isChecked
this.saveSettings()
}
get theme() {
return this.settings.theme
}
set theme(newValue: string) {
this.settings.theme = newValue
this.changeTheme(newValue)
this.saveSettings()
}
get rateLimitDelay() {
return this.settings.rateLimitDelay
}
set rateLimitDelay(delay: number) {
this.settings.rateLimitDelay = delay
this.saveSettings()
}
}