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"; import {RouterLink} from "@angular/router"; import {CalculatePageRangePipe} from "../../corelib/calculate-page-range.pipe"; import {DownloadFilesService} from "../../corelib/service/download-files.service"; 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[]; pagination: SearchPagination; } interface SearchPagination { currentPage: number; pageSize: number; totalResults: number; totalPages: number; } 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, QueryBuilderComponent, RouterLink, CalculatePageRangePipe ], templateUrl: './search.component.html', styleUrl: './search.component.css' }) export class SearchComponent implements OnInit { constructor(private httpClient: HttpClient, public downloadFilesService: DownloadFilesService) { } isError = false; isLoading = false; 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' }; } deselectEntireFieldCategory(categoryName: string): void { this.fields.forEach(field => { if (field.category === categoryName) { field.active = false; } }); this.saveColumnsStatusToLocalStorage(); } selectEntireFieldCategory(categoryName: string): void { this.fields.forEach(field => { if (field.category === categoryName) { field.active = true; } }); this.saveColumnsStatusToLocalStorage(); } mapSchemaFieldsToFields(): Field[] { return this.fields.map(field => { return { name: field.name, type: field.type, }; }); } 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; } 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)); } search(pageNumber: number = 1): void { this.isLoading = true; this.isError = false; this.response = null; const body = { queries: this.queries, sorting: this.sortingOrder, page: pageNumber } this.httpClient.post(`${environment.apiUrl}/search`, body) .subscribe({ next: (response) => { this.response = response; this.isLoading = false; }, error: (error) => { this.isError = true; this.isLoading = false; } }); this.updateLocalStorage(); } // Add this method to the SearchComponent class getValue(entry: SearchResponseEntry, columnName: string): any { return entry[columnName as keyof SearchResponseEntry]; } protected readonly countryCodeToFlag = countryCodeToFlag; protected readonly Math = Math; }