mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Restructure
This commit is contained in:
12
src-angular/app/components/browse/browse.component.html
Normal file
12
src-angular/app/components/browse/browse.component.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<app-search-bar></app-search-bar>
|
||||
<div class="ui celled two column grid">
|
||||
<div id="table-row" class="row">
|
||||
<div id="table-column" class="column twelve wide">
|
||||
<app-result-table #resultTable (rowClicked)="chartSidebar.onRowClicked($event)"></app-result-table>
|
||||
</div>
|
||||
<div id="sidebar-column" class="column four wide">
|
||||
<app-chart-sidebar #chartSidebar></app-chart-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<app-status-bar #statusBar></app-status-bar>
|
||||
27
src-angular/app/components/browse/browse.component.scss
Normal file
27
src-angular/app/components/browse/browse.component.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.two.column.grid {
|
||||
display: contents;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#table-row {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
flex-wrap: nowrap;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#table-column {
|
||||
overflow-y: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#sidebar-column {
|
||||
display: flex;
|
||||
min-width: 175px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
35
src-angular/app/components/browse/browse.component.ts
Normal file
35
src-angular/app/components/browse/browse.component.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { AfterViewInit, Component, ViewChild } from '@angular/core'
|
||||
|
||||
import { SearchService } from 'src/app/core/services/search.service'
|
||||
import { ChartSidebarComponent } from './chart-sidebar/chart-sidebar.component'
|
||||
import { ResultTableComponent } from './result-table/result-table.component'
|
||||
import { StatusBarComponent } from './status-bar/status-bar.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-browse',
|
||||
templateUrl: './browse.component.html',
|
||||
styleUrls: ['./browse.component.scss'],
|
||||
})
|
||||
export class BrowseComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('resultTable', { static: true }) resultTable: ResultTableComponent
|
||||
@ViewChild('chartSidebar', { static: true }) chartSidebar: ChartSidebarComponent
|
||||
@ViewChild('statusBar', { static: true }) statusBar: StatusBarComponent
|
||||
|
||||
constructor(private searchService: SearchService) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
const $tableColumn = $('#table-column')
|
||||
$tableColumn.on('scroll', () => {
|
||||
const pos = $tableColumn[0].scrollTop + $tableColumn[0].offsetHeight
|
||||
const max = $tableColumn[0].scrollHeight
|
||||
if (pos >= max - 5) {
|
||||
this.searchService.updateScroll()
|
||||
}
|
||||
})
|
||||
|
||||
this.searchService.onNewSearch(() => {
|
||||
$tableColumn.scrollTop(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<div id="sidebarCard" *ngIf="selectedVersion" class="ui fluid card">
|
||||
<div class="ui placeholder" [ngClass]="{ placeholder: albumArtSrc === '', inverted: settingsService.theme === 'Dark' }">
|
||||
<img *ngIf="albumArtSrc !== null" class="ui square image" [src]="albumArtSrc" />
|
||||
</div>
|
||||
<div *ngIf="charts.length > 1" id="chartDropdown" class="ui fluid right labeled scrolling icon dropdown button">
|
||||
<input type="hidden" name="Chart" />
|
||||
<i id="chartDropdownIcon" class="dropdown icon"></i>
|
||||
<div class="default text"></div>
|
||||
<div id="chartDropdownMenu" class="menu"></div>
|
||||
</div>
|
||||
<div id="textPanel" class="content">
|
||||
<span class="header">{{ selectedVersion.chartName }}</span>
|
||||
<div class="description">
|
||||
<div *ngIf="songResult.album === null"><b>Album:</b> {{ selectedVersion.album }} ({{ selectedVersion.year }})</div>
|
||||
<div *ngIf="songResult.album !== null"><b>Year:</b> {{ selectedVersion.year }}</div>
|
||||
<div *ngIf="songResult.genre === null"><b>Genre:</b> {{ selectedVersion.genre }}</div>
|
||||
<div>
|
||||
<b>{{ charterPlural }}</b> {{ selectedVersion.charters }}
|
||||
</div>
|
||||
<div *ngIf="selectedVersion.tags"><b>Tags:</b> {{ selectedVersion.tags }}</div>
|
||||
<div><b>Audio Length:</b> {{ songLength }}</div>
|
||||
<div class="ui divider"></div>
|
||||
<div class="ui horizontal list">
|
||||
<div *ngFor="let difficulty of difficultiesList" class="item">
|
||||
<img class="ui avatar image" src="assets/images/instruments/{{ difficulty.instrument }}" />
|
||||
<div class="content">
|
||||
<div class="header">Diff: {{ difficulty.diffNumber }}</div>
|
||||
{{ difficulty.chartedDifficulties }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="sourceLinks">
|
||||
<a id="sourceLink" (click)="onSourceLinkClicked()">{{ selectedVersion.driveData.source.sourceName }}</a>
|
||||
<button *ngIf="shownFolderButton()" id="folderButton" class="mini ui icon button" (click)="onFolderButtonClicked()">
|
||||
<i class="folder open outline icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="downloadButtons" class="ui positive buttons">
|
||||
<div id="downloadButton" class="ui button" (click)="onDownloadClicked()">{{ downloadButtonText }}</div>
|
||||
<div *ngIf="getSelectedChartVersions().length > 1" id="versionDropdown" class="ui floating dropdown icon button">
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,64 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.ui.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.ui.placeholder {
|
||||
border-radius: 0px !important;
|
||||
}
|
||||
|
||||
#textPanel {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#versionDropdown, #folderButton {
|
||||
max-width: min-content;
|
||||
}
|
||||
|
||||
#chartDropdown {
|
||||
min-height: min-content;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
::ng-deep #chartDropdownMenu > div.item {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
#sourceLinks {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#sourceLink {
|
||||
align-self: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
#chartDropdownIcon, #versionDropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.ui.horizontal.list>.item {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.ui.divider {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
#downloadButton {
|
||||
width: min-content;
|
||||
margin: 0px 1px 1px 1px;
|
||||
}
|
||||
|
||||
#versionDropdown {
|
||||
margin: 0px 1px 1px -3px;
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser'
|
||||
|
||||
import { SearchService } from 'src/app/core/services/search.service'
|
||||
import { SettingsService } from 'src/app/core/services/settings.service'
|
||||
import { groupBy } from 'src/electron/shared/UtilFunctions'
|
||||
import { SongResult } from '../../../../electron/shared/interfaces/search.interface'
|
||||
import { ChartedDifficulty, getInstrumentIcon, Instrument, VersionResult } from '../../../../electron/shared/interfaces/songDetails.interface'
|
||||
import { AlbumArtService } from '../../../core/services/album-art.service'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { ElectronService } from '../../../core/services/electron.service'
|
||||
|
||||
interface Difficulty {
|
||||
instrument: string
|
||||
diffNumber: string
|
||||
chartedDifficulties: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-sidebar',
|
||||
templateUrl: './chart-sidebar.component.html',
|
||||
styleUrls: ['./chart-sidebar.component.scss'],
|
||||
})
|
||||
export class ChartSidebarComponent implements OnInit {
|
||||
|
||||
songResult: SongResult
|
||||
selectedVersion: VersionResult
|
||||
charts: VersionResult[][]
|
||||
|
||||
albumArtSrc: SafeUrl = ''
|
||||
charterPlural: string
|
||||
songLength: string
|
||||
difficultiesList: Difficulty[]
|
||||
downloadButtonText: string
|
||||
|
||||
constructor(
|
||||
private electronService: ElectronService,
|
||||
private albumArtService: AlbumArtService,
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
private sanitizer: DomSanitizer,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.selectVersion(undefined)
|
||||
this.songResult = undefined
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the information for the selected song.
|
||||
*/
|
||||
async onRowClicked(result: SongResult) {
|
||||
if (this.songResult == undefined || result.id != this.songResult.id) { // Clicking the same row again will not reload
|
||||
this.songResult = result
|
||||
const albumArt = this.albumArtService.getImage(result.id)
|
||||
const results = await this.electronService.invoke('song-details', result.id)
|
||||
this.charts = groupBy(results, 'chartID').sort((v1, v2) => v1[0].chartName.length - v2[0].chartName.length)
|
||||
this.sortCharts()
|
||||
await this.selectChart(this.charts[0][0].chartID)
|
||||
this.initChartDropdown()
|
||||
|
||||
this.updateAlbumArtSrc(await albumArt)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts `this.charts` and its subarrays in the correct order.
|
||||
* The chart dropdown should display in a random order, but verified charters are prioritized.
|
||||
* The version dropdown should be ordered by lastModified date.
|
||||
* (but prefer the non-pack version if it's only a few days older)
|
||||
*/
|
||||
private sortCharts() {
|
||||
for (const chart of this.charts) {
|
||||
// TODO: sort by verified charter
|
||||
this.searchService.sortChart(chart)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sidebar to display the album art.
|
||||
*/
|
||||
updateAlbumArtSrc(albumArtBase64String?: string) {
|
||||
if (albumArtBase64String) {
|
||||
this.albumArtSrc = this.sanitizer.bypassSecurityTrustUrl('data:image/jpg;base64,' + albumArtBase64String)
|
||||
} else {
|
||||
this.albumArtSrc = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chart dropdown from `this.charts` (or removes it if there's only one chart).
|
||||
*/
|
||||
private initChartDropdown() {
|
||||
const values = this.charts.map(chart => {
|
||||
const version = chart[0]
|
||||
return {
|
||||
value: version.chartID,
|
||||
text: version.chartName,
|
||||
name: `${version.chartName} <b>[${version.charters}]</b>`,
|
||||
}
|
||||
})
|
||||
const $chartDropdown = $('#chartDropdown')
|
||||
$chartDropdown.dropdown('setup menu', { values })
|
||||
$chartDropdown.dropdown('setting', 'onChange', (chartID: number) => this.selectChart(chartID))
|
||||
$chartDropdown.dropdown('set selected', values[0].value)
|
||||
}
|
||||
|
||||
private async selectChart(chartID: number) {
|
||||
const chart = this.charts.find(chart => chart[0].chartID == chartID)
|
||||
await this.selectVersion(chart[0])
|
||||
this.initVersionDropdown()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sidebar to display the metadata for `selectedVersion`.
|
||||
*/
|
||||
async selectVersion(selectedVersion: VersionResult) {
|
||||
this.selectedVersion = selectedVersion
|
||||
await new Promise<void>(resolve => setTimeout(() => resolve(), 0)) // Wait for *ngIf to update DOM
|
||||
|
||||
if (this.selectedVersion != undefined) {
|
||||
this.updateCharterPlural()
|
||||
this.updateSongLength()
|
||||
this.updateDifficultiesList()
|
||||
this.updateDownloadButtonText()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses to display 'Charter:' or 'Charters:'.
|
||||
*/
|
||||
private updateCharterPlural() {
|
||||
this.charterPlural = this.selectedVersion.charterIDs.split('&').length == 1 ? 'Charter:' : 'Charters:'
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts `this.selectedVersion.chartMetadata.length` into a readable duration.
|
||||
*/
|
||||
private updateSongLength() {
|
||||
let seconds = this.selectedVersion.songLength
|
||||
if (seconds < 60) { this.songLength = `${seconds} second${seconds == 1 ? '' : 's'}`; return }
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = 0
|
||||
while (minutes > 59) {
|
||||
hours++
|
||||
minutes -= 60
|
||||
}
|
||||
seconds = Math.floor(seconds % 60)
|
||||
this.songLength = `${hours == 0 ? '' : hours + ':'}${minutes == 0 ? '' : minutes + ':'}${seconds < 10 ? '0' + seconds : seconds}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates `dfficultiesList` with the difficulty information for the selected version.
|
||||
*/
|
||||
private updateDifficultiesList() {
|
||||
const instruments = Object.keys(this.selectedVersion.chartData.noteCounts) as Instrument[]
|
||||
this.difficultiesList = []
|
||||
for (const instrument of instruments) {
|
||||
if (instrument != 'undefined') {
|
||||
this.difficultiesList.push({
|
||||
instrument: getInstrumentIcon(instrument),
|
||||
diffNumber: this.getDiffNumber(instrument),
|
||||
chartedDifficulties: this.getChartedDifficultiesText(instrument),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a string describing the difficulty number in the selected version.
|
||||
*/
|
||||
private getDiffNumber(instrument: Instrument) {
|
||||
const diffNumber: number = (this.selectedVersion as any)[`diff_${instrument}`]
|
||||
return diffNumber == -1 || diffNumber == undefined ? 'Unknown' : String(diffNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a string describing the list of charted difficulties in the selected version.
|
||||
*/
|
||||
private getChartedDifficultiesText(instrument: Instrument) {
|
||||
const difficulties = Object.keys(this.selectedVersion.chartData.noteCounts[instrument]) as ChartedDifficulty[]
|
||||
if (difficulties.length == 4) { return 'Full Difficulty' }
|
||||
const difficultyNames = []
|
||||
if (difficulties.includes('x')) { difficultyNames.push('Expert') }
|
||||
if (difficulties.includes('h')) { difficultyNames.push('Hard') }
|
||||
if (difficulties.includes('m')) { difficultyNames.push('Medium') }
|
||||
if (difficulties.includes('e')) { difficultyNames.push('Easy') }
|
||||
|
||||
return difficultyNames.join(', ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the text to display on the download button.
|
||||
*/
|
||||
private updateDownloadButtonText() {
|
||||
this.downloadButtonText = 'Download'
|
||||
if (this.selectedVersion.driveData.inChartPack) {
|
||||
this.downloadButtonText += ' Chart Pack'
|
||||
} else {
|
||||
this.downloadButtonText += (this.selectedVersion.driveData.isArchive ? ' Archive' : ' Files')
|
||||
}
|
||||
|
||||
if (this.getSelectedChartVersions().length > 1) {
|
||||
if (this.selectedVersion.versionID == this.selectedVersion.latestVersionID) {
|
||||
this.downloadButtonText += ' (Latest)'
|
||||
} else {
|
||||
this.downloadButtonText += ` (${this.getLastModifiedText(this.selectedVersion.lastModified)})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the version dropdown from `this.selectedVersion` (or removes it if there's only one version).
|
||||
*/
|
||||
private initVersionDropdown() {
|
||||
const $versionDropdown = $('#versionDropdown')
|
||||
const versions = this.getSelectedChartVersions()
|
||||
const values = versions.map(version => ({
|
||||
value: version.versionID,
|
||||
text: 'Uploaded ' + this.getLastModifiedText(version.lastModified),
|
||||
name: 'Uploaded ' + this.getLastModifiedText(version.lastModified),
|
||||
}))
|
||||
|
||||
$versionDropdown.dropdown('setup menu', { values })
|
||||
$versionDropdown.dropdown('setting', 'onChange', (versionID: number) => {
|
||||
this.selectVersion(versions.find(version => version.versionID == versionID))
|
||||
})
|
||||
$versionDropdown.dropdown('set selected', values[0].value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of versions for the selected chart, sorted by `lastModified`.
|
||||
*/
|
||||
getSelectedChartVersions() {
|
||||
return this.charts.find(chart => chart[0].chartID == this.selectedVersion.chartID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the <lastModified> value to a user-readable format.
|
||||
* @param lastModified The UNIX timestamp for the lastModified date.
|
||||
*/
|
||||
private getLastModifiedText(lastModified: string) {
|
||||
const date = new Date(lastModified)
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const year = date.getFullYear().toString().substr(-2)
|
||||
return `${month}/${day}/${year}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the proxy link or source folder in the default browser.
|
||||
*/
|
||||
onSourceLinkClicked() {
|
||||
const source = this.selectedVersion.driveData.source
|
||||
this.electronService.sendIPC('open-url', source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if the source folder button should be shown.
|
||||
*/
|
||||
shownFolderButton() {
|
||||
const driveData = this.selectedVersion.driveData
|
||||
return driveData.source.proxyLink || driveData.source.sourceDriveID != driveData.folderID
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the chart folder in the default browser.
|
||||
*/
|
||||
onFolderButtonClicked() {
|
||||
this.electronService.sendIPC('open-url', `https://drive.google.com/drive/folders/${this.selectedVersion.driveData.folderID}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the selected version to the download queue.
|
||||
*/
|
||||
onDownloadClicked() {
|
||||
this.downloadService.addDownload(
|
||||
this.selectedVersion.versionID, {
|
||||
chartName: this.selectedVersion.chartName,
|
||||
artist: this.songResult.artist,
|
||||
charter: this.selectedVersion.charters,
|
||||
driveData: this.selectedVersion.driveData,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<td>
|
||||
<div #checkbox class="ui checkbox" (click)="$event.stopPropagation()">
|
||||
<input type="checkbox" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span id="chartCount" *ngIf="result.chartCount > 1">{{ result.chartCount }}</span
|
||||
>{{ result.name }}
|
||||
</td>
|
||||
<td>{{ result.artist }}</td>
|
||||
<td>{{ result.album || 'Various' }}</td>
|
||||
<td>{{ result.genre || 'Various' }}</td>
|
||||
@@ -0,0 +1,10 @@
|
||||
.ui.checkbox {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#chartCount {
|
||||
background-color: lightgray;
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px 2px 4px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'
|
||||
|
||||
import { SongResult } from '../../../../../electron/shared/interfaces/search.interface'
|
||||
import { SelectionService } from '../../../../core/services/selection.service'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[app-result-table-row]',
|
||||
templateUrl: './result-table-row.component.html',
|
||||
styleUrls: ['./result-table-row.component.scss'],
|
||||
})
|
||||
export class ResultTableRowComponent implements AfterViewInit {
|
||||
@Input() result: SongResult
|
||||
|
||||
@ViewChild('checkbox', { static: true }) checkbox: ElementRef
|
||||
|
||||
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.selectionService.selectSong(this.songID)
|
||||
},
|
||||
onUnchecked: () => {
|
||||
this.selectionService.deselectSong(this.songID)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<table
|
||||
id="resultTable"
|
||||
class="ui stackable selectable single sortable fixed line striped compact small table"
|
||||
[class.inverted]="settingsService.theme === 'Dark'">
|
||||
<!-- TODO: maybe have some of these tags customizable? E.g. small/large/compact/padded -->
|
||||
<!-- TODO: learn semantic themes in order to change the $mobileBreakpoint global variable (better search table adjustment) -->
|
||||
<thead>
|
||||
<!-- NOTE: it would be nice to make this header sticky, but Fomantic-UI doesn't currently support that -->
|
||||
<tr>
|
||||
<th class="collapsing" id="checkboxColumn">
|
||||
<div class="ui checkbox" id="checkbox" #checkboxColumn appCheckbox (checked)="checkAll($event)">
|
||||
<input type="checkbox" />
|
||||
</div>
|
||||
</th>
|
||||
<th class="four wide" [class.sorted]="sortColumn === 'name'" [ngClass]="sortDirection" (click)="onColClicked('name')">Name</th>
|
||||
<th class="four wide" [class.sorted]="sortColumn === 'artist'" [ngClass]="sortDirection" (click)="onColClicked('artist')">Artist</th>
|
||||
<th class="four wide" [class.sorted]="sortColumn === 'album'" [ngClass]="sortDirection" (click)="onColClicked('album')">Album</th>
|
||||
<th class="four wide" [class.sorted]="sortColumn === 'genre'" [ngClass]="sortDirection" (click)="onColClicked('genre')">Genre</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
app-result-table-row
|
||||
#tableRow
|
||||
*ngFor="let result of results"
|
||||
(click)="onRowClicked(result)"
|
||||
[class.active]="activeRowID === result.id"
|
||||
[result]="result"></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,25 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.ui.checkbox {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#checkboxColumn {
|
||||
width: 34.64px;
|
||||
}
|
||||
|
||||
#resultTable {
|
||||
border-radius: 0px;
|
||||
|
||||
thead>tr:first-child>th:first-child,
|
||||
thead>tr:first-child>th:last-child {
|
||||
border-radius: 0px;
|
||||
}
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index:1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChildren } from '@angular/core'
|
||||
|
||||
import Comparators from 'comparators'
|
||||
|
||||
import { SettingsService } from 'src/app/core/services/settings.service'
|
||||
import { SongResult } from '../../../../electron/shared/interfaces/search.interface'
|
||||
import { CheckboxDirective } from '../../../core/directives/checkbox.directive'
|
||||
import { SearchService } from '../../../core/services/search.service'
|
||||
import { SelectionService } from '../../../core/services/selection.service'
|
||||
import { ResultTableRowComponent } from './result-table-row/result-table-row.component'
|
||||
|
||||
@Component({
|
||||
selector: 'app-result-table',
|
||||
templateUrl: './result-table.component.html',
|
||||
styleUrls: ['./result-table.component.scss'],
|
||||
})
|
||||
export class ResultTableComponent implements OnInit {
|
||||
|
||||
@Output() rowClicked = new EventEmitter<SongResult>()
|
||||
|
||||
@ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective
|
||||
@ViewChildren('tableRow') tableRows: QueryList<ResultTableRowComponent>
|
||||
|
||||
results: SongResult[] = []
|
||||
activeRowID: number = null
|
||||
sortDirection: 'ascending' | 'descending' = 'descending'
|
||||
sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.selectionService.onSelectAllChanged(selected => {
|
||||
this.checkboxColumn.check(selected)
|
||||
})
|
||||
|
||||
this.searchService.onSearchChanged(results => {
|
||||
this.activeRowID = null
|
||||
this.results = results
|
||||
this.updateSort()
|
||||
})
|
||||
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.sortColumn = null
|
||||
})
|
||||
}
|
||||
|
||||
onRowClicked(result: SongResult) {
|
||||
this.activeRowID = result.id
|
||||
this.rowClicked.emit(result)
|
||||
}
|
||||
|
||||
onColClicked(column: 'name' | 'artist' | 'album' | 'genre') {
|
||||
if (this.results.length == 0) { return }
|
||||
if (this.sortColumn != column) {
|
||||
this.sortColumn = column
|
||||
this.sortDirection = 'descending'
|
||||
} else if (this.sortDirection == 'descending') {
|
||||
this.sortDirection = 'ascending'
|
||||
} else {
|
||||
this.sortDirection = 'descending'
|
||||
}
|
||||
this.updateSort()
|
||||
}
|
||||
|
||||
private updateSort() {
|
||||
if (this.sortColumn != null) {
|
||||
this.results.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection == 'ascending' }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user checks the `checkboxColumn`.
|
||||
*/
|
||||
checkAll(isChecked: boolean) {
|
||||
if (isChecked) {
|
||||
this.selectionService.selectAll()
|
||||
} else {
|
||||
this.selectionService.deselectAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<div id="searchMenu" class="ui bottom attached borderless menu">
|
||||
<div class="item">
|
||||
<div class="ui icon input" [class.loading]="isLoading()">
|
||||
<input #searchBox type="text" placeholder=" Search..." (keyup.enter)="onSearch(searchBox.value)" />
|
||||
<i
|
||||
#searchIcon
|
||||
id="searchIcon"
|
||||
class="link icon"
|
||||
[ngClass]="isError ? 'red exclamation triangle' : 'search'"
|
||||
data-content="Failed to connect to the Bridge database"
|
||||
data-position="bottom right"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="quantityDropdownItem" [class.hidden]="!showAdvanced" class="item">
|
||||
<div #quantityDropdown class="ui compact selection dropdown">
|
||||
<input name="quantityMatch" type="hidden" />
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="text"><b>all</b> words match</div>
|
||||
<div class="menu">
|
||||
<div id="dropdownItem" class="active selected item" data-value="all"><b>all</b> words match</div>
|
||||
<div id="dropdownItem" class="item" data-value="any"><b>any</b> words match</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="similarityDropdownItem" [class.hidden]="!showAdvanced" class="item">
|
||||
<div #similarityDropdown class="ui compact selection dropdown">
|
||||
<input name="similarityMatch" type="hidden" value="similar" />
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="text"><b>similar</b> words match</div>
|
||||
<div class="menu">
|
||||
<div id="dropdownItem" class="active selected item" data-value="similar"><b>similar</b> words match</div>
|
||||
<div id="dropdownItem" class="item" data-value="exact"><b>exact</b> words match</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item right">
|
||||
<button class="mini ui right labeled icon button" (click)="onAdvancedSearchClick()">
|
||||
Advanced Search
|
||||
<i class="angle double icon" [ngClass]="showAdvanced ? 'up' : 'down'"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="advancedSearchForm" [class.collapsed]="!showAdvanced" class="ui horizontal segments">
|
||||
<div class="ui secondary segment">
|
||||
<div class="grouped fields">
|
||||
<h5 class="ui dividing header">Search for</h5>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="name" [(ngModel)]="searchSettings.fields.name" />
|
||||
<label>Name</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="artist" [(ngModel)]="searchSettings.fields.artist" />
|
||||
<label>Artist</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="album" [(ngModel)]="searchSettings.fields.album" />
|
||||
<label>Album</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="genre" [(ngModel)]="searchSettings.fields.genre" />
|
||||
<label>Genre</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="year" [(ngModel)]="searchSettings.fields.year" />
|
||||
<label>Year</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="charter" [(ngModel)]="searchSettings.fields.charter" />
|
||||
<label>Charter</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="tag" [(ngModel)]="searchSettings.fields.tag" />
|
||||
<label>Tag</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui secondary segment">
|
||||
<div class="grouped fields">
|
||||
<h5 class="ui dividing header">Must include</h5>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="sections" [(ngModel)]="searchSettings.tags.sections" />
|
||||
<label>Sections</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="starpower" [(ngModel)]="searchSettings.tags['star power']" />
|
||||
<label>Star Power</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="forcing" [(ngModel)]="searchSettings.tags.forcing" />
|
||||
<label>Forcing</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="taps" [(ngModel)]="searchSettings.tags.taps" />
|
||||
<label>Taps</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="lyrics" [(ngModel)]="searchSettings.tags.lyrics" />
|
||||
<label>Lyrics</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="videobackground" [(ngModel)]="searchSettings.tags.video" />
|
||||
<label>Video Background</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="stems" [(ngModel)]="searchSettings.tags.stems" />
|
||||
<label>Audio Stems</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="solosections" [(ngModel)]="searchSettings.tags['solo sections']" />
|
||||
<label>Solo Sections</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="opennotes" [(ngModel)]="searchSettings.tags['open notes']" />
|
||||
<label>Open Notes</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui secondary segment">
|
||||
<div class="grouped fields">
|
||||
<h5 class="ui dividing header">Instruments</h5>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="guitar" [(ngModel)]="searchSettings.instruments.guitar" />
|
||||
<label>Guitar</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="bass" [(ngModel)]="searchSettings.instruments.bass" />
|
||||
<label>Bass</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="rhythm" [(ngModel)]="searchSettings.instruments.rhythm" />
|
||||
<label>Rhythm</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="keys" [(ngModel)]="searchSettings.instruments.keys" />
|
||||
<label>Keys</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="drums" [(ngModel)]="searchSettings.instruments.drums" />
|
||||
<label>Drums</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="guitarghl" [(ngModel)]="searchSettings.instruments.guitarghl" />
|
||||
<label>GHL Guitar</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="bassghl" [(ngModel)]="searchSettings.instruments.bassghl" />
|
||||
<label>GHL Bass</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="vocals" [(ngModel)]="searchSettings.instruments.vocals" />
|
||||
<label>Vocals</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ui secondary segment">
|
||||
<h5 class="ui dividing header">Charted difficulties</h5>
|
||||
<div class="grouped fields">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="expert" [(ngModel)]="searchSettings.difficulties.expert" />
|
||||
<label>Expert</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="hard" [(ngModel)]="searchSettings.difficulties.hard" />
|
||||
<label>Hard</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="medium" [(ngModel)]="searchSettings.difficulties.medium" />
|
||||
<label>Medium</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" name="easy" [(ngModel)]="searchSettings.difficulties.easy" />
|
||||
<label>Easy</label>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="ui dividing header">Difficulty range</h5>
|
||||
<div class="field">
|
||||
<div #diffSlider class="ui labeled ticked inverted range slider" id="diffSlider"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,51 @@
|
||||
#searchMenu {
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0em;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#searchMenu>.item {
|
||||
padding: .3em .4em;
|
||||
min-height: inherit;
|
||||
}
|
||||
|
||||
#searchMenu>.item:first-child {
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
#searchIcon {
|
||||
cursor: default;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#advancedSearchForm {
|
||||
margin: 0em;
|
||||
border-width: 0px;
|
||||
overflow: hidden;
|
||||
transition: max-height 350ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
max-height: 243.913px; /* This is its preferred height. Transition needs a static target number to work. */
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
max-height: 0px !important;
|
||||
}
|
||||
|
||||
#quantityDropdownItem, #similarityDropdownItem {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
transition: visibility 0s, opacity 350ms cubic-bezier(0.45, 0, 0.55, 1);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
max-width: 0px !important;
|
||||
max-height: 0px !important;
|
||||
transition:
|
||||
opacity 350ms cubic-bezier(0.45, 0, 0.55, 1),
|
||||
visibility 0s linear 350ms,
|
||||
max-width 0s linear 350ms,
|
||||
max-height 0s linear 350ms !important;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
|
||||
|
||||
import { SearchService } from 'src/app/core/services/search.service'
|
||||
import { getDefaultSearch } from 'src/electron/shared/interfaces/search.interface'
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-bar',
|
||||
templateUrl: './search-bar.component.html',
|
||||
styleUrls: ['./search-bar.component.scss'],
|
||||
})
|
||||
export class SearchBarComponent implements AfterViewInit {
|
||||
|
||||
@ViewChild('searchIcon', { static: true }) searchIcon: ElementRef
|
||||
@ViewChild('quantityDropdown', { static: true }) quantityDropdown: ElementRef
|
||||
@ViewChild('similarityDropdown', { static: true }) similarityDropdown: ElementRef
|
||||
@ViewChild('diffSlider', { static: true }) diffSlider: ElementRef
|
||||
|
||||
isError = false
|
||||
showAdvanced = false
|
||||
searchSettings = getDefaultSearch()
|
||||
private sliderInitialized = false
|
||||
|
||||
constructor(public searchService: SearchService) { }
|
||||
|
||||
ngAfterViewInit() {
|
||||
$(this.searchIcon.nativeElement).popup({
|
||||
onShow: () => this.isError, // Only show the popup if there is an error
|
||||
})
|
||||
this.searchService.onSearchErrorStateUpdate(isError => {
|
||||
this.isError = isError
|
||||
})
|
||||
$(this.quantityDropdown.nativeElement).dropdown({
|
||||
onChange: (value: string) => {
|
||||
this.searchSettings.quantity = value as 'all' | 'any'
|
||||
},
|
||||
})
|
||||
$(this.similarityDropdown.nativeElement).dropdown({
|
||||
onChange: (value: string) => {
|
||||
this.searchSettings.similarity = value as 'similar' | 'exact'
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onSearch(query: string) {
|
||||
this.searchSettings.query = query
|
||||
this.searchSettings.limit = 50 + 1
|
||||
this.searchSettings.offset = 0
|
||||
this.searchService.newSearch(this.searchSettings)
|
||||
}
|
||||
|
||||
onAdvancedSearchClick() {
|
||||
this.showAdvanced = !this.showAdvanced
|
||||
|
||||
if (!this.sliderInitialized) {
|
||||
setTimeout(() => { // Initialization requires this element to not be collapsed
|
||||
$(this.diffSlider.nativeElement).slider({
|
||||
min: 0,
|
||||
max: 6,
|
||||
start: 0,
|
||||
end: 6,
|
||||
step: 1,
|
||||
onChange: (_length: number, min: number, max: number) => {
|
||||
this.searchSettings.minDiff = min
|
||||
this.searchSettings.maxDiff = max
|
||||
},
|
||||
})
|
||||
}, 50)
|
||||
this.sliderInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.searchService.isLoading()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<div *ngIf="downloads.length > 0" class="ui segments">
|
||||
<div *ngFor="let download of downloads; trackBy: trackByVersionID" class="ui segment" [style.background-color]="getBackgroundColor(download)">
|
||||
<h3 id="downloadTitle">
|
||||
<span style="flex-grow: 1">{{ download.title }}</span>
|
||||
<i class="inside right close icon" (click)="cancelDownload(download.versionID)"></i>
|
||||
</h3>
|
||||
|
||||
<div id="downloadText">
|
||||
<span style="flex-grow: 1">{{ download.header }}</span>
|
||||
<span *ngIf="!download.isLink" class="description">{{ download.description }}</span>
|
||||
<span *ngIf="download.isLink" class="description">
|
||||
<a (click)="openFolder(download.description)">{{ download.description }}</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="downloadProgressDiv">
|
||||
<div id="downloadProgressBar" appProgressBar [percent]="download.percent" class="ui small progress">
|
||||
<div class="bar" [style.height]="download.type === 'error' ? '-webkit-fill-available' : undefined">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button *ngIf="download.type === 'error'" class="ui right attached labeled compact icon button" (click)="retryDownload(download.versionID)">
|
||||
<i class="redo icon"></i>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,26 @@
|
||||
#downloadTitle,
|
||||
#downloadText {
|
||||
display: flex;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
#downloadText {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#downloadProgressDiv {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#downloadProgressBar {
|
||||
flex-grow: 1;
|
||||
margin: 0;
|
||||
.bar {
|
||||
transform: translateY(-50%);
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
i.close.icon, a {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core'
|
||||
|
||||
import { DownloadProgress } from '../../../../../electron/shared/interfaces/download.interface'
|
||||
import { DownloadService } from '../../../../core/services/download.service'
|
||||
import { ElectronService } from '../../../../core/services/electron.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-downloads-modal',
|
||||
templateUrl: './downloads-modal.component.html',
|
||||
styleUrls: ['./downloads-modal.component.scss'],
|
||||
})
|
||||
export class DownloadsModalComponent {
|
||||
|
||||
downloads: DownloadProgress[] = []
|
||||
|
||||
constructor(private electronService: ElectronService, private downloadService: DownloadService, ref: ChangeDetectorRef) {
|
||||
electronService.receiveIPC('queue-updated', order => {
|
||||
this.downloads.sort((a, b) => order.indexOf(a.versionID) - order.indexOf(b.versionID))
|
||||
})
|
||||
|
||||
downloadService.onDownloadUpdated(download => {
|
||||
const index = this.downloads.findIndex(thisDownload => thisDownload.versionID == download.versionID)
|
||||
if (download.type == 'cancel') {
|
||||
this.downloads = this.downloads.filter(thisDownload => thisDownload.versionID != download.versionID)
|
||||
} else if (index == -1) {
|
||||
this.downloads.push(download)
|
||||
} else {
|
||||
this.downloads[index] = download
|
||||
}
|
||||
ref.detectChanges()
|
||||
})
|
||||
}
|
||||
|
||||
trackByVersionID(_index: number, item: DownloadProgress) {
|
||||
return item.versionID
|
||||
}
|
||||
|
||||
cancelDownload(versionID: number) {
|
||||
this.downloadService.cancelDownload(versionID)
|
||||
}
|
||||
|
||||
retryDownload(versionID: number) {
|
||||
this.downloadService.retryDownload(versionID)
|
||||
}
|
||||
|
||||
getBackgroundColor(download: DownloadProgress) {
|
||||
switch (download.type) {
|
||||
case 'error': return '#a63a3a'
|
||||
default: return undefined
|
||||
}
|
||||
}
|
||||
|
||||
openFolder(filepath: string) {
|
||||
this.electronService.showFolder(filepath)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<div id="bottomMenu" class="ui bottom borderless menu">
|
||||
<div *ngIf="resultCount > 0" class="item">{{ resultCount }}{{ allResultsVisible ? '' : '+' }} Result{{ resultCount === 1 ? '' : 's' }}</div>
|
||||
<div class="item">
|
||||
<button *ngIf="selectedResults.length > 1" (click)="downloadSelected()" class="ui positive button">
|
||||
Download {{ selectedResults.length }} Results
|
||||
</button>
|
||||
</div>
|
||||
<a *ngIf="downloading" class="item right" (click)="showDownloads()">
|
||||
<div #progressBar appProgressBar [percent]="percent" class="ui progress" [style.background-color]="error ? 'indianred' : undefined">
|
||||
<div class="bar">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div id="selectedModal" class="ui modal">
|
||||
<div class="header">Some selected songs have more than one chart!</div>
|
||||
<div class="scrolling content">
|
||||
<div class="ui segments">
|
||||
<div class="ui segment" *ngFor="let chartGroup of chartGroups">
|
||||
<p *ngFor="let chart of chartGroup">
|
||||
{{ chart.chartName }} <b>[{{ chart.charters }}]</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="ui approve button" (click)="downloadAllCharts()">Download all charts for each song</div>
|
||||
<div class="ui cancel button" (click)="deselectSongsWithMultipleCharts()">Deselect these songs</div>
|
||||
<div class="ui cancel button">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="downloadsModal" class="ui modal">
|
||||
<i class="inside close icon"></i>
|
||||
<div class="header">
|
||||
<span>Downloads</span>
|
||||
<div *ngIf="multipleCompleted" class="ui positive compact button" (click)="clearCompleted()">Clear completed</div>
|
||||
</div>
|
||||
<div class="scrolling content">
|
||||
<app-downloads-modal></app-downloads-modal>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
.ui.progress {
|
||||
margin: 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
#bottomMenu {
|
||||
border-radius: 0px;
|
||||
border-width: 1px 0px 0px 0px;
|
||||
}
|
||||
|
||||
#downloadsModal {
|
||||
.header {
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.ui.positive.button {
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core'
|
||||
|
||||
import { VersionResult } from '../../../../electron/shared/interfaces/songDetails.interface'
|
||||
import { groupBy } from '../../../../electron/shared/UtilFunctions'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { ElectronService } from '../../../core/services/electron.service'
|
||||
import { SearchService } from '../../../core/services/search.service'
|
||||
import { SelectionService } from '../../../core/services/selection.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-status-bar',
|
||||
templateUrl: './status-bar.component.html',
|
||||
styleUrls: ['./status-bar.component.scss'],
|
||||
})
|
||||
export class StatusBarComponent {
|
||||
|
||||
resultCount = 0
|
||||
multipleCompleted = false
|
||||
downloading = false
|
||||
error = false
|
||||
percent = 0
|
||||
batchResults: VersionResult[]
|
||||
chartGroups: VersionResult[][]
|
||||
|
||||
constructor(
|
||||
private electronService: ElectronService,
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
ref: ChangeDetectorRef
|
||||
) {
|
||||
downloadService.onDownloadUpdated(() => {
|
||||
setTimeout(() => { // Make sure this is the last callback executed to get the accurate downloadCount
|
||||
this.downloading = downloadService.downloadCount > 0
|
||||
this.multipleCompleted = downloadService.completedCount > 1
|
||||
this.percent = downloadService.totalDownloadingPercent
|
||||
this.error = downloadService.anyErrorsExist
|
||||
ref.detectChanges()
|
||||
}, 0)
|
||||
})
|
||||
|
||||
searchService.onSearchChanged(() => {
|
||||
this.resultCount = searchService.resultCount
|
||||
})
|
||||
}
|
||||
|
||||
get allResultsVisible() {
|
||||
return this.searchService.allResultsVisible
|
||||
}
|
||||
|
||||
get selectedResults() {
|
||||
return this.selectionService.getSelectedResults()
|
||||
}
|
||||
|
||||
showDownloads() {
|
||||
$('#downloadsModal').modal('show')
|
||||
}
|
||||
|
||||
async downloadSelected() {
|
||||
this.chartGroups = []
|
||||
this.batchResults = await this.electronService.invoke('batch-song-details', this.selectedResults.map(result => result.id))
|
||||
const versionGroups = groupBy(this.batchResults, 'songID')
|
||||
for (const versionGroup of versionGroups) {
|
||||
if (versionGroup.findIndex(version => version.chartID != versionGroup[0].chartID) != -1) {
|
||||
// Must have multiple charts of this song
|
||||
this.chartGroups.push(versionGroup.filter(version => version.versionID == version.latestVersionID))
|
||||
}
|
||||
}
|
||||
|
||||
if (this.chartGroups.length == 0) {
|
||||
for (const versions of versionGroups) {
|
||||
this.searchService.sortChart(versions)
|
||||
const downloadVersion = versions[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID)
|
||||
this.downloadService.addDownload(
|
||||
downloadVersion.versionID, {
|
||||
chartName: downloadVersion.chartName,
|
||||
artist: downloadSong.artist,
|
||||
charter: downloadVersion.charters,
|
||||
driveData: downloadVersion.driveData,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
$('#selectedModal').modal('show')
|
||||
// [download all charts for each song] [deselect these songs] [X]
|
||||
}
|
||||
}
|
||||
|
||||
downloadAllCharts() {
|
||||
const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID')
|
||||
for (const chart of songChartGroups) {
|
||||
this.searchService.sortChart(chart)
|
||||
const downloadVersion = chart[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id == downloadVersion.songID)
|
||||
this.downloadService.addDownload(
|
||||
downloadVersion.versionID, {
|
||||
chartName: downloadVersion.chartName,
|
||||
artist: downloadSong.artist,
|
||||
charter: downloadVersion.charters,
|
||||
driveData: downloadVersion.driveData,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deselectSongsWithMultipleCharts() {
|
||||
for (const chartGroup of this.chartGroups) {
|
||||
this.selectionService.deselectSong(chartGroup[0].songID)
|
||||
}
|
||||
}
|
||||
|
||||
clearCompleted() {
|
||||
this.downloadService.cancelCompleted()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user