diff --git a/src/app/components/browse/browse.component.html b/src/app/components/browse/browse.component.html
index be73022..bd722a8 100644
--- a/src/app/components/browse/browse.component.html
+++ b/src/app/components/browse/browse.component.html
@@ -5,8 +5,6 @@
@@ -14,4 +12,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts b/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts
index 3e89508..e58a1f2 100644
--- a/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts
+++ b/src/app/components/browse/result-table/result-table-row/result-table-row.component.ts
@@ -1,5 +1,6 @@
-import { Component, AfterViewInit, Input, ViewChild, ElementRef, Output, EventEmitter } from '@angular/core'
+import { Component, AfterViewInit, Input, ViewChild, ElementRef } from '@angular/core'
import { SongResult } from '../../../../../electron/shared/interfaces/search.interface'
+import { SelectionService } from 'src/app/core/services/selection.service'
@Component({
selector: 'tr[app-result-table-row]',
@@ -8,35 +9,31 @@ import { SongResult } from '../../../../../electron/shared/interfaces/search.int
})
export class ResultTableRowComponent implements AfterViewInit {
@Input() result: SongResult
- @Output() songChecked = new EventEmitter()
- @Output() songUnchecked = new EventEmitter()
@ViewChild('checkbox', { static: true }) checkbox: ElementRef
- constructor() { }
+ constructor(private selectionService: SelectionService) { }
get songID() {
return this.result.id
}
ngAfterViewInit() {
+ this.selectionService.onSelectionChanged(this.songID, (isChecked) => {
+ if (isChecked) {
+ $(this.checkbox.nativeElement).checkbox('check')
+ } else {
+ $(this.checkbox.nativeElement).checkbox('uncheck')
+ }
+ })
+
$(this.checkbox.nativeElement).checkbox({
onChecked: () => {
- this.songChecked.emit(this.result)
+ this.selectionService.selectSong(this.songID)
},
onUnchecked: () => {
- this.songUnchecked.emit(this.result)
+ this.selectionService.deselectSong(this.songID)
}
})
}
-
- check(isChecked: boolean) {
- if (isChecked) {
- $(this.checkbox.nativeElement).checkbox('check')
- this.songChecked.emit(this.result)
- } else {
- $(this.checkbox.nativeElement).checkbox('uncheck')
- this.songUnchecked.emit(this.result)
- }
- }
}
\ No newline at end of file
diff --git a/src/app/components/browse/result-table/result-table.component.html b/src/app/components/browse/result-table/result-table.component.html
index b75e8a0..24de5e7 100644
--- a/src/app/components/browse/result-table/result-table.component.html
+++ b/src/app/components/browse/result-table/result-table.component.html
@@ -22,8 +22,6 @@
#tableRow
*ngFor="let result of results"
(click)="onRowClicked(result)"
- (songChecked)="onSongChecked($event)"
- (songUnchecked)="onSongUnchecked($event)"
[result]="result">
\ No newline at end of file
diff --git a/src/app/components/browse/result-table/result-table.component.ts b/src/app/components/browse/result-table/result-table.component.ts
index e3b35f8..fb4ede9 100644
--- a/src/app/components/browse/result-table/result-table.component.ts
+++ b/src/app/components/browse/result-table/result-table.component.ts
@@ -3,6 +3,7 @@ import { SongResult } from '../../../../electron/shared/interfaces/search.interf
import { ResultTableRowComponent } from './result-table-row/result-table-row.component'
import { CheckboxDirective } from 'src/app/core/directives/checkbox.directive'
import { SearchService } from 'src/app/core/services/search.service'
+import { SelectionService } from 'src/app/core/services/selection.service'
@Component({
selector: 'app-result-table',
@@ -12,27 +13,21 @@ import { SearchService } from 'src/app/core/services/search.service'
export class ResultTableComponent implements OnInit {
@Output() rowClicked = new EventEmitter()
- @Output() songChecked = new EventEmitter()
- @Output() songUnchecked = new EventEmitter()
@ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective
@ViewChildren('tableRow') tableRows: QueryList
results: SongResult[]
- constructor(private searchService: SearchService) { }
+ constructor(private searchService: SearchService, private selectionService: SelectionService) { }
ngOnInit() {
- this.searchService.onNewSearch(() => {
- this.checkboxColumn.check(false)
- this.checkAll(false)
+ this.selectionService.onSelectAllChanged((selected) => {
+ this.checkboxColumn.check(selected)
})
this.searchService.onSearchChanged(results => {
this.results = results
- if (this.checkboxColumn.isChecked) {
- this.checkAll(true)
- }
})
}
@@ -40,19 +35,14 @@ export class ResultTableComponent implements OnInit {
this.rowClicked.emit(result)
}
- onSongChecked(result: SongResult) {
- this.songChecked.emit(result)
- }
-
- onSongUnchecked(result: SongResult) {
- this.songUnchecked.emit(result)
- }
-
+ /**
+ * Called when the user checks the `checkboxColumn`.
+ */
checkAll(isChecked: boolean) {
- this.tableRows.forEach(row => row.check(isChecked))
- }
-
- onSongsDeselected(songs: SongResult['id'][]) {
- this.tableRows.forEach(row => row.check(!songs.includes(row.songID)))
+ if (isChecked) {
+ this.selectionService.selectAll()
+ } else {
+ this.selectionService.deselectAll()
+ }
}
}
\ No newline at end of file
diff --git a/src/app/components/browse/status-bar/status-bar.component.ts b/src/app/components/browse/status-bar/status-bar.component.ts
index 4ed1789..c659473 100644
--- a/src/app/components/browse/status-bar/status-bar.component.ts
+++ b/src/app/components/browse/status-bar/status-bar.component.ts
@@ -1,10 +1,10 @@
-import { Component, ChangeDetectorRef, Output, EventEmitter } from '@angular/core'
-import { SongResult } from 'src/electron/shared/interfaces/search.interface'
+import { Component, ChangeDetectorRef } from '@angular/core'
import { DownloadService } from 'src/app/core/services/download.service'
import { ElectronService } from 'src/app/core/services/electron.service'
import { groupBy } from 'src/electron/shared/UtilFunctions'
import { VersionResult } from 'src/electron/shared/interfaces/songDetails.interface'
import { SearchService } from 'src/app/core/services/search.service'
+import { SelectionService } from 'src/app/core/services/selection.service'
@Component({
selector: 'app-status-bar',
@@ -13,12 +13,9 @@ import { SearchService } from 'src/app/core/services/search.service'
})
export class StatusBarComponent {
- @Output() deselectSongs = new EventEmitter()
-
resultCount = 0
downloading = false
percent = 0
- selectedResults: SongResult[] = []
batchResults: VersionResult[]
chartGroups: VersionResult[][]
@@ -26,6 +23,7 @@ export class StatusBarComponent {
private electronService: ElectronService,
private downloadService: DownloadService,
private searchService: SearchService,
+ private selectionService: SelectionService,
ref: ChangeDetectorRef
) {
downloadService.onDownloadUpdated(() => {
@@ -39,30 +37,20 @@ export class StatusBarComponent {
searchService.onSearchChanged(() => {
this.resultCount = searchService.resultCount
})
-
- searchService.onNewSearch(() => {
- this.selectedResults = []
- })
}
get allResultsVisible() {
return this.searchService.allResultsVisible
}
+ get selectedResults() {
+ return this.selectionService.getSelectedResults()
+ }
+
showDownloads() {
$('#downloadsModal').modal('show')
}
- onSongChecked(result: SongResult) {
- if (this.selectedResults.findIndex(oldResult => oldResult.id == result.id) == -1) {
- this.selectedResults.push(result)
- }
- }
-
- onSongUnchecked(result: SongResult) {
- this.selectedResults = this.selectedResults.filter(oldResult => oldResult.id != result.id)
- }
-
async downloadSelected() {
this.chartGroups = []
this.batchResults = await this.electronService.invoke('batch-song-details', this.selectedResults.map(result => result.id))
@@ -107,6 +95,8 @@ export class StatusBarComponent {
}
deselectSongsWithMultipleCharts() {
- this.deselectSongs.emit(this.chartGroups.map(group => group[0].songID))
+ for (const chartGroup of this.chartGroups) {
+ this.selectionService.deselectSong(chartGroup[0].songID)
+ }
}
}
\ No newline at end of file
diff --git a/src/app/core/services/search.service.ts b/src/app/core/services/search.service.ts
index 08bbace..d337eec 100644
--- a/src/app/core/services/search.service.ts
+++ b/src/app/core/services/search.service.ts
@@ -22,14 +22,23 @@ export class SearchService {
this.results = this.trimLastChart(await this.electronService.invoke('song-search', this.currentQuery))
this.awaitingResults = false
- this.newResultsEmitter.emit(this.results)
this.resultsChangedEmitter.emit(this.results)
+ this.newResultsEmitter.emit(this.results)
}
+ /**
+ * Event emitted when new search results are returned
+ * or when more results are added to an existing search.
+ * (emitted before `onNewSearch`)
+ */
onSearchChanged(callback: (results: SongResult[]) => void) {
this.resultsChangedEmitter.subscribe(callback)
}
+ /**
+ * Event emitted when a new search query is typed in.
+ * (emitted after `onSearchChanged`)
+ */
onNewSearch(callback: (results: SongResult[]) => void) {
this.newResultsEmitter.subscribe(callback)
}
diff --git a/src/app/core/services/selection.service.ts b/src/app/core/services/selection.service.ts
new file mode 100644
index 0000000..d0b5094
--- /dev/null
+++ b/src/app/core/services/selection.service.ts
@@ -0,0 +1,96 @@
+import { Injectable, EventEmitter } from '@angular/core'
+import { SongResult } from 'src/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()
+ private selectionChangedEmitters: { [songID: number]: EventEmitter } = {}
+
+ private allSelected = false
+ private selections: { [songID: number]: boolean | undefined } = {}
+
+ constructor(searchService: SearchService) {
+ searchService.onSearchChanged((results) => {
+ this.searchResults = results
+ this.removeOldListeners(results.map(result => result.id))
+
+ if (this.allSelected) {
+ this.selectAll() // Select newly added rows if allSelected
+ }
+ })
+
+ searchService.onNewSearch((results) => {
+ this.searchResults = results
+ this.removeOldListeners(results.map(result => result.id))
+
+ this.deselectAll()
+ })
+ }
+
+ private removeOldListeners(songIDs: number[]) {
+ for (const oldSongID in this.selectionChangedEmitters) {
+ if (!songIDs.find(newSongID => newSongID == Number(oldSongID))) {
+ delete this.selectionChangedEmitters[oldSongID]
+ delete this.selections[oldSongID]
+ }
+ }
+ }
+
+ 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.selectionChangedEmitters[songID] = new EventEmitter()
+ this.selectionChangedEmitters[songID].subscribe(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.selectionChangedEmitters[songID].emit(false)
+ }
+ }
+
+ selectSong(songID: number) {
+ if (!this.selections[songID] && this.selectionChangedEmitters[songID] != undefined) {
+ this.selections[songID] = true
+ this.selectionChangedEmitters[songID].emit(true)
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/electron/ipc/download/DownloadQueue.ts b/src/electron/ipc/download/DownloadQueue.ts
index e2078fa..52d4bc9 100644
--- a/src/electron/ipc/download/DownloadQueue.ts
+++ b/src/electron/ipc/download/DownloadQueue.ts
@@ -32,7 +32,7 @@ export class DownloadQueue {
}
}
- private sort() { // TODO: make this order be reflected in the GUI (along with currentDownload)
+ private sort() {
let comparator = Comparators.comparing('allFilesProgress', { reversed: true })
const prioritizeArchives = true