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:
@@ -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
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EventEmitter, Injectable } from '@angular/core'
|
||||
|
||||
import { SongResult } from '../../../../src-shared/interfaces/search.interface'
|
||||
import { SearchResult } from '../../../../src-shared/interfaces/search.interface'
|
||||
import { SearchService } from './search.service'
|
||||
|
||||
// Note: this class prevents event cycles by only emitting events if the checkbox changes
|
||||
@@ -10,7 +10,7 @@ import { SearchService } from './search.service'
|
||||
})
|
||||
export class SelectionService {
|
||||
|
||||
private searchResults: SongResult[] = []
|
||||
private searchResults: Partial<SearchResult>
|
||||
|
||||
private selectAllChangedEmitter = new EventEmitter<boolean>()
|
||||
private selectionChangedCallbacks: { [songID: number]: (selection: boolean) => void } = {}
|
||||
@@ -19,14 +19,14 @@ export class SelectionService {
|
||||
private selections: { [songID: number]: boolean | undefined } = {}
|
||||
|
||||
constructor(searchService: SearchService) {
|
||||
searchService.onSearchChanged(results => {
|
||||
searchService.searchUpdated.subscribe(results => {
|
||||
this.searchResults = results
|
||||
if (this.allSelected) {
|
||||
this.selectAll() // Select newly added rows if allSelected
|
||||
}
|
||||
})
|
||||
|
||||
searchService.onNewSearch(results => {
|
||||
searchService.searchUpdated.subscribe(results => {
|
||||
this.searchResults = results
|
||||
this.selectionChangedCallbacks = {}
|
||||
this.selections = {}
|
||||
@@ -35,7 +35,9 @@ export class SelectionService {
|
||||
}
|
||||
|
||||
getSelectedResults() {
|
||||
return this.searchResults.filter(result => this.selections[result.id] === true)
|
||||
// TODO
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return [] as any[] // this.searchResults.filter(result => this.selections[result.id] === true)
|
||||
}
|
||||
|
||||
onSelectAllChanged(callback: (selected: boolean) => void) {
|
||||
@@ -57,7 +59,8 @@ export class SelectionService {
|
||||
this.selectAllChangedEmitter.emit(false)
|
||||
}
|
||||
|
||||
setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
|
||||
// TODO
|
||||
// setTimeout(() => this.searchResults.forEach(result => this.deselectSong(result.id)), 0)
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
@@ -66,7 +69,8 @@ export class SelectionService {
|
||||
this.selectAllChangedEmitter.emit(true)
|
||||
}
|
||||
|
||||
setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
|
||||
// TODO
|
||||
// setTimeout(() => this.searchResults.forEach(result => this.selectSong(result.id)), 0)
|
||||
}
|
||||
|
||||
deselectSong(songID: number) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
|
||||
import { Difficulty, Instrument } from 'scan-chart'
|
||||
|
||||
import { Settings, themes } from '../../../../src-shared/Settings'
|
||||
|
||||
@Injectable({
|
||||
@@ -31,6 +33,22 @@ export class SettingsService {
|
||||
this.document.documentElement.setAttribute('data-theme', theme)
|
||||
}
|
||||
|
||||
get instrument() {
|
||||
return this.settings.instrument
|
||||
}
|
||||
set instrument(newValue: Instrument | null) {
|
||||
this.settings.instrument = newValue
|
||||
this.saveSettings()
|
||||
}
|
||||
|
||||
get difficulty() {
|
||||
return this.settings.difficulty
|
||||
}
|
||||
set difficulty(newValue: Difficulty | null) {
|
||||
this.settings.difficulty = newValue
|
||||
this.saveSettings()
|
||||
}
|
||||
|
||||
// Individual getters/setters
|
||||
get libraryDirectory() {
|
||||
return this.settings.libraryPath
|
||||
|
||||
Reference in New Issue
Block a user