215 lines
12 KiB
TypeScript
215 lines
12 KiB
TypeScript
|
|
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";
|
||
|
|
|
||
|
|
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[];
|
||
|
|
}
|
||
|
|
|
||
|
|
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
|
||
|
|
],
|
||
|
|
templateUrl: './search.component.html',
|
||
|
|
styleUrl: './search.component.css'
|
||
|
|
})
|
||
|
|
export class SearchComponent implements OnInit {
|
||
|
|
|
||
|
|
constructor(private httpClient: HttpClient) { }
|
||
|
|
|
||
|
|
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,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
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(): void {
|
||
|
|
const body = {
|
||
|
|
queries: this.queries,
|
||
|
|
sorting: this.sortingOrder
|
||
|
|
}
|
||
|
|
this.httpClient.post<SearchResponse>(`${environment.apiUrl}/search`, body).subscribe(response => {
|
||
|
|
this.response = response;
|
||
|
|
});
|
||
|
|
// 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;
|
||
|
|
}
|