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";
|
2024-02-25 12:09:27 +00:00
|
|
|
import {countryCodeToFlag, formatDuration} from "../format";
|
2024-02-24 11:16:21 +00:00
|
|
|
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
|
2024-02-25 19:12:50 +00:00
|
|
|
import {
|
|
|
|
|
FieldType,
|
|
|
|
|
Operator,
|
|
|
|
|
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";
|
2024-02-25 12:09:27 +00:00
|
|
|
import {CuteLoadingComponent} from "../../corelib/components/cute-loading/cute-loading.component";
|
2024-02-25 19:12:50 +00:00
|
|
|
import {Title} from "@angular/platform-browser";
|
2024-02-24 11:16:21 +00:00
|
|
|
|
2024-02-25 12:09:27 +00:00
|
|
|
export interface SchemaField {
|
2024-02-24 11:16:21 +00:00
|
|
|
name: string;
|
2024-02-24 17:38:37 +00:00
|
|
|
shortName: string;
|
2024-02-25 12:09:27 +00:00
|
|
|
category: 'user' | 'score' | 'beatmap' | 'metrics';
|
|
|
|
|
type: 'number' | 'string' | 'flag' | 'grade' | 'boolean' | 'datetime' | 'playtime';
|
2024-02-25 19:12:50 +00:00
|
|
|
validOperators: Operator[];
|
2024-02-24 11:16:21 +00:00
|
|
|
active: boolean;
|
|
|
|
|
description: string;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-24 17:21:27 +00:00
|
|
|
interface SchemaResponse {
|
|
|
|
|
fields: SchemaField[];
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-24 11:16:21 +00:00
|
|
|
interface SearchResponse {
|
2024-02-25 01:00:45 +00:00
|
|
|
scores: any[];
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
selector: 'app-search',
|
|
|
|
|
standalone: true,
|
|
|
|
|
imports: [
|
|
|
|
|
ReactiveFormsModule,
|
|
|
|
|
NgForOf,
|
|
|
|
|
FormsModule,
|
|
|
|
|
JsonPipe,
|
|
|
|
|
NgIf,
|
|
|
|
|
DecimalPipe,
|
|
|
|
|
OsuGradeComponent,
|
2024-02-24 13:59:17 +00:00
|
|
|
QueryBuilderComponent,
|
|
|
|
|
RouterLink,
|
2024-02-25 12:09:27 +00:00
|
|
|
CalculatePageRangePipe,
|
|
|
|
|
CuteLoadingComponent
|
2024-02-24 11:16:21 +00:00
|
|
|
],
|
|
|
|
|
templateUrl: './search.component.html',
|
|
|
|
|
styleUrl: './search.component.css'
|
|
|
|
|
})
|
|
|
|
|
export class SearchComponent implements OnInit {
|
|
|
|
|
|
2024-02-24 17:21:27 +00:00
|
|
|
constructor(private httpClient: HttpClient,
|
2024-02-25 19:12:50 +00:00
|
|
|
private title: Title,
|
2024-02-24 17:21:27 +00:00
|
|
|
public downloadFilesService: DownloadFilesService) { }
|
2024-02-24 11:16:21 +00:00
|
|
|
|
2024-02-25 19:42:33 +00:00
|
|
|
currentSchemaVersion = 2
|
|
|
|
|
|
2024-02-24 16:58:10 +00:00
|
|
|
isError = false;
|
2024-02-24 13:59:17 +00:00
|
|
|
isLoading = false;
|
2024-02-24 19:31:15 +00:00
|
|
|
isLoadingSchema = true;
|
2024-02-24 11:16:21 +00:00
|
|
|
response: SearchResponse | null = null;
|
|
|
|
|
|
2024-02-24 17:21:27 +00:00
|
|
|
fields: SchemaField[] = [];
|
2024-02-24 11:16:21 +00:00
|
|
|
|
|
|
|
|
sortingOrder: Sorting | null = null;
|
|
|
|
|
queries: Query[] | null = null;
|
|
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
2024-02-25 19:12:50 +00:00
|
|
|
this.title.setTitle("/k/ - advanced search");
|
2024-02-24 19:31:15 +00:00
|
|
|
this.isLoadingSchema = true;
|
2024-02-25 01:00:45 +00:00
|
|
|
this.httpClient.get<SchemaResponse>(`${environment.apiUrl}/search/schema`,).subscribe({
|
2024-02-24 17:21:27 +00:00
|
|
|
next: (response) => {
|
|
|
|
|
this.fields = response.fields;
|
2024-02-25 19:12:50 +00:00
|
|
|
this.fields.forEach(field => {
|
|
|
|
|
field.validOperators = this.getOperators(field.type);
|
|
|
|
|
})
|
2024-02-24 17:21:27 +00:00
|
|
|
this.loadPreviousFromLocalStorage();
|
2024-02-24 19:31:15 +00:00
|
|
|
this.isLoadingSchema = false;
|
2024-02-24 17:21:27 +00:00
|
|
|
},
|
|
|
|
|
error: () => {
|
|
|
|
|
alert('Error fetching schema');
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-25 19:12:50 +00:00
|
|
|
getOperators(fieldType: FieldType | undefined): Operator[] {
|
|
|
|
|
switch (fieldType) {
|
|
|
|
|
case 'number':
|
|
|
|
|
return ['=', '>', '<', '>=', '<=', '!=']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
|
|
|
|
|
case 'string':
|
|
|
|
|
return ['=', 'contains', 'like']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
|
|
|
|
|
case 'boolean':
|
|
|
|
|
return ['=', '!=']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'boolean'}) as Operator);
|
|
|
|
|
case 'flag':
|
|
|
|
|
return ['=', '!=']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'flag'}) as Operator);
|
|
|
|
|
case 'grade':
|
|
|
|
|
return ['=', '!=']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'grade'}) as Operator);
|
|
|
|
|
case 'datetime':
|
|
|
|
|
return ['before', 'after']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'datetime'}) as Operator);
|
|
|
|
|
case 'playtime':
|
|
|
|
|
return ['>', '<', '>=', '<=']
|
|
|
|
|
.map((operatorType: String) => ({operatorType: operatorType, acceptsValues: 'any'}) as Operator);
|
|
|
|
|
default:
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-25 19:42:33 +00:00
|
|
|
private loadPreviousFromLocalStorage(): void {
|
2024-02-24 18:29:10 +00:00
|
|
|
const storedQueries = localStorage.getItem('search_settings');
|
2024-02-25 19:42:33 +00:00
|
|
|
let parsedQueries = storedQueries ? JSON.parse(storedQueries) : null;
|
|
|
|
|
|
|
|
|
|
if (parsedQueries && this.verifySchema(parsedQueries)) {
|
|
|
|
|
this.queries = parsedQueries.queries;
|
|
|
|
|
this.sortingOrder = parsedQueries.sortingOrder;
|
2024-02-24 11:16:21 +00:00
|
|
|
this.fields.forEach(field => {
|
2024-02-25 19:42:33 +00:00
|
|
|
field.active = parsedQueries.columns[field.name] ?? field.active;
|
2024-02-24 11:16:21 +00:00
|
|
|
});
|
2024-02-24 18:29:10 +00:00
|
|
|
} else {
|
2024-02-25 19:42:33 +00:00
|
|
|
localStorage.removeItem('search_settings');
|
2024-02-24 18:29:10 +00:00
|
|
|
this.queries = [];
|
|
|
|
|
this.sortingOrder = {
|
|
|
|
|
field: 'user_id',
|
|
|
|
|
order: 'ASC'
|
|
|
|
|
};
|
2024-02-24 11:16:21 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-24 16:58:10 +00:00
|
|
|
deselectEntireFieldCategory(categoryName: string): void {
|
|
|
|
|
this.fields.forEach(field => {
|
|
|
|
|
if (field.category === categoryName) {
|
|
|
|
|
field.active = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-02-24 18:29:10 +00:00
|
|
|
this.saveSettingsToLocalStorage();
|
2024-02-24 16:58:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
selectEntireFieldCategory(categoryName: string): void {
|
|
|
|
|
this.fields.forEach(field => {
|
|
|
|
|
if (field.category === categoryName) {
|
|
|
|
|
field.active = true;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-02-24 18:29:10 +00:00
|
|
|
this.saveSettingsToLocalStorage();
|
2024-02-24 16:58:10 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-24 18:29:10 +00:00
|
|
|
private serializeSettings() {
|
|
|
|
|
return {
|
|
|
|
|
queries: this.queries,
|
|
|
|
|
sortingOrder: this.sortingOrder,
|
2024-02-25 19:42:33 +00:00
|
|
|
columns: this.getColumnSettings(),
|
|
|
|
|
schemaVersion: this.currentSchemaVersion
|
2024-02-24 18:29:10 +00:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getColumnSettings() {
|
|
|
|
|
return this.fields.reduce<{ [key: string]: boolean }>((acc, field) => {
|
|
|
|
|
acc[field.name] = field.active;
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveSettingsToLocalStorage(): void {
|
|
|
|
|
const settings = this.serializeSettings();
|
|
|
|
|
localStorage.setItem('search_settings', JSON.stringify(settings));
|
2024-02-24 13:59:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
exportSettings(): void {
|
2024-02-24 18:29:10 +00:00
|
|
|
const settings = this.serializeSettings();
|
|
|
|
|
this.downloadFilesService.downloadJSON(settings as any, 'nise-settings');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
importSettings(json: any): void {
|
|
|
|
|
if (this.verifySchema(json)) {
|
|
|
|
|
this.queries = json.queries;
|
|
|
|
|
this.sortingOrder = json.sortingOrder;
|
|
|
|
|
this.fields.forEach(field => {
|
|
|
|
|
if (json.columns.hasOwnProperty(field.name)) {
|
|
|
|
|
field.active = json.columns[field.name];
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-02-25 19:42:33 +00:00
|
|
|
} else {
|
|
|
|
|
alert('Invalid settings file.');
|
2024-02-24 18:29:10 +00:00
|
|
|
}
|
2024-02-24 17:38:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getColumns(): string[] {
|
|
|
|
|
return this.fields.map(field => field.name);
|
2024-02-24 13:59:17 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-24 18:29:10 +00:00
|
|
|
uploadSettingsFile(event: any): void {
|
2024-02-24 13:59:17 +00:00
|
|
|
const file = event.target.files[0];
|
|
|
|
|
if (file) {
|
|
|
|
|
const fileReader = new FileReader();
|
|
|
|
|
fileReader.onload = (e) => {
|
|
|
|
|
try {
|
|
|
|
|
const json = JSON.parse(fileReader.result as string);
|
2024-02-24 18:29:10 +00:00
|
|
|
this.importSettings(json);
|
2024-02-24 13:59:17 +00:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error parsing JSON', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
fileReader.readAsText(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifySchema(json: any): boolean {
|
2024-02-25 19:42:33 +00:00
|
|
|
if(!('schemaVersion' in json) || json.schemaVersion < this.currentSchemaVersion) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2024-02-24 18:29:10 +00:00
|
|
|
return 'queries' in json && 'sortingOrder' in json && 'columns' in json;
|
2024-02-24 11:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-24 13:59:17 +00:00
|
|
|
search(pageNumber: number = 1): void {
|
|
|
|
|
this.isLoading = true;
|
2024-02-24 16:58:10 +00:00
|
|
|
this.isError = false;
|
|
|
|
|
this.response = null;
|
|
|
|
|
|
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 16:58:10 +00:00
|
|
|
this.httpClient.post<SearchResponse>(`${environment.apiUrl}/search`, body)
|
|
|
|
|
.subscribe({
|
|
|
|
|
next: (response) => {
|
|
|
|
|
this.response = response;
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
},
|
2024-02-24 18:29:10 +00:00
|
|
|
error: () => {
|
2024-02-24 16:58:10 +00:00
|
|
|
this.isError = true;
|
|
|
|
|
this.isLoading = false;
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-02-24 18:29:10 +00:00
|
|
|
this.saveSettingsToLocalStorage();
|
2024-02-24 11:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
2024-02-25 01:00:45 +00:00
|
|
|
getValue(entry: any, columnName: string): any {
|
2024-02-25 12:09:27 +00:00
|
|
|
if (entry.hasOwnProperty(columnName)) {
|
|
|
|
|
return entry[columnName];
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getId(entry: any): any {
|
|
|
|
|
return this.getValue(entry, 'replay_id');
|
2024-02-24 11:16:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected readonly countryCodeToFlag = countryCodeToFlag;
|
2024-02-24 13:59:17 +00:00
|
|
|
protected readonly Math = Math;
|
2024-02-25 12:09:27 +00:00
|
|
|
protected readonly formatDuration = formatDuration;
|
2024-02-25 19:12:50 +00:00
|
|
|
|
2024-02-24 11:16:21 +00:00
|
|
|
}
|