2024-02-24 11:16:21 +00:00
import { Component , OnInit } from '@angular/core' ;
import { FormsModule , ReactiveFormsModule } from "@angular/forms" ;
import { DecimalPipe , JsonPipe , NgForOf , NgIf } from "@angular/common" ;
import { HttpClient } from "@angular/common/http" ;
import { environment } from "../../environments/environment" ;
import { countryCodeToFlag } from "../format" ;
import { OsuGradeComponent } from "../../corelib/components/osu-grade/osu-grade.component" ;
import { Field , Query , QueryBuilderComponent } from "../../corelib/components/query-builder/query-builder.component" ;
2024-02-24 13:59:17 +00:00
import { RouterLink } from "@angular/router" ;
import { CalculatePageRangePipe } from "../../corelib/calculate-page-range.pipe" ;
import { DownloadFilesService } from "../../corelib/service/download-files.service" ;
import { catchError , throwError } from "rxjs" ;
2024-02-24 11:16:21 +00:00
interface SchemaField {
name : string ;
short_name : string ;
category : 'user' | 'score' | 'beatmap' ;
type : 'number' | 'string' | 'flag' | 'grade' | 'boolean' ;
active : boolean ;
description : string ;
}
interface SearchResponse {
scores : SearchResponseEntry [ ] ;
2024-02-24 13:59:17 +00:00
pagination : SearchPagination ;
}
interface SearchPagination {
currentPage : number ;
pageSize : number ;
totalResults : number ;
totalPages : number ;
2024-02-24 11:16:21 +00:00
}
interface Sorting {
field : string ;
order : 'ASC' | 'DESC' ;
}
interface SearchResponseEntry {
// User fields
user_id? : number ;
user_username? : string ;
user_join_date? : string ;
user_country? : string ;
user_country_rank? : number ;
user_rank? : number ;
user_pp_raw? : number ;
user_accuracy? : number ;
user_playcount? : number ;
user_total_score? : number ;
user_ranked_score? : number ;
user_seconds_played? : number ;
user_count_300? : number ;
user_count_100? : number ;
user_count_50? : number ;
user_count_miss? : number ;
// Score fields
replay_id? : number ;
date? : string ;
beatmap_id? : number ;
pp? : number ;
frametime? : number ;
ur? : number ;
// Beatmap fields
beatmap_artist? : string ;
beatmap_beatmapset_id? : number ;
beatmap_creator? : string ;
beatmap_source? : string ;
beatmap_star_rating? : number ;
beatmap_title? : string ;
beatmap_version? : string ;
}
@Component ( {
selector : 'app-search' ,
standalone : true ,
imports : [
ReactiveFormsModule ,
NgForOf ,
FormsModule ,
JsonPipe ,
NgIf ,
DecimalPipe ,
OsuGradeComponent ,
2024-02-24 13:59:17 +00:00
QueryBuilderComponent ,
RouterLink ,
CalculatePageRangePipe
2024-02-24 11:16:21 +00:00
] ,
templateUrl : './search.component.html' ,
styleUrl : './search.component.css'
} )
export class SearchComponent implements OnInit {
2024-02-24 13:59:17 +00:00
constructor ( private httpClient : HttpClient , public downloadFilesService : DownloadFilesService ) { }
2024-02-24 11:16:21 +00:00
2024-02-24 13:59:17 +00:00
isLoading = false ;
2024-02-24 11:16:21 +00:00
response : SearchResponse | null = null ;
fields : SchemaField [ ] = [
// User fields
{ name : "user_id" , short_name : "ID" , category : "user" , type : "number" , active : true , description : "unique identifier for a user" } ,
{ name : "user_username" , short_name : "Username" , category : "user" , type : "string" , active : true , description : "user's name" } ,
{ name : "user_join_date" , short_name : "Join Date" , category : "user" , type : "string" , active : true , description : "when the user joined" } ,
{ name : "user_country" , short_name : "Country" , category : "user" , type : "flag" , active : true , description : "user's country flag" } ,
{ name : "user_country_rank" , short_name : "Country Rank" , category : "user" , type : "number" , active : true , description : "ranking within user's country" } ,
{ name : "user_rank" , short_name : "Rank" , category : "user" , type : "number" , active : true , description : "global ranking" } ,
{ name : "user_pp_raw" , short_name : "User PP" , category : "user" , type : "number" , active : true , description : "performance points" } ,
{ name : "user_accuracy" , short_name : "User Accuracy" , category : "user" , type : "number" , active : true , description : "hit accuracy percentage" } ,
{ name : "user_playcount" , short_name : "Playcount" , category : "user" , type : "number" , active : true , description : "total plays" } ,
{ name : "user_total_score" , short_name : "Total Score" , category : "user" , type : "number" , active : true , description : "cumulative score" } ,
{ name : "user_ranked_score" , short_name : "Ranked Score" , category : "user" , type : "number" , active : true , description : "score from ranked maps" } ,
{ name : "user_seconds_played" , short_name : "Play Time" , category : "user" , type : "number" , active : true , description : "total play time in seconds" } ,
{ name : "user_count_300" , short_name : "300s" , category : "user" , type : "number" , active : true , description : "number of 300 hits" } ,
{ name : "user_count_100" , short_name : "100s" , category : "user" , type : "number" , active : true , description : "number of 100 hits" } ,
{ name : "user_count_50" , short_name : "50s" , category : "user" , type : "number" , active : true , description : "number of 50 hits" } ,
{ name : "user_count_miss" , short_name : "Misses" , category : "user" , type : "number" , active : true , description : "missed hits" } ,
// Score fields
{ name : "beatmap_id" , short_name : "Beatmap ID" , category : "score" , type : "number" , active : true , description : "identifies the beatmap" } ,
{ name : "count_300" , short_name : "300s" , category : "score" , type : "number" , active : true , description : "number of 300 hits in score" } ,
{ name : "count_100" , short_name : "100s" , category : "score" , type : "number" , active : true , description : "number of 100 hits in score" } ,
{ name : "count_50" , short_name : "50s" , category : "score" , type : "number" , active : true , description : "number of 50 hits in score" } ,
{ name : "count_miss" , short_name : "Misses" , category : "score" , type : "number" , active : true , description : "missed hits in score" } ,
{ name : "date" , short_name : "Date" , category : "score" , type : "string" , active : true , description : "when score was achieved" } ,
{ name : "max_combo" , short_name : "Max Combo" , category : "score" , type : "number" , active : true , description : "highest combo in score" } ,
{ name : "mods" , short_name : "Mods" , category : "score" , type : "number" , active : true , description : "game modifiers used" } ,
{ name : "perfect" , short_name : "Perfect" , category : "score" , type : "boolean" , active : true , description : "if score is a full combo" } ,
{ name : "pp" , short_name : "Score PP" , category : "score" , type : "number" , active : true , description : "performance points for score" } ,
{ name : "rank" , short_name : "Rank" , category : "score" , type : "grade" , active : true , description : "score grade" } ,
{ name : "replay_id" , short_name : "Replay ID" , category : "score" , type : "number" , active : true , description : "identifier for replay" } ,
{ name : "score" , short_name : "Score" , category : "score" , type : "number" , active : true , description : "score value" } ,
{ name : "ur" , short_name : "UR" , category : "score" , type : "number" , active : true , description : "unstable rate" } ,
{ name : "frametime" , short_name : "Frame Time" , category : "score" , type : "number" , active : true , description : "average frame time during play" } ,
{ name : "edge_hits" , short_name : "Edge Hits" , category : "score" , type : "number" , active : true , description : "hits at the edge of hit window" } ,
{ name : "snaps" , short_name : "Snaps" , category : "score" , type : "number" , active : true , description : "rapid cursor movements" } ,
{ name : "adjusted_ur" , short_name : "Adj. UR" , category : "score" , type : "number" , active : true , description : "adjusted unstable rate" } ,
{ name : "mean_error" , short_name : "Mean Error" , category : "score" , type : "number" , active : true , description : "average timing error" } ,
{ name : 'error_variance' , short_name : 'Error Var.' , category : 'score' , type : 'number' , active : true , description : 'variability of error in scores' } ,
{ name : 'error_standard_deviation' , short_name : 'Error SD' , category : 'score' , type : 'number' , active : true , description : 'standard deviation of error' } ,
{ name : 'minimum_error' , short_name : 'Min Error' , category : 'score' , type : 'number' , active : true , description : 'smallest error recorded' } ,
{ name : 'maximum_error' , short_name : 'Max Error' , category : 'score' , type : 'number' , active : true , description : 'largest error recorded' } ,
{ name : 'error_range' , short_name : 'Error Range' , category : 'score' , type : 'number' , active : true , description : 'range between min and max error' } ,
{ name : 'error_coefficient_of_variation' , short_name : 'Error CV' , category : 'score' , type : 'number' , active : true , description : 'relative variability of error' } ,
{ name : 'error_kurtosis' , short_name : 'Kurtosis' , category : 'score' , type : 'number' , active : true , description : 'peakedness of error distribution' } ,
{ name : 'error_skewness' , short_name : 'Skewness' , category : 'score' , type : 'number' , active : true , description : 'asymmetry of error distribution' } ,
{ name : 'keypresses_median_adjusted' , short_name : 'KP Median Adj.' , category : 'score' , type : 'number' , active : true , description : 'median of adjusted keypresses' } ,
{ name : 'keypresses_standard_deviation_adjusted' , short_name : 'KP std. Adj.' , category : 'score' , type : 'number' , active : true , description : 'std. dev of adjusted keypresses' } ,
{ name : 'sliderend_release_median_adjusted' , short_name : 'Sliderend Median Adj.' , category : 'score' , type : 'number' , active : true , description : 'median of adjusted sliderend releases' } ,
{ name : 'sliderend_release_standard_deviation_adjusted' , short_name : 'Sliderend std. Adj.' , category : 'score' , type : 'number' , active : true , description : 'std. dev of adjusted sliderend releases' } ,
// Beatmap fields
{ name : 'beatmap_artist' , short_name : 'Artist' , category : 'beatmap' , type : 'string' , active : true , description : 'artist of the beatmap' } ,
{ name : 'beatmap_beatmapset_id' , short_name : 'Set ID' , category : 'beatmap' , type : 'number' , active : true , description : 'id of the beatmap set' } ,
{ name : 'beatmap_creator' , short_name : 'Creator' , category : 'beatmap' , type : 'string' , active : true , description : 'creator of the beatmap' } ,
{ name : 'beatmap_source' , short_name : 'Source' , category : 'beatmap' , type : 'string' , active : true , description : 'source of the beatmap music' } ,
{ name : 'beatmap_star_rating' , short_name : 'Stars' , category : 'beatmap' , type : 'number' , active : true , description : '(★) difficulty rating of the beatmap' } ,
{ name : 'beatmap_title' , short_name : 'Title' , category : 'beatmap' , type : 'string' , active : true , description : 'title of the beatmap' } ,
{ name : 'beatmap_version' , short_name : 'Version' , category : 'beatmap' , type : 'string' , active : true , description : 'version or difficulty name of the beatmap' }
] ;
sortingOrder : Sorting | null = null ;
queries : Query [ ] | null = null ;
ngOnInit ( ) : void {
const storedQueries = localStorage . getItem ( 'search_queries' ) ;
if ( storedQueries ) {
this . queries = JSON . parse ( storedQueries ) ;
} else {
this . queries = [ ] ;
}
// Load active/inactive status from localStorage
const storedStatus = localStorage . getItem ( 'columns_status' ) ;
if ( storedStatus ) {
const statusMap = JSON . parse ( storedStatus ) ;
this . fields . forEach ( field = > {
if ( statusMap . hasOwnProperty ( field . name ) ) {
field . active = statusMap [ field . name ] ;
}
} ) ;
}
this . sortingOrder = {
field : 'user_id' ,
order : 'ASC'
} ;
}
mapSchemaFieldsToFields ( ) : Field [ ] {
return this . fields . map ( field = > {
return {
name : field.name ,
type : field . type ,
} ;
} ) ;
}
2024-02-24 13:59:17 +00:00
updateLocalStorage ( ) : void {
console . warn ( 'Updating local storage' ) ;
localStorage . setItem ( 'search_queries' , JSON . stringify ( this . queries ) ) ;
}
exportSettings ( ) : void {
const settings = {
queries : this.queries ,
sorting : this.sortingOrder
} as any ;
this . downloadFilesService . downloadJSON ( settings ) ;
}
importSettings ( event : any ) : void {
const file = event . target . files [ 0 ] ;
if ( file ) {
const fileReader = new FileReader ( ) ;
fileReader . onload = ( e ) = > {
try {
const json = JSON . parse ( fileReader . result as string ) ;
if ( this . verifySchema ( json ) ) {
this . queries = json . queries ;
this . sortingOrder = json . sorting ;
} else {
console . error ( 'Invalid file schema' ) ;
}
} catch ( error ) {
console . error ( 'Error parsing JSON' , error ) ;
}
} ;
fileReader . readAsText ( file ) ;
}
}
verifySchema ( json : any ) : boolean {
// TODO: Implement schema verification logic here
return 'queries' in json && 'sorting' in json ;
}
2024-02-24 11:16:21 +00:00
saveColumnsStatusToLocalStorage ( ) : void {
const statusMap = this . fields . reduce < { [ key : string ] : boolean } > ( ( acc , field ) = > {
acc [ field . name ] = field . active ;
return acc ;
} , { } ) ;
localStorage . setItem ( 'columns_status' , JSON . stringify ( statusMap ) ) ;
}
2024-02-24 13:59:17 +00:00
search ( pageNumber : number = 1 ) : void {
this . isLoading = true ;
2024-02-24 11:16:21 +00:00
const body = {
queries : this.queries ,
2024-02-24 13:59:17 +00:00
sorting : this.sortingOrder ,
page : pageNumber
2024-02-24 11:16:21 +00:00
}
2024-02-24 13:59:17 +00:00
this . httpClient . post < SearchResponse > ( ` ${ environment . apiUrl } /search ` , body ) . pipe (
catchError ( error = > {
// Handle the error or rethrow
console . error ( 'An error occurred:' , error ) ;
return throwError ( ( ) = > new Error ( 'An error occurred' ) ) ; // Rethrow or return a new observable
} )
) . subscribe ( {
next : ( response ) = > {
this . response = response ;
this . isLoading = false ;
} ,
error : ( error ) = > {
// Handle subscription error
console . error ( 'Subscription error:' , error ) ;
this . isLoading = false ;
}
2024-02-24 11:16:21 +00:00
} ) ;
2024-02-24 13:59:17 +00:00
this . updateLocalStorage ( ) ;
2024-02-24 11:16:21 +00:00
}
// Add this method to the SearchComponent class
getValue ( entry : SearchResponseEntry , columnName : string ) : any {
return entry [ columnName as keyof SearchResponseEntry ] ;
}
protected readonly countryCodeToFlag = countryCodeToFlag ;
2024-02-24 13:59:17 +00:00
protected readonly Math = Math ;
2024-02-24 11:16:21 +00:00
}