Implemented slider end release times and keypress release times, along with a privileged auth system

This commit is contained in:
nise.moe 2024-02-18 14:25:14 +01:00
parent 1ce7d4c599
commit 01bd4e4948
17 changed files with 232 additions and 36 deletions

View File

@ -16,7 +16,7 @@ import org.jooq.ForeignKey
import org.jooq.Name import org.jooq.Name
import org.jooq.Record import org.jooq.Record
import org.jooq.Records import org.jooq.Records
import org.jooq.Row16 import org.jooq.Row17
import org.jooq.Schema import org.jooq.Schema
import org.jooq.SelectField import org.jooq.SelectField
import org.jooq.Table import org.jooq.Table
@ -142,6 +142,11 @@ open class Users(
*/ */
val SYS_LAST_UPDATE: TableField<UsersRecord, LocalDateTime?> = createField(DSL.name("sys_last_update"), SQLDataType.LOCALDATETIME(6), this, "") val SYS_LAST_UPDATE: TableField<UsersRecord, LocalDateTime?> = createField(DSL.name("sys_last_update"), SQLDataType.LOCALDATETIME(6), this, "")
/**
* The column <code>public.users.is_admin</code>.
*/
val IS_ADMIN: TableField<UsersRecord, Boolean?> = createField(DSL.name("is_admin"), SQLDataType.BOOLEAN.defaultValue(DSL.field(DSL.raw("false"), SQLDataType.BOOLEAN)), this, "")
private constructor(alias: Name, aliased: Table<UsersRecord>?): this(alias, null, null, aliased, null) private constructor(alias: Name, aliased: Table<UsersRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<UsersRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters) private constructor(alias: Name, aliased: Table<UsersRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
@ -183,18 +188,18 @@ open class Users(
override fun rename(name: Table<*>): Users = Users(name.getQualifiedName(), null) override fun rename(name: Table<*>): Users = Users(name.getQualifiedName(), null)
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Row16 type methods // Row17 type methods
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> = super.fieldsRow() as Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> override fun fieldsRow(): Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?> = super.fieldsRow() as Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?>
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}. * Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/ */
fun <U> mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?) -> U): SelectField<U> = convertFrom(Records.mapping(from)) fun <U> mapping(from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?) -> U): SelectField<U> = convertFrom(Records.mapping(from))
/** /**
* Convenience mapping calling {@link SelectField#convertFrom(Class, * Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}. * Function)}.
*/ */
fun <U> mapping(toType: Class<U>, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from)) fun <U> mapping(toType: Class<U>, from: (Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?) -> U): SelectField<U> = convertFrom(toType, Records.mapping(from))
} }

View File

@ -10,8 +10,8 @@ import java.time.LocalDateTime
import org.jooq.Field import org.jooq.Field
import org.jooq.Record1 import org.jooq.Record1
import org.jooq.Record16 import org.jooq.Record17
import org.jooq.Row16 import org.jooq.Row17
import org.jooq.impl.UpdatableRecordImpl import org.jooq.impl.UpdatableRecordImpl
@ -19,7 +19,7 @@ import org.jooq.impl.UpdatableRecordImpl
* This class is generated by jOOQ. * This class is generated by jOOQ.
*/ */
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(Users.USERS), Record16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> { open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(Users.USERS), Record17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?> {
open var userId: Long? open var userId: Long?
set(value): Unit = set(0, value) set(value): Unit = set(0, value)
@ -85,6 +85,12 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
set(value): Unit = set(15, value) set(value): Unit = set(15, value)
get(): LocalDateTime? = get(15) as LocalDateTime? get(): LocalDateTime? = get(15) as LocalDateTime?
@Suppress("INAPPLICABLE_JVM_NAME")
@set:JvmName("setIsAdmin")
open var isAdmin: Boolean?
set(value): Unit = set(16, value)
get(): Boolean? = get(16) as Boolean?
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Primary key information // Primary key information
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@ -92,11 +98,11 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun key(): Record1<Long?> = super.key() as Record1<Long?> override fun key(): Record1<Long?> = super.key() as Record1<Long?>
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Record16 type implementation // Record17 type implementation
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
override fun fieldsRow(): Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> = super.fieldsRow() as Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> override fun fieldsRow(): Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?> = super.fieldsRow() as Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?>
override fun valuesRow(): Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> = super.valuesRow() as Row16<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?> override fun valuesRow(): Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?> = super.valuesRow() as Row17<Long?, String?, LocalDateTime?, String?, Long?, Long?, Double?, Double?, Long?, Long?, Long?, Long?, Long?, Long?, Long?, LocalDateTime?, Boolean?>
override fun field1(): Field<Long?> = Users.USERS.USER_ID override fun field1(): Field<Long?> = Users.USERS.USER_ID
override fun field2(): Field<String?> = Users.USERS.USERNAME override fun field2(): Field<String?> = Users.USERS.USERNAME
override fun field3(): Field<LocalDateTime?> = Users.USERS.JOIN_DATE override fun field3(): Field<LocalDateTime?> = Users.USERS.JOIN_DATE
@ -113,6 +119,7 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun field14(): Field<Long?> = Users.USERS.COUNT_300 override fun field14(): Field<Long?> = Users.USERS.COUNT_300
override fun field15(): Field<Long?> = Users.USERS.COUNT_50 override fun field15(): Field<Long?> = Users.USERS.COUNT_50
override fun field16(): Field<LocalDateTime?> = Users.USERS.SYS_LAST_UPDATE override fun field16(): Field<LocalDateTime?> = Users.USERS.SYS_LAST_UPDATE
override fun field17(): Field<Boolean?> = Users.USERS.IS_ADMIN
override fun component1(): Long? = userId override fun component1(): Long? = userId
override fun component2(): String? = username override fun component2(): String? = username
override fun component3(): LocalDateTime? = joinDate override fun component3(): LocalDateTime? = joinDate
@ -129,6 +136,7 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun component14(): Long? = count_300 override fun component14(): Long? = count_300
override fun component15(): Long? = count_50 override fun component15(): Long? = count_50
override fun component16(): LocalDateTime? = sysLastUpdate override fun component16(): LocalDateTime? = sysLastUpdate
override fun component17(): Boolean? = isAdmin
override fun value1(): Long? = userId override fun value1(): Long? = userId
override fun value2(): String? = username override fun value2(): String? = username
override fun value3(): LocalDateTime? = joinDate override fun value3(): LocalDateTime? = joinDate
@ -145,6 +153,7 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
override fun value14(): Long? = count_300 override fun value14(): Long? = count_300
override fun value15(): Long? = count_50 override fun value15(): Long? = count_50
override fun value16(): LocalDateTime? = sysLastUpdate override fun value16(): LocalDateTime? = sysLastUpdate
override fun value17(): Boolean? = isAdmin
override fun value1(value: Long?): UsersRecord { override fun value1(value: Long?): UsersRecord {
set(0, value) set(0, value)
@ -226,7 +235,12 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
return this return this
} }
override fun values(value1: Long?, value2: String?, value3: LocalDateTime?, value4: String?, value5: Long?, value6: Long?, value7: Double?, value8: Double?, value9: Long?, value10: Long?, value11: Long?, value12: Long?, value13: Long?, value14: Long?, value15: Long?, value16: LocalDateTime?): UsersRecord { override fun value17(value: Boolean?): UsersRecord {
set(16, value)
return this
}
override fun values(value1: Long?, value2: String?, value3: LocalDateTime?, value4: String?, value5: Long?, value6: Long?, value7: Double?, value8: Double?, value9: Long?, value10: Long?, value11: Long?, value12: Long?, value13: Long?, value14: Long?, value15: Long?, value16: LocalDateTime?, value17: Boolean?): UsersRecord {
this.value1(value1) this.value1(value1)
this.value2(value2) this.value2(value2)
this.value3(value3) this.value3(value3)
@ -243,13 +257,14 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
this.value14(value14) this.value14(value14)
this.value15(value15) this.value15(value15)
this.value16(value16) this.value16(value16)
this.value17(value17)
return this return this
} }
/** /**
* Create a detached, initialised UsersRecord * Create a detached, initialised UsersRecord
*/ */
constructor(userId: Long? = null, username: String? = null, joinDate: LocalDateTime? = null, country: String? = null, countryRank: Long? = null, rank: Long? = null, ppRaw: Double? = null, accuracy: Double? = null, playcount: Long? = null, totalScore: Long? = null, rankedScore: Long? = null, secondsPlayed: Long? = null, count_100: Long? = null, count_300: Long? = null, count_50: Long? = null, sysLastUpdate: LocalDateTime? = null): this() { constructor(userId: Long? = null, username: String? = null, joinDate: LocalDateTime? = null, country: String? = null, countryRank: Long? = null, rank: Long? = null, ppRaw: Double? = null, accuracy: Double? = null, playcount: Long? = null, totalScore: Long? = null, rankedScore: Long? = null, secondsPlayed: Long? = null, count_100: Long? = null, count_300: Long? = null, count_50: Long? = null, sysLastUpdate: LocalDateTime? = null, isAdmin: Boolean? = null): this() {
this.userId = userId this.userId = userId
this.username = username this.username = username
this.joinDate = joinDate this.joinDate = joinDate
@ -266,6 +281,7 @@ open class UsersRecord private constructor() : UpdatableRecordImpl<UsersRecord>(
this.count_300 = count_300 this.count_300 = count_300
this.count_50 = count_50 this.count_50 = count_50
this.sysLastUpdate = sysLastUpdate this.sysLastUpdate = sysLastUpdate
this.isAdmin = isAdmin
resetChangedOnNotNull() resetChangedOnNotNull()
} }
} }

View File

@ -69,6 +69,13 @@ data class ReplayPair(
val statistics: ReplayPairStatistics val statistics: ReplayPairStatistics
) )
data class ReplayDataChart(
val title: String,
val tableSamples: Int,
val table: List<Triple<String, String, String>>,
val data: List<Pair<Double, Double>>
)
data class ReplayData( data class ReplayData(
val replay_id: Long, val replay_id: Long,
val user_id: Int, val user_id: Int,
@ -121,7 +128,8 @@ data class ReplayData(
val count_50: Int, val count_50: Int,
val count_miss: Int, val count_miss: Int,
val error_distribution: Map<Int, DistributionEntry> val error_distribution: Map<Int, DistributionEntry>,
val charts: List<ReplayDataChart>
) { ) {
fun calculateAccuracy(): Double { fun calculateAccuracy(): Double {

View File

@ -4,13 +4,13 @@ import com.nisemoe.generated.tables.records.ScoresJudgementsRecord
import com.nisemoe.generated.tables.references.* import com.nisemoe.generated.tables.references.*
import com.nisemoe.nise.* import com.nisemoe.nise.*
import com.nisemoe.nise.osu.Mod import com.nisemoe.nise.osu.Mod
import com.nisemoe.nise.service.AuthService
import org.jooq.Condition import org.jooq.Condition
import org.jooq.DSLContext import org.jooq.DSLContext
import org.jooq.Record import org.jooq.Record
import org.jooq.Result import org.jooq.Result
import org.jooq.impl.DSL import org.jooq.impl.DSL
import org.jooq.impl.DSL.avg import org.jooq.impl.DSL.avg
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.LocalDateTime import java.time.LocalDateTime
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -18,7 +18,8 @@ import kotlin.math.roundToInt
@Service @Service
class ScoreService( class ScoreService(
private val dslContext: DSLContext, private val dslContext: DSLContext,
private val beatmapService: BeatmapService private val beatmapService: BeatmapService,
private val authService: AuthService
) { ) {
companion object { companion object {
@ -30,6 +31,75 @@ class ScoreService(
} }
fun getCharts(db: Record): List<ReplayDataChart> {
// We only return additional charts if the user is an admin.
if (!authService.isAdmin()) {
return emptyList()
}
// Slider end chart
val sliderEndData = db.get(SCORES.SLIDEREND_RELEASE_TIMES)!!
.filterNotNull()
val sliderFrequencyData: List<Pair<Double, Double>> = sliderEndData
.groupingBy { it }
.eachCount()
.map { (value, count) -> Pair(value, count / sliderEndData.size.toDouble() * 100) }
// Slider end table
val sliderEndTable = mutableListOf<Triple<String, String, String>>()
sliderEndTable.add(Triple(
"Median",
String.format("%.2f", db.get(SCORES.SLIDEREND_RELEASE_MEDIAN)),
String.format("%.2f", db.get(SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED))
))
sliderEndTable.add(Triple(
"Std. deviation",
String.format("%.2f", db.get(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION)),
String.format("%.2f", db.get(SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED))
))
val sliderEndChart = ReplayDataChart(
title = "slider end release times",
tableSamples = 0,
table = sliderEndTable,
data = sliderFrequencyData
)
// --------------------
// Slider end chart
val keypressData = db.get(SCORES.KEYPRESSES_TIMES)!!
.filterNotNull()
val keypressFrequencyData: List<Pair<Double, Double>> = keypressData
.groupingBy { it }
.eachCount()
.map { (value, count) -> Pair(value, count / keypressData.size.toDouble() * 100) }
// Slider end table
val keypressTable = mutableListOf<Triple<String, String, String>>()
keypressTable.add(Triple(
"Median",
String.format("%.2f", db.get(SCORES.KEYPRESSES_MEDIAN)),
String.format("%.2f", db.get(SCORES.KEYPRESSES_MEDIAN_ADJUSTED))
))
keypressTable.add(Triple(
"Std. deviation",
String.format("%.2f", db.get(SCORES.KEYPRESSES_STANDARD_DEVIATION)),
String.format("%.2f", db.get(SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED))
))
val keypressChart = ReplayDataChart(
title = "keypress release times",
tableSamples = 0,
table = keypressTable,
data = keypressFrequencyData
)
return listOf(sliderEndChart, keypressChart)
}
fun getReplayData(replayId: Long): ReplayData? { fun getReplayData(replayId: Long): ReplayData? {
val result = dslContext.select(DSL.asterisk()) val result = dslContext.select(DSL.asterisk())
.from(SCORES) .from(SCORES)
@ -41,6 +111,7 @@ class ScoreService(
val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java) val beatmapId = result.get(BEATMAPS.BEATMAP_ID, Int::class.java)
val averageUR = beatmapService.getAverageUR(beatmapId = beatmapId, excludeReplayId = replayId) val averageUR = beatmapService.getAverageUR(beatmapId = beatmapId, excludeReplayId = replayId)
val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java)) val hitDistribution = this.getHitDistribution(scoreId = result.get(SCORES.ID, Int::class.java))
val charts = this.getCharts(result)
val replayData = ReplayData( val replayData = ReplayData(
replay_id = replayId, replay_id = replayId,
@ -80,6 +151,7 @@ class ScoreService(
error_coefficient_of_variation = result.get(SCORES.ERROR_COEFFICIENT_OF_VARIATION, Double::class.java), error_coefficient_of_variation = result.get(SCORES.ERROR_COEFFICIENT_OF_VARIATION, Double::class.java),
error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java), error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java),
error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java), error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java),
charts = charts
) )
this.loadComparableReplayData(replayData) this.loadComparableReplayData(replayData)
return replayData return replayData

View File

@ -1,18 +1,30 @@
package com.nisemoe.nise.service package com.nisemoe.nise.service
import com.nisemoe.generated.tables.references.USERS
import org.jooq.DSLContext
import org.springframework.security.authentication.AnonymousAuthenticationToken import org.springframework.security.authentication.AnonymousAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.oauth2.core.user.DefaultOAuth2User import org.springframework.security.oauth2.core.user.DefaultOAuth2User
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class AuthService { class AuthService(
private val dslContext: DSLContext
) {
data class UserInfo( data class UserInfo(
val userId: Int, val userId: Long,
val username: String val username: String
) )
fun isAdmin(): Boolean {
if(!this.isLoggedIn())
return false
val currentUser = this.getCurrentUser()
return dslContext.fetchExists(USERS, USERS.USER_ID.eq(currentUser.userId).and(USERS.IS_ADMIN))
}
fun isLoggedIn(): Boolean { fun isLoggedIn(): Boolean {
val authentication = SecurityContextHolder.getContext().authentication val authentication = SecurityContextHolder.getContext().authentication
return !(authentication == null || authentication is AnonymousAuthenticationToken || authentication.principal == null) return !(authentication == null || authentication is AnonymousAuthenticationToken || authentication.principal == null)
@ -26,10 +38,9 @@ class AuthService {
val userDetails = authentication.principal as DefaultOAuth2User val userDetails = authentication.principal as DefaultOAuth2User
return UserInfo( return UserInfo(
userId = userDetails.attributes["id"] as Int, userId = (userDetails.attributes["id"] as Int).toLong(),
username = userDetails.attributes["username"] as String username = userDetails.attributes["username"] as String
) )
} }
} }

View File

@ -0,0 +1,2 @@
ALTER TABLE public.users
ADD COLUMN is_admin boolean DEFAULT false;

View File

@ -26,5 +26,5 @@
</div> </div>
<router-outlet></router-outlet> <router-outlet></router-outlet>
<div class="text-center version"> <div class="text-center version">
v20240217 v20240218
</div> </div>

View File

@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import {HttpClientModule } from "@angular/common/http"; import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http";
import { ViewSuspiciousScoresComponent } from './view-suspicious-scores/view-suspicious-scores.component'; import { ViewSuspiciousScoresComponent } from './view-suspicious-scores/view-suspicious-scores.component';
import { ViewSimilarReplaysComponent } from './view-similar-replays/view-similar-replays.component'; import { ViewSimilarReplaysComponent } from './view-similar-replays/view-similar-replays.component';
import { HomeComponent } from './home/home.component'; import { HomeComponent } from './home/home.component';
@ -12,6 +12,7 @@ import {FormsModule} from "@angular/forms";
import {NgOptimizedImage} from "@angular/common"; import {NgOptimizedImage} from "@angular/common";
import {rxStompServiceFactory} from "../corelib/stomp/stomp.factory"; import {rxStompServiceFactory} from "../corelib/stomp/stomp.factory";
import {RxStompService} from "../corelib/stomp/stomp.service"; import {RxStompService} from "../corelib/stomp/stomp.service";
import {NiseHttpInterceptor} from "../corelib/nise-http.interceptor";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -32,7 +33,8 @@ import {RxStompService} from "../corelib/stomp/stomp.service";
{ {
provide: RxStompService, provide: RxStompService,
useFactory: rxStompServiceFactory, useFactory: rxStompServiceFactory,
} },
{ provide: HTTP_INTERCEPTORS, useClass: NiseHttpInterceptor, multi: true }
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -1,7 +1,7 @@
import {Component, OnDestroy, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {Observable, Subscription} from "rxjs"; import {Observable, Subscription} from "rxjs";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {LocalCacheService} from "../../corelib/local-cache.service"; import {LocalCacheService} from "../../corelib/service/local-cache.service";
import {RxStompService} from "../../corelib/stomp/stomp.service"; import {RxStompService} from "../../corelib/stomp/stomp.service";
import {Message} from "@stomp/stompjs/esm6"; import {Message} from "@stomp/stompjs/esm6";
import {ReplayData} from "../replays"; import {ReplayData} from "../replays";

View File

@ -1,3 +1,10 @@
export interface ReplayDataChart {
title: string;
tableSamples: number;
table: Array<{ first: string, second: string, third: string }>;
data: Array<{ first: number, second: number }>;
}
export interface ReplayData { export interface ReplayData {
replay_id: number; replay_id: number;
user_id: number; user_id: number;
@ -51,6 +58,7 @@ export interface ReplayData {
count_miss: number; count_miss: number;
error_distribution: ErrorDistribution; error_distribution: ErrorDistribution;
charts: ReplayDataChart[];
} }
export interface DistributionEntry { export interface DistributionEntry {

View File

@ -98,7 +98,7 @@
</div> </div>
</div> </div>
<div class="main term" *ngIf="this.replayData.mean_error"> <div class="main term mb-2" *ngIf="this.replayData.mean_error">
<h1># nerd stats</h1> <h1># nerd stats</h1>
<table> <table>
<thead> <thead>
@ -150,6 +150,36 @@
</table> </table>
</div> </div>
<div class="main term mb-2" *ngFor="let chart of this.replayData.charts">
<h1>
# {{ chart.title }}
</h1>
<table class="mb-4">
<thead>
<th></th>
<th>
value
</th>
<th>
adjusted value (no outliers)
</th>
</thead>
<tr *ngFor="let entry of chart.table">
<td>{{ entry.first }}</td>
<td class="text-center">{{ entry.second }}</td>
<td class="text-center">{{ entry.third }}</td>
</tr>
</table>
<canvas baseChart
[data]="this.buildChartData(chart)"
[options]="barChartOptions"
[plugins]="barChartPlugins"
[legend]="false"
[type]="'bar'"
class="chart">
</canvas>
</div>
<div class="main term" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0"> <div class="main term" *ngIf="this.replayData.error_distribution && Object.keys(this.replayData.error_distribution).length > 0">
<h1># hit distribution</h1> <h1># hit distribution</h1>
<canvas baseChart <canvas baseChart

View File

@ -1,12 +1,12 @@
import {Component, OnInit, ViewChild} from '@angular/core'; import {Component, OnInit, ViewChild} from '@angular/core';
import {ChartConfiguration} from 'chart.js'; import {ChartConfiguration, ChartData, DefaultDataPoint} from 'chart.js';
import {BaseChartDirective, NgChartsModule} from 'ng2-charts'; import {BaseChartDirective, NgChartsModule} from 'ng2-charts';
import {HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common"; import {DecimalPipe, JsonPipe, NgForOf, NgIf, NgOptimizedImage} from "@angular/common";
import {ActivatedRoute, RouterLink} from "@angular/router"; import {ActivatedRoute, RouterLink} from "@angular/router";
import {catchError, throwError} from "rxjs"; import {catchError, throwError} from "rxjs";
import {DistributionEntry, ReplayData} from "../replays"; import {DistributionEntry, ReplayData, ReplayDataChart} from "../replays";
import {calculateAccuracy} from "../format"; import {calculateAccuracy} from "../format";
import {Title} from "@angular/platform-browser"; import {Title} from "@angular/platform-browser";
import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component"; import {OsuGradeComponent} from "../../corelib/components/osu-grade/osu-grade.component";
@ -76,6 +76,34 @@ export class ViewScoreComponent implements OnInit {
}); });
} }
buildChartData(chart: ReplayDataChart): ChartData<"bar", DefaultDataPoint<"bar">, any> {
const sortedData = chart.data.sort((a, b) => a.first - b.first);
const minFirst = Math.floor(sortedData[0].first / 2) * 2; // Round down to nearest even number
const maxFirst = Math.ceil(sortedData[sortedData.length - 1].first / 2) * 2; // Round up to nearest even number
const groupRanges = Array.from({length: (maxFirst - minFirst) / 2 + 1}, (_, i) => minFirst + i * 2);
const groupedData = groupRanges.map(rangeStart => {
const rangeEnd = rangeStart + 2;
const entriesInGroup = sortedData.filter(e => e.first >= rangeStart && e.first < rangeEnd);
const sumSecond = entriesInGroup.reduce((acc, curr) => acc + curr.second, 0);
return { first: rangeStart, second: sumSecond };
});
const labels = groupedData.map(({ first }) => `${first}ms to ${first + 2}ms`);
const datasets = [
{
data: groupedData.map(({ second }) => second),
label: chart.title + " (%)",
backgroundColor: 'rgba(0,255,41,0.66)',
borderRadius: 5
}
];
return { labels, datasets };
}
buildCircleguardUrl(): string { buildCircleguardUrl(): string {
if(!this.replayData) { if(!this.replayData) {
return ""; return "";
@ -147,7 +175,7 @@ export class ViewScoreComponent implements OnInit {
return filledEntries.map(([key, _]) => { return filledEntries.map(([key, _]) => {
const start = parseInt(String(key)); const start = parseInt(String(key));
const end = start + 2; const end = start + 2;
return `${start} to ${end}ms`; return `${start}ms to ${end}ms`;
}); });
} }

View File

@ -2,7 +2,7 @@ import {Component, OnInit} from '@angular/core';
import {SimilarReplay} from "../replays"; import {SimilarReplay} from "../replays";
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {LocalCacheService} from "../../corelib/local-cache.service"; import {LocalCacheService} from "../../corelib/service/local-cache.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {FilterManagerService} from "../filter-manager.service"; import {FilterManagerService} from "../filter-manager.service";

View File

@ -2,7 +2,7 @@ import {Component, OnDestroy, OnInit} from '@angular/core';
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
import {SuspiciousScore} from "../replays"; import {SuspiciousScore} from "../replays";
import {Observable, Subscription, timer} from 'rxjs'; import {Observable, Subscription, timer} from 'rxjs';
import {LocalCacheService} from "../../corelib/local-cache.service"; import {LocalCacheService} from "../../corelib/service/local-cache.service";
import {ActivatedRoute, Router} from "@angular/router"; import {ActivatedRoute, Router} from "@angular/router";
import {FilterManagerService} from "../filter-manager.service"; import {FilterManagerService} from "../filter-manager.service";

View File

@ -0,0 +1,17 @@
import {Injectable} from '@angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {Observable} from 'rxjs';
@Injectable()
export class NiseHttpInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const modifiedReq = req.clone({
headers: req.headers.set('X-NISE-API', '20240218'),
withCredentials: true
});
return next.handle(modifiedReq);
}
}

View File

@ -1,5 +1,5 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {HttpBackend, HttpClient} from "@angular/common/http"; import {HttpClient} from "@angular/common/http";
import {environment} from "../../environments/environment"; import {environment} from "../../environments/environment";
interface UserInfo { interface UserInfo {
@ -12,15 +12,12 @@ interface UserInfo {
}) })
export class UserService { export class UserService {
private httpClient: HttpClient;
currentUser: UserInfo | null = null; currentUser: UserInfo | null = null;
loginCallback: () => void = () => {}; loginCallback: () => void = () => {};
logoutCallback: () => void = () => {}; logoutCallback: () => void = () => {};
constructor(private httpBackend: HttpBackend) { constructor(private httpClient: HttpClient) {
this.httpClient = new HttpClient(httpBackend);
this.currentUser = this.loadCurrentUserFromLocalStorage(); this.currentUser = this.loadCurrentUserFromLocalStorage();
this.updateUser() this.updateUser()
.catch(reason => console.debug(reason)); .catch(reason => console.debug(reason));
@ -40,7 +37,7 @@ export class UserService {
public updateUser(): Promise<any> { public updateUser(): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.httpClient.get<UserInfo>(`${environment.apiUrl}/auth`, {withCredentials: true}) this.httpClient.get<UserInfo>(`${environment.apiUrl}/auth`)
.subscribe({ .subscribe({
next: (user) => { next: (user) => {
this.currentUser = user; this.currentUser = user;