Interface conversion, search bar layout

This commit is contained in:
Geomitron
2023-12-09 18:21:01 -06:00
parent d689843f27
commit ece0f75b99
37 changed files with 1531 additions and 760 deletions

View File

@@ -1,116 +1,183 @@
import { HttpClient } from '@angular/common/http'
import { EventEmitter, Injectable } from '@angular/core'
import { FormControl } from '@angular/forms'
import { SongResult, SongSearch } from '../../../../src-shared/interfaces/search.interface'
import { VersionResult } from '../../../../src-shared/interfaces/songDetails.interface'
import { chain, xorBy } from 'lodash'
import { catchError, mergeMap, tap, throwError, timer } from 'rxjs'
import { Difficulty, Instrument } from 'scan-chart'
import { AdvancedSearch, ChartData, SearchResult } from 'src-shared/interfaces/search.interface'
@Injectable({
providedIn: 'root',
})
export class SearchService {
private resultsChangedEmitter = new EventEmitter<SongResult[]>() // For when any results change
private newResultsEmitter = new EventEmitter<SongResult[]>() // For when a new search happens
private errorStateEmitter = new EventEmitter<boolean>() // To indicate the search's error state
private results: SongResult[] = []
private awaitingResults = false
private currentQuery: SongSearch
private _allResultsVisible = true
public searchLoading = false
public songsResponse: Partial<SearchResult>
public currentPage = 1
public searchUpdated = new EventEmitter<Partial<SearchResult>>()
public isDefaultSearch = true
async newSearch(query: SongSearch) {
if (this.awaitingResults) { return }
this.awaitingResults = true
this.currentQuery = query
try {
this.results = this.trimLastChart(await window.electron.invoke.songSearch(this.currentQuery))
this.errorStateEmitter.emit(false)
} catch (err) {
this.results = []
console.log(err.message)
this.errorStateEmitter.emit(true)
}
this.awaitingResults = false
public groupedSongs: ChartData[][]
this.newResultsEmitter.emit(this.results)
this.resultsChangedEmitter.emit(this.results)
}
public availableIcons: string[]
isLoading() {
return this.awaitingResults
}
public searchControl = new FormControl('', { nonNullable: true })
public isSng: FormControl<boolean>
public instrument: FormControl<Instrument | null>
public difficulty: FormControl<Difficulty | null>
/**
* Event emitted when new search results are returned
* or when more results are added to an existing search.
* (emitted after `onNewSearch`)
*/
onSearchChanged(callback: (results: SongResult[]) => void) {
this.resultsChangedEmitter.subscribe(callback)
}
constructor(
private http: HttpClient,
) {
this.isSng = new FormControl<boolean>((localStorage.getItem('isSng') ?? 'true') === 'true', { nonNullable: true })
this.isSng.valueChanges.subscribe(isSng => localStorage.setItem('isSng', `${isSng}`))
/**
* Event emitted when a new search query is typed in.
* (emitted before `onSearchChanged`)
*/
onNewSearch(callback: (results: SongResult[]) => void) {
this.newResultsEmitter.subscribe(callback)
}
/**
* Event emitted when the error state of the search changes.
* (emitted before `onSearchChanged`)
*/
onSearchErrorStateUpdate(callback: (isError: boolean) => void) {
this.errorStateEmitter.subscribe(callback)
}
get resultCount() {
return this.results.length
}
async updateScroll() {
if (!this.awaitingResults && !this._allResultsVisible) {
this.awaitingResults = true
this.currentQuery.offset += 50
this.results.push(...this.trimLastChart(await window.electron.invoke.songSearch(this.currentQuery)))
this.awaitingResults = false
this.resultsChangedEmitter.emit(this.results)
}
}
trimLastChart(results: SongResult[]) {
if (results.length > 50) {
results.splice(50, 1)
this._allResultsVisible = false
} else {
this._allResultsVisible = true
}
return results
}
get allResultsVisible() {
return this._allResultsVisible
}
/**
* Orders `versionResults` by lastModified date, but prefer the
* non-pack version if it's only a few days older.
*/
sortChart(versionResults: VersionResult[]) {
const dates: { [versionID: number]: number } = {}
versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime())
versionResults.sort((v1, v2) => {
const diff = dates[v2.versionID] - dates[v1.versionID]
if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack !== v2.driveData.inChartPack) {
if (v1.driveData.inChartPack) {
return 1 // prioritize v2
} else {
return -1 // prioritize v1
}
} else {
return diff
this.instrument = new FormControl<Instrument>(
(localStorage.getItem('instrument') === 'null' ? null : localStorage.getItem('instrument')) as Instrument
)
this.instrument.valueChanges.subscribe(instrument => {
localStorage.setItem('instrument', `${instrument}`)
if (this.songsResponse.page) {
this.search(this.searchControl.value || '*').subscribe()
}
})
this.difficulty = new FormControl<Difficulty>(
(localStorage.getItem('difficulty') === 'null' ? null : localStorage.getItem('difficulty')) as Difficulty
)
this.difficulty.valueChanges.subscribe(difficulty => {
localStorage.setItem('difficulty', `${difficulty}`)
if (this.songsResponse.page) {
this.search(this.searchControl.value || '*').subscribe()
}
})
this.http.get<{ "name": string; "sha1": string }[]>('https://clonehero.gitlab.io/sources/icons.json').subscribe(result => {
this.availableIcons = result.map(r => r.name)
})
this.search().subscribe()
}
get areMorePages() { return this.songsResponse.page && this.groupedSongs.length === this.songsResponse.page * 20 }
/**
* General search, uses the `/search?q=` endpoint.
*
* If fetching the next page, set `nextPage=true` to incremement the page count in the search.
*
* Leave the search term blank to fetch the songs with charts most recently added.
*/
public search(search = '*', nextPage = false) {
this.searchLoading = true
this.isDefaultSearch = search === '*'
if (nextPage) {
this.currentPage++
} else {
this.currentPage = 1
}
let retries = 10
return this.http.post<SearchResult>(`/api/search`, {
search,
page: this.currentPage,
instrument: this.instrument.value,
difficulty: this.difficulty.value,
}).pipe(
catchError((err, caught) => {
if (err.status === 400 || retries-- <= 0) {
this.searchLoading = false
console.log(err)
return throwError(() => err)
} else {
return timer(2000).pipe(mergeMap(() => caught))
}
}),
tap(response => {
this.searchLoading = false
if (!nextPage) {
// Don't reload results if they are the same
if (this.groupedSongs && xorBy(this.songsResponse.data, response.data, r => r.chartId).length === 0) {
return
} else {
this.groupedSongs = []
}
}
this.songsResponse = response
this.groupedSongs.push(
...chain(response.data)
.groupBy(c => c.songId ?? -1 * c.chartId)
.values()
.value()
)
this.searchUpdated.emit(response)
})
)
}
public advancedSearch(search: AdvancedSearch) {
this.searchLoading = true
this.isDefaultSearch = false
let retries = 10
return this.http.post<{ data: SearchResult['data'] }>(`/api/search/advanced`, search).pipe(
catchError((err, caught) => {
if (err.status === 400 || retries-- <= 0) {
this.searchLoading = false
console.log(err)
return throwError(() => err)
} else {
return timer(2000).pipe(mergeMap(() => caught))
}
}),
tap(response => {
this.searchLoading = false
// Don't reload results if they are the same
if (this.groupedSongs && xorBy(this.songsResponse.data, response.data, r => r.chartId).length === 0) {
return
} else {
this.groupedSongs = []
}
this.songsResponse = response
this.groupedSongs.push(
...chain(response.data)
.groupBy(c => c.songId ?? -1 * c.chartId)
.values()
.value()
)
this.searchUpdated.emit(response)
})
)
}
}
// TODO: maybe use this, or delete it
/**
* Orders `versionResults` by lastModified date, but prefer the
* non-pack version if it's only a few days older.
*/
// sortChart(versionResults: VersionResult[]) {
// const dates: { [versionID: number]: number } = {}
// versionResults.forEach(version => dates[version.versionID] = new Date(version.lastModified).getTime())
// versionResults.sort((v1, v2) => {
// const diff = dates[v2.versionID] - dates[v1.versionID]
// if (Math.abs(diff) < 6.048e+8 && v1.driveData.inChartPack !== v2.driveData.inChartPack) {
// if (v1.driveData.inChartPack) {
// return 1 // prioritize v2
// } else {
// return -1 // prioritize v1
// }
// } else {
// return diff
// }
// })
// }