mirror of
https://github.com/Myxelium/Bridge-Multi.git
synced 2026-04-11 14:19:38 +00:00
Interface conversion, search bar layout
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<div class="bg-neutral rounded-md">
|
||||
<div class="text-center text-neutral-content">
|
||||
{{ getEMHXString() }}
|
||||
</div>
|
||||
<div class="indicator">
|
||||
<img class="w-12 m-2 mt-0" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
<span class="indicator-item indicator-bottom indicator-start badge badge-error badge-md ml-4 mb-4 font-bold">{{ getDiff() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
|
||||
import { capitalize } from 'lodash'
|
||||
import { Instrument } from 'scan-chart'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { instrumentToDiff } from 'src-shared/UtilFunctions'
|
||||
|
||||
@Component({
|
||||
selector: 'app-chart-sidebar-instrument',
|
||||
templateUrl: './chart-sidebar-instrument.component.html',
|
||||
})
|
||||
export class ChartSidebarInstrumentComponent {
|
||||
|
||||
@Input() chart: ChartData
|
||||
@Input() instrument: Instrument | 'vocals'
|
||||
|
||||
getDiff() {
|
||||
const diff = this.chart[instrumentToDiff(this.instrument)]
|
||||
return diff === null || diff < 0 ? '?' : diff
|
||||
}
|
||||
|
||||
getEMHXString() {
|
||||
if (this.instrument === 'vocals') { return 'Vocals' }
|
||||
|
||||
const difficulties = this.chart.notesData.noteCounts
|
||||
.filter(nc => nc.instrument === this.instrument && nc.count > 0)
|
||||
.map(nc => nc.difficulty)
|
||||
|
||||
if (difficulties.length === 1) {
|
||||
return capitalize(difficulties[0])
|
||||
}
|
||||
|
||||
let str = ''
|
||||
if (difficulties.includes('easy')) { str += 'E' }
|
||||
if (difficulties.includes('medium')) { str += 'M' }
|
||||
if (difficulties.includes('hard')) { str += 'H' }
|
||||
if (difficulties.includes('expert')) { str += 'X' }
|
||||
|
||||
return str
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,34 @@
|
||||
<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 id="sidebarCard" *ngIf="selectedChart" class="ui fluid card">
|
||||
<div class="ui placeholder">
|
||||
@if (albumArtMd5) {
|
||||
<img src="https://files.enchor.us/{{ albumArtMd5 }}.jpg" alt="Album art" loading="lazy" class="object-cover w-40" />
|
||||
}
|
||||
</div>
|
||||
<div *ngIf="charts.length > 1" id="chartDropdown" class="ui fluid right labeled scrolling icon dropdown button">
|
||||
<div *ngIf="charts && 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>
|
||||
<span class="header">{{ selectedChart.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 *ngIf="selectedChart.chartAlbum"><b>Album:</b> {{ selectedChart.chartAlbum }}</div>
|
||||
<div *ngIf="selectedChart.chartGenre"><b>Genre:</b> {{ selectedChart.chartGenre }}</div>
|
||||
<div *ngIf="selectedChart.chartYear"><b>Year:</b> {{ selectedChart.chartYear }}</div>
|
||||
<div><b>Charter:</b> {{ selectedChart.charter }}</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>
|
||||
@if (selectedChart.notesData.hasVocals) {
|
||||
<app-chart-sidebar-instrument [chart]="selectedChart" instrument="vocals" />
|
||||
}
|
||||
@for (instrument of instruments; track $index) {
|
||||
<app-chart-sidebar-instrument [chart]="selectedChart" [instrument]="instrument" />
|
||||
}
|
||||
</div>
|
||||
<div id="sourceLinks">
|
||||
<a id="sourceLink" (click)="onSourceLinkClicked()">{{ selectedVersion.driveData.source.sourceName }}</a>
|
||||
<a id="sourceLink" (click)="onSourceLinkClicked()">{{ selectedChart.packName ?? selectedChart.applicationUsername + "'s Chart" }}</a>
|
||||
<button *ngIf="shownFolderButton()" id="folderButton" class="mini ui icon button" (click)="onFolderButtonClicked()">
|
||||
<i class="folder open outline icon"></i>
|
||||
</button>
|
||||
@@ -38,8 +36,8 @@
|
||||
</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">
|
||||
<div id="downloadButton" class="ui button" (click)="onDownloadClicked()">Download</div>
|
||||
<div id="versionDropdown" class="ui floating dropdown icon button">
|
||||
<i class="dropdown icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { SafeUrl } from '@angular/platform-browser'
|
||||
|
||||
import { chain, flatMap, sortBy } from 'lodash'
|
||||
import { Instrument } from 'scan-chart'
|
||||
import { SearchService } from 'src-angular/app/core/services/search.service'
|
||||
import { SettingsService } from 'src-angular/app/core/services/settings.service'
|
||||
|
||||
import { SongResult } from '../../../../../src-shared/interfaces/search.interface'
|
||||
import { ChartedDifficulty, getInstrumentIcon, Instrument, VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
|
||||
import { groupBy } from '../../../../../src-shared/UtilFunctions'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
import { driveLink, instruments } from 'src-shared/UtilFunctions'
|
||||
|
||||
interface Difficulty {
|
||||
instrument: string
|
||||
@@ -22,252 +20,81 @@ interface Difficulty {
|
||||
})
|
||||
export class ChartSidebarComponent implements OnInit {
|
||||
|
||||
songResult: SongResult | undefined
|
||||
selectedVersion: VersionResult
|
||||
charts: VersionResult[][]
|
||||
selectedChart: ChartData | null = null
|
||||
charts: ChartData[][] | null = null
|
||||
|
||||
albumArtSrc: SafeUrl = ''
|
||||
charterPlural: string
|
||||
songLength: string
|
||||
difficultiesList: Difficulty[]
|
||||
downloadButtonText: string
|
||||
|
||||
constructor(
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.selectVersion(undefined)
|
||||
this.songResult = undefined
|
||||
this.searchService.searchUpdated.subscribe(() => {
|
||||
this.charts = null
|
||||
this.selectedChart = null
|
||||
})
|
||||
}
|
||||
|
||||
public get albumArtMd5() {
|
||||
return flatMap(this.charts ?? []).find(c => !!c.albumArtMd5)?.albumArtMd5 || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 results = await window.electron.invoke.getSongDetails(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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the chart dropdown from `this.charts` (or removes it if there's only one chart).
|
||||
*/
|
||||
private initChartDropdown() {
|
||||
// TODO
|
||||
// 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 | undefined) {
|
||||
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) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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() {
|
||||
// TODO
|
||||
// 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}`
|
||||
async onRowClicked(song: ChartData[]) {
|
||||
this.charts = chain(song)
|
||||
.groupBy(c => c.versionGroupId)
|
||||
.values()
|
||||
.map(versionGroup => sortBy(versionGroup, vg => vg.modifiedTime).reverse())
|
||||
.value()
|
||||
this.selectedChart = this.charts[0][0]
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the proxy link or source folder in the default browser.
|
||||
*/
|
||||
onSourceLinkClicked() {
|
||||
const source = this.selectedVersion.driveData.source
|
||||
window.electron.emit.openUrl(source.proxyLink ?? `https://drive.google.com/drive/folders/${source.sourceDriveID}`)
|
||||
window.electron.emit.openUrl(driveLink(this.selectedChart!.applicationDriveId))
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
return this.selectedChart!.applicationDriveId !== this.selectedChart!.parentFolderId
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the chart folder in the default browser.
|
||||
*/
|
||||
onFolderButtonClicked() {
|
||||
window.electron.emit.openUrl(`https://drive.google.com/drive/folders/${this.selectedVersion.driveData.folderID}`)
|
||||
window.electron.emit.openUrl(driveLink(this.selectedChart!.parentFolderId))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
})
|
||||
// TODO
|
||||
// this.downloadService.addDownload(
|
||||
// this.selectedChart.versionID, {
|
||||
// chartName: this.selectedChart.chartName,
|
||||
// artist: this.songResult!.artist,
|
||||
// charter: this.selectedChart.charters,
|
||||
// driveData: this.selectedChart.driveData,
|
||||
// })
|
||||
}
|
||||
|
||||
public get instruments(): Instrument[] {
|
||||
if (!this.selectedChart) { return [] }
|
||||
return chain(this.selectedChart.notesData.noteCounts)
|
||||
.map(nc => nc.instrument)
|
||||
.uniq()
|
||||
.sortBy(i => instruments.indexOf(i))
|
||||
.value()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span id="chartCount" *ngIf="result.chartCount > 1">{{ result.chartCount }}</span
|
||||
>{{ result.name }}
|
||||
<span id="chartCount" *ngIf="song.length > 1">{{ song.length }}</span> {{ song[0].name }}
|
||||
</td>
|
||||
<td>{{ result.artist }}</td>
|
||||
<td>{{ result.album || 'Various' }}</td>
|
||||
<td>{{ result.genre || 'Various' }}</td>
|
||||
<td>{{ song[0].artist }}</td>
|
||||
<td>{{ song[0].album || 'Various' }}</td>
|
||||
<td>{{ song[0].genre || 'Various' }}</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core'
|
||||
|
||||
import { SongResult } from '../../../../../../src-shared/interfaces/search.interface'
|
||||
import { ChartData } from '../../../../../../src-shared/interfaces/search.interface'
|
||||
import { SelectionService } from '../../../../core/services/selection.service'
|
||||
|
||||
@Component({
|
||||
@@ -9,14 +9,14 @@ import { SelectionService } from '../../../../core/services/selection.service'
|
||||
styleUrls: ['./result-table-row.component.scss'],
|
||||
})
|
||||
export class ResultTableRowComponent implements AfterViewInit {
|
||||
@Input() result: SongResult
|
||||
@Input() song: ChartData[]
|
||||
|
||||
@ViewChild('checkbox', { static: true }) checkbox: ElementRef
|
||||
|
||||
constructor(private selectionService: SelectionService) { }
|
||||
|
||||
get songID() {
|
||||
return this.result.id
|
||||
return this.song[0].songId ?? this.song[0].chartId
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
|
||||
@@ -19,12 +19,8 @@
|
||||
</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>
|
||||
@for (song of songs; track song) {
|
||||
<tr app-result-table-row (click)="onRowClicked(song)" [class.active]="activeSong === song" [song]="song"></tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Component, EventEmitter, OnInit, Output, QueryList, ViewChild, ViewChil
|
||||
|
||||
import Comparators from 'comparators'
|
||||
import { SettingsService } from 'src-angular/app/core/services/settings.service'
|
||||
import { ChartData } from 'src-shared/interfaces/search.interface'
|
||||
|
||||
import { SongResult } from '../../../../../src-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'
|
||||
@@ -16,18 +16,17 @@ import { ResultTableRowComponent } from './result-table-row/result-table-row.com
|
||||
})
|
||||
export class ResultTableComponent implements OnInit {
|
||||
|
||||
@Output() rowClicked = new EventEmitter<SongResult>()
|
||||
@Output() rowClicked = new EventEmitter<ChartData[]>()
|
||||
|
||||
@ViewChild(CheckboxDirective, { static: true }) checkboxColumn: CheckboxDirective
|
||||
@ViewChildren('tableRow') tableRows: QueryList<ResultTableRowComponent>
|
||||
|
||||
results: SongResult[] = []
|
||||
activeRowID: number | null = null
|
||||
activeSong: ChartData[] | null = null
|
||||
sortDirection: 'ascending' | 'descending' = 'descending'
|
||||
sortColumn: 'name' | 'artist' | 'album' | 'genre' | null = null
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
public searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
public settingsService: SettingsService
|
||||
) { }
|
||||
@@ -37,24 +36,25 @@ export class ResultTableComponent implements OnInit {
|
||||
this.checkboxColumn.check(selected)
|
||||
})
|
||||
|
||||
this.searchService.onSearchChanged(results => {
|
||||
this.activeRowID = null
|
||||
this.results = results
|
||||
this.searchService.searchUpdated.subscribe(() => {
|
||||
this.activeSong = null
|
||||
this.updateSort()
|
||||
})
|
||||
|
||||
this.searchService.onNewSearch(() => {
|
||||
this.sortColumn = null
|
||||
})
|
||||
}
|
||||
|
||||
onRowClicked(result: SongResult) {
|
||||
this.activeRowID = result.id
|
||||
this.rowClicked.emit(result)
|
||||
get songs() {
|
||||
return this.searchService.groupedSongs
|
||||
}
|
||||
|
||||
onRowClicked(song: ChartData[]) {
|
||||
if (this.activeSong !== song) {
|
||||
this.activeSong = song
|
||||
this.rowClicked.emit(song)
|
||||
}
|
||||
}
|
||||
|
||||
onColClicked(column: 'name' | 'artist' | 'album' | 'genre') {
|
||||
if (this.results.length === 0) { return }
|
||||
if (this.songs.length === 0) { return }
|
||||
if (this.sortColumn !== column) {
|
||||
this.sortColumn = column
|
||||
this.sortDirection = 'descending'
|
||||
@@ -68,7 +68,7 @@ export class ResultTableComponent implements OnInit {
|
||||
|
||||
private updateSort() {
|
||||
if (this.sortColumn !== null) {
|
||||
this.results.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection === 'ascending' }))
|
||||
this.songs.sort(Comparators.comparing(this.sortColumn, { reversed: this.sortDirection === 'ascending' }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="searchMenu" class="ui bottom attached borderless menu">
|
||||
<!-- <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)" />
|
||||
@@ -237,4 +237,334 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="collapse navbar grid bg-base-100 overflow-visible rounded-none" [ngClass]="showAdvanced ? 'collapse-open' : 'collapse-close'">
|
||||
<div class="flex flex-wrap justify-end gap-1">
|
||||
<!-- Search Input -->
|
||||
<div class="flex items-center order-3 md:order-2 xl:order-2 flex-none w-full md:w-auto md:flex-1 xl:flex-grow-[6] min-w-[21rem]">
|
||||
<div class="form-control w-full">
|
||||
<input type="text" [formControl]="searchControl" placeholder="What do you feel like playing today?" class="input input-bordered pr-14" />
|
||||
</div>
|
||||
<i class="bi bi-search -ml-11"></i>
|
||||
</div>
|
||||
<div class="basis-full h-0 order-4 xl:order-7"></div>
|
||||
<div class="flex order-5 md:order-5 xl:order-3">
|
||||
<!-- Instrument Dropdown -->
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-neutral rounded-btn rounded-r-none my-1">
|
||||
@if (instrument) {
|
||||
<img class="w-8 hidden sm:block" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
}
|
||||
{{ instrumentDisplay(instrument) }}
|
||||
</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-[2] p-2 shadow bg-neutral text-neutral-content rounded-box w-64">
|
||||
<li>
|
||||
<a (click)="setInstrument(null, $event)">{{ instrumentDisplay(null) }}</a>
|
||||
</li>
|
||||
@for (instrument of instruments; track $index) {
|
||||
<li>
|
||||
<a (click)="setInstrument(instrument, $event)">
|
||||
<img class="w-8" src="assets/images/instruments/{{ instrument }}.png" />
|
||||
{{ instrumentDisplay(instrument) }}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Difficulty Dropdown -->
|
||||
<div class="dropdown">
|
||||
<label tabindex="0" class="btn btn-neutral rounded-btn rounded-l-none my-1">{{ difficultyDisplay(difficulty) }}</label>
|
||||
<ul tabindex="0" class="menu dropdown-content z-[2] p-2 shadow bg-neutral text-neutral-content rounded-box w-40">
|
||||
<li>
|
||||
<a (click)="setDifficulty(null, $event)">{{ difficultyDisplay(null) }}</a>
|
||||
</li>
|
||||
@for (difficulty of difficulties; track $index) {
|
||||
<li>
|
||||
<a (click)="setDifficulty(difficulty, $event)">{{ difficultyDisplay(difficulty) }}</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Advanced Search -->
|
||||
<div class="order-6 md:order-6 xl:order-4 xl:flex-grow-[5]">
|
||||
<button class="btn btn-ghost" (click)="setShowAdvanced(!showAdvanced)">
|
||||
Advanced Search
|
||||
<div class="cursor-pointer swap swap-rotate" [class.swap-active]="showAdvanced">
|
||||
<i class="swap-off bi bi-chevron-down"></i>
|
||||
<i class="swap-on bi bi-chevron-up"></i>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse-content justify-center">
|
||||
<form [formGroup]="advancedSearchForm">
|
||||
<div class="flex flex-wrap gap-5 justify-center">
|
||||
<div>
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div
|
||||
class="tooltip tooltip-bottom font-normal [text-wrap:balance]"
|
||||
data-tip='Search for text in these specific chart properties. Note: you can put a minus sign (-) before words to return only results without that word. (e.g. "Dragon -Dragonforce")'>
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Search by</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div
|
||||
class="tooltip tooltip-bottom font-normal [text-wrap:balance]"
|
||||
data-tip="Only include results that match perfectly. (not case sensitive)">
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Exact</span>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="tooltip tooltip-bottom font-normal [text-wrap:balance]" data-tip="Do not include results that match this.">
|
||||
<span class="font-bold underline decoration-dotted cursor-help">Exclude</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="border-b-0" formGroupName="name">
|
||||
<td><input type="text" placeholder="Name" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="artist">
|
||||
<td><input type="text" placeholder="Artist" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="album">
|
||||
<td><input type="text" placeholder="Album" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="genre">
|
||||
<td><input type="text" placeholder="Genre" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="year">
|
||||
<td><input type="text" placeholder="Year" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
<tr class="border-b-0" formGroupName="charter">
|
||||
<td><input type="text" placeholder="Charter" class="input input-bordered input-sm" formControlName="value" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exact" /></td>
|
||||
<td><input type="checkbox" class="checkbox" formControlName="exclude" /></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 justify-end">
|
||||
<table class="table table-xs">
|
||||
<tbody>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Length (minutes)</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minLength" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxLength" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="Also known as chart difficulty. Typically a number between 0 and 6.">
|
||||
Intensity
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minIntensity" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxIntensity" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Average NPS</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minAverageNPS" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxAverageNPS" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">Max NPS</td>
|
||||
<td>
|
||||
<div class="join">
|
||||
<input type="number" placeholder="Min" class="input input-bordered join-item input-sm w-16" formControlName="minMaxNPS" />
|
||||
<input type="number" placeholder="Max" class="input input-bordered join-item input-sm w-16" formControlName="maxMaxNPS" />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="The date of the last time this chart was modified in Google Drive.">
|
||||
Modified After
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="date"
|
||||
min="2012-01-01"
|
||||
[max]="todayDate"
|
||||
placeholder="YYYY/MM/DD"
|
||||
class="input input-bordered join-item input-sm w-32"
|
||||
formControlName="modifiedAfter"
|
||||
(blur)="startValidation = true"
|
||||
[class.input-error]="advancedSearchForm.invalid && startValidation" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b-0">
|
||||
<td class="text-sm">
|
||||
<span
|
||||
class="label-text underline decoration-dotted cursor-help tooltip [text-wrap:balance]"
|
||||
data-tip="The MD5 hash of the chart folder or .sng file. You can enter multiple values if they are separated by commas.">
|
||||
Hash
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" class="input input-bordered join-item input-sm w-32" formControlName="hash" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="flex flex-col justify-between">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasSoloSections
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasSoloSections', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasSoloSections') === null">
|
||||
{{ formValue('hasSoloSections') === false ? 'No ' : '' }}Solo Sections
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasForcedNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasForcedNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasForcedNotes') === null">
|
||||
{{ formValue('hasForcedNotes') === false ? 'No ' : '' }}Forced Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasOpenNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasOpenNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasOpenNotes') === null">
|
||||
{{ formValue('hasOpenNotes') === false ? 'No ' : '' }}Open Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasTapNotes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasTapNotes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasTapNotes') === null">
|
||||
{{ formValue('hasTapNotes') === false ? 'No ' : '' }}Tap Notes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasLyrics', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasLyrics') === null">
|
||||
{{ formValue('hasLyrics') === false ? 'No ' : '' }}Lyrics
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasVocals', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasVocals') === null">
|
||||
{{ formValue('hasVocals') === false ? 'No ' : '' }}Vocals
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input
|
||||
#hasRollLanes
|
||||
type="checkbox"
|
||||
class="toggle toggle-sm"
|
||||
[indeterminate]="true"
|
||||
(click)="clickCheckbox('hasRollLanes', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasRollLanes') === null">
|
||||
{{ formValue('hasRollLanes') === false ? 'No ' : '' }}Roll Lanes
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input #has2xKick type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('has2xKick', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('has2xKick') === null">
|
||||
{{ formValue('has2xKick') === false ? 'No ' : '' }}2x Kick
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasIssues', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasIssues') === null">
|
||||
{{ formValue('hasIssues') === false ? 'No ' : '' }}Chart Issues
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('hasVideoBackground', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('hasVideoBackground') === null">
|
||||
{{ formValue('hasVideoBackground') === false ? 'No ' : '' }}Video Background
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-normal gap-2">
|
||||
<input type="checkbox" class="toggle toggle-sm" [indeterminate]="true" (click)="clickCheckbox('modchart', $event)" />
|
||||
<span class="label-text" [class.text-opacity-70]="formValue('modchart') === null">
|
||||
{{ formValue('modchart') === false ? 'Not a ' : '' }}Modchart
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" [class.btn-disabled]="advancedSearchForm.invalid && startValidation" (click)="searchAdvanced()">
|
||||
Search{{ advancedSearchForm.invalid && startValidation ? ' ("Modified After" is invalid)' : '' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#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;
|
||||
}
|
||||
@@ -1,78 +1,246 @@
|
||||
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
|
||||
import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'
|
||||
import { AbstractControl, FormBuilder, FormControl } from '@angular/forms'
|
||||
|
||||
import dayjs from 'dayjs'
|
||||
import { distinctUntilChanged, switchMap, throttleTime } from 'rxjs'
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
import { SearchService } from 'src-angular/app/core/services/search.service'
|
||||
|
||||
import { getDefaultSearch } from '../../../../../src-shared/interfaces/search.interface'
|
||||
import { difficulties, difficultyDisplay, instrumentDisplay, instruments } from 'src-shared/UtilFunctions'
|
||||
|
||||
@Component({
|
||||
selector: 'app-search-bar',
|
||||
templateUrl: './search-bar.component.html',
|
||||
styleUrls: ['./search-bar.component.scss'],
|
||||
})
|
||||
export class SearchBarComponent implements AfterViewInit {
|
||||
export class SearchBarComponent implements OnInit, 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
|
||||
@ViewChild('hasSoloSections') hasSoloSections: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasForcedNotes') hasForcedNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasOpenNotes') hasOpenNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasTapNotes') hasTapNotes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('hasRollLanes') hasRollLanes: ElementRef<HTMLInputElement>
|
||||
@ViewChild('has2xKick') has2xKick: ElementRef<HTMLInputElement>
|
||||
|
||||
isError = false
|
||||
showAdvanced = false
|
||||
searchSettings = getDefaultSearch()
|
||||
private sliderInitialized = false
|
||||
public showAdvanced = false
|
||||
public instruments = instruments
|
||||
public difficulties = difficulties
|
||||
public instrumentDisplay = instrumentDisplay
|
||||
public difficultyDisplay = difficultyDisplay
|
||||
|
||||
constructor(public searchService: SearchService) { }
|
||||
public advancedSearchForm: ReturnType<this['getAdvancedSearchForm']>
|
||||
public startValidation = false
|
||||
|
||||
constructor(
|
||||
private searchService: SearchService,
|
||||
private fb: FormBuilder,
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.searchControl.valueChanges.pipe(
|
||||
throttleTime(400, undefined, { leading: true, trailing: true }),
|
||||
distinctUntilChanged(),
|
||||
switchMap(search => this.searchService.search(search || '*'))
|
||||
).subscribe()
|
||||
|
||||
this.initializeAdvancedSearchForm()
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// TODO
|
||||
// $(this.searchIcon.nativeElement).popup({
|
||||
// onShow: () => this.isError, // Only show the popup if there is an error
|
||||
// })
|
||||
this.searchService.onSearchErrorStateUpdate(isError => {
|
||||
this.isError = isError
|
||||
this.updateDisabledControls()
|
||||
this.searchService.instrument.valueChanges.subscribe(() => {
|
||||
this.updateDisabledControls()
|
||||
})
|
||||
// $(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
|
||||
// TODO
|
||||
// $(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
|
||||
setShowAdvanced(showAdvanced: boolean) {
|
||||
this.showAdvanced = showAdvanced
|
||||
if (showAdvanced) {
|
||||
this.startValidation = false
|
||||
this.searchControl.disable()
|
||||
} else {
|
||||
this.searchControl.enable()
|
||||
}
|
||||
}
|
||||
|
||||
isLoading() {
|
||||
return this.searchService.isLoading()
|
||||
get searchControl() {
|
||||
return this.searchService.searchControl
|
||||
}
|
||||
get instrument() {
|
||||
return this.searchService.instrument.value
|
||||
}
|
||||
setInstrument(instrument: Instrument | null, event: MouseEvent) {
|
||||
this.searchService.instrument.setValue(instrument)
|
||||
if (event.target instanceof HTMLElement) {
|
||||
event.target.parentElement?.parentElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
get difficulty() {
|
||||
return this.searchService.difficulty.value
|
||||
}
|
||||
setDifficulty(difficulty: Difficulty | null, event: MouseEvent) {
|
||||
this.searchService.difficulty.setValue(difficulty)
|
||||
if (event.target instanceof HTMLElement) {
|
||||
event.target.parentElement?.parentElement?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
get logoType() {
|
||||
switch (localStorage.getItem('theme')) {
|
||||
case 'emerald': return 'emerald'
|
||||
case 'halloween': return 'halloween'
|
||||
case 'lemonade': return 'lemonade'
|
||||
case 'night': return 'night'
|
||||
case 'synthwave': return 'synthwave'
|
||||
case 'aqua': return 'orange'
|
||||
case 'valentine': return 'valentine'
|
||||
case 'winter': return 'winter'
|
||||
case 'aren': return 'aren'
|
||||
case 'froogs': return 'froogs'
|
||||
default: return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
get todayDate() {
|
||||
return dayjs().format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
// TODO: run this when infinite scroll should happen
|
||||
// @HostListener("window:scroll", [])
|
||||
// onScroll(): void {
|
||||
// if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) {
|
||||
// if (this.searchService.areMorePages && !this.searchService.searchLoading) {
|
||||
// this.searchService.search(this.searchControl.value || '*', true).subscribe()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
initializeAdvancedSearchForm() {
|
||||
this.advancedSearchForm = this.getAdvancedSearchForm() as ReturnType<this['getAdvancedSearchForm']>
|
||||
|
||||
for (const key of ['name', 'artist', 'album', 'genre', 'year', 'charter'] as const) {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('value')?.valueChanges.subscribe(value => {
|
||||
if (value) {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.enable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.enable()
|
||||
} else {
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exact')?.setValue(false)
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.disable()
|
||||
this.advancedSearchForm.get(key)?.get('exclude')?.setValue(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
updateDisabledControls() {
|
||||
const isDrums = this.searchService.instrument.value === 'drums'
|
||||
const isAny = this.searchService.instrument.value === null
|
||||
const explanation = 'Not available for the current instrument.'
|
||||
|
||||
this.hasSoloSections.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasForcedNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasOpenNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasTapNotes.nativeElement.disabled = isDrums && !isAny
|
||||
this.hasRollLanes.nativeElement.disabled = !isDrums && !isAny
|
||||
this.has2xKick.nativeElement.disabled = !isDrums && !isAny
|
||||
|
||||
this.hasSoloSections.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasForcedNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasOpenNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasTapNotes.nativeElement.title = isDrums && !isAny ? explanation : ''
|
||||
this.hasRollLanes.nativeElement.title = !isDrums && !isAny ? explanation : ''
|
||||
this.has2xKick.nativeElement.title = !isDrums && !isAny ? explanation : ''
|
||||
|
||||
if (!isAny) {
|
||||
if (isDrums) {
|
||||
this.advancedSearchForm.get('hasSoloSections')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasForcedNotes')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasOpenNotes')?.setValue(null)
|
||||
this.advancedSearchForm.get('hasTapNotes')?.setValue(null)
|
||||
this.hasSoloSections.nativeElement.indeterminate = true
|
||||
this.hasForcedNotes.nativeElement.indeterminate = true
|
||||
this.hasOpenNotes.nativeElement.indeterminate = true
|
||||
this.hasTapNotes.nativeElement.indeterminate = true
|
||||
} else {
|
||||
this.advancedSearchForm.get('hasRollLanes')?.setValue(null)
|
||||
this.advancedSearchForm.get('has2xKick')?.setValue(null)
|
||||
this.hasRollLanes.nativeElement.indeterminate = true
|
||||
this.has2xKick.nativeElement.indeterminate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAdvancedSearchForm() {
|
||||
return this.fb.group({
|
||||
name: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
artist: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
album: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
genre: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
year: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
charter: this.fb.nonNullable.group({ value: '', exact: false, exclude: false }),
|
||||
minLength: null as number | null,
|
||||
maxLength: null as number | null,
|
||||
minIntensity: null as number | null,
|
||||
maxIntensity: null as number | null,
|
||||
minAverageNPS: null as number | null,
|
||||
maxAverageNPS: null as number | null,
|
||||
minMaxNPS: null as number | null,
|
||||
maxMaxNPS: null as number | null,
|
||||
modifiedAfter: this.fb.nonNullable.control('', { validators: dateVaidator }),
|
||||
hash: this.fb.nonNullable.control(''),
|
||||
hasSoloSections: null as boolean | null,
|
||||
hasForcedNotes: null as boolean | null,
|
||||
hasOpenNotes: null as boolean | null,
|
||||
hasTapNotes: null as boolean | null,
|
||||
hasLyrics: null as boolean | null,
|
||||
hasVocals: null as boolean | null,
|
||||
hasRollLanes: null as boolean | null,
|
||||
has2xKick: null as boolean | null,
|
||||
hasIssues: null as boolean | null,
|
||||
hasVideoBackground: null as boolean | null,
|
||||
modchart: null as boolean | null,
|
||||
})
|
||||
}
|
||||
|
||||
clickCheckbox(key: string, event: MouseEvent) {
|
||||
if (event.target instanceof HTMLInputElement) {
|
||||
const control = this.advancedSearchForm.get(key) as FormControl<boolean | null>
|
||||
if (control.value === true) {
|
||||
control.setValue(false)
|
||||
event.target.checked = false
|
||||
} else if (control.value === false) {
|
||||
control.setValue(null)
|
||||
event.target.checked = false
|
||||
event.target.indeterminate = true
|
||||
} else if (control.value === null) {
|
||||
control.setValue(true)
|
||||
event.target.checked = true
|
||||
event.target.indeterminate = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formValue(key: string) {
|
||||
return this.advancedSearchForm.get(key)?.value
|
||||
}
|
||||
|
||||
searchAdvanced() {
|
||||
this.startValidation = true
|
||||
if (this.advancedSearchForm.valid && !this.searchService.searchLoading) {
|
||||
this.searchService.advancedSearch({
|
||||
instrument: this.instrument,
|
||||
difficulty: this.difficulty,
|
||||
...this.advancedSearchForm.getRawValue(),
|
||||
}).subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function dateVaidator(control: AbstractControl) {
|
||||
if (control.value && isNaN(Date.parse(control.value))) {
|
||||
return { 'dateVaidator': true }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<div id="bottomMenu" class="ui bottom borderless menu">
|
||||
<div *ngIf="resultCount > 0" class="item">{{ resultCount }}{{ allResultsVisible ? '' : '+' }} Result{{ resultCount === 1 ? '' : 's' }}</div>
|
||||
<div *ngIf="(searchService.groupedSongs?.length ?? 0) > 0" class="item">
|
||||
{{ searchService.groupedSongs.length }}{{ allResultsVisible ? '' : '+' }} Result{{ searchService.groupedSongs.length === 1 ? '' : 's' }}
|
||||
</div>
|
||||
<div class="item">
|
||||
<button *ngIf="selectedResults.length > 1" (click)="downloadSelected()" class="ui positive button">
|
||||
Download {{ selectedResults.length }} Results
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ChangeDetectorRef, Component } from '@angular/core'
|
||||
|
||||
import { VersionResult } from '../../../../../src-shared/interfaces/songDetails.interface'
|
||||
import { groupBy } from '../../../../../src-shared/UtilFunctions'
|
||||
import { DownloadService } from '../../../core/services/download.service'
|
||||
import { SearchService } from '../../../core/services/search.service'
|
||||
@@ -13,17 +12,19 @@ import { SelectionService } from '../../../core/services/selection.service'
|
||||
})
|
||||
export class StatusBarComponent {
|
||||
|
||||
resultCount = 0
|
||||
multipleCompleted = false
|
||||
downloading = false
|
||||
error = false
|
||||
percent = 0
|
||||
batchResults: VersionResult[]
|
||||
chartGroups: VersionResult[][]
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
batchResults: any[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
chartGroups: any[][]
|
||||
|
||||
constructor(
|
||||
private downloadService: DownloadService,
|
||||
private searchService: SearchService,
|
||||
public searchService: SearchService,
|
||||
private selectionService: SelectionService,
|
||||
ref: ChangeDetectorRef
|
||||
) {
|
||||
@@ -36,14 +37,10 @@ export class StatusBarComponent {
|
||||
ref.detectChanges()
|
||||
}, 0)
|
||||
})
|
||||
|
||||
searchService.onSearchChanged(() => {
|
||||
this.resultCount = searchService.resultCount
|
||||
})
|
||||
}
|
||||
|
||||
get allResultsVisible() {
|
||||
return this.searchService.allResultsVisible
|
||||
return false // this.searchService.allResultsVisible
|
||||
}
|
||||
|
||||
get selectedResults() {
|
||||
@@ -57,7 +54,8 @@ export class StatusBarComponent {
|
||||
|
||||
async downloadSelected() {
|
||||
this.chartGroups = []
|
||||
this.batchResults = await window.electron.invoke.getBatchSongDetails(this.selectedResults.map(result => result.id))
|
||||
// TODO
|
||||
// this.batchResults = await window.electron.invoke.getBatchSongDetails(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) {
|
||||
@@ -68,7 +66,7 @@ export class StatusBarComponent {
|
||||
|
||||
if (this.chartGroups.length === 0) {
|
||||
for (const versions of versionGroups) {
|
||||
this.searchService.sortChart(versions)
|
||||
// this.searchService.sortChart(versions)
|
||||
const downloadVersion = versions[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
|
||||
this.downloadService.addDownload(
|
||||
@@ -89,7 +87,7 @@ export class StatusBarComponent {
|
||||
downloadAllCharts() {
|
||||
const songChartGroups = groupBy(this.batchResults, 'songID', 'chartID')
|
||||
for (const chart of songChartGroups) {
|
||||
this.searchService.sortChart(chart)
|
||||
// this.searchService.sortChart(chart)
|
||||
const downloadVersion = chart[0]
|
||||
const downloadSong = this.selectedResults.find(song => song.id === downloadVersion.songID)!
|
||||
this.downloadService.addDownload(
|
||||
|
||||
Reference in New Issue
Block a user