Some optimizations and virtual list

This commit is contained in:
2023-05-04 21:35:33 +02:00
parent 7755022227
commit dc772a2dab
10 changed files with 203 additions and 70 deletions
+7
View File
@@ -0,0 +1,7 @@
import { Ref } from '@vue/reactivity'
declare module '@vue/reactivity' {
export interface Ref<T> extends Ref<T> {
_rawValue: T
}
}
+2 -7
View File
@@ -16,16 +16,11 @@
</template>
<script lang="ts">
import { Player, Weapon, Server, useKillStore } from '@/stores/kill'
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
/* const store = useKillStore()
store.fetchPlayers({})
store.fetchServers({})
store.fetchWeapons({}) */
}
})
</script>
+23
View File
@@ -0,0 +1,23 @@
<template>
<div >
test
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref, toRaw, unref } from 'vue'
export default defineComponent({
components: { },
props: {
},
emits: ['highlightPlayer'],
data () {
return { test: 1 }
}
})
</script>
<style scoped>
</style>
+100
View File
@@ -0,0 +1,100 @@
<template>
<div ref="vlist" class="vlist" @scroll="scroll">
<div ref="vscroll" class="vscroll" :style="`padding: ${(vIndex*$props.rowHeight!)}px 0px ${(list.length*$props.rowHeight!)-(vIndex*$props.rowHeight!)}px;`">
<div :class="'playerRow ' + (player.id === $props.highlighted ? 'selected' : '')" v-for="(player, index) in visibleList"
v-bind:key="player.id" v-on:click="$emit('highlightPlayer', player.id)" :ref="`player:` + player.id">
<div><span>{{ index + 1 + vIndex }}</span></div>
<div><span>{{ player.value.username }}</span></div>
<div><span>{{ player.value.kills }}</span></div>
<div><span>{{ player.value.deaths }}</span></div>
<div><span>{{ Math.round(player.value.kills / Math.max(1, player.value.deaths) * 100) / 100 }}</span></div>
<div><span>{{ player.value.max_distance }}</span></div>
<div><span>{{ Math.round(((player.value.total_distance / player.value.kills) || 0) * 100) / 100 }}</span></div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Player } from '@/stores/kill'
import { defineComponent, PropType, Ref } from 'vue'
export default defineComponent({
data () {
return {
toLoad: 10,
vlistHeight: 0,
vIndex: 0
}
},
props: {
list: { type: Array as PropType<(Ref<Player> & { id: string })[]>, default: () => [] },
rowHeight: { type: Number, default: () => 32 },
highlighted: String
},
emits: ['highlightPlayer'],
computed: {
visibleList () {
return this.list.slice(Math.max(this.vIndex, 0), Math.min(this.vIndex + this.visibleCount * 3, this.list.length))
},
visibleCount () {
return Math.ceil(this.vlistHeight / this.$props.rowHeight)
}
},
watch: {
list () {
this.vlistHeight = (this.$refs.vlist as HTMLElement)?.getBoundingClientRect().height || 0
},
highlighted (newVal) {
setTimeout(() => (this.$refs.vlist as HTMLElement)?.scrollBy({ top: ((this.list.findIndex(e => e.id === newVal)) * this.rowHeight) - ((this.$refs.vlist as HTMLElement))?.scrollTop - (this.vlistHeight / 2 - this.$props.rowHeight), behavior: 'smooth' }), 1)
}
},
methods: {
scroll (e:Event) {
const scrollTop = (e.target as HTMLElement)?.scrollTop
const scrollIndex = Math.round(scrollTop / this.$props.rowHeight)
const mustScrollUp = Math.max(0, scrollIndex - (this.visibleCount)) < this.vIndex
const mustScrollDown = Math.min(this.list.length, scrollIndex + (this.visibleCount)) > this.vIndex + (this.visibleCount * 2)
if (mustScrollUp) {
this.vIndex = Math.max(0, Math.floor((scrollIndex - this.visibleCount) / 4) * 4)
} else if (mustScrollDown) {
this.vIndex = Math.max(0, Math.floor(scrollIndex / 4) * 4)
}
}
}
})
</script>
<style scoped>
.vlist{
overflow-y:auto;
}
.vlist, .vscroll {
height: calc(100% - 3em);
}
.playerRow:nth-child(2n+1) {
background: var(--accent);
}
.playerRow:nth-child(2n) {
background: var(--current-line);
}
.playerRow:hover {
background: var(--bg-color);
}
.playerRow>div:not(:last-child){
border-right: solid var(--bg-color) 2px;
}
.playerRow>div {
padding: .5em .8em 0 .25em;
overflow: hidden;
}
</style>
+15 -3
View File
@@ -22,7 +22,7 @@ import dataLabel from 'chartjs-plugin-datalabels'
import { useKillStore, Player, Filters } from '@/stores/kill'
import { Scatter } from 'vue-chartjs'
import { defineComponent, PropType, toRaw, unref } from 'vue'
import { defineComponent, PropType, Ref, toRaw, triggerRef, unref } from 'vue'
import { ChartEvent } from 'chart.js/dist/core/core.plugins'
import { _DeepPartialObject } from 'chart.js/dist/types/utils'
@@ -74,7 +74,6 @@ export default defineComponent({
}
return colors
},
chart () {
const playerIndex = this.playerList.findIndex(e => e.id === this.$props.playerHighlighted)
const defaultColor = this.colors.cyan
@@ -132,6 +131,7 @@ export default defineComponent({
},
onClick: (e: ChartEvent, element: any) => {
this.$emit('highlightPlayer', element.length > 0 ? this.playerList[element[0].index].id : undefined)
if (this.store.getPlayerList(this.filters || {}) !== undefined) triggerRef(this.store.getPlayerList(this.filters || {}) as unknown as Ref)
},
onHover: (e: any, element: any) => {
if (!e.native.target) return
@@ -210,6 +210,11 @@ export default defineComponent({
}
}
}
/* methods: {
refreshChart () {
if (this.store.getPlayerList(this.filters || {}) !== undefined) triggerRef(this.store.getPlayerList(this.filters || {}) as unknown as Ref)
}
} */
})
</script>
@@ -217,7 +222,14 @@ export default defineComponent({
canvas {
background: var(--accent);
}
.playerChart{
position:relative;
}
#refreshChart{
position:absolute;
top: 0;
left:0;
}
@media only screen and (max-width: 922px) {
canvas {
display: none !important
+44 -41
View File
@@ -7,13 +7,14 @@
<span v-on:click="updateSort('kills')" :class="sortingData.argument == 'kills' ? 'selected' : ''">K</span>
<span v-on:click="updateSort('deaths')" :class="sortingData.argument == 'deaths' ? 'selected' : ''">D</span>
<span v-on:click="updateSort('k/d')" :class="sortingData.argument == 'k/d' ? 'selected' : ''">K/D</span>
<span v-on:click="updateSort('max_distance')"
:class="sortingData.argument == 'max_distance' ? 'selected' : ''">max distance</span>
<span v-on:click="updateSort('max_distance')" :class="sortingData.argument == 'max_distance' ? 'selected' : ''">max
distance</span>
<span v-on:click="updateSort('avg_distance')"
:class="sortingData.argument == 'avg_distance' ? 'selected' : ''">average distance</span>
</div>
<div :class="'playerRow ' + (player.id === $props.playerHighlighted ? 'selected' : '')"
v-for="(player, index) in playerList" v-bind:key="player.id" v-on:click="$emit('highlightPlayer', player.id)"
<VirtualList :list="playerList" :row-height="32" @highlightPlayer="$emit('highlightPlayer', $event)" :highlighted="playerHighlighted"></VirtualList>
<!-- <div :class="'playerRow ' + (player.id === $props.playerHighlighted ? 'selected' : '')"
v-on:click="$emit('highlightPlayer', player.id)"
:ref="`player:` + player.id">
<div><span>{{ index+1 }}</span></div>
<div><span>{{ player.value.username }}</span></div>
@@ -22,15 +23,17 @@
<div><span>{{ Math.round(player.value.kills / Math.max(1, player.value.deaths) * 100) / 100 }}</span></div>
<div><span>{{ player.value.max_distance }}</span></div>
<div><span>{{ Math.round(((player.value.total_distance / player.value.kills) || 0) * 100) / 100 }}</span></div>
</div>
</div> -->
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref, toRaw, unref } from 'vue'
import { defineComponent, PropType, Ref } from 'vue'
import { useKillStore, Player, Filters } from '@/stores/kill'
import VirtualList from './List/VirtualList.vue'
export default defineComponent({
components: { VirtualList },
props: {
filters: { type: Object as PropType<Filters>, default: () => ({}) },
playerHighlighted: String
@@ -44,45 +47,57 @@ export default defineComponent({
},
computed: {
players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayer } = this.filters
const data = this.store.getPlayerList(withoutPlayer)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayer).value.data
return data
},
playerList ():(Ref<Player> & {id:string})[] {
playerList (): (Ref<Player> & { id: string })[] {
if (!this.players) return []
let players = Object.entries(this.players).map(e => {
const players = Object.entries(this.players).map(e => {
const target = {} as ProxyHandler<Ref<Player>>
const player = new Proxy(e[1], target) as Ref<Player> & {id:string}
const player = new Proxy(e[1], target) as Ref<Player> & { id: string }
player.id = e[0]
return player
}) as (Ref<Player> & {id:string})[]
}) as (Ref<Player> & { id: string })[]
const date = new Date()
if (this.sortingData.argument === 'username') {
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' })
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return collator.compare(toRaw(a.value).username, toRaw(b.value).username)
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return collator.compare(aVal.username, bVal.username)
})
} else if (this.sortingData.argument === 'k/d') {
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return toRaw(a.value).kills / Math.max(1, toRaw(a.value).deaths) - toRaw(b.value).kills / Math.max(1, toRaw(b.value).deaths)
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return aVal.kills / Math.max(1, aVal.deaths) - bVal.kills / Math.max(1, bVal.deaths)
})
} else if (this.sortingData.argument === 'avg_distance') {
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return ((toRaw(a.value).total_distance / toRaw(a.value).kills) || 0) - ((toRaw(b.value).total_distance / toRaw(b.value).kills) || 0)
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return ((aVal.total_distance / aVal.kills) || 0) - ((bVal.total_distance / bVal.kills) || 0)
})
} else {
const argument = this.sortingData.argument
players.sort((a: Ref<Player>, b: Ref<Player>) => {
return toRaw(a.value)[argument] - toRaw(b.value)[argument]
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
const bVal = b._rawValue
return aVal[argument] - bVal[argument]
})
}
console.log('Sorting the player object took + ' + ((new Date()).getTime() - date.getTime()) + 'ms')
if (this.sortingData.direction < 0) {
players.reverse()
}
players = players.slice(0, 500)
return players
}
},
@@ -120,7 +135,7 @@ export default defineComponent({
if (nextIndex < 0) nextIndex = 0
this.$emit('highlightPlayer', this.playerList[nextIndex].id)
},
updateSort (argument:string) {
updateSort (argument: string) {
if (this.sortingData.argument === argument) {
this.sortingData.direction *= -1
} else {
@@ -128,7 +143,7 @@ export default defineComponent({
}
this.sortingData.argument = argument as typeof this.sortingData.argument
},
scrollToPlayer (playerid:string) {
scrollToPlayer (playerid: string) {
const newElement = this.$refs['player:' + playerid] as HTMLElement[]
// Some hack because of some inconsistency
setTimeout(function () {
@@ -140,15 +155,14 @@ export default defineComponent({
})
</script>
<style scoped>
* {
<style>
.playerTable * {
box-sizing: border-box;
}
.playerTable {
grid-area: list;
margin-right: 1rem;
overflow: auto;
height: 100%;
user-select: none;
}
@@ -177,26 +191,14 @@ export default defineComponent({
padding: .5em .8em 0 .25em;
}
.playerRow>div {
padding: .5em .8em 0 .25em;
overflow: hidden;
}
.playerHeaders,
.playerRow:nth-child(2n) {
.playerHeaders{
background: var(--current-line);
}
.playerRow:nth-child(2n+1) {
background: var(--accent);
}
.playerHeaders div:hover,
.playerRow:hover {
.playerHeaders div:hover {
background: var(--bg-color);
}
.playerRow>div:not(:last-child),
.playerHeaders>div:not(:last-child) {
border-right: solid var(--bg-color) 2px;
}
@@ -226,4 +228,5 @@ export default defineComponent({
.selected {
background: var(--comment) !important;
}</style>
}
</style>
+1 -1
View File
@@ -9,7 +9,7 @@ import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
import dataLabel from 'chartjs-plugin-datalabels'
import { Weapon, Filters, useKillStore } from '@/stores/kill'
import { Doughnut } from 'vue-chartjs'
import { defineComponent, PropType, Ref, unref } from 'vue'
import { defineComponent, PropType, Ref } from 'vue'
import weapons from '../stores/weapons.json'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
+4 -2
View File
@@ -23,8 +23,10 @@ const websocketData = {
host: 1
}
// dev stuff
const store = useKillStore()
setInterval(() => {
function registerWebSocketKill () {
const allPlayerArrays = store.$state.players.filter((e) => {
return (
(!e.value.filter.server ||
@@ -106,4 +108,4 @@ setInterval(() => {
unref(e.value.data[websocketData.cause_of_death]).kills++
unref(e.value.data[websocketData.cause_of_death]).max_distance = Math.max(websocketData.distance, unref(e.value.data[websocketData.cause_of_death]).max_distance)
})
}, 100)
}
+6 -16
View File
@@ -1,16 +1,5 @@
<template>
<div id="filters">
<!-- <input type="number" min="0" v-model="filters.minKills">
<input type="number" min="0" v-model="filters.minDeaths"> -->
<!-- <select v-on:change="changeFilter({ server: ($event.target as HTMLInputElement).value })">
<option></option>
<option v-for="(serverData) in servers" v-bind:key="serverData.id" :value="serverData.id">{{ serverData.name }}
</option>
</select>
<select v-on:change="changeFilter({ weapon: ($event.target as HTMLInputElement).value })">
<option></option>
<option v-for="weaponId in sortedWeaponList" v-bind:key="weaponId" :value="weaponId">{{ weaponId }}</option>
</select> -->
<span class="multiselect-wrapper">
<VueMultiselect
selectLabel=""
@@ -67,6 +56,7 @@
:playerHighlighted="playerHighlighted?.id"
>
</PlayerList>
<PlayerChart
:filters="filters"
v-on:highlightPlayer="highlight_player"
@@ -86,7 +76,7 @@ import { Player, Weapon, Server, useKillStore, Filters } from '@/stores/kill'
import PlayerList from '@/components/PlayerList.vue'
import PlayerChart from '@/components/PlayerChart.vue'
import WeaponChart from '@/components/WeaponChart.vue'
import { Ref, defineComponent, toRaw, unref } from 'vue'
import { Ref, defineComponent, unref } from 'vue'
import VueMultiselect from 'vue-multiselect'
import 'vue-multiselect/dist/vue-multiselect.css'
import weapons from '../stores/weapons.json'
@@ -171,9 +161,9 @@ export default defineComponent({
if (!this.players) return []
const players = Object.entries(this.players).map((e) => ({
id: e[0],
...toRaw(e[1].value)
...e[1]._rawValue
}))
return players.sort((a: Player, b: Player) => {
return players.sort((a, b) => {
if (a.username < b.username) {
return -1
}
@@ -202,7 +192,7 @@ export default defineComponent({
handler (newValue) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { server: _, weapon: _2, ...withoutFilters } = this.$route.query
router.push({ query: { ...newValue, ...withoutFilters } }).then(e => { console.log(e) })
router.push({ query: { ...newValue, ...withoutFilters } })// .then(e => { console.log(e) })
},
deep: true
},
@@ -210,7 +200,7 @@ export default defineComponent({
if (newValue?.id === oldValue?.id) return
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayer } = this.$route.query
router.push({ query: { player: newValue?.id, ...withoutPlayer } }).then(e => { console.log(e) })
router.push({ query: { player: newValue?.id, ...withoutPlayer } })// .then(e => { console.log(e) })
},
'$route' (to) {
if (this.playerHighlighted?.id !== to.query.player) {
+1
View File
@@ -14,6 +14,7 @@
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"typeRoots" : ["node_modules/@types", "src/@types"],
"types": [
"webpack-env"
],